diff --git a/.github/workflows/addon-checker.yml b/.github/workflows/addon-checker.yml new file mode 100644 index 0000000000..91630d38fc --- /dev/null +++ b/.github/workflows/addon-checker.yml @@ -0,0 +1,34 @@ +name: Kodi Addon-Checker + +on: [pull_request] + +jobs: + kodi-addon-checker: + runs-on: ubuntu-latest + name: Kodi Addon-Checker + steps: + + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip3 install --user kodi-addon-checker + + - name: Extract job variables + shell: bash + + run: | + echo "addon=$(git diff --diff-filter=d --name-only HEAD~ | grep / | cut -d / -f1 | sort | uniq)" >> $GITHUB_OUTPUT + id: extract_vars + + - name: Addon-Check + run: $HOME/.local/bin/kodi-addon-checker --branch=${{ github.event.pull_request.base.ref }} --PR ${{ steps.extract_vars.outputs.addon }} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..7ddf15f462 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +dist: bionic +language: python +python: 3.8 + +install: + - pip install kodi-addon-checker + +before_script: +- git config core.quotepath false + +# command to run our tests +script: + - kodi-addon-checker --branch=matrix $([ "$TRAVIS_PULL_REQUEST" != "false" ] && echo --PR $(git diff --diff-filter=d --name-only HEAD~ | grep / | cut -d / -f1 | sort | uniq)) + +notifications: + webhooks: https://www.travisbuddy.com/ + email: + on_failure: change # default: always + +travisBuddy: + successBuildLog: "true" diff --git a/README.md b/README.md index c33146f25d..bb049eb9ba 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,11 @@ This branch is not used for public. Please use one of the other branches availab ## Quick Kodi development links -* [Add-on rules](https://github.com/xbmc/xbmc/blob/master/CONTRIBUTING.md) +* [Add-on rules](https://github.com/xbmc/repo-plugins/blob/master/CONTRIBUTING.md) * [Submitting an add-on details](http://kodi.wiki/view/Submitting_Add-ons) * [Code guidelines](http://kodi.wiki/view/Official:Code_guidelines_and_formatting_conventions) * [Kodi development](http://kodi.wiki/view/Development) +* [Kodi Addon checker](https://pypi.org/project/kodi-addon-checker/) ## Other useful links diff --git a/plugin.audio.ampache/LICENSE b/plugin.audio.ampache/LICENSE new file mode 100644 index 0000000000..c0a3c35c63 --- /dev/null +++ b/plugin.audio.ampache/LICENSE @@ -0,0 +1,17 @@ +XBMC Ampache Plugin + Copyright (C) 2011-2017 Michael Better, Carlo Sardi, Jeffmeister, abeiro, Chris Slamar, Reno Reckling + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + diff --git a/plugin.audio.ampache/README b/plugin.audio.ampache/README new file mode 100644 index 0000000000..7acf5c1d7f --- /dev/null +++ b/plugin.audio.ampache/README @@ -0,0 +1,33 @@ +ampache-xbmc-plugin + +This is a plugin for XBMC/KODI providing basic connectivity to the Streaming Software Ampache. +The plugin is included in Kodi Official Repository + +If you want to use the cutting edge version, download the zip file and add it to your XBMC/KODI via System->AddOns->Install from ZIP. +In the new version of KODI you have to do AddOns->Addon Icon ( top/left in Kodi default Skin )->Install from ZIP. + + +This plugin supports the ampache API from 350001 to the last one. + +Tested with kodi 19, 20 + +Tested with web controls and kore app ( kore app offers only a partial plugin support ) + +It is tested with the latest stable ampache server and nextcloud music. + + +Troubleshooting: + +Nextcloud music doesn't use api-key, but username/password, if you have problems to connect unckeck api_key box + +Web controls work better with the old search interface, it is possibile to enable it in the settings + +The crashes in kodi are due to bugs in kodi from 18.5 to 20 ( double busy dialog bug ) and in kore app. +To avoid random crashes in kodi don't do any operation in the last five seconds of a song, +due to a kodi bug, playing next song generates a busy dialog ( impossible to avoid ) +and operating on kodi generates another busy dialog. +The combination of two busy dialogs working crashes kodi (https://github.com/xbmc/xbmc/issues/16756) + +When you update the 2.0 version from an old version, expecially on raspberry pi, the plugin could not work. +This behaviour is due to the kodi addon cache. To correct this one time problem, it is necessary to reboot the +mediacenter or, if the problem continues, uninstall and reinstall the plugin. diff --git a/plugin.audio.ampache/addon.xml b/plugin.audio.ampache/addon.xml new file mode 100644 index 0000000000..37613242a8 --- /dev/null +++ b/plugin.audio.ampache/addon.xml @@ -0,0 +1,27 @@ + + + + + + + + audio + + + + + Stream music from Ampache XML-API + Streame Musik von der Ampache XML-API + Streaming Musicale per l'XML-API di Ampache + Retransmitir música a través de la XML-API de Ampache + A web based audio/video streaming application and file manager allowing you to access your music and videos from anywhere, using almost any internet enabled device. + all + https://forum.kodi.tv/showthread.php?tid=230736 + https://ampache.org/ + GPL-2.0-or-later + + resources/icon.png + resources/fanart.jpg + + + diff --git a/plugin.audio.ampache/default.py b/plugin.audio.ampache/default.py new file mode 100644 index 0000000000..2610938cbf --- /dev/null +++ b/plugin.audio.ampache/default.py @@ -0,0 +1,4 @@ + +from resources.lib.ampache_plugin import Main + +Main() diff --git a/plugin.audio.ampache/resources/__init__.py b/plugin.audio.ampache/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.ampache/resources/fanart.jpg b/plugin.audio.ampache/resources/fanart.jpg new file mode 100644 index 0000000000..765771c5ba Binary files /dev/null and b/plugin.audio.ampache/resources/fanart.jpg differ diff --git a/plugin.audio.ampache/resources/icon.png b/plugin.audio.ampache/resources/icon.png new file mode 100644 index 0000000000..3872e7410b Binary files /dev/null and b/plugin.audio.ampache/resources/icon.png differ diff --git a/plugin.audio.ampache/resources/language/resource.language.de_de/strings.po b/plugin.audio.ampache/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..6a4f9fd623 --- /dev/null +++ b/plugin.audio.ampache/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,409 @@ +# Kodi Media Center language file +# Addon Name: Ampache plugin +# Addon id: plugin.audio.ampache +# Addon Provider: lusum +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2020-02-06 22:42+0100\n" +"Last-Translator: \n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/" +"language/en/)\n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# settings +msgctxt "#30001" +msgid "General" +msgstr "" + +msgctxt "#30002" +msgid "Servers" +msgstr "" + +msgctxt "#30010" +msgid "Random Items" +msgstr "" + +msgctxt "#30011" +msgid "Disable Ssl certs" +msgstr "" + +# empty string with id 32003 +msgctxt "#30012" +msgid "Api Version" +msgstr "" + +msgctxt "#30013" +msgid "Old search gui (web controller friendly)" +msgstr "" + +msgctxt "#30020" +msgid "Add Server" +msgstr "" + +msgctxt "#30021" +msgid "Remove Server" +msgstr "" + +msgctxt "#30022" +msgid "Modify Server" +msgstr "" + +msgctxt "#30023" +msgid "Switch Server" +msgstr "" + +# code +msgctxt "#30101" +msgid "Search..." +msgstr "" + +msgctxt "#30102" +msgid "Quick access..." +msgstr "" + +msgctxt "#30103" +msgid "Explore..." +msgstr "" + +msgctxt "#30104" +msgid "Library..." +msgstr "" + +msgctxt "#30105" +msgid "Settings" +msgstr "" + +msgctxt "#30106" +msgid "Artist" +msgstr "" + +msgctxt "#30107" +msgid "Album" +msgstr "" + +msgctxt "#30108" +msgid "Song" +msgstr "" + +msgctxt "#30109" +msgid "Playlist" +msgstr "" + +msgctxt "#30110" +msgid "All" +msgstr "" + +msgctxt "#30111" +msgid "Tag" +msgstr "" + +msgctxt "#30112" +msgid "Artist tag" +msgstr "" + +msgctxt "#30113" +msgid "Album tag" +msgstr "" + +msgctxt "#30114" +msgid "Song tag" +msgstr "" + +msgctxt "#30115" +msgid "Artists" +msgstr "" + +msgctxt "#30116" +msgid "Albums" +msgstr "" + +msgctxt "#30117" +msgid "Songs" +msgstr "" + +msgctxt "#30118" +msgid "Playlists" +msgstr "" + +msgctxt "#30119" +msgid "Tags" +msgstr "" + +msgctxt "#30120" +msgid "Search Artists..." +msgstr "" + +msgctxt "#30121" +msgid "Search Albums..." +msgstr "" + +msgctxt "#30122" +msgid "Search Songs..." +msgstr "" + +msgctxt "#30123" +msgid "Search Playlists..." +msgstr "" + +msgctxt "#30124" +msgid "Search All..." +msgstr "" + +msgctxt "#30125" +msgid "Search Tags..." +msgstr "" + +msgctxt "#30126" +msgid "Recent Artists..." +msgstr "" + +msgctxt "#30127" +msgid "Recent Albums..." +msgstr "" + +msgctxt "#30128" +msgid "Recent Songs..." +msgstr "" + +msgctxt "#30129" +msgid "Recent Playlists..." +msgstr "" + +msgctxt "#30130" +msgid "Last Update" +msgstr "" + +msgctxt "#30131" +msgid "1 Week" +msgstr "" + +msgctxt "#30132" +msgid "1 Month" +msgstr "" + +msgctxt "#30133" +msgid "3 Months" +msgstr "" + +msgctxt "#30134" +msgid "Random Artists..." +msgstr "" + +msgctxt "#30135" +msgid "Random Albums..." +msgstr "" + +msgctxt "#30136" +msgid "Random Songs..." +msgstr "" + +msgctxt "#30137" +msgid "Random Playlists..." +msgstr "" + +msgctxt "#30138" +msgid "Show artist from this song" +msgstr "" + +msgctxt "#30139" +msgid "Show album from this song" +msgstr "" + +msgctxt "#30140" +msgid "Search all songs with this title" +msgstr "" + +msgctxt "#30141" +msgid "Show all albums from artist" +msgstr "" + +msgctxt "#30142" +msgid "Artist tags..." +msgstr "" + +msgctxt "#30143" +msgid "Album tags..." +msgstr "" + +msgctxt "#30144" +msgid "Song tags..." +msgstr "" + +msgctxt "#30145" +msgid "Recent..." +msgstr "" + +msgctxt "#30146" +msgid "Random..." +msgstr "" + +msgctxt "#30147" +msgid "Server playlist..." +msgstr "" + +msgctxt "#30148" +msgid "Highest Rated..." +msgstr "" + +msgctxt "#30149" +msgid "Highest Rated Artists..." +msgstr "" + +msgctxt "#30150" +msgid "Highest Rated Albums..." +msgstr "" + +msgctxt "#30151" +msgid "Highest Rated Songs..." +msgstr "" + +msgctxt "#30152" +msgid "Frequent Artists..." +msgstr "" + +msgctxt "#30153" +msgid "Frequent Albums..." +msgstr "" + +msgctxt "#30154" +msgid "Frequent Songs..." +msgstr "" + +msgctxt "#30155" +msgid "Flagged Artists..." +msgstr "" + +msgctxt "#30156" +msgid "Flagged Albums..." +msgstr "" + +msgctxt "#30157" +msgid "Flagged Songs..." +msgstr "" + +msgctxt "#30158" +msgid "Forgotten Artists..." +msgstr "" + +msgctxt "#30159" +msgid "Forgotten Albums..." +msgstr "" + +msgctxt "#30160" +msgid "Forgotten Songs..." +msgstr "" + +msgctxt "#30161" +msgid "Newest Artists..." +msgstr "" + +msgctxt "#30162" +msgid "Newest Albums..." +msgstr "" + +msgctxt "#30163" +msgid "Newest Songs..." +msgstr "" + +msgctxt "#30164" +msgid "Frequent..." +msgstr "" + +msgctxt "#30165" +msgid "Flagged..." +msgstr "" + +msgctxt "#30166" +msgid "Forgotten..." +msgstr "" + +msgctxt "#30167" +msgid "Newest..." +msgstr "" + +msgctxt "#30168" +msgid "Modify the data, cancel to exit" +msgstr "" + +msgctxt "#30169" +msgid "Choose a default server" +msgstr "" + +msgctxt "#30170" +msgid "Enter the Server name" +msgstr "" + +msgctxt "#30171" +msgid "Enter the url of the server" +msgstr "" + +msgctxt "#30173" +msgid "Do you want to use an api-key?" +msgstr "" + +msgctxt "#30174" +msgid "Enter the Api key" +msgstr "" + +msgctxt "#30175" +msgid "Enter the username" +msgstr "" + +msgctxt "#30177" +msgid "The server needs a password?" +msgstr "" + +msgctxt "#30178" +msgid "Enter the password" +msgstr "" + +msgctxt "#30179" +msgid "Choose a server to remove" +msgstr "" + +msgctxt "#30180" +msgid "Modify a server" +msgstr "" + +msgctxt "#30181" +msgid "Server name" +msgstr "" + +msgctxt "#30182" +msgid "Server url" +msgstr "" + +msgctxt "#30183" +msgid "Username" +msgstr "" + +msgctxt "#30184" +msgid "Enable password" +msgstr "" + +msgctxt "#30185" +msgid "Password" +msgstr "" + +msgctxt "#30186" +msgid "Use api key" +msgstr "" + +msgctxt "#30187" +msgid "Api key" +msgstr "" + +msgctxt "#30188" +msgid "Are you sure?" +msgstr "" + +msgctxt "#30189" +msgid "Ampache plugin" +msgstr "" diff --git a/plugin.audio.ampache/resources/language/resource.language.en_gb/strings.po b/plugin.audio.ampache/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..33fbcbb877 --- /dev/null +++ b/plugin.audio.ampache/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,495 @@ +# Kodi Media Center language file +# Addon Name: Ampache plugin +# Addon id: plugin.audio.ampache +# Addon Provider: lusum +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2020-02-06 18:18+0100\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# settings +msgctxt "#30001" +msgid "General" +msgstr "" + +msgctxt "#30002" +msgid "Servers" +msgstr "" + +msgctxt "#30010" +msgid "Random Items" +msgstr "" + +msgctxt "#30011" +msgid "Disable Ssl certs" +msgstr "" + +msgctxt "#30012" +msgid "Api Version" +msgstr "" + +msgctxt "#30013" +msgid "Old search gui (web controller friendly)" +msgstr "" + +msgctxt "#30014" +msgid "Enable images on long lists" +msgstr "" + +msgctxt "#30015" +msgid "Auto fullscreen" +msgstr "" + +msgctxt "#30020" +msgid "Add Server" +msgstr "" + +msgctxt "#30021" +msgid "Remove Server" +msgstr "" + +msgctxt "#30022" +msgid "Modify Server" +msgstr "" + +msgctxt "#30023" +msgid "Switch Server" +msgstr "" + +# code +msgctxt "#30101" +msgid "Search..." +msgstr "" + +msgctxt "#30102" +msgid "Quick access..." +msgstr "" + +msgctxt "#30103" +msgid "Explore..." +msgstr "" + +msgctxt "#30104" +msgid "Library..." +msgstr "" + +msgctxt "#30105" +msgid "Settings" +msgstr "" + +msgctxt "#30106" +msgid "Artist" +msgstr "" + +msgctxt "#30107" +msgid "Album" +msgstr "" + +msgctxt "#30108" +msgid "Song" +msgstr "" + +msgctxt "#30109" +msgid "Playlist" +msgstr "" + +msgctxt "#30110" +msgid "All" +msgstr "" + +msgctxt "#30111" +msgid "Tag" +msgstr "" + +msgctxt "#30112" +msgid "Artist tag" +msgstr "" + +msgctxt "#30113" +msgid "Album tag" +msgstr "" + +msgctxt "#30114" +msgid "Song tag" +msgstr "" + +msgctxt "#30115" +msgid "Artists" +msgstr "" + +msgctxt "#30116" +msgid "Albums" +msgstr "" + +msgctxt "#30117" +msgid "Songs" +msgstr "" + +msgctxt "#30118" +msgid "Playlists" +msgstr "" + +msgctxt "#30119" +msgid "Tags" +msgstr "" + +msgctxt "#30120" +msgid "Search Artists..." +msgstr "" + +msgctxt "#30121" +msgid "Search Albums..." +msgstr "" + +msgctxt "#30122" +msgid "Search Songs..." +msgstr "" + +msgctxt "#30123" +msgid "Search Playlists..." +msgstr "" + +msgctxt "#30124" +msgid "Search All..." +msgstr "" + +msgctxt "#30125" +msgid "Search Tags..." +msgstr "" + +msgctxt "#30126" +msgid "Recently Added Artists..." +msgstr "" + +msgctxt "#30127" +msgid "Recently Added Albums..." +msgstr "" + +msgctxt "#30128" +msgid "Recently Added Songs..." +msgstr "" + +msgctxt "#30129" +msgid "Recently Added Playlists..." +msgstr "" + +msgctxt "#30130" +msgid "Last Update" +msgstr "" + +msgctxt "#30131" +msgid "1 Week" +msgstr "" + +msgctxt "#30132" +msgid "1 Month" +msgstr "" + +msgctxt "#30133" +msgid "3 Months" +msgstr "" + +msgctxt "#30134" +msgid "Random Artists..." +msgstr "" + +msgctxt "#30135" +msgid "Random Albums..." +msgstr "" + +msgctxt "#30136" +msgid "Random Songs..." +msgstr "" + +msgctxt "#30137" +msgid "Random Playlists..." +msgstr "" + +msgctxt "#30138" +msgid "Show artist from this song" +msgstr "" + +msgctxt "#30139" +msgid "Show album from this song" +msgstr "" + +msgctxt "#30140" +msgid "Search all songs with this title" +msgstr "" + +msgctxt "#30141" +msgid "Show all albums from artist" +msgstr "" + +msgctxt "#30142" +msgid "Artist tags..." +msgstr "" + +msgctxt "#30143" +msgid "Album tags..." +msgstr "" + +msgctxt "#30144" +msgid "Song tags..." +msgstr "" + +msgctxt "#30145" +msgid "Recently Added..." +msgstr "" + +msgctxt "#30146" +msgid "Random..." +msgstr "" + +msgctxt "#30147" +msgid "Server playlist..." +msgstr "" + +msgctxt "#30148" +msgid "Highest Rated..." +msgstr "" + +msgctxt "#30149" +msgid "Highest Rated Artists..." +msgstr "" + +msgctxt "#30150" +msgid "Highest Rated Albums..." +msgstr "" + +msgctxt "#30151" +msgid "Highest Rated Songs..." +msgstr "" + +msgctxt "#30152" +msgid "Frequent Artists..." +msgstr "" + +msgctxt "#30153" +msgid "Frequent Albums..." +msgstr "" + +msgctxt "#30154" +msgid "Frequent Songs..." +msgstr "" + +msgctxt "#30155" +msgid "Flagged Artists..." +msgstr "" + +msgctxt "#30156" +msgid "Flagged Albums..." +msgstr "" + +msgctxt "#30157" +msgid "Flagged Songs..." +msgstr "" + +msgctxt "#30158" +msgid "Forgotten Artists..." +msgstr "" + +msgctxt "#30159" +msgid "Forgotten Albums..." +msgstr "" + +msgctxt "#30160" +msgid "Forgotten Songs..." +msgstr "" + +msgctxt "#30161" +msgid "Newest Artists..." +msgstr "" + +msgctxt "#30162" +msgid "Newest Albums..." +msgstr "" + +msgctxt "#30163" +msgid "Newest Songs..." +msgstr "" + +msgctxt "#30164" +msgid "Frequent..." +msgstr "" + +msgctxt "#30165" +msgid "Flagged..." +msgstr "" + +msgctxt "#30166" +msgid "Forgotten..." +msgstr "" + +msgctxt "#30167" +msgid "Newest..." +msgstr "" + +msgctxt "#30168" +msgid "Modify the data, cancel to exit" +msgstr "" + +msgctxt "#30169" +msgid "Choose a default server" +msgstr "" + +msgctxt "#30170" +msgid "Enter the Server name" +msgstr "" + +msgctxt "#30171" +msgid "Enter the url of the server" +msgstr "" + +msgctxt "#30173" +msgid "Do you want to use an api-key?" +msgstr "" + +msgctxt "#30174" +msgid "Enter the Api key" +msgstr "" + +msgctxt "#30175" +msgid "Enter the username" +msgstr "" + +msgctxt "#30177" +msgid "The server needs a password?" +msgstr "" + +msgctxt "#30178" +msgid "Enter the password" +msgstr "" + +msgctxt "#30179" +msgid "Choose a server to remove" +msgstr "" + +msgctxt "#30180" +msgid "Modify a server" +msgstr "" + +msgctxt "#30181" +msgid "Server name" +msgstr "" + +msgctxt "#30182" +msgid "Server url" +msgstr "" + +msgctxt "#30183" +msgid "Username" +msgstr "" + +msgctxt "#30184" +msgid "Enable password" +msgstr "" + +msgctxt "#30185" +msgid "Password" +msgstr "" + +msgctxt "#30186" +msgid "Use api key" +msgstr "" + +msgctxt "#30187" +msgid "Api key" +msgstr "" + +msgctxt "#30188" +msgid "Are you sure?" +msgstr "" + +msgctxt "#30189" +msgid "Ampache plugin" +msgstr "" + +msgctxt "#30190" +msgid "Recently Played Artists..." +msgstr "" + +msgctxt "#30191" +msgid "Recently Played Albums..." +msgstr "" + +msgctxt "#30192" +msgid "Recently Played Songs..." +msgstr "" + +msgctxt "#30193" +msgid "Recently Played..." +msgstr "" + +msgctxt "#30194" +msgid "Next items..." +msgstr "" + +msgctxt "#30195" +msgid "Disk" +msgstr "" + +msgctxt "#30197" +msgid "Information" +msgstr "" + +msgctxt "#30198" +msgid "Error" +msgstr "" + +msgctxt "#30202" +msgid "Connection Error" +msgstr "" + +msgctxt "#30203" +msgid "Connection OK" +msgstr "" + +msgctxt "#30204" +msgid "Permission error. If you are using Nextcloud don't check api_key box" +msgstr "" + +msgctxt "#30220" +msgid "Video" +msgstr "" + +msgctxt "#30221" +msgid "Videos" +msgstr "" + +msgctxt "#30222" +msgid "Search Videos..." +msgstr "" + +msgctxt "#30225" +msgid "Podcast" +msgstr "" + +msgctxt "#30226" +msgid "Podcasts" +msgstr "" + +msgctxt "#30227" +msgid "Search Podcasts..." +msgstr "" + +msgctxt "#30228" +msgid "Live stream" +msgstr "" + +msgctxt "#30229" +msgid "Live streams" +msgstr "" + +msgctxt "#30230" +msgid "Search Live Streams..." +msgstr "" diff --git a/plugin.audio.ampache/resources/language/resource.language.es_es/strings.po b/plugin.audio.ampache/resources/language/resource.language.es_es/strings.po new file mode 100644 index 0000000000..2c8969a436 --- /dev/null +++ b/plugin.audio.ampache/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,493 @@ +# Kodi Media Center language file +# Addon Name: Ampache plugin +# Addon id: plugin.audio.ampache +# Addon Provider: lusum +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-03-18 00:01+0100\n" +"Last-Translator: \n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/" +"language/en/)\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.4.1\n" + +msgctxt "#30001" +msgid "General" +msgstr "General" + +msgctxt "#30002" +msgid "Servers" +msgstr "Servidores" + +msgctxt "#30010" +msgid "Random Items" +msgstr "Núumero de elementos aleatorios" + +msgctxt "#30011" +msgid "Disable Ssl certs" +msgstr "Deshabilitar certificados Ssl" + +# empty string with id 32003 +msgctxt "#30012" +msgid "Api Version" +msgstr "Versión de la Api" + +msgctxt "#30013" +msgid "Old search gui (web controller friendly)" +msgstr "Interfaz de búsqueda para el controlador web" + +msgctxt "#30014" +msgid "Enable images on long lists" +msgstr "Habilita imágenes en listas largas" + +msgctxt "#30015" +msgid "Auto fullscreen" +msgstr "Pantalla completa automática" + +msgctxt "#30020" +msgid "Add Server" +msgstr "Añadir servidor" + +msgctxt "#30021" +msgid "Remove Server" +msgstr "Eliminar servidor" + +msgctxt "#30022" +msgid "Modify Server" +msgstr "Modificar servidor" + +msgctxt "#30023" +msgid "Switch Server" +msgstr "Cambiar servidor" + +# code +msgctxt "#30101" +msgid "Search..." +msgstr "Buscar..." + +msgctxt "#30102" +msgid "Quick access..." +msgstr "Acceso rápido..." + +msgctxt "#30103" +msgid "Explore..." +msgstr "Explorar..." + +msgctxt "#30104" +msgid "Library..." +msgstr "Librería..." + +msgctxt "#30105" +msgid "Settings" +msgstr "Configuración" + +msgctxt "#30106" +msgid "Artist" +msgstr "Artista" + +msgctxt "#30107" +msgid "Album" +msgstr "Álbum" + +msgctxt "#30108" +msgid "Song" +msgstr "Canción" + +msgctxt "#30109" +msgid "Playlist" +msgstr "Lista de reproducción" + +msgctxt "#30110" +msgid "All" +msgstr "Todo" + +msgctxt "#30111" +msgid "Tag" +msgstr "Género" + +msgctxt "#30112" +msgid "Artist tag" +msgstr "Género del artista" + +msgctxt "#30113" +msgid "Album tag" +msgstr "Género del álbum" + +msgctxt "#30114" +msgid "Song tag" +msgstr "Género de la canción" + +msgctxt "#30115" +msgid "Artists" +msgstr "Artistas" + +msgctxt "#30116" +msgid "Albums" +msgstr "Álbumes" + +msgctxt "#30117" +msgid "Songs" +msgstr "Canciones" + +msgctxt "#30118" +msgid "Playlists" +msgstr "Listas de reproducción" + +msgctxt "#30119" +msgid "Tags" +msgstr "Géneros" + +# code +msgctxt "#30120" +msgid "Search Artists..." +msgstr "Buscar de artistas..." + +# code +msgctxt "#30121" +msgid "Search Albums..." +msgstr "Buscar de álbumes..." + +# code +msgctxt "#30122" +msgid "Search Songs..." +msgstr "Buscar de canciones..." + +# code +msgctxt "#30123" +msgid "Search Playlists..." +msgstr "Buscar de listas de reproducción..." + +# code +msgctxt "#30124" +msgid "Search All..." +msgstr "Buscar todo..." + +# code +msgctxt "#30125" +msgid "Search Tags..." +msgstr "Buscar géneros..." + +msgctxt "#30126" +msgid "Recently Added Artists..." +msgstr "Artistas añadidos recientemente..." + +msgctxt "#30127" +msgid "Recently Added Albums..." +msgstr "Álbumes añadidos recientemente..." + +msgctxt "#30128" +msgid "Recently Added Songs..." +msgstr "Canciones añadidas recientemente..." + +msgctxt "#30129" +msgid "Recently Added Playlists..." +msgstr "Listas de reproducción añadidas recientemente..." + +msgctxt "#30130" +msgid "Last Update" +msgstr "Últimas actualizaciones" + +msgctxt "#30131" +msgid "1 Week" +msgstr "1 semana" + +msgctxt "#30132" +msgid "1 Month" +msgstr "1 mes" + +msgctxt "#30133" +msgid "3 Months" +msgstr "3 meses" + +msgctxt "#30134" +msgid "Random Artists..." +msgstr "Artistas aleatorios..." + +msgctxt "#30135" +msgid "Random Albums..." +msgstr "Álbumes aleatorios..." + +msgctxt "#30136" +msgid "Random Songs..." +msgstr "Canciones aleatorios..." + +msgctxt "#30137" +msgid "Random Playlists..." +msgstr "Listas de reproducción aleatorias..." + +msgctxt "#30138" +msgid "Show artist from this song" +msgstr "Mostrar artistas de esta canción" + +msgctxt "#30139" +msgid "Show album from this song" +msgstr "Mostrar álbum de esta canción" + +msgctxt "#30140" +msgid "Search all songs with this title" +msgstr "Buscar todas las canciones con este título" + +msgctxt "#30141" +msgid "Show all albums from artist" +msgstr "Mostrar todos los álbumes del artista" + +msgctxt "#30142" +msgid "Artist tags..." +msgstr "Géneros de artistas..." + +msgctxt "#30143" +msgid "Album tags..." +msgstr "Géneros de álbumes..." + +msgctxt "#30144" +msgid "Song tags..." +msgstr "Géneros de canciones..." + +msgctxt "#30145" +msgid "Recently Added..." +msgstr "Añadido recientemente..." + +msgctxt "#30146" +msgid "Random..." +msgstr "Aleatorio..." + +msgctxt "#30147" +msgid "Server playlist..." +msgstr "Lista de reproducción del servidor..." + +msgctxt "#30148" +msgid "Highest Rated..." +msgstr "Puntuaciones mas altas..." + +msgctxt "#30149" +msgid "Highest Rated Artists..." +msgstr "Artistas con mejor puntuación..." + +msgctxt "#30150" +msgid "Highest Rated Albums..." +msgstr "Álbumes con mejor puntuación..." + +msgctxt "#30151" +msgid "Highest Rated Songs..." +msgstr "Canciones con mejor puntuación..." + +msgctxt "#30152" +msgid "Frequent Artists..." +msgstr "Artitas frequentes..." + +msgctxt "#30153" +msgid "Frequent Albums..." +msgstr "Álbumes frequentes..." + +msgctxt "#30154" +msgid "Frequent Songs..." +msgstr "Canciones frequentes..." + +msgctxt "#30155" +msgid "Flagged Artists..." +msgstr "Artistas favoritos..." + +msgctxt "#30156" +msgid "Flagged Albums..." +msgstr "Álbumes favoritos..." + +msgctxt "#30157" +msgid "Flagged Songs..." +msgstr "Canciones favoritas..." + +msgctxt "#30158" +msgid "Forgotten Artists..." +msgstr "Artistas olvidados..." + +msgctxt "#30159" +msgid "Forgotten Albums..." +msgstr "Álbumes olvidados..." + +msgctxt "#30160" +msgid "Forgotten Songs..." +msgstr "Canciones olvidadas..." + +msgctxt "#30161" +msgid "Newest Artists..." +msgstr "Artistas añadidos recientemente..." + +msgctxt "#30162" +msgid "Newest Albums..." +msgstr "Álbumes añadidos recientemente" + +msgctxt "#30163" +msgid "Newest Songs..." +msgstr "Canciones añadidas recientemente..." + +msgctxt "#30164" +msgid "Frequent..." +msgstr "Frequentes..." + +msgctxt "#30165" +msgid "Flagged..." +msgstr "Favoritos..." + +msgctxt "#30166" +msgid "Forgotten..." +msgstr "Olvidados..." + +msgctxt "#30167" +msgid "Newest..." +msgstr "Añadido recientemente..." + +msgctxt "#30168" +msgid "Modify the data, cancel to exit" +msgstr "Modifique los datos, para salir pulse cancelar" + +msgctxt "#30169" +msgid "Choose a default server" +msgstr "Elija el servidor predeterminador" + +msgctxt "#30170" +msgid "Enter the Server name" +msgstr "Inserte el nombre del servidor" + +msgctxt "#30171" +msgid "Enter the url of the server" +msgstr "Inserte la url del servidor" + +msgctxt "#30173" +msgid "Do you want to use an api-key?" +msgstr "¿Quiere usar una clave api?" + +msgctxt "#30174" +msgid "Enter the Api key" +msgstr "Inserte la clave Api" + +msgctxt "#30175" +msgid "Enter the username" +msgstr "Inserte el nombre de usuario" + +msgctxt "#30177" +msgid "The server needs a password?" +msgstr "¿Necesita contraseña el servidor?" + +msgctxt "#30178" +msgid "Enter the password" +msgstr "Inserte la contraseña" + +msgctxt "#30179" +msgid "Choose a server to remove" +msgstr "Elija un servidor a eliminar" + +msgctxt "#30180" +msgid "Modify a server" +msgstr "Modificar un servidor" + +msgctxt "#30181" +msgid "Server name" +msgstr "Nombre del servidor" + +msgctxt "#30182" +msgid "Server url" +msgstr "Url del servidor" + +msgctxt "#30183" +msgid "Username" +msgstr "Nombre de usuario" + +msgctxt "#30184" +msgid "Enable password" +msgstr "Habilitar contraseña" + +msgctxt "#30185" +msgid "Password" +msgstr "Contraseña" + +msgctxt "#30186" +msgid "Use api key" +msgstr "Usar clave api" + +msgctxt "#30187" +msgid "Api key" +msgstr "Clave api" + +msgctxt "#30188" +msgid "Are you sure?" +msgstr "Está seguro?" + +msgctxt "#30189" +msgid "Ampache plugin" +msgstr "Plugin Ampache" + +msgctxt "#30190" +msgid "Recently Played Artists..." +msgstr "Artistas reproducidos recientemente..." + +msgctxt "#30191" +msgid "Recently Played Albums..." +msgstr "Álbumes reproducidos recientemente..." + +msgctxt "#30192" +msgid "Recently Played Songs..." +msgstr "Canciones reproducidas recientemente..." + +msgctxt "#30193" +msgid "Recently Played..." +msgstr "Reproducido recientemente..." + +msgctxt "#30194" +msgid "Next items..." +msgstr "Siguientes elementos..." + +msgctxt "#30195" +msgid "Disk" +msgstr "Disco" + +msgctxt "#30197" +msgid "Information" +msgstr "Información" + +msgctxt "#30198" +msgid "Error" +msgstr "Error" + +msgctxt "#30202" +msgid "Connection Error" +msgstr "Error de conexión" + +msgctxt "#30203" +msgid "Connection OK" +msgstr "Conexión correcta" + +msgctxt "#30204" +msgid "Permission error. If you are using Nextcloud don't check api_key box" +msgstr "Error de permiso. Si se está usando Nextcloud, no utilizar clave api" + +msgctxt "#30220" +msgid "Video" +msgstr "Vídeo" + +msgctxt "#30221" +msgid "Videos" +msgstr "Videos" + +# code +msgctxt "#30222" +msgid "Search Videos..." +msgstr "Buscar vídeos..." + +msgctxt "#30225" +msgid "Podcast" +msgstr "Podcast" + +msgctxt "#30226" +msgid "Podcasts" +msgstr "Podcasts" + +# code +msgctxt "#30227" +msgid "Search Podcasts..." +msgstr "Busca podcasts..." diff --git a/plugin.audio.ampache/resources/language/resource.language.it_it/strings.po b/plugin.audio.ampache/resources/language/resource.language.it_it/strings.po new file mode 100644 index 0000000000..ada0013128 --- /dev/null +++ b/plugin.audio.ampache/resources/language/resource.language.it_it/strings.po @@ -0,0 +1,506 @@ +# Kodi Media Center language file +# Addon Name: Ampache plugin +# Addon id: plugin.audio.ampache +# Addon Provider: lusum +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2022-02-07 19:47+0100\n" +"Last-Translator: \n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/" +"language/en/)\n" +"Language: it_IT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0\n" + +msgctxt "#30001" +msgid "General" +msgstr "Generale" + +msgctxt "#30002" +msgid "Servers" +msgstr "Server" + +msgctxt "#30010" +msgid "Random Items" +msgstr "Numero di elementi casuali" + +msgctxt "#30011" +msgid "Disable Ssl certs" +msgstr "Disabilita i certificati Ssl" + +# empty string with id 32003 +msgctxt "#30012" +msgid "Api Version" +msgstr "Versione dell'Api" + +msgctxt "#30013" +msgid "Old search gui (web controller friendly)" +msgstr "Interfaccia di ricerca per il controller web" + +msgctxt "#30014" +msgid "Enable images on long lists" +msgstr "Abilita le immagini sulle liste lunghe" + +msgctxt "#30015" +msgid "Auto fullscreen" +msgstr "Fullscreen auto" + +msgctxt "#30020" +msgid "Add Server" +msgstr "Aggiungi Server" + +msgctxt "#30021" +msgid "Remove Server" +msgstr "Rimuovi Server" + +msgctxt "#30022" +msgid "Modify Server" +msgstr "Modifica Server" + +msgctxt "#30023" +msgid "Switch Server" +msgstr "Cambia Server" + +# code +msgctxt "#30101" +msgid "Search..." +msgstr "Ricerca..." + +msgctxt "#30102" +msgid "Quick access..." +msgstr "Accesso Veloce..." + +msgctxt "#30103" +msgid "Explore..." +msgstr "Esplora..." + +msgctxt "#30104" +msgid "Library..." +msgstr "Libreria..." + +msgctxt "#30105" +msgid "Settings" +msgstr "Impostazioni" + +msgctxt "#30106" +msgid "Artist" +msgstr "Artista" + +msgctxt "#30107" +msgid "Album" +msgstr "" + +msgctxt "#30108" +msgid "Song" +msgstr "Canzone" + +msgctxt "#30109" +msgid "Playlist" +msgstr "" + +msgctxt "#30110" +msgid "All" +msgstr "Tutto" + +msgctxt "#30111" +msgid "Tag" +msgstr "Genere" + +msgctxt "#30112" +msgid "Artist tag" +msgstr "Genere artista" + +msgctxt "#30113" +msgid "Album tag" +msgstr "Genere album" + +msgctxt "#30114" +msgid "Song tag" +msgstr "Genere canzone" + +msgctxt "#30115" +msgid "Artists" +msgstr "Artisti" + +msgctxt "#30116" +msgid "Albums" +msgstr "Album" + +msgctxt "#30117" +msgid "Songs" +msgstr "Canzoni" + +msgctxt "#30118" +msgid "Playlists" +msgstr "" + +msgctxt "#30119" +msgid "Tags" +msgstr "Generi" + +# code +msgctxt "#30120" +msgid "Search Artists..." +msgstr "Ricerca artisti..." + +# code +msgctxt "#30121" +msgid "Search Albums..." +msgstr "Ricerca album..." + +# code +msgctxt "#30122" +msgid "Search Songs..." +msgstr "Ricerca canzoni..." + +# code +msgctxt "#30123" +msgid "Search Playlists..." +msgstr "Ricerca playlist..." + +# code +msgctxt "#30124" +msgid "Search All..." +msgstr "Ricerca tutto..." + +# code +msgctxt "#30125" +msgid "Search Tags..." +msgstr "Ricerca tag..." + +msgctxt "#30126" +msgid "Recently Added Artists..." +msgstr "Artisti aggiunti recentemente..." + +msgctxt "#30127" +msgid "Recently Added Albums..." +msgstr "Album aggiunti recentemente..." + +msgctxt "#30128" +msgid "Recently Added Songs..." +msgstr "Canzoni aggiunte recentemente..." + +msgctxt "#30129" +msgid "Recently Added Playlists..." +msgstr "Playlist aggiunte recentemente..." + +msgctxt "#30130" +msgid "Last Update" +msgstr "Ultimo aggiornamento" + +msgctxt "#30131" +msgid "1 Week" +msgstr "1settimana" + +msgctxt "#30132" +msgid "1 Month" +msgstr "1 mese" + +msgctxt "#30133" +msgid "3 Months" +msgstr "3 mesi" + +msgctxt "#30134" +msgid "Random Artists..." +msgstr "Artisti casuali..." + +msgctxt "#30135" +msgid "Random Albums..." +msgstr "Album casuali..." + +msgctxt "#30136" +msgid "Random Songs..." +msgstr "Canzoni casuali..." + +msgctxt "#30137" +msgid "Random Playlists..." +msgstr "Playlist casuali..." + +msgctxt "#30138" +msgid "Show artist from this song" +msgstr "Mostra l'artista della canzone" + +msgctxt "#30139" +msgid "Show album from this song" +msgstr "Mostra l'album della canzone" + +msgctxt "#30140" +msgid "Search all songs with this title" +msgstr "Cerca tutte le canzoni con questo titolo" + +msgctxt "#30141" +msgid "Show all albums from artist" +msgstr "Mostra tutti gli album di questo artista" + +msgctxt "#30142" +msgid "Artist tags..." +msgstr "Generi Artisti..." + +msgctxt "#30143" +msgid "Album tags..." +msgstr "Generi Album..." + +msgctxt "#30144" +msgid "Song tags..." +msgstr "Generi Canzoni..." + +msgctxt "#30145" +msgid "Recently Added..." +msgstr "Aggiunti recentemente..." + +msgctxt "#30146" +msgid "Random..." +msgstr "Casuali..." + +msgctxt "#30147" +msgid "Server playlist..." +msgstr "Playlist casuale del server..." + +msgctxt "#30148" +msgid "Highest Rated..." +msgstr "Punteggio più alto..." + +msgctxt "#30149" +msgid "Highest Rated Artists..." +msgstr "Artisti col punteggio più alto..." + +msgctxt "#30150" +msgid "Highest Rated Albums..." +msgstr "Album col punteggio più alto..." + +msgctxt "#30151" +msgid "Highest Rated Songs..." +msgstr "Canzoni col punteggio più alto..." + +msgctxt "#30152" +msgid "Frequent Artists..." +msgstr "Artisti frequenti..." + +msgctxt "#30153" +msgid "Frequent Albums..." +msgstr "Album frequenti..." + +msgctxt "#30154" +msgid "Frequent Songs..." +msgstr "Canzoni frequenti..." + +msgctxt "#30155" +msgid "Flagged Artists..." +msgstr "Artisti contrassegnati..." + +msgctxt "#30156" +msgid "Flagged Albums..." +msgstr "Album contrassegnati..." + +msgctxt "#30157" +msgid "Flagged Songs..." +msgstr "Brani contrassegnati..." + +msgctxt "#30158" +msgid "Forgotten Artists..." +msgstr "Artisti Dimenticati..." + +msgctxt "#30159" +msgid "Forgotten Albums..." +msgstr "Album Dimenticati..." + +msgctxt "#30160" +msgid "Forgotten Songs..." +msgstr "Canzoni Dimenticate..." + +msgctxt "#30161" +msgid "Newest Artists..." +msgstr "Nuovi artisti..." + +msgctxt "#30162" +msgid "Newest Albums..." +msgstr "Nuovi album..." + +msgctxt "#30163" +msgid "Newest Songs..." +msgstr "Nuove canzoni..." + +msgctxt "#30164" +msgid "Frequent..." +msgstr "Frequenti..." + +msgctxt "#30165" +msgid "Flagged..." +msgstr "Contrassegnati..." + +msgctxt "#30166" +msgid "Forgotten..." +msgstr "Dimenticati..." + +msgctxt "#30167" +msgid "Newest..." +msgstr "Nuovi.." + +msgctxt "#30168" +msgid "Modify the data, cancel to exit" +msgstr "Modifica i dati, premi il tasto annulla per uscire" + +msgctxt "#30169" +msgid "Choose a default server" +msgstr "Scegli il server attivo" + +msgctxt "#30170" +msgid "Enter the Server name" +msgstr "Inserisci il nome del server" + +msgctxt "#30171" +msgid "Enter the url of the server" +msgstr "Inserisci l'url del server" + +msgctxt "#30173" +msgid "Do you want to use an api-key?" +msgstr "Vuoi utilizzare una chiave api?" + +msgctxt "#30174" +msgid "Enter the Api key" +msgstr "Inserisci la chiave api" + +msgctxt "#30175" +msgid "Enter the username" +msgstr "Inserisci il nome utente" + +msgctxt "#30177" +msgid "The server needs a password?" +msgstr "Il server necessita di una password?" + +msgctxt "#30178" +msgid "Enter the password" +msgstr "Inserisci la password" + +msgctxt "#30179" +msgid "Choose a server to remove" +msgstr "Scegli un server da rimuovere" + +msgctxt "#30180" +msgid "Modify a server" +msgstr "Modifica un server" + +msgctxt "#30181" +msgid "Server name" +msgstr "Nome server" + +msgctxt "#30182" +msgid "Server url" +msgstr "Url del server" + +msgctxt "#30183" +msgid "Username" +msgstr "Nome utente" + +msgctxt "#30184" +msgid "Enable password" +msgstr "Abilita la password" + +msgctxt "#30185" +msgid "Password" +msgstr "" + +msgctxt "#30186" +msgid "Use api key" +msgstr "Usa la chiave api" + +msgctxt "#30187" +msgid "Api key" +msgstr "Chiave api" + +msgctxt "#30188" +msgid "Are you sure?" +msgstr "Sei sicuro?" + +msgctxt "#30189" +msgid "Ampache plugin" +msgstr "Plugin Ampache" + +msgctxt "#30190" +msgid "Recently Played Artists..." +msgstr "Artisti ascoltati recentemente..." + +msgctxt "#30191" +msgid "Recently Played Albums..." +msgstr "Album ascoltati recentemente..." + +msgctxt "#30192" +msgid "Recently Played Songs..." +msgstr "Canzoni ascoltate recentemente..." + +msgctxt "#30193" +msgid "Recently Played..." +msgstr "Ascoltati recentemente..." + +msgctxt "#30194" +msgid "Next items..." +msgstr "Prossimi elementi..." + +msgctxt "#30195" +msgid "Disk" +msgstr "Disco" + +msgctxt "#30197" +msgid "Information" +msgstr "Informazione" + +msgctxt "#30198" +msgid "Error" +msgstr "Errore" + +msgctxt "#30202" +msgid "Connection Error" +msgstr "Errore di connessione" + +msgctxt "#30203" +msgid "Connection OK" +msgstr "Connessione stabilita" + +msgctxt "#30204" +msgid "Permission error. If you are using Nextcloud don't check api_key box" +msgstr "Permessi errati. Se state usando Nextcloud non utilizzare l'api_key" + +msgctxt "#30220" +msgid "Video" +msgstr "" + +msgctxt "#30221" +msgid "Videos" +msgstr "Video" + +# code +msgctxt "#30222" +msgid "Search Videos..." +msgstr "Ricerca video..." + +msgctxt "#30225" +msgid "Podcast" +msgstr "" + +msgctxt "#30226" +msgid "Podcasts" +msgstr "" + +# code +msgctxt "#30227" +msgid "Search Podcasts..." +msgstr "Ricerca podcast..." + +msgctxt "#30228" +msgid "Live stream" +msgstr "Stazione Radio" + +msgctxt "#30229" +msgid "Live streams" +msgstr "Stazioni Radio" + +# code +msgctxt "#30230" +msgid "Search Live Streams..." +msgstr "Ricerca stazioni radio..." diff --git a/plugin.audio.ampache/resources/lib/__init__.py b/plugin.audio.ampache/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.ampache/resources/lib/ampache_connect.py b/plugin.audio.ampache/resources/lib/ampache_connect.py new file mode 100644 index 0000000000..493847bfc2 --- /dev/null +++ b/plugin.audio.ampache/resources/lib/ampache_connect.py @@ -0,0 +1,259 @@ +from future import standard_library +from future.utils import PY2 +standard_library.install_aliases() +from builtins import str +from builtins import object +import hashlib +import ssl +import socket +import time +import urllib.request, urllib.parse, urllib.error +import xbmc, xbmcaddon, xbmcgui +import sys +import xml.etree.ElementTree as ET + +#main plugin library +from resources.lib import json_storage +from resources.lib import utils as ut +from resources.lib.art_clean import clean_settings + +class AmpacheConnect(object): + + class ConnectionError(Exception): + pass + + def __init__(self): + self._ampache = xbmcaddon.Addon("plugin.audio.ampache") + jsStorServer = json_storage.JsonStorage("servers.json") + serverStorage = jsStorServer.getData() + self._connectionData = serverStorage["servers"][serverStorage["current_server"]] + #self._connectionData = None + self.filter=None + self.add=None + self.limit=None + self.offset=None + self.type=None + self.exact=None + self.mode=None + self.id=None + self.rating=None + #force the latest version on the server + self.version="600001" + + def getBaseUrl(self): + return '/server/xml.server.php' + + def fillConnectionSettings(self,tree,nTime): + clean_settings() + token = tree.findtext('auth') + version = tree.findtext('api') + if not version: + #old api + version = tree.findtext('version') + #setSettings only string or unicode + self._ampache.setSetting("api-version",version) + self._ampache.setSetting("artists", tree.findtext("artists")) + self._ampache.setSetting("albums", tree.findtext("albums")) + self._ampache.setSetting("songs", tree.findtext("songs")) + apiVersion = int(version) + if apiVersion < 500001: + self._ampache.setSetting("playlists", tree.findtext("playlists")) + else: + self._ampache.setSetting("playlists", tree.findtext("playlists_searches")) + self._ampache.setSetting("videos", tree.findtext("videos") ) + self._ampache.setSetting("podcasts", tree.findtext("podcasts") ) + self._ampache.setSetting("live_streams", tree.findtext("live_streams") ) + self._ampache.setSetting("session_expire", tree.findtext("session_expire")) + self._ampache.setSetting("add", tree.findtext("add")) + self._ampache.setSetting("token", token) + #not 24000 seconds ( 6 hours ) , but 2400 ( 40 minutes ) expiration time + self._ampache.setSetting("token-exp", str(nTime+2400)) + + def getCodeMessError(self,tree): + errormess = None + errornode = tree.find("error") + if errornode is not None: + #ampache api 4 and below + try: + errormess = tree.findtext('error') + return errormess + except: + #do nothing + pass + #ampache api 5 and above + try: + errormess = errornode.findtext("errorMessage") + return errormess + except: + #do nothing + pass + + return errormess + + def getHashedPassword(self,timeStamp): + enablePass = self._connectionData["enable_password"] + if enablePass: + sdf = self._connectionData["password"] + else: + sdf = "" + hasher = hashlib.new('sha256') + sdf = sdf.encode() + hasher.update(sdf) + myKey = hasher.hexdigest() + hasher = hashlib.new('sha256') + timeK = timeStamp + myKey + timeK = timeK.encode() + hasher.update(timeK) + passwordHash = hasher.hexdigest() + return passwordHash + + def get_user_pwd_login_url(self,nTime): + myTimeStamp = str(nTime) + myPassphrase = self.getHashedPassword(myTimeStamp) + myURL = self._connectionData["url"] + self.getBaseUrl() + '?action=handshake&auth=' + myURL += myPassphrase + "×tamp=" + myTimeStamp + myURL += '&version=' + self.version + '&user=' + self._connectionData["username"] + return myURL + + def get_auth_key_login_url(self): + myURL = self._connectionData["url"] + self.getBaseUrl() + '?action=handshake&auth=' + myURL += self._connectionData["api_key"] + myURL += '&version=' + self.version + return myURL + + def handle_request(self,url): + xbmc.log("AmpachePlugin::handle_request: url " + url, xbmc.LOGDEBUG) + ssl_certs_str = self._ampache.getSetting("disable_ssl_certs") + try: + req = urllib.request.Request(url) + if ut.strBool_to_bool(ssl_certs_str): + if PY2: + response = urllib.request.urlopen(req, timeout=400) + else: + gcontext = ssl.create_default_context() + gcontext.check_hostname = False + gcontext.verify_mode = ssl.CERT_NONE + response = urllib.request.urlopen(req, context=gcontext, timeout=400) + xbmc.log("AmpachePlugin::handle_request: disable ssl certificates",xbmc.LOGDEBUG) + else: + if PY2: + gcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + response = urllib.request.urlopen(req, context=gcontext, timeout=400) + else: + response = urllib.request.urlopen(req, timeout=400) + xbmc.log("AmpachePlugin::handle_request: ssl certificates",xbmc.LOGDEBUG) + except urllib.error.HTTPError as e: + xbmc.log("AmpachePlugin::handle_request: HTTPError " +\ + repr(e),xbmc.LOGDEBUG) + raise self.ConnectionError + except urllib.error.URLError as e: + xbmc.log("AmpachePlugin::handle_request: URLError " +\ + repr(e),xbmc.LOGDEBUG) + raise self.ConnectionError + except Exception as e: + xbmc.log("AmpachePlugin::handle_request: Generic Error " +\ + repr(e),xbmc.LOGDEBUG) + raise self.ConnectionError + headers = response.headers + contents = response.read() + response.close() + return headers,contents + + def AMPACHECONNECT(self,showok=False): + socket.setdefaulttimeout(3600) + nTime = int(time.time()) + use_api_key = self._connectionData["use_api_key"] + if ut.strBool_to_bool(use_api_key): + xbmc.log("AmpachePlugin::AMPACHECONNECT api_key",xbmc.LOGDEBUG) + myURL = self.get_auth_key_login_url() + else: + xbmc.log("AmpachePlugin::AMPACHECONNECT login password",xbmc.LOGDEBUG) + myURL = self.get_user_pwd_login_url(nTime) + try: + headers,contents = self.handle_request(myURL) + except self.ConnectionError: + xbmc.log("AmpachePlugin::AMPACHECONNECT ConnectionError",xbmc.LOGDEBUG) + #connection error + xbmcgui.Dialog().notification(ut.tString(30198),ut.tString(30202)) + raise self.ConnectionError + except Exception as e: + xbmc.log("AmpachePlugin::AMPACHECONNECT: Generic Error " +\ + repr(e),xbmc.LOGDEBUG) + try: + xbmc.log("AmpachePlugin::AMPACHECONNECT: contents " +\ + contents.decode(),xbmc.LOGDEBUG) + except Exception as e: + xbmc.log("AmpachePlugin::AMPACHECONNECT: unable to print contents " + \ + repr(e) , xbmc.LOGDEBUG) + tree=ET.XML(contents) + errormess = self.getCodeMessError(tree) + if errormess: + #connection error + xbmcgui.Dialog().notification(ut.tString(30198),ut.tString(30202)) + raise self.ConnectionError + xbmc.log("AmpachePlugin::AMPACHECONNECT ConnectionOk",xbmc.LOGDEBUG) + if showok: + #use it only if notification of connection is necessary, like + #switch server, display connection ok and the name of the + #current server + amp_notif = ut.tString(30203) + "\n" + ut.tString(30181) +\ + " : " + self._connectionData["name"] + #connection ok + xbmcgui.Dialog().notification(ut.tString(30197),amp_notif) + self.fillConnectionSettings(tree,nTime) + return + + #handle request to the xml api that return binary files + def ampache_binary_request(self,action): + thisURL = self.build_ampache_url(action) + try: + headers,contents = self.handle_request(thisURL) + except self.ConnectionError: + raise self.ConnectionError + return headers,contents + + #handle request to the xml api that return xml content + def ampache_http_request(self,action): + thisURL = self.build_ampache_url(action) + try: + headers,contents = self.handle_request(thisURL) + except self.ConnectionError: + raise self.ConnectionError + if PY2: + contents = contents.replace("\0", "") + try: + xbmc.log("AmpachePlugin::ampache_http_request: contents " + \ + contents.decode(),xbmc.LOGDEBUG) + except Exception as e: + xbmc.log("AmpachePlugin::ampache_http_request: unable print contents " + \ + repr(e) , xbmc.LOGDEBUG) + tree=ET.XML(contents) + errormess = self.getCodeMessError(tree) + if errormess: + raise self.ConnectionError + return tree + + def build_ampache_url(self,action): + token = self._ampache.getSetting("token") + thisURL = self._connectionData["url"] + self.getBaseUrl() + '?action=' + action + thisURL += '&auth=' + token + if self.limit: + thisURL += '&limit=' +str(self.limit) + if self.offset: + thisURL += '&offset=' +str(self.offset) + if self.filter: + thisURL += '&filter=' +urllib.parse.quote_plus(str(self.filter)) + if self.add: + thisURL += '&add=' + self.add + if self.type: + thisURL += '&type=' + self.type + if self.mode: + thisURL += '&mode=' + self.mode + if self.exact: + thisURL += '&exact=' + self.exact + if self.id: + thisURL += '&id=' + self.id + if self.rating: + thisURL += '&rating=' + self.rating + return thisURL + diff --git a/plugin.audio.ampache/resources/lib/ampache_monitor.py b/plugin.audio.ampache/resources/lib/ampache_monitor.py new file mode 100644 index 0000000000..20cf71e787 --- /dev/null +++ b/plugin.audio.ampache/resources/lib/ampache_monitor.py @@ -0,0 +1,50 @@ +import xbmc +import xbmcaddon + +#service class +ampache = xbmcaddon.Addon("plugin.audio.ampache") + +from resources.lib.utils import get_objectId_from_fileURL + +class AmpacheMonitor( xbmc.Monitor ): + + onPlay = False + + def __init__(self): + xbmc.log( 'AmpacheMonitor::ServiceMonitor called', xbmc.LOGDEBUG) + + # start mainloop + def run(self): + while not self.abortRequested(): + if self.waitForAbort(1): + # Abort was requested while waiting. We should exit + break + + def close(self): + pass + + def onNotification(self, sender, method, data): + #i don't know why i have called monitor.onNotification, but now it + #seems useless + #xbmc.Monitor.onNotification(self, sender, method, data) + xbmc.log('AmpacheMonitor:Notification %s from %s, params: %s' % (method, sender, str(data))) + + #a little hack to avoid calling rate every time a song start + if method == 'Player.OnStop': + self.onPlay = False + if method == 'Player.OnPlay': + self.onPlay = True + #called on infoChanged ( rating ) + if method == 'Info.OnChanged' and self.onPlay: + #call setRating + if xbmc.Player().isPlaying(): + try: + file_url = xbmc.Player().getPlayingFile() + #it is not our file + if not (get_objectId_from_fileURL( file_url )): + return + except: + xbmc.log("AmpacheMonitor::no playing file " , xbmc.LOGDEBUG) + return + xbmc.executebuiltin('RunPlugin(plugin://plugin.audio.ampache/?mode=205)') + diff --git a/plugin.audio.ampache/resources/lib/ampache_plugin.py b/plugin.audio.ampache/resources/lib/ampache_plugin.py new file mode 100644 index 0000000000..5f71d53609 --- /dev/null +++ b/plugin.audio.ampache/resources/lib/ampache_plugin.py @@ -0,0 +1,1125 @@ +from future import standard_library +from future.utils import PY2 +standard_library.install_aliases() +from builtins import str +from builtins import range +import xbmc,xbmcaddon,xbmcplugin,xbmcgui +import urllib.request,urllib.parse,urllib.error +import sys, os +import math,random +import xml.etree.ElementTree as ET +import threading + +#main plugin +from resources.lib import ampache_connect +from resources.lib import servers_manager +from resources.lib import gui +from resources.lib import utils as ut +from resources.lib import art + +# Shared resources + +#addon name : plugin.audio.ampache +#do not use xbmcaddon.Addon() to avoid crashes when kore app is used ( it is +#possible to start a song without initialising the plugin +ampache = xbmcaddon.Addon("plugin.audio.ampache") + +def searchGui(): + dialog = xbmcgui.Dialog() + ret = dialog.contextmenu([ut.tString(30106),ut.tString(30107),ut.tString(30108),\ + ut.tString(30109),ut.tString(30110),ut.tString(30220),ut.tString(30225),ut.tString(30228),ut.tString(30111)]) + endDir = False + if ret == 0: + endDir = do_search("artists") + elif ret == 1: + endDir = do_search("albums") + elif ret == 2: + endDir = do_search("songs") + elif ret == 3: + endDir = do_search("playlists") + elif ret == 4: + endDir = do_search("songs","search_songs") + elif ret == 5: + endDir = do_search("videos") + elif ret == 6: + endDir = do_search("podcasts") + elif ret == 7: + endDir = do_search("songs","live_streams") + elif ret == 8: + ret2 = dialog.contextmenu([ut.tString(30112),ut.tString(30113),ut.tString(30114)]) + if(int(ampache.getSetting("api-version"))) < 500000: + if ret2 == 0: + endDir = do_search("tags","tag_artists") + elif ret2 == 1: + endDir = do_search("tags","tag_albums") + elif ret2 == 2: + endDir = do_search("tags","tag_songs") + else: + if ret2 == 0: + endDir = do_search("genres","genre_artists") + elif ret2 == 1: + endDir = do_search("genres","genre_albums") + elif ret2 == 2: + endDir = do_search("genres","genre_songs") + return endDir + +#necessary due the api changes in 6.0 +def get_name(node,amType): + if(int(ampache.getSetting("api-version"))) < 600000: + artist_name = str(node.findtext(amType)) + else: + artist_name = str(getNestedTypeText(node, "name" ,amType)) + return artist_name + +#return album and artist name, only album could be confusing +def get_album_artist_name(node): + + disknumber = str(node.findtext("disk")) + album_name = str(node.findtext("name")) + artist_name = get_name(node,"artist") + fullname = album_name + + if PY2: + fullname += u" - " + else: + #no encode utf-8 in python3, not necessary + fullname += " - " + fullname += artist_name + #disknumber = "None" when disk number is not sent + if disknumber!="None" and disknumber != "1" and disknumber !="0": + if PY2: + fullname = fullname + u" - [ " + ut.tString(30195) + u" " +\ + disknumber + u" ]" + else: + fullname = fullname + " - [ " + ut.tString(30195) + " " + disknumber + " ]" + return fullname + +def get_infolabels(elem_type , node): + infoLabels = None + rating = ut.getRating(node.findtext("rating")) + if elem_type == 'album': + infoLabels = { + #'Title' : str(node.findtext("name")) , + 'Album' : str(node.findtext("name")) , + 'Artist' : get_name(node,"artist"), + 'DiscNumber' : str(node.findtext("disk")), + 'Year' : node.findtext("year") , + 'UserRating' : rating, + 'Mediatype' : 'album' + } + + elif elem_type == 'artist': + infoLabels = { + #'Title' : str(node.findtext("name")) , + 'Artist' : str(node.findtext("name")), + 'Mediatype' : 'artist' + } + + elif elem_type == 'song': + infoLabels = { + 'Title' : str(node.findtext("title")) , + 'Artist' : get_name(node,"artist"), + 'Album' : get_name(node,"album"), + 'Size' : node.findtext("size") , + 'Duration' : node.findtext("time"), + 'Year' : node.findtext("year") , + 'TrackNumber' : node.findtext("track"), + 'UserRating' : rating, + 'Mediatype' : 'song' + } + + elif elem_type == 'podcast_episode': + infoLabels = { + 'Title' : str(node.findtext("title")) , + 'UserRating' : rating, + 'Mediatype' : 'song' + } + + elif elem_type == 'video': + infoLabels = { + 'Title' : str(node.findtext("name")) , + 'Size' : node.findtext("size") , + 'Mediatype' : 'video' + } + + return infoLabels + +def getNestedTypeText(node, elem_tag ,elem_type): + try: + obj_elem = node.find(elem_type) + if obj_elem is not None or obj_elem != '': + obj_tag = obj_elem.findtext(elem_tag) + return obj_tag + except: + return None + return None + +def getNestedTypeId(node,elem_type): + try: + obj_elem = node.find(elem_type) + if obj_elem is not None or obj_elem != '': + obj_id = obj_elem.attrib["id"] + return obj_id + except: + return None + return None + +#this function is used to speed up the loading of the images using differents +#theads, one for request +def precacheArt(elem,elem_type): + + allid=set() + if elem_type != "album" and elem_type != "song" and\ + elem_type != "artist" and elem_type != "podcast" and elem_type!= "playlist": + return + + threadList = [] + for node in elem.iter(elem_type): + if elem_type == "song": + art_type = "album" + object_id = getNestedTypeId(node, "album") + else: + art_type = elem_type + object_id = node.attrib["id"] + #avoid to have duplicate threads with the same object_id + if object_id not in allid: + allid.add(object_id) + else: + continue + image_url = node.findtext("art") + if not object_id or not image_url: + continue + x = threading.Thread(target=art.get_art,args=(object_id,art_type,image_url,)) + threadList.append(x) + #start threads + for x in threadList: + x.start() + #join threads + for x in threadList: + x.join() + +def addLinks(elem,elem_type,useCacheArt,mode): + + image = "DefaultFolder.png" + it=[] + allid = set() + + for node in elem.iter(elem_type): + cm = [] + object_id = node.attrib["id"] + if not object_id: + continue + + name = str(node.findtext("name")) + + if elem_type == "album": + #remove duplicates in album names ( workaround for a problem in server comunication ) + if object_id not in allid: + allid.add(object_id) + else: + continue + artist_id = getNestedTypeId(node, "artist") + if artist_id: + cm.append( ( ut.tString(30141),"Container.Update(%s?object_id=%s&mode=1&submode=6)" % + ( sys.argv[0],artist_id ) ) ) + + name = get_album_artist_name(node) + if useCacheArt: + image_url = node.findtext("art") + image = art.get_art(object_id,elem_type,image_url) + elif elem_type == "artist": + if useCacheArt: + image_url = node.findtext("art") + image = art.get_art(object_id,elem_type,image_url) + elif elem_type == "podcast": + if useCacheArt: + image = art.get_art(object_id,"podcast") + elif elem_type == "playlist": + if useCacheArt: + image = art.get_art(object_id,"playlist") + try: + numItems = str(node.findtext("items")) + name = name + " (" + numItems + ")" + except: + pass + else: + useCacheArt = False + + infoLabels=get_infolabels(elem_type,node) + + if infoLabels == None: + infoLabels={ "Title": name } + + liz=xbmcgui.ListItem(name) + liz.setInfo( type="Music", infoLabels=infoLabels ) + + if useCacheArt: + #faster loading for libraries + liz.setArt( art.get_artLabels(image) ) + liz.setProperty('IsPlayable', 'false') + + if cm: + liz.addContextMenuItems(cm) + + u=sys.argv[0]+"?object_id="+object_id+"&mode="+str(mode)+"&submode=71" + #xbmc.log("AmpachePlugin::addLinks: u - " + u, xbmc.LOGDEBUG ) + isFolder=True + tu= (u,liz,isFolder) + it.append(tu) + + xbmcplugin.addDirectoryItems(handle=int(sys.argv[1]),items=it,totalItems=len(elem)) + +# Used to populate items for songs on XBMC. Calls plugin script with mode == +# 45 and play_url == (ampache item url) +def addPlayLinks(elem, elem_type): + + it=[] + + #we don't use sort method for track cause songs are already sorted + #by the server and it make a mess in random playlists + if elem_type == "video": + xbmcplugin.addSortMethod(int(sys.argv[1]),xbmcplugin.SORT_METHOD_TITLE) + elif elem_type == "podcast_episode": + xbmcplugin.addSortMethod(int(sys.argv[1]),xbmcplugin.SORT_METHOD_DATE) + + allid=set() + albumTrack={} + + for node in elem.iter(elem_type): + object_id = node.attrib["id"] + if not object_id: + continue + + play_url = str(node.findtext("url")) + object_title = str(node.findtext("title")) + if elem_type == "live_stream": + object_title = str(node.findtext("name")) + + liz=xbmcgui.ListItem(object_title) + liz.setProperty("IsPlayable", "true") + liz.setPath(play_url) + + if elem_type == "song": + image_url = node.findtext("art") + #speed up art management for album songs, avoid duplicate + #calls + album_id = getNestedTypeId(node,"album") + if album_id: + if album_id not in allid: + allid.add(album_id) + albumArt = art.get_art(album_id,"album",image_url) + albumTrack[album_id]=albumArt + else: + albumArt=albumTrack[album_id] + else: + albumArt = art.get_art(None,"album",image_url) + + liz.setArt( art.get_artLabels(albumArt) ) + liz.setInfo( type="music", infoLabels=get_infolabels("song", node) ) + liz.setMimeType(node.findtext("mime")) + + cm = [] + + artist_id = getNestedTypeId(node, "artist") + if artist_id: + cm.append( ( ut.tString(30138), + "Container.Update(%s?object_id=%s&mode=1&submode=6)" % ( + sys.argv[0],artist_id ) ) ) + + if album_id: + cm.append( ( ut.tString(30139), + "Container.Update(%s?object_id=%s&mode=2&submode=6)" % ( + sys.argv[0],album_id ) ) ) + + cm.append( ( ut.tString(30140), + "Container.Update(%s?title=%s&mode=3&submode=12)" % ( + sys.argv[0],urllib.parse.quote_plus(object_title) ) ) ) + + if cm != []: + liz.addContextMenuItems(cm) + elif elem_type == "podcast_episode": + liz.setInfo( type="music", infoLabels=get_infolabels(elem_type, node) ) + elif elem_type == "video": + liz.setInfo( type="video", infoLabels=get_infolabels("video", node) ) + liz.setMimeType(node.findtext("mime")) + + track_parameters = { "mode": 200, "play_url" : play_url} + url = sys.argv[0] + '?' + urllib.parse.urlencode(track_parameters) + tu= (url,liz) + it.append(tu) + + xbmcplugin.addDirectoryItems(handle=int(sys.argv[1]),items=it,totalItems=len(elem)) + +#The function that actually plays an Ampache URL by using setResolvedUrl +def play_track(url): + if url == None: + xbmc.log("AmpachePlugin::play_track url null", xbmc.LOGINFO ) + return + + #read here the setting, cause delay problems + autofull = ut.strBool_to_bool(ampache.getSetting("auto-fullscreen")) + + liz = xbmcgui.ListItem() + liz.setPath(url) + + xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True,listitem=liz) + + #enable auto fullscreen playing the track ( closes #17 ) + if autofull is True: + xbmc.executebuiltin("ActivateWindow(visualisation)") + +#Main function to add xbmc plugin elements +def addDir(name,mode,submode,offset=None,object_id=None): + infoLabels={ "Title": name } + + liz=xbmcgui.ListItem(name) + liz.setInfo( type="Music", infoLabels=infoLabels ) + liz.setProperty('IsPlayable', 'false') + + handle=int(sys.argv[1]) + + u=sys.argv[0]+"?mode="+str(mode)+"&submode="+str(submode) + #offset, in case of very long lists + if offset: + u = u + "&offset="+str(offset) + if object_id: + u = u + "&object_id="+object_id + xbmc.log("AmpachePlugin::addDir url " + u, xbmc.LOGDEBUG) + xbmcplugin.addDirectoryItem(handle=handle,url=u,listitem=liz,isFolder=True) + +#this function add items to the directory using the low level addLinks of ddSongLinks functions +def addItems( object_type, elem, object_subtype=None,precache=True): + + ut.setContent(int(sys.argv[1]), object_type) + + xbmc.log("AmpachePlugin::addItems: object_type - " + str(object_type) , xbmc.LOGDEBUG ) + if object_subtype: + xbmc.log("AmpachePlugin::addItems: object_subtype - " + str(object_subtype) , xbmc.LOGDEBUG ) + + elem_type = ut.otype_to_type(object_type,object_subtype) + xbmc.log("AmpachePlugin::addItems: elem_type - " + str(elem_type) , xbmc.LOGDEBUG ) + + useCacheArt = True + + if elem_type != "song": + limit = len(elem.findall(elem_type)) + if limit > 100: + #to not overload servers + if (not ut.strBool_to_bool(ampache.getSetting("images-long-list"))): + useCacheArt = False + + if useCacheArt and precache: + precacheArt(elem,elem_type) + + if object_type == 'songs' or object_type == 'videos': + addPlayLinks(elem,elem_type) + else: + #set the mode + mode = ut.otype_to_mode(object_type, object_subtype) + addLinks(elem,elem_type,useCacheArt,mode) + return + +def get_all(object_type, mode ,offset=None): + if offset == None: + offset=0 + try: + limit = int(ampache.getSetting(object_type)) + if limit == 0: + return + except: + return + + step = 500 + newLimit = offset+step + get_items(object_type, limit=step, offset=offset) + if newLimit < limit: + pass + else: + newLimit = None + + if newLimit: + addDir(ut.tString(30194),mode,5,offset=newLimit) + +#this functions handles the majority of the requests to the server +#so, we have a lot of optional params +def get_items(object_type, object_id=None, add=None,\ + thisFilter=None,limit=5000, object_subtype=None,\ + exact=None, offset=None ): + + if object_type: + xbmc.log("AmpachePlugin::get_items: object_type " + object_type, xbmc.LOGDEBUG) + else: + #it should be not possible + xbmc.log("AmpachePlugin::get_items: object_type set to None" , xbmc.LOGDEBUG) + return + + if object_subtype: + xbmc.log("AmpachePlugin::get_items: object_subtype " + object_subtype, xbmc.LOGDEBUG) + + #object_id could be None in some requests, like recently added and get_all + #items + if object_id: + xbmc.log("AmpachePlugin::get_items: object_id " + object_id, xbmc.LOGDEBUG) + + if limit == None: + limit = int(ampache.getSetting(object_type)) + + #default: object_type is the action,otherwise see the if list below + action = object_type + + artist_action_subtypes = [ + 'artist_albums','tag_albums','genre_albums','album'] + + album_action_subtypes = [ 'tag_artists','genre_artists','artist'] + + song_action_subtypes = [ 'tag_songs','genre_songs', 'playlist_songs', + 'album_songs', 'artist_songs','search_songs', + 'podcast_episodes','live_streams'] + + #do not use action = object_subtype cause in tags it is used only to + #discriminate between subtypes + if object_type == 'albums': + if object_subtype == 'artist_albums': + addDir("All Songs",1,72, object_id=object_id) + #do not use elif, artist_albums is checked two times + if object_subtype in artist_action_subtypes: + action = object_subtype + elif object_type == 'artists': + if object_subtype in album_action_subtypes: + action = object_subtype + elif object_type == 'songs': + if object_subtype in song_action_subtypes: + action = object_subtype + + if object_id: + thisFilter = object_id + + #here the documentation for an ampache connection + #first create the connection object + #second choose the api function to call in action variable + #third add params using public AmpacheConnect attributes + #( i know, it is ugly, but python doesnt' support structs, so..., if + #someone has a better idea, i'm open to change ) + #if the params are not set, they simply are not added to the url + #forth call ampache_http_request if the server return an xml file + #or ampache_binary_request if the server return a binary file (eg. an + #image ) + #it could be very simply to add json api, but we have to rewrite all + #function that rely on xml input, like additems + + try: + ampConn = ampache_connect.AmpacheConnect() + ampConn.add = add + ampConn.filter = thisFilter + ampConn.limit = limit + ampConn.exact = exact + ampConn.offset = offset + + elem = ampConn.ampache_http_request(action) + addItems( object_type, elem, object_subtype) + except: + return + + +def setRating(): + try: + file_url = xbmc.Player().getPlayingFile() + xbmc.log("AmpachePlugin::setRating url " + file_url , xbmc.LOGDEBUG) + except: + xbmc.log("AmpachePlugin::no playing file " , xbmc.LOGDEBUG) + return + + object_id = ut.get_objectId_from_fileURL( file_url ) + if not object_id: + return + rating = xbmc.getInfoLabel('MusicPlayer.UserRating') + if rating == "": + rating = "0" + + xbmc.log("AmpachePlugin::setRating, user Rating " + rating , xbmc.LOGDEBUG) + #converts from five stats ampache rating to ten stars kodi rating + amp_rating = math.ceil(int(rating)/2.0) + + try: + ampConn = ampache_connect.AmpacheConnect() + + action = "rate" + ampConn.id = object_id + ampConn.type = "song" + ampConn.rating = str(amp_rating) + + ampConn.ampache_http_request(action) + except: + #do nothing + return + +def do_search(object_type,object_subtype=None,thisFilter=None): + """ + do_search(object_type,object_subtype=None,thisFilter=None) -> boolean + requires: + object_type : ( albums, songs... ) + object_subtype : ( search song, tag artists ) + filter : the test to search + return true or false, used to check if call endDirectoryItem or not + """ + if not thisFilter: + thisFilter = gui.getFilterFromUser() + if thisFilter: + get_items(object_type=object_type,thisFilter=thisFilter,object_subtype=object_subtype) + return True + return False + +def get_stats(object_type, object_subtype=None, limit=5000 ): + + xbmc.log("AmpachePlugin::get_stats ", xbmc.LOGDEBUG) + + action = 'stats' + if(int(ampache.getSetting("api-version"))) < 400001: + amtype = object_subtype + thisFilter = None + else: + amtype = ut.otype_to_type(object_type) + thisFilter = object_subtype + + try: + ampConn = ampache_connect.AmpacheConnect() + + ampConn.filter = thisFilter + ampConn.limit = limit + ampConn.type = amtype + + elem = ampConn.ampache_http_request(action) + addItems( object_type, elem) + except: + return + +def get_recent(object_type,submode,object_subtype=None): + + if submode == 31: + update = ampache.getSetting("add") + xbmc.log(update[:10],xbmc.LOGINFO) + get_items(object_type=object_type,add=update[:10],object_subtype=object_subtype) + elif submode == 32: + get_items(object_type=object_type,add=ut.get_time(-7),object_subtype=object_subtype) + elif submode == 33: + get_items(object_type=object_type,add=ut.get_time(-30),object_subtype=object_subtype) + elif submode == 34: + get_items(object_type=object_type,add=ut.get_time(-90),object_subtype=object_subtype) + +def get_random(object_type, num_items): + #object type can be : albums, artists, songs, playlists + + tot_items = int(ampache.getSetting(object_type)) + + xbmc.log("AmpachePlugin::get_random: object_type " + object_type + " num_items " + str(num_items) + " tot_items " +\ + str(tot_items), xbmc.LOGDEBUG) + + if num_items > tot_items: + #if tot_items are less than num_itmes, return all items + get_items(object_type, limit=tot_items) + return + + seq = random.sample(list(range(tot_items)),num_items) + action = object_type + xbmc.log("AmpachePlugin::get_random: seq " + str(seq), xbmc.LOGDEBUG ) + ampConn = ampache_connect.AmpacheConnect() + for item_id in seq: + try: + ampConn.offset = item_id + ampConn.limit = 1 + elem = ampConn.ampache_http_request(action) + addItems( object_type, elem,precache=False) + except: + pass + +def switchFromMusicPlaylist(addon_url, mode, submode, object_id=None, title=None): + """ + this function checks if musicplaylist window is active and switchs to the music window + necessary when we have to call a function like "get album from this + artist" + """ + if xbmc.getCondVisibility("Window.IsActive(musicplaylist)"): + #close busydialog to activate music window + #remove the line below once the busydialog bug is correct + xbmc.executebuiltin('Dialog.Close(busydialog)') + xbmc.executebuiltin("ActivateWindow(music)") + if object_id: + xbmc.executebuiltin("Container.Update(%s?object_id=%s&mode=%s&submode=%s)" %\ + ( addon_url,object_id, mode, submode ) ) + elif title: + xbmc.executebuiltin("Container.Update(%s?title=%s&mode=%s&submode=%s)" %\ + ( addon_url,title, mode, submode ) ) + + +def main_params(plugin_url): + """ + main_params(plugin_url) -> associative array + this function extracts the params from plugin url + and put the in an associative array + not all params are present in url so we need to handle it with exceptions + """ + m_params={} + m_params['mode'] = None + m_params['submode'] = None + m_params['object_id'] = None + m_params['title'] = None + #used only in play tracks + m_params['play_url'] = None + #used to managed very long lists + m_params['offset'] = None + + params=ut.get_params(plugin_url) + + try: + m_params['mode']=int(params["mode"]) + xbmc.log("AmpachePlugin::mode " + str(m_params['mode']), xbmc.LOGDEBUG) + except: + pass + try: + m_params['submode']=int(params["submode"]) + xbmc.log("AmpachePlugin::submode " + str(m_params['submode']), xbmc.LOGDEBUG) + except: + pass + try: + m_params['object_id']=params["object_id"] + xbmc.log("AmpachePlugin::object_id " + m_params['object_id'], xbmc.LOGDEBUG) + except: + pass + try: + m_params['title']=urllib.parse.unquote_plus(params["title"]) + xbmc.log("AmpachePlugin::title " + m_params['title'], xbmc.LOGDEBUG) + except: + pass + try: + m_params['play_url']=urllib.parse.unquote_plus(params["play_url"]) + xbmc.log("AmpachePlugin::play_url " + m_params['play_url'], xbmc.LOGDEBUG) + except: + pass + try: + m_params['offset']=int(params["offset"]) + xbmc.log("AmpachePlugin::offset " + str(m_params['offset']), xbmc.LOGDEBUG) + except: + pass + + return m_params + +#add new line in case of new stat function implemented, checking the version +#in menus +def manage_stats_menu(object_type,submode): + + num_items = (int(ampache.getSetting("random_items"))*3)+3 + apiVersion = int(ampache.getSetting("api-version")) + + if submode == 40: + #playlists are not in the new stats api, so, use the old mode + if(apiVersion < 400001 or (object_type == 'playlists' and apiVersion < 510000 )): + get_random(object_type, num_items) + else: + get_stats(object_type=object_type,object_subtype="random",limit=num_items) + elif submode == 41: + get_stats(object_type=object_type,object_subtype="highest",limit=num_items) + elif submode == 42: + get_stats(object_type=object_type,object_subtype="frequent",limit=num_items) + elif submode == 43: + get_stats(object_type=object_type,object_subtype="flagged",limit=num_items) + elif submode == 44: + get_stats(object_type=object_type,object_subtype="forgotten",limit=num_items) + elif submode == 45: + get_stats(object_type=object_type,object_subtype="newest",limit=num_items) + elif submode == 46: + get_stats(object_type=object_type,object_subtype="recent",limit=num_items) + +def Main(): + + mode=None + object_id=None + #sometimes we need to not endDirectory, but + #we need to check if the connection is alive + #until endDirectoryMode -> endDirectoy and checkConnection + #from endDirectoryMode to endCheckConnection -> no endDirectory but checkConnection + #else no end and no check + endDirectoryMode = 200 + endCheckConnection = 300 + modeMax = 1000 + endDir = True + + addon_url = sys.argv[0] + handle = int(sys.argv[1]) + plugin_url=sys.argv[2] + + xbmc.log("AmpachePlugin::init handle: " + str(handle) + " url: " + plugin_url, xbmc.LOGDEBUG) + + m_params=main_params(plugin_url) + #faster to change variable + mode = m_params['mode'] + submode = m_params['submode'] + object_id = m_params['object_id'] + + #check if the connection is expired + #connect to the server + #do not connect on main screen and when we operate setting; + #do not block the main screen in case the connection to a server it is not available and we kwow it + if mode!=None and mode < endCheckConnection: + if ut.check_tokenexp(): + try: + #check server file only when necessary + servers_manager.initializeServer() + ampacheConnect = ampache_connect.AmpacheConnect() + ampacheConnect.AMPACHECONNECT() + except: + pass + + apiVersion = int(ampache.getSetting("api-version")) + + #start menu + if mode==None: + #search + addDir(ut.tString(30101),53,None) + #quick access + addDir(ut.tString(30102),52,None) + #explore + addDir(ut.tString(30103),50,None) + #library + addDir(ut.tString(30104),51,None) + #switch server + addDir(ut.tString(30023),304,None) + #settings + addDir(ut.tString(30105),300,None) + + #artist mode + elif mode==1: + #artist, album, songs, playlist follow the same structure + #get all artists + if submode == 5: + get_all("artists", mode ,m_params['offset']) + #get the artist from this album's artist_id + elif submode == 6: + switchFromMusicPlaylist(addon_url, mode, submode, object_id=object_id ) + get_items(object_type="artists",object_id=object_id,object_subtype="artist") + #search function + #10-30 search + elif submode == 10: + endDir = do_search("artists") + #recent function + #30-40 recent + elif submode > 30 and submode < 35: + get_recent( "artists", submode ) + #submode between 40-46( random.. recent ) + #40-70 stats + elif submode >= 40 and submode <= 46: + manage_stats_menu("artists",submode) + #get all albums from an artist_id + elif submode == 71: + get_items(object_type="albums",object_id=object_id,object_subtype="artist_albums") + #get all songs from an artist_id + elif submode == 72: + get_items(object_type="songs",object_id=object_id,object_subtype="artist_songs" ) + + + #albums mode + elif mode==2: + #get all albums + if submode == 5: + get_all("albums", mode ,m_params['offset']) + #get the album from the song's album_id + elif submode == 6: + switchFromMusicPlaylist(addon_url, mode, submode, object_id=object_id ) + get_items(object_type="albums",object_id=object_id,object_subtype="album") + elif submode == 10: + endDir = do_search("albums") + elif submode > 30 and submode < 35: + get_recent( "albums", submode ) + elif submode >= 40 and submode <= 46: + manage_stats_menu("albums",submode) + #get all songs from an album_id + elif submode == 71: + get_items(object_type="songs",object_id=object_id,object_subtype="album_songs") + + #song mode + elif mode == 3: + #10-30 search + if submode == 10: + endDir = do_search("songs") + # submode 11 : search all + elif submode == 11: + endDir = do_search("songs","search_songs") + #get all song with this title + elif submode == 12: + switchFromMusicPlaylist(addon_url, mode,submode,title=m_params['title'] ) + endDir = do_search("songs",thisFilter=m_params['title']) + #30-40 recent + elif submode > 30 and submode < 35: + get_recent( "songs", submode ) + #40-70 stats + elif submode >= 40 and submode <= 46: + manage_stats_menu("songs",submode) + + #playlist mode + elif mode==4: + if submode == 5: + get_all("playlists", mode ,m_params['offset']) + elif submode == 10: + endDir = do_search("playlists") + elif submode > 30 and submode < 35: + get_recent( "playlists", submode ) + elif submode == 40: + manage_stats_menu("playlists", submode) + #get all songs from a playlist_id + elif submode == 71: + get_items(object_type="songs",object_id=object_id,object_subtype="playlist_songs") + + #podcasts + elif mode==5: + if submode == 5: + get_all("podcasts", mode ,m_params['offset']) + elif submode == 10: + endDir = do_search("podcasts") + #get all episodes + elif submode == 71: + if apiVersion >= 440000: + get_items(object_type="songs",object_id=object_id,object_subtype="podcast_episodes") + + #live_streams + elif mode==6: + if submode == 10: + endDir = do_search("songs","live_streams") + #get all streams + elif submode == 71: + if apiVersion >= 440000: + get_items(object_type="songs",object_id=object_id,object_subtype="live_streams") + + #video + elif mode==8: + if submode == 5: + get_all("videos", mode ,m_params['offset']) + elif submode == 10: + endDir = do_search("videos") + + #19-21 tags/genres mode + elif mode>=19 and mode <=21: + object_type, object_subtype = ut.mode_to_tags(mode) + #get_all tags/genres + if submode == 5: + get_items(object_type = object_type, object_subtype=object_subtype) + #search tag/genre + elif submode == 10: + endDir = do_search(object_type,object_subtype) + #get all songs from a tag_id/genre_id + elif submode == 71: + if mode == 19: + get_items(object_type="artists", object_subtype=object_subtype,object_id=object_id) + elif mode == 20: + get_items(object_type="albums", object_subtype=object_subtype,object_id=object_id) + elif mode == 21: + get_items(object_type="songs", object_subtype=object_subtype,object_id=object_id) + + #main menus 50-100 + #explore + elif mode==50: + #recently added + addDir(ut.tString(30145),107,None) + #random + addDir(ut.tString(30146),100,None) + if apiVersion >= 400001: + #highest + addDir(ut.tString(30148),101,None) + #frequent + addDir(ut.tString(30164),102,None) + #flagged + addDir(ut.tString(30165),103,None) + #forgotten + addDir(ut.tString(30166),104,None) + #newest + addDir(ut.tString(30167),105,None) + #recent + addDir(ut.tString(30193),106,None) + + #Library + elif mode==51: + addDir(ut.tString(30115) +" (" + ampache.getSetting("artists")+ ")",1,5) + addDir(ut.tString(30116) + " (" + ampache.getSetting("albums") + ")",2,5) + addDir(ut.tString(30118) + " (" + ampache.getSetting("playlists")+ ")",4,5) + if ampache.getSetting("videos"): + addDir(ut.tString(30221) + " (" + ampache.getSetting("videos")+ ")",8,5) + if ampache.getSetting("podcasts"): + addDir(ut.tString(30226) + " (" + ampache.getSetting("podcasts")+ ")",5,5) + if ampache.getSetting("live_streams"): + addDir(ut.tString(30229) + " (" + + ampache.getSetting("live_streams")+ ")",6,71) + if apiVersion >= 380001: + #get all tags ( submode 5 ) + addDir(ut.tString(30119),54,5) + + #quick access + elif mode==52: + #random album + addDir(ut.tString(30135),2,40) + if apiVersion >= 400001: + #newest albums + addDir(ut.tString(30162),2,45) + #frequent albums + addDir(ut.tString(30153),2,42) + #recently played albums + addDir(ut.tString(30191),2,46) + else: + #use recently added albums for old api versions + addDir(ut.tString(30127),55,32) + #server playlist ( AKA random songs ) + addDir(ut.tString(30147),3,40) + + #search mode + elif mode==53: + if not (ut.strBool_to_bool(ampache.getSetting("old-search-gui"))): + endDir = searchGui() + else: + #old search gui + #search artist + addDir(ut.tString(30120),1,10) + #search album + addDir(ut.tString(30121),2,10) + #search song + addDir(ut.tString(30122),3,10) + #search playlist + addDir(ut.tString(30123),4,10) + #search all + addDir(ut.tString(30124),3,11) + #search tag + addDir(ut.tString(30125),54,10) + #search video + addDir(ut.tString(30222),8,10) + #search podcast + addDir(ut.tString(30227),5,10) + #search live_streams + addDir(ut.tString(30230),6,10) + + #search tags + elif mode==54: + #search tag_artist + addDir(ut.tString(30142),19,submode) + #search tag_album + addDir(ut.tString(30143),20,submode) + #search tag_song + addDir(ut.tString(30144),21,submode) + + #screen with recent time possibilities ( subscreen of recent artists, + #recent albums, recent songs ) + elif mode==55: + mode_new = submode - 30 + + #last update + addDir(ut.tString(30130),mode_new,31) + #1 week + addDir(ut.tString(30131),mode_new,32) + addDir(ut.tString(30132),mode_new,33) + addDir(ut.tString(30133),mode_new,34) + + + #stats 100-150 + #random + elif mode==100: + #artists + addDir(ut.tString(30134),1,40) + #albums + addDir(ut.tString(30135),2,40) + #songs + addDir(ut.tString(30136),3,40) + #playlists + addDir(ut.tString(30137),4,40) + + #highest + elif mode==101: + #artists + addDir(ut.tString(30149),1,41) + #albums + addDir(ut.tString(30150),2,41) + #songs + addDir(ut.tString(30151),3,41) + + #frequent + elif mode==102: + addDir(ut.tString(30152),1,42) + addDir(ut.tString(30153),2,42) + addDir(ut.tString(30154),3,42) + + #flagged + elif mode==103: + addDir(ut.tString(30155),1,43) + addDir(ut.tString(30156),2,43) + addDir(ut.tString(30157),3,43) + + #forgotten + elif mode==104: + addDir(ut.tString(30158),1,44) + addDir(ut.tString(30159),2,44) + addDir(ut.tString(30160),3,44) + + #newest + elif mode==105: + addDir(ut.tString(30161),1,45) + addDir(ut.tString(30162),2,45) + addDir(ut.tString(30163),3,45) + + #recently added + elif mode==106: + addDir(ut.tString(30190),1,46) + addDir(ut.tString(30191),2,46) + addDir(ut.tString(30192),3,46) + + # recent + elif mode==107: + #recently added artist + addDir(ut.tString(30126),55,31) + #recently added album + addDir(ut.tString(30127),55,32) + #recently added song + addDir(ut.tString(30128),55,33) + #recently added playlist + addDir(ut.tString(30129),55,34) + + + #others mode 200-250 + #play track mode ( mode set in add_links function ) + #mode 200 to avoid endDirectory + elif mode==200: + #workaround busydialog bug + xbmc.executebuiltin('Dialog.Close(busydialog)') + play_track(m_params['play_url']) + + #change rating + elif mode==205: + setRating() + + #settings mode 300-350 + #settings + elif mode==300: + ampache.openSettings() + + #the four modes below are used to manage servers + elif mode==301: + servers_manager.initializeServer() + if servers_manager.addServer(): + servers_manager.switchServer() + + elif mode==302: + servers_manager.initializeServer() + if servers_manager.deleteServer(): + servers_manager.switchServer() + + elif mode==303: + servers_manager.initializeServer() + servers_manager.modifyServer() + + elif mode==304: + servers_manager.initializeServer() + servers_manager.switchServer() + + #no end directory item ( problem with failed searches ) + #endDir is the result of the search function + if endDir == False: + mode = modeMax + + if mode == None or mode < endDirectoryMode: + xbmc.log("AmpachePlugin::endOfDirectory " + str(handle), xbmc.LOGDEBUG) + xbmcplugin.endOfDirectory(handle) + + diff --git a/plugin.audio.ampache/resources/lib/ampache_service.py b/plugin.audio.ampache/resources/lib/ampache_service.py new file mode 100644 index 0000000000..b2eb0d751f --- /dev/null +++ b/plugin.audio.ampache/resources/lib/ampache_service.py @@ -0,0 +1,7 @@ +from resources.lib.ampache_monitor import AmpacheMonitor +from resources.lib.art_clean import clean_cache_art, clean_settings + +def Main(): + clean_settings() + clean_cache_art() + AmpacheMonitor().run() diff --git a/plugin.audio.ampache/resources/lib/art.py b/plugin.audio.ampache/resources/lib/art.py new file mode 100644 index 0000000000..a62b267848 --- /dev/null +++ b/plugin.audio.ampache/resources/lib/art.py @@ -0,0 +1,108 @@ +from future.utils import PY2 +import os +import cgi +import xbmc,xbmcaddon +import xbmcvfs + +#main plugin library + +from resources.lib import ampache_connect + +ampache = xbmcaddon.Addon("plugin.audio.ampache") + +#different functions in kodi 19 (python3) and kodi 18 (python2) +if PY2: + user_dir = xbmc.translatePath( ampache.getAddonInfo('profile')) + user_dir = user_dir.decode('utf-8') +else: + user_dir = xbmcvfs.translatePath( ampache.getAddonInfo('profile')) +user_mediaDir = os.path.join( user_dir , 'media' ) +cacheDir = os.path.join( user_mediaDir , 'cache' ) + +def cacheArt(imageID,elem_type,url=None): + if not imageID and not url: + raise NameError + + cacheDirType = os.path.join( cacheDir , elem_type ) + + possible_ext = ["jpg", "png" , "bmp", "gif", "tiff"] + for ext in possible_ext: + imageName = imageID + "." + ext + pathImage = os.path.join( cacheDirType , imageName ) + if os.path.exists( pathImage ): + xbmc.log("AmpachePlugin::CacheArt: cached, id " + imageID + " extension " + ext ,xbmc.LOGDEBUG) + return pathImage + + #no return, not found + ampacheConnect = ampache_connect.AmpacheConnect() + action = 'get_art' + ampacheConnect.id = imageID + ampacheConnect.type = elem_type + + try: + if(int(ampache.getSetting("api-version"))) < 400001: + #old api version + headers,contents = ampacheConnect.handle_request(url) + else: + headers,contents = ampacheConnect.ampache_binary_request(action) + except AmpacheConnect.ConnectionError: + raise NameError + #xbmc.log("AmpachePlugin::CacheArt: File needs fetching, id " + imageID,xbmc.LOGDEBUG) + extension = headers['content-type'] + if extension: + mimetype, options = cgi.parse_header(extension) + #little hack when content-type is not standard + if mimetype == "JPG" or mimetype == "jpeg": + maintype = "image" + subtype = "jpg" + else: + try: + maintype, subtype = mimetype.split("/") + except ValueError: + xbmc.log("AmpachePlugin::CacheArt: content-type not standard " +\ + mimetype,xbmc.LOGDEBUG) + raise NameError + if maintype == 'image': + if subtype == "jpeg": + fname = imageID + ".jpg" + else: + fname = imageID + '.' + subtype + + pathImage = os.path.join( cacheDirType , fname ) + with open( pathImage, 'wb') as f: + f.write(contents) + f.close() + #xbmc.log("AmpachePlugin::CacheArt: Cached " + fname, xbmc.LOGDEBUG ) + return pathImage + else: + xbmc.log("AmpachePlugin::CacheArt: It didnt work, id " + imageID , xbmc.LOGDEBUG ) + raise NameError + else: + xbmc.log("AmpachePlugin::CacheArt: No file found, id " + imageID , xbmc.LOGDEBUG ) + raise NameError + +def get_artLabels(albumArt): + art_labels = { + 'banner' : albumArt, + 'thumb': albumArt, + 'icon': albumArt, + 'fanart': albumArt + } + return art_labels + +#get_art, url is used for legacy purposes +def get_art(object_id,elem_type,url=None): + + albumArt = "DefaultFolder.png" + #no url, no art, so no need to activate a connection + if not object_id and not url: + return albumArt + try: + albumArt = cacheArt(object_id,elem_type,url) + except NameError: + albumArt = "DefaultFolder.png" + + #xbmc.log("AmpachePlugin::get_art: id - " + object_id + " - albumArt - " + str(albumArt), xbmc.LOGDEBUG ) + return albumArt + + diff --git a/plugin.audio.ampache/resources/lib/art_clean.py b/plugin.audio.ampache/resources/lib/art_clean.py new file mode 100644 index 0000000000..1e94c11bc1 --- /dev/null +++ b/plugin.audio.ampache/resources/lib/art_clean.py @@ -0,0 +1,59 @@ +from future.utils import PY2 +import os +import xbmc,xbmcaddon +import xbmcvfs + +#split the art library to not have import problems, as the function is used by +#service and the main plugin +#library used both by service and main plugin, DO NOT INCLUDE OTHER LOCAL +#LIBRARIES + +ampache = xbmcaddon.Addon("plugin.audio.ampache") + +#different functions in kodi 19 (python3) and kodi 18 (python2) +if PY2: + user_dir = xbmc.translatePath( ampache.getAddonInfo('profile')) + user_dir = user_dir.decode('utf-8') +else: + user_dir = xbmcvfs.translatePath( ampache.getAddonInfo('profile')) +user_mediaDir = os.path.join( user_dir , 'media' ) +cacheDir = os.path.join( user_mediaDir , 'cache' ) + +def clean_settings(): + ampache.setSetting("session_expire", "") + ampache.setSetting("add", "") + ampache.setSetting("token", "") + ampache.setSetting("token-exp", "") + ampache.setSetting("artists", "") + ampache.setSetting("albums", "") + ampache.setSetting("songs", "") + ampache.setSetting("playlists", "") + ampache.setSetting("videos", "") + ampache.setSetting("podcasts", "") + ampache.setSetting("live_streams", "") + +def clean_cache_art(): + #hack to force the creation of profile directory if don't exists + if not os.path.isdir(user_dir): + ampache.setSetting("api-version","350001") + + cacheTypes = ["album", "artist" , "song", "podcast","playlist"] + #if cacheDir doesn't exist, create it + if not os.path.isdir(user_mediaDir): + os.mkdir(user_mediaDir) + if not os.path.isdir(cacheDir): + os.mkdir(cacheDir) + for c_type in cacheTypes: + cacheDirType = os.path.join( cacheDir , c_type ) + if not os.path.isdir(cacheDirType): + os.mkdir( cacheDirType ) + + #clean cache on start + for c_type in cacheTypes: + cacheDirType = os.path.join( cacheDir , c_type ) + for currentFile in os.listdir(cacheDirType): + #xbmc.log("Clear Cache Art " + str(currentFile),xbmc.LOGDEBUG) + pathDel = os.path.join( cacheDirType, currentFile) + os.remove(pathDel) + + diff --git a/plugin.audio.ampache/resources/lib/gui.py b/plugin.audio.ampache/resources/lib/gui.py new file mode 100644 index 0000000000..0d173730d6 --- /dev/null +++ b/plugin.audio.ampache/resources/lib/gui.py @@ -0,0 +1,15 @@ + +import xbmc +import xbmcgui + +#main plugin library + +def getFilterFromUser(title='',thisType=xbmcgui.INPUT_ALPHANUM): + kb = xbmcgui.Dialog() + result = kb.input(title, type=thisType) + if result: + thisFilter = result + else: + return False + return(thisFilter) + diff --git a/plugin.audio.ampache/resources/lib/json_storage.py b/plugin.audio.ampache/resources/lib/json_storage.py new file mode 100644 index 0000000000..2e2738525a --- /dev/null +++ b/plugin.audio.ampache/resources/lib/json_storage.py @@ -0,0 +1,37 @@ +from builtins import object +from future.utils import PY2 +import json +import os +import xbmc +import xbmcaddon +import xbmcvfs +from copy import deepcopy + +#main plugin library + +class JsonStorage(object): + + def __init__(self,filename): + ampache = xbmcaddon.Addon("plugin.audio.ampache") + if PY2: + base_dir = xbmc.translatePath( ampache.getAddonInfo('profile')) + base_dir = base_dir.decode('utf-8') + else: + base_dir = xbmcvfs.translatePath( ampache.getAddonInfo('profile')) + self._filename = os.path.join(base_dir, filename) + self._data = dict() + self.load() + + def load(self): + if xbmcvfs.exists(self._filename): + with open(self._filename, 'r') as fd: + self._data = json.load(fd) + + def save(self,data): + if data != self._data: + self._data = deepcopy(data) + with open(self._filename, 'w') as fd: + json.dump(self._data, fd, indent=4, sort_keys=True) + + def getData(self): + return deepcopy(self._data) diff --git a/plugin.audio.ampache/resources/lib/servers_manager.py b/plugin.audio.ampache/resources/lib/servers_manager.py new file mode 100644 index 0000000000..de6765da35 --- /dev/null +++ b/plugin.audio.ampache/resources/lib/servers_manager.py @@ -0,0 +1,208 @@ +from __future__ import print_function + +import xbmc,xbmcgui + +#main plugin library + +from resources.lib import gui +from resources.lib.art_clean import clean_cache_art +from resources.lib import utils as ut +from resources.lib import json_storage +from resources.lib import ampache_connect + +def initializeServer(): + jsStorServer = json_storage.JsonStorage("servers.json") + serverData = jsStorServer.getData() + if serverData: + pass + else: + xbmc.log( "AmpachePlugin::initializeServer: no servers file",xbmc.LOGDEBUG) + serverData["servers"] = {} + tempd = {} + tempd["0"] = {} + serverData["servers"].update(tempd) + serverData["servers"]["0"]["name"] = "Develop Demo" + serverData["servers"]["0"]["url"] = "http://develop.ampache.dev/" + serverData["servers"]["0"]["use_api_key"] = "false" + serverData["servers"]["0"]["enable_password"] = "true" + serverData["servers"]["0"]["username"] = "kodi_demo" + serverData["servers"]["0"]["password"] = "aNNKvApsECw7Tpc" + serverData["servers"]["0"]["api_key"] = "" + serverData["current_server"] = "0" + jsStorServer.save(serverData) + + +#input: serverData and title +#output: number of server in data +def serversDialog(data,title=''): + templist = [] + showlist = [] + dialog = xbmcgui.Dialog() + for i in data["servers"]: + item = data["servers"][i]["name"] + if i == data["current_server"]: + item = item + " *" + showlist.append(item) + templist.append(data["servers"][i]["name"]) + ret = dialog.select(title, showlist) + i_temp= "" + if ret == -1: + return False + for i in data["servers"]: + if(data["servers"][i]["name"]) == templist[ret]: + i_temp = i + return i_temp + +def showServerData(data,title=ut.tString(30168)): + padding_size = 20 + #order of the data + ordlist = ["name","url","username","enable_password","password","use_api_key","api_key"] + #name to display + dispList = [ut.tString(30181),ut.tString(30182),ut.tString(30183),ut.tString(30184),ut.tString(30185),ut.tString(30186),ut.tString(30187)] + templist = [] + showlist = [] + n = 0 + dialog = xbmcgui.Dialog() + for i in ordlist: + templist.append(i) + pad_i = dispList[n] + " "*(padding_size - len(i)) + tempStr = pad_i + data[i] + showlist.append(tempStr) + n = n + 1 + ret = dialog.select(title, showlist) + i_temp= "" + if ret == -1: + return False + for i in data: + if i == templist[ret]: + i_temp = i + return i_temp + +def switchServer(): + jsStorServer = json_storage.JsonStorage("servers.json") + serverData = jsStorServer.getData() + i_curr = serversDialog(serverData,ut.tString(30169)) + if i_curr == False: + return + xbmc.executebuiltin("PlayerControl(Stop)") + serverData["current_server"] = i_curr + jsStorServer.save(serverData) + #clean cache_art, the server is different, so the cache is invalid + clean_cache_art() + #if we switch, reconnect + try: + ampacheConnect = ampache_connect.AmpacheConnect() + ampacheConnect.AMPACHECONNECT(showok=True) + except: + pass + +def addServer(): + xbmc.log("AmpachePlugin::addServer" , xbmc.LOGDEBUG ) + jsStorServer = json_storage.JsonStorage("servers.json") + serverData = jsStorServer.getData() + if len(list(serverData["servers"])) > 0: + #choose the max number of the server list plus one + stnum = str(max([int(i) for i in list(serverData["servers"])])+1) + else: + #empty list + stnum = "0" + username = "" + password = "" + apikey = "" + enablepassword = True + is_api_key = False + tempd = {} + tempd[stnum] = {} + serverData["servers"].update(tempd) + servername = gui.getFilterFromUser(ut.tString(30170)) + if servername == False: + return False + url = gui.getFilterFromUser(ut.tString(30171)) + if url == False: + return False + dialog = xbmcgui.Dialog() + is_api_key = dialog.yesno(ut.tString(30189),ut.tString(30173)) + if is_api_key == True: + apikey = gui.getFilterFromUser(ut.tString(30174)) + if apikey == False: + return False + else: + username = gui.getFilterFromUser(ut.tString(30175)) + if username == False: + return False + enablepassword = dialog.yesno(ut.tString(30189),ut.tString(30177)) + if enablepassword == True: + password = gui.getFilterFromUser(ut.tString(30178)) + if password == False: + return False + serverData["servers"][stnum]["name"] = servername + serverData["servers"][stnum]["url"] = url + serverData["servers"][stnum]["use_api_key"] = ut.int_to_strBool(is_api_key) + serverData["servers"][stnum]["username"] = username + serverData["servers"][stnum]["enable_password"] = ut.int_to_strBool(enablepassword) + serverData["servers"][stnum]["password"] = password + serverData["servers"][stnum]["api_key"] = apikey + jsStorServer.save(serverData) + showServerData(serverData["servers"][stnum]) + return True + +def deleteServer(): + jsStorServer = json_storage.JsonStorage("servers.json") + serverData = jsStorServer.getData() + i_rem = serversDialog(serverData,ut.tString(30179)) + if i_rem == False: + return False + dialog = xbmcgui.Dialog() + confirm = dialog.yesno(ut.tString(30189),ut.tString(30188)) + if confirm: + #replace old server position with the latest server in the list + repl_num = str(max([int(i) for i in list(serverData["servers"])])) + serverData["servers"][i_rem] = serverData["servers"][repl_num].copy() + del serverData["servers"][repl_num] + jsStorServer.save(serverData) + return True + else: + return False + +def modifyServer(): + jsStorServer = json_storage.JsonStorage("servers.json") + serverData = jsStorServer.getData() + i = serversDialog(serverData,ut.tString(30180)) + if i == False: + return + while True: + if xbmc.Monitor().abortRequested(): + return + key = showServerData(serverData["servers"][i]) + if key == False: + break + elif key == "use_api_key": + dialog = xbmcgui.Dialog() + value_int = dialog.yesno(ut.tString(30189),ut.tString(30173)) + value = ut.int_to_strBool(value_int) + elif key == "enable_password": + dialog = xbmcgui.Dialog() + value_int = dialog.yesno(ut.tString(30189),ut.tString(30177)) + value = ut.int_to_strBool(value_int) + elif key == "name": + value = gui.getFilterFromUser(ut.tString(30181)) + elif key == "url": + value = gui.getFilterFromUser(ut.tString(30182)) + elif key == "username": + value = gui.getFilterFromUser(ut.tString(30183)) + elif key == "password": + value = gui.getFilterFromUser(ut.tString(30185)) + elif key == "api_key": + value = gui.getFilterFromUser(ut.tString(30187)) + else: + pass + if value != False: + serverData["servers"][i][key] = value + xbmc.executebuiltin("PlayerControl(Stop)") + jsStorServer.save(serverData) + #just to be sure, having potentially changed default server + try: + ampacheConnect = ampache_connect.AmpacheConnect() + ampacheConnect.AMPACHECONNECT() + except: + pass diff --git a/plugin.audio.ampache/resources/lib/utils.py b/plugin.audio.ampache/resources/lib/utils.py new file mode 100644 index 0000000000..4d9a60123e --- /dev/null +++ b/plugin.audio.ampache/resources/lib/utils.py @@ -0,0 +1,173 @@ +import time +import datetime +import xbmcaddon,xbmcplugin +import sys + +#main plugin/service library +ampache = xbmcaddon.Addon("plugin.audio.ampache") + +def setContent(handle,object_type): + if object_type == 'artists' or object_type == 'albums' or object_type == 'songs' or object_type == 'videos': + xbmcplugin.setContent(handle, object_type) + +def otype_to_mode(object_type, object_subtype=None): + mode = None + if object_type == 'artists': + mode = 1 + elif object_type == 'albums': + mode = 2 + elif object_type == 'songs': + mode = 3 + elif object_type == 'playlists': + mode = 4 + elif object_type == 'podcasts': + mode = 5 + elif object_type == 'live_streams': + mode = 6 + elif object_type == 'videos': + mode = 8 + elif object_type == 'tags' or object_type == 'genres': + if object_subtype == 'tag_artists' or object_subtype == 'genre_artists': + mode = 19 + elif object_subtype == 'tag_albums' or object_subtype == 'genre_albums': + mode = 20 + elif object_subtype == 'tag_songs' or object_subtype == 'genre_songs': + mode = 21 + + return mode + +def mode_to_tags(mode): + if(int(ampache.getSetting("api-version"))) < 500000: + if mode == 19: + return "tags","tag_artists" + if mode == 20: + return "tags","tag_albums" + if mode == 21: + return "tags","tag_songs" + else: + if mode == 19: + return "genres","genre_artists" + if mode == 20: + return "genres","genre_albums" + if mode == 21: + return "genres","genre_songs" + +def otype_to_type(object_type,object_subtype=None): + if object_type == 'albums': + return 'album' + elif object_type == 'artists': + return 'artist' + elif object_type == 'playlists': + return 'playlist' + elif object_type == 'tags': + return 'tag' + elif object_type == 'genres': + return 'genre' + elif object_type == 'podcasts': + return 'podcast' + elif object_type == 'videos': + return 'video' + elif object_type == 'songs': + if object_subtype == 'podcast_episodes': + return 'podcast_episode' + elif object_subtype == 'live_streams': + return 'live_stream' + return 'song' + return None + +def int_to_strBool(s): + if s == 1: + return 'true' + elif s == 0: + return 'false' + else: + raise ValueError + +# string to bool function : from string 'true' or 'false' to boolean True or +# False, raise ValueError +def strBool_to_bool(s): + if s == 'true': + return True + elif s == 'false': + return False + else: + raise ValueError + +def check_tokenexp(): + session_time = ampache.getSetting("session_expire") + if session_time is None or session_time == "": + return True + + #from python 3.7 we can easly compare the dates, otherwise we use the old + #method + + if sys.version_info >= (3, 7): + try: + s_time = datetime.datetime.fromisoformat(session_time) + if datetime.datetime.now(datetime.timezone.utc) > s_time: + return True + return False + except: + return False + else: + try: + tokenexp = int(ampache.getSetting("token-exp")) + if int(time.time()) > tokenexp: + return True + return False + except: + return True + +def get_time(time_offset): + d = datetime.date.today() + dt = datetime.timedelta(days=time_offset) + nd = d + dt + return nd.isoformat() + +#return the translated String +def tString(code): + return ampache.getLocalizedString(code) + +def get_params(plugin_url): + param=[] + paramstring=plugin_url + if len(paramstring)>=2: + params=plugin_url + cleanedparams=params.replace('?','') + if (params[len(params)-1]=='/'): + params=params[0:len(params)-2] + pairsofparams=cleanedparams.split('&') + param={} + for i in range(len(pairsofparams)): + splitparams={} + splitparams=pairsofparams[i].split('=') + if (len(splitparams))==2: + param[splitparams[0]]=splitparams[1] + + return param + +def get_objectId_from_fileURL( file_url ): + params = get_params(file_url) + object_id = None + #i use two kind of object_id, i don't know, but sometime i have different + #url, btw, no problem, i handle both and i solve the problem in this way + try: + object_id=params["object_id"] + xbmc.log("AmpachePlugin::object_id " + object_id, xbmc.LOGDEBUG) + except: + pass + try: + object_id=params["oid"] + xbmc.log("AmpachePlugin::object_id " + object_id, xbmc.LOGDEBUG) + except: + pass + return object_id + +def getRating(rating): + if rating: + #converts from five stats ampache rating to ten stars kodi rating + rating = int(float(rating)*2) + else: + #zero equals no rating + rating = 0 + return rating diff --git a/plugin.audio.ampache/resources/media/images/refresh_icon.png b/plugin.audio.ampache/resources/media/images/refresh_icon.png new file mode 100644 index 0000000000..b162e2c476 Binary files /dev/null and b/plugin.audio.ampache/resources/media/images/refresh_icon.png differ diff --git a/plugin.audio.ampache/resources/settings.xml b/plugin.audio.ampache/resources/settings.xml new file mode 100644 index 0000000000..eba32f12c2 --- /dev/null +++ b/plugin.audio.ampache/resources/settings.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/plugin.audio.ampache/service.py b/plugin.audio.ampache/service.py new file mode 100644 index 0000000000..d90231cb37 --- /dev/null +++ b/plugin.audio.ampache/service.py @@ -0,0 +1,4 @@ + +from resources.lib.ampache_service import Main + +Main() diff --git a/plugin.audio.arteconcert/LICENSE.txt b/plugin.audio.arteconcert/LICENSE.txt new file mode 100644 index 0000000000..23cb790338 --- /dev/null +++ b/plugin.audio.arteconcert/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.audio.arteconcert/addon.xml b/plugin.audio.arteconcert/addon.xml new file mode 100644 index 0000000000..6d1aca976a --- /dev/null +++ b/plugin.audio.arteconcert/addon.xml @@ -0,0 +1,27 @@ + + + + + + + + audio + + + all + an de fr + GPL-2.0-only + https://github.com/sarbes/plugin.audio.arteconcert + https://forum.kodi.tv/showthread.php?tid=353899 + https://www.arte.tv/en/arte-concert/ + This add-on allows access to the concert section of the French/German broadcaster Arte. + This add-on allows access to the concert section of the French/German broadcaster Arte. + Dieses Add-on bietet Zugriff auf die Konzerte von Arte. + Dieses Add-on bietet Zugriff auf die Konzerte von Arte. Das Angebot reicht von Klassischer Musik bis hin zu Elektro. + Dieses Add-on wird von keiner Sendeanstalt unterstützt und ist daher inoffiziell. + + resources/icon.png + resources/fanart.png + + + diff --git a/plugin.audio.arteconcert/default.py b/plugin.audio.arteconcert/default.py new file mode 100644 index 0000000000..d0bd34ba95 --- /dev/null +++ b/plugin.audio.arteconcert/default.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +import libarte + + +class arteMusic(libarte.libarte): + def __init__(self): + libarte.libarte.__init__(self) + self.modes['libArteListCategories'] = self.libArteListCategories + + def libArteListMain(self): + l = [] + l.append({'metadata':{'name':self.translation(32032)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(32031)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","videoType":"MOST_VIEWED"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(32033)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","videoType":"LAST_CHANCE"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30503)}, 'params':{'mode':'libArteListCategories'}, 'type':'dir'}) + return {'items':l,'name':'root'} + + def libArteListCategories(self): + l = [] + l.append({'metadata':{'name':self.translation(30510)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"MUA","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30511)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"MUE","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30512)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"HIP","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30513)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"MET","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30514)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"CLA","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30515)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"OPE","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30516)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"JAZ","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30517)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"MUD","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30518)}, 'params':{'mode':'libArteListData', 'data':'VIDEO_LISTING', 'uriParams':'{"category":"ARS","subcategories":"ADS","videoType":"MOST_RECENT"}'}, 'type':'dir'}) + return {'items':l,'name':self.translation(30503)} + + +o = arteMusic() +o.action() \ No newline at end of file diff --git a/plugin.audio.arteconcert/resources/fanart.png b/plugin.audio.arteconcert/resources/fanart.png new file mode 100644 index 0000000000..c56fd088c7 Binary files /dev/null and b/plugin.audio.arteconcert/resources/fanart.png differ diff --git a/plugin.audio.arteconcert/resources/icon.png b/plugin.audio.arteconcert/resources/icon.png new file mode 100644 index 0000000000..513034421a Binary files /dev/null and b/plugin.audio.arteconcert/resources/icon.png differ diff --git a/plugin.audio.arteconcert/resources/language/resource.language.de_de/strings.po b/plugin.audio.arteconcert/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..53e26ef422 --- /dev/null +++ b/plugin.audio.arteconcert/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,100 @@ +# KODI Media Center language file +# Addon Name: arteconcert +# Addon id: plugin.video.arteconcert +# Addon version: 1.0.0 +# Addon Provider: sarbes +msgid "" +msgstr "" +"Project-Id-Version: XBMC Main\n" + +msgctxt "#30000" +msgid "General" +msgstr "Allgmein" + +msgctxt "#30010" +msgid "Language" +msgstr "Sprache" + +msgctxt "#30011" +msgid "System (if available)" +msgstr "System (wenn verfügbar)" + +msgctxt "#30012" +msgid "English" +msgstr "Englisch" + +msgctxt "#30013" +msgid "German" +msgstr "Deutsch" + +msgctxt "#30014" +msgid "Spanish" +msgstr "Spanisch" + +msgctxt "#30015" +msgid "French" +msgstr "Französisch" + +msgctxt "#30016" +msgid "Hungarian" +msgstr "Ungarisch" + +msgctxt "#30017" +msgid "Italian" +msgstr "Italienisch" + +msgctxt "#30018" +msgid "Polish" +msgstr "Polnisch" + +msgctxt "#30019" +msgid "Portuguese" +msgstr "Portugisisch" + +msgctxt "#30020" +msgid "Romainian" +msgstr "Romänisch" + +msgctxt "#30021" +msgid "Ukrainian" +msgstr "Ukrainisch" + +msgctxt "#30503" +msgid "Categories" +msgstr "Kategorien" + +msgctxt "#30510" +msgid "Pop & Rock" +msgstr "Pop & Rock" + +msgctxt "#30511" +msgid "Electronic" +msgstr "Electronic" + +msgctxt "#30512" +msgid "Hip-Hop" +msgstr "Hip-Hop" + +msgctxt "#30513" +msgid "Metal" +msgstr "Metal" + +msgctxt "#30514" +msgid "Classical" +msgstr "Klassik" + +msgctxt "#30515" +msgid "Opera" +msgstr "Oper" + +msgctxt "#30516" +msgid "Jazz" +msgstr "Jazz" + +msgctxt "#30517" +msgid "World Music" +msgstr "Welt Musik" + +msgctxt "#30518" +msgid "Performing Arts" +msgstr "Bühnenperformance" \ No newline at end of file diff --git a/plugin.audio.arteconcert/resources/language/resource.language.en_gb/strings.po b/plugin.audio.arteconcert/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..41f8812f27 --- /dev/null +++ b/plugin.audio.arteconcert/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,100 @@ +# KODI Media Center language file +# Addon Name: arteconcert +# Addon id: plugin.video.arteconcert +# Addon version: 1.0.0 +# Addon Provider: sarbes +msgid "" +msgstr "" +"Project-Id-Version: XBMC Main\n" + +msgctxt "#30000" +msgid "General" +msgstr "General" + +msgctxt "#30010" +msgid "Language" +msgstr "Language" + +msgctxt "#30011" +msgid "System (if available)" +msgstr "System (if available)" + +msgctxt "#30012" +msgid "English" +msgstr "English" + +msgctxt "#30013" +msgid "German" +msgstr "German" + +msgctxt "#30014" +msgid "Spanish" +msgstr "Spanish" + +msgctxt "#30015" +msgid "French" +msgstr "French" + +msgctxt "#30016" +msgid "Hungarian" +msgstr "Hungarian" + +msgctxt "#30017" +msgid "Italian" +msgstr "Italian" + +msgctxt "#30018" +msgid "Polish" +msgstr "Polish" + +msgctxt "#30019" +msgid "Portuguese" +msgstr "Portuguese" + +msgctxt "#30020" +msgid "Romainian" +msgstr "Romainian" + +msgctxt "#30021" +msgid "Ukrainian" +msgstr "Ukrainian" + +msgctxt "#30503" +msgid "Categories" +msgstr "Categories" + +msgctxt "#30510" +msgid "Pop & Rock" +msgstr "Pop & Rock" + +msgctxt "#30511" +msgid "Electronic" +msgstr "Electronic" + +msgctxt "#30512" +msgid "Hip-Hop" +msgstr "Hip-Hop" + +msgctxt "#30513" +msgid "Metal" +msgstr "Metal" + +msgctxt "#30514" +msgid "Classical" +msgstr "Classical" + +msgctxt "#30515" +msgid "Opera" +msgstr "Opera" + +msgctxt "#30516" +msgid "Jazz" +msgstr "Jazz" + +msgctxt "#30517" +msgid "World Music" +msgstr "World Music" + +msgctxt "#30518" +msgid "Performing Arts" +msgstr "Performing Arts" \ No newline at end of file diff --git a/plugin.audio.arteconcert/resources/settings.xml b/plugin.audio.arteconcert/resources/settings.xml new file mode 100644 index 0000000000..d897865b39 --- /dev/null +++ b/plugin.audio.arteconcert/resources/settings.xml @@ -0,0 +1,26 @@ + + +
+ + + + 0 + + + + + + + + + + + + true + + + + + +
+
diff --git a/plugin.audio.bbcpodcasts/addon.py b/plugin.audio.bbcpodcasts/addon.py new file mode 100644 index 0000000000..94d950aa8c --- /dev/null +++ b/plugin.audio.bbcpodcasts/addon.py @@ -0,0 +1,7 @@ +from resources.lib.bbcpodcasts.bbcpodcastsaddon import BbcPodcastsAddon +import sys + +if __name__ == '__main__': + + bbcPodcastsAddon = BbcPodcastsAddon(int(sys.argv[1])) + bbcPodcastsAddon.handle(sys.argv) diff --git a/plugin.audio.bbcpodcasts/addon.xml b/plugin.audio.bbcpodcasts/addon.xml new file mode 100644 index 0000000000..b517abc90e --- /dev/null +++ b/plugin.audio.bbcpodcasts/addon.xml @@ -0,0 +1,65 @@ + + + + + + + + + + audio + + + Podcasts vom BBC in KODI + Podcasts by BBC in KODI + Kodi Addon für Podcasts der BBC. Alle Inhalte werden gestreamt von https://www.bbc.co.uk/podcasts + Podcasts by BBC in KODI. All contents are streamed from https://www.bbc.co.uk/podcasts + "Der Author des Addons ist nicht verantwortlich für die Inhalte, die auf Ihr Gerät gestreamt werden. Insbesondere liegt es in der alleinigen Verantwortung und Pflicht des Addon-Nutzers sich davon zu überzeugen, dass die Urheberrechts- und Nutzungsvereinbarungen der zum Streaming aufgerufenden Websites und Inhalte nicht verletzt werden. Alle Inhalte stammen von https://www.bbc.co.uk/podcasts" + The author of this addon is not responsible for the contents which are streamed to this device. Especially the user of this addon is in responsibility to convince that copyrights and right of use are not violated. All contents are streamed from https://www.bbc.co.uk/podcasts + de_DE + en_GB + all + MIT + https://github.com/Heckie75/kodi-addon-bbc-podcasts + https://github.com/Heckie75/kodi-addon-bbc-podcasts/tree/main/plugin.audio.bbcpodcasts + +v2.0.7 (2024-02-21) +- Minor bugfix + +v2.0.6 (2024-02-10) +- Changed parser after changes on website, i.e. switched to API + +v2.0.5 (2023-11-15) +- Changed parser after changes on website, i.e. fix paging + +v2.0.4 (2023-02-02) +- Improved thumbnail if item is added to favourites + +v2.0.3 (2022-06-02) +- Changed parser after changes on website, i.e. fix paging of all podcasts +- Improved performance when loading rss feed with episodes + +v2.0.2 (2022-01-31) +- Changed parser after changes on website + +v2.0.1 (2021-09-12) +- Fixed exception so that 'all podcasts' menu didn't work caused by NoneType + +v2.0.0 (2021-08-14) +- Changes after relaunch of BBC podcasts website + +v1.0.1 (2021-08-01) +- migrated to new settings format + +v1.0.0 (2021-05-24) +- Initial version + + + resources/assets/icon.png + resources/assets/fanart.png + resources/assets/screenshot_1.png + resources/assets/screenshot_2.png + resources/assets/screenshot_3.png + + + diff --git a/plugin.audio.bbcpodcasts/resources/assets/fanart.png b/plugin.audio.bbcpodcasts/resources/assets/fanart.png new file mode 100644 index 0000000000..bfb93ea672 Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/fanart.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/icon.png b/plugin.audio.bbcpodcasts/resources/assets/icon.png new file mode 100644 index 0000000000..0f13791cdf Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/icon.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/icon_arrow_left.png b/plugin.audio.bbcpodcasts/resources/assets/icon_arrow_left.png new file mode 100644 index 0000000000..600bf69edb Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/icon_arrow_left.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/icon_arrow_right.png b/plugin.audio.bbcpodcasts/resources/assets/icon_arrow_right.png new file mode 100644 index 0000000000..83489092f2 Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/icon_arrow_right.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/icon_category.png b/plugin.audio.bbcpodcasts/resources/assets/icon_category.png new file mode 100644 index 0000000000..0b35cb8631 Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/icon_category.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/icon_search.png b/plugin.audio.bbcpodcasts/resources/assets/icon_search.png new file mode 100644 index 0000000000..8772563a02 Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/icon_search.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/icon_selection.png b/plugin.audio.bbcpodcasts/resources/assets/icon_selection.png new file mode 100644 index 0000000000..5c4e1a2dda Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/icon_selection.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/icon_station.png b/plugin.audio.bbcpodcasts/resources/assets/icon_station.png new file mode 100644 index 0000000000..8eaf015f3d Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/icon_station.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/screenshot_1.png b/plugin.audio.bbcpodcasts/resources/assets/screenshot_1.png new file mode 100644 index 0000000000..9ae9ea3af6 Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/screenshot_1.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/screenshot_2.png b/plugin.audio.bbcpodcasts/resources/assets/screenshot_2.png new file mode 100644 index 0000000000..b1bf55fc49 Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/screenshot_2.png differ diff --git a/plugin.audio.bbcpodcasts/resources/assets/screenshot_3.png b/plugin.audio.bbcpodcasts/resources/assets/screenshot_3.png new file mode 100644 index 0000000000..fa51198163 Binary files /dev/null and b/plugin.audio.bbcpodcasts/resources/assets/screenshot_3.png differ diff --git a/plugin.audio.bbcpodcasts/resources/language/resource.language.de_de/strings.po b/plugin.audio.bbcpodcasts/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..f5aee763b6 --- /dev/null +++ b/plugin.audio.bbcpodcasts/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,74 @@ +# Kodi Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32001" +msgid "Editor's picks" +msgstr "Editor's picks" + +msgctxt "#32002" +msgid "" +msgstr "" + +msgctxt "#32003" +msgid "" +msgstr "" + +msgctxt "#32004" +msgid "Search podcast ..." +msgstr "Suche Podcasts ..." + +msgctxt "#32005" +msgid "Disclaimer" +msgstr "Haftungsauschluss" + +msgctxt "#32006" +msgid "I haven't agreed yet" +msgstr "Ich habe noch nicht zugestimmt" + +msgctxt "#32007" +msgid "I have agreed" +msgstr "Ich habe zugestimmt" + +msgctxt "#32008" +msgid "Agreement" +msgstr "Zustimmung" + +msgctxt "#32010" +msgid "The author of this addon is not responsible for the contents which are streamed to this device. Especially the user of this addon is in responsibility to convince that copyrights and right of use are not violated. All contents are taken and streamed from https://www.bbc.co.uk/podcasts. Do you agree?" +msgstr "Der Author des Addons ist nicht verantwortlich für die Inhalte, die auf Ihr Gerät gestreamt werden. Insbesondere liegt es in der alleinigen Verantwortung und Pflicht des Addon-Nutzers sich davon zu überzeugen, dass die Urheberrechts- und Nutzungsvereinbarungen der zum Streaming aufgerufenden Websites und Inhalte nicht verletzt werden. Alle Inhalte stammen von https://www.bbc.co.uk/podcasts. Bist Du damit einverstanden?" + +msgctxt "#32101" +msgid "Most recent episode" +msgstr "aktuelle Sendung" + +msgctxt "#32151" +msgid "Connection Error" +msgstr "Verbindungsfehler" + +msgctxt "#32152" +msgid "HTTP Method %s not supported" +msgstr "HTTP Methode %s wird nicht unterstützt" + +msgctxt "#32153" +msgid "Request Exception: Pls. check URL and port. See logs for further details." +msgstr "Request Fehler: Prüfe URL und Port. Für weitere Details prüfe die Log-Datei." + +msgctxt "#32154" +msgid "Unexpected HTTP Status %i for %s" +msgstr "Unerwarteter HTTP Status %i für %s" + +msgctxt "#32155" +msgid "Unexpected content for podcast" +msgstr "Unerwarteter Inhalt für Podcast" diff --git a/plugin.audio.bbcpodcasts/resources/language/resource.language.en_gb/strings.po b/plugin.audio.bbcpodcasts/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..663633377a --- /dev/null +++ b/plugin.audio.bbcpodcasts/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,74 @@ +# Kodi Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32001" +msgid "Editor's picks" +msgstr "" + +msgctxt "#32002" +msgid "" +msgstr "" + +msgctxt "#32003" +msgid "" +msgstr "" + +msgctxt "#32004" +msgid "Search podcast ..." +msgstr "" + +msgctxt "#32005" +msgid "Disclaimer" +msgstr "" + +msgctxt "#32006" +msgid "I haven't agreed yet" +msgstr "" + +msgctxt "#32007" +msgid "I have agreed" +msgstr "" + +msgctxt "#32008" +msgid "Agreement" +msgstr "" + +msgctxt "#32010" +msgid "The author of this addon is not responsible for the contents which are streamed to this device. Especially the user of this addon is in responsibility to convince that copyrights and right of use are not violated. All contents are taken and streamed from https://www.bbc.co.uk/podcasts. Do you agree?" +msgstr "" + +msgctxt "#32101" +msgid "Most recent episode" +msgstr "" + +msgctxt "#32151" +msgid "Connection Error" +msgstr "" + +msgctxt "#32152" +msgid "HTTP Method %s not supported" +msgstr "" + +msgctxt "#32153" +msgid "Request Exception: Pls. check URL and port. See logs for further details." +msgstr "" + +msgctxt "#32154" +msgid "Unexpected HTTP Status %i for %s" +msgstr "" + +msgctxt "#32155" +msgid "Unexpected content for podcast" +msgstr "" diff --git a/plugin.audio.bbcpodcasts/resources/lib/bbcpodcasts/bbcpodcastsaddon.py b/plugin.audio.bbcpodcasts/resources/lib/bbcpodcasts/bbcpodcastsaddon.py new file mode 100644 index 0000000000..2c09d9a756 --- /dev/null +++ b/plugin.audio.bbcpodcasts/resources/lib/bbcpodcasts/bbcpodcastsaddon.py @@ -0,0 +1,137 @@ +import json +import os + +import xbmcgui +import xbmcplugin +from resources.lib.rssaddon.abstract_rss_addon import AbstractRssAddon +from resources.lib.rssaddon.http_client import http_request + + +class BbcPodcastsAddon(AbstractRssAddon): + + API_BASE = "https://rms.api.bbc.co.uk" + API_SPEECH = "/v2/experience/inline/speech" + RSS_URL_PATTERN = "https://podcasts.files.bbci.co.uk/%s.rss" + + def __init__(self, addon_handle) -> None: + + super().__init__(addon_handle) + + def _make_root_menu(self) -> None: + + entries = list() + entries += self._get_entries_for_categories() + + for entry in entries: + self.add_list_item(entry, "") + + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_LABEL) + + xbmcplugin.endOfDirectory(self.addon_handle, updateListing=True) + + def _make_menu(self, path: str, params: 'dict[str]') -> None: + + if path.endswith("/"): + path = path[:-1] + + entries = self._get_podcasts(path, params) + for entry in entries: + self.add_list_item(entry, path) + + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_LABEL) + + xbmcplugin.endOfDirectory(self.addon_handle, updateListing=False) + + def _get_podcasts(self, url: str, params: 'dict[str]') -> 'list[dict]': + + url_param = "?%s" % "&".join( + ["%s=%s" % (k, params[k][0]) for k in params]) if len(params) > 0 else "" + url = self.API_BASE + "/" + "/".join(url.split("/")[2:]) + _data, _cookies = http_request(self.addon, url + url_param) + _json = json.loads(_data) + + entries = list() + + for _d in _json["data"]: + if "uris" not in _d or "download" not in _d or not _d["download"] or "quality_variants" not in _d["download"] or not _d["container"]: + continue + + has_media = [True for _quality in _d["download"]["quality_variants"] + if _d["download"]["quality_variants"][_quality]["file_url"]] + + entry = { + "path": "", + "name": _d["titles"]["primary"] + ("" if has_media else " ˟"), + "icon": _d["image_url"].replace("{recipe}", "896x896"), + "type": "music", + "params": [ + { + "rss": self.RSS_URL_PATTERN % _d["container"]["id"] + } + ], + "node": [] + } + + entries.append(entry) + + return entries + + def _get_entries_for_categories(self) -> 'list[dict]': + + _data, _cookies = http_request( + self.addon, "%s%s" % (self.API_BASE, self.API_SPEECH)) + _json = json.loads(_data) + + result = list() + + for _d in _json["data"]: + + if _d["type"] != "inline_display_module" or not _d["uris"]: + continue + + _name = _d["title"].strip() + _path: str = _d["uris"]["pagination"]["uri"] + _path = _path.replace("{offset}", str( + _d["uris"]["pagination"]["offset"])) + _path = _path.replace("{limit}", str( + _d["uris"]["pagination"]["total"])) + _data = "data" in _d and type(_d["data"]) == list + + if _path and _name and _data: + result.append({ + "path": "/__CATEGORIES__%s" % _path, + "name": _name, + "icon": os.path.join(self.addon_dir, "resources", "assets", "icon_category.png"), + "node": [] + }) + + return result + + def check_disclaimer(self) -> bool: + + if self.addon.getSetting("agreement") != "1": + answer = xbmcgui.Dialog().yesno(self.addon.getLocalizedString(32005), + self.addon.getLocalizedString(32010)) + + if answer: + self.addon.setSetting("agreement", "1") + return True + else: + return False + + else: + return True + + def route(self, path: str, url_params: 'dict[str]') -> None: + + if path in ["/"]: + self._make_root_menu() + + elif "__CATEGORIES__" in path: + self._make_menu(path, url_params) diff --git a/plugin.audio.bbcpodcasts/resources/lib/rssaddon/abstract_rss_addon.py b/plugin.audio.bbcpodcasts/resources/lib/rssaddon/abstract_rss_addon.py new file mode 100644 index 0000000000..3525df1daa --- /dev/null +++ b/plugin.audio.bbcpodcasts/resources/lib/rssaddon/abstract_rss_addon.py @@ -0,0 +1,316 @@ +import base64 +import os +import re +import urllib.parse +from datetime import datetime +from io import StringIO +from xml.etree.ElementTree import iterparse + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +import xbmcvfs +from resources.lib.rssaddon.http_client import http_request +from resources.lib.rssaddon.http_status_error import HttpStatusError + +# see https://forum.kodi.tv/showthread.php?tid=112916 +_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", + "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + + +class AbstractRssAddon: + + addon = None + addon_handle = None + addon_dir = None + anchor_for_latest = True + + def __init__(self, addon_handle): + + self.addon = xbmcaddon.Addon() + self.addon_handle = addon_handle + self.addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + + def handle(self, argv: 'list[str]') -> None: + + path = urllib.parse.urlparse(argv[0]).path.replace("//", "/") + url_params = urllib.parse.parse_qs(argv[2][1:]) + + if not self.check_disclaimer(): + path = "/" + url_params = list() + + if "rss" in url_params: + url = self.decode_param(url_params["rss"][0]) + limit = int(self.decode_param( + url_params["limit"][0])) if "limit" in url_params else 0 + offset = int(self.decode_param( + url_params["offset"][0])) if "offset" in url_params else 0 + self.render_rss(path, url, limit=limit, offset=offset) + + elif "play_latest" in url_params: + url = self.decode_param(url_params["play_latest"][0]) + self.play_latest(url) + else: + self.route(path, url_params) + + def decode_param(self, encoded_param: str) -> str: + + return base64.urlsafe_b64decode(encoded_param).decode("utf-8") + + def check_disclaimer(self) -> bool: + + return True + + def route(self, path: str, url_params): + + pass + + def is_force_http(self) -> bool: + + return False + + def _load_rss(self, url: str) -> 'tuple[str,str,str,list[dict]]': + + def parse_rss_feed(xml: str) -> 'tuple[str,str,str,list[dict]]': + + path = list() + + title = None + description = "" + image = None + items = list() + + for event, elem in iterparse(StringIO(xml), ("start", "end")): + + if event == "start": + path.append(elem.tag) + + if path == ["rss", "channel", "item"]: + item = dict() + + elif event == "end": + + if path == ["rss", "channel"]: + pass + + elif path == ["rss", "channel", "title"] and elem.text: + title = elem.text.strip() + + elif path == ["rss", "channel", "description"] and elem.text: + description = elem.text.strip() + + elif path == ["rss", "channel", "image", "url"] and elem.text: + image = elem.text.strip() + + elif (path == ["rss", "channel", "{http://www.itunes.com/dtds/podcast-1.0.dtd}image"] + and "href" in elem.attrib and not image): + image = elem.attrib["href"] + + elif path == ["rss", "channel", "item", "title"] and elem.text: + item["name"] = elem.text.strip() + + elif path == ["rss", "channel", "item", "description"] and elem.text: + item["description"] = elem.text.strip() + + elif path == ["rss", "channel", "item", "enclosure"]: + item["stream_url"] = elem.attrib["url"] if not self.is_force_http( + ) else elem.attrib["url"].replace("https://", "http://") + item["type"] = "video" if elem.attrib["type"].split( + "/")[0] == "video" else "music" + + elif (path == ["rss", "channel", "item", "{http://www.itunes.com/dtds/podcast-1.0.dtd}image"] + and elem.attrib["href"]): + item["icon"] = elem.attrib["href"] + + elif path == ["rss", "channel", "item", "pubDate"] and elem.text: + _f = re.findall( + "(\d{1,2}) (\w{3}) (\d{4}) (\d{2}):(\d{2}):(\d{2})", elem.text) + + if _f: + _m = _MONTHS.index(_f[0][1]) + 1 + item["date"] = datetime(year=int(_f[0][2]), month=_m, day=int(_f[0][0]), hour=int( + _f[0][3]), minute=int(_f[0][4]), second=int(_f[0][5])) + + elif path == ["rss", "channel", "item", "{http://www.itunes.com/dtds/podcast-1.0.dtd}duration"] and elem.text: + try: + duration = 0 + for i, s in enumerate(reversed(elem.text.split(":"))): + duration += 60**i * int(s) + + item["duration"] = duration + + except: + pass + + elif path == ["rss", "channel", "item"]: + + if "description" not in item: + item["description"] = "" + + if "icon" not in item: + item["icon"] = image + + if "stream_url" in item and item["stream_url"]: + items.append(item) + + elem.clear() + path.pop() + + return title, description, image, items + + xml, cookies = http_request(self.addon, url) + + if not xml.startswith(" None: + + pass + + def _create_list_item(self, item: dict) -> xbmcgui.ListItem: + + li = xbmcgui.ListItem(label=item["name"]) + + if "description" in item: + li.setProperty("label2", item["description"]) + + if "stream_url" in item: + li.setPath(item["stream_url"]) + + if "type" in item: + infos = { + "title": item["name"] + } + + if item["type"] == "video": + infos["plot"] = item["description"] if "description" in item else "" + + if "duration" in item and item["duration"] >= 0: + infos["duration"] = item["duration"] + + li.setInfo(item["type"], infos) + + if "icon" in item and item["icon"]: + li.setArt({"thumb": item["icon"]}) + else: + addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + li.setArt({"icon": os.path.join( + addon_dir, "resources", "assets", "icon.png")} + ) + + if "date" in item and item["date"]: + if "setDateTime" in dir(li): # available since Kodi v20 + li.setDateTime(item["date"].strftime("%Y-%m-%dT%H:%M:%SZ")) + else: + pass + + if "specialsort" in item: + li.setProperty("SpecialSort", item["specialsort"]) + + return li + + def add_list_item(self, entry: dict, path: str) -> None: + + def _build_param_string(params: 'list[str]', current="") -> str: + + if params == None: + return current + + for obj in params: + for name in obj: + enc_value = base64.urlsafe_b64encode( + obj[name].encode("utf-8")) + current += "?" if len(current) == 0 else "&" + current += name + "=" + str(enc_value, "utf-8") + + return current + + if path == "/": + path = "" + + item_path = path + "/" + entry["path"] + + param_string = "" + if "params" in entry: + param_string = _build_param_string(entry["params"], + current=param_string) + + li = self._create_list_item(entry) + + if "stream_url" in entry: + url = entry["stream_url"] + + else: + url = "".join( + ["plugin://", self.addon.getAddonInfo("id"), item_path, param_string]) + + is_folder = "node" in entry + li.setProperty("IsPlayable", "false" if is_folder else "true") + + xbmcplugin.addDirectoryItem(handle=self.addon_handle, + listitem=li, + url=url, + isFolder=is_folder) + + def render_rss(self, path: str, url: str, limit=0, offset=0) -> None: + + try: + title, description, image, items = self._load_rss(url) + + except HttpStatusError as error: + xbmc.log("HTTP Status Error: %s, path=%s" % + (error.message, path), xbmc.LOGERROR) + xbmcgui.Dialog().notification(self.addon.getLocalizedString(32151), error.message) + + else: + if len(items) > 0 and self.anchor_for_latest: + entry = { + "path": "latest", + "name": "%s (%s)" % (title, self.addon.getLocalizedString(32101)), + "description": description, + "icon": image, + "date": datetime.now(), + "specialsort": "top", + "type": items[0]["type"], + "params": [ + { + "play_latest": url + } + ] + } + self.add_list_item(entry, path) + + li = None + for i, item in enumerate(items): + if i >= offset and (not limit or i < offset + limit): + li = self._create_list_item(item) + xbmcplugin.addDirectoryItem(handle=self.addon_handle, + listitem=li, + url=item["stream_url"], + isFolder=False) + + if li and "setDateTime" in dir(li): # available since Kodi v20 + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.endOfDirectory(self.addon_handle) + + def play_latest(self, url: str) -> None: + + try: + title, description, image, items = self._load_rss(url) + item = items[0] + li = self._create_list_item(item) + xbmcplugin.setResolvedUrl(self.addon_handle, True, li) + + except HttpStatusError as error: + + xbmcgui.Dialog().notification(self.addon.getLocalizedString(32151), error.message) diff --git a/plugin.audio.bbcpodcasts/resources/lib/rssaddon/http_client.py b/plugin.audio.bbcpodcasts/resources/lib/rssaddon/http_client.py new file mode 100644 index 0000000000..2f34fd4cf1 --- /dev/null +++ b/plugin.audio.bbcpodcasts/resources/lib/rssaddon/http_client.py @@ -0,0 +1,36 @@ +from resources.lib.rssaddon.http_status_error import HttpStatusError + +import requests + +import xbmc + +def http_request(addon, url, headers=dict(), method="GET"): + + useragent = f"{addon.getAddonInfo('id')}/{addon.getAddonInfo('version')} (Kodi/{xbmc.getInfoLabel('System.BuildVersionShort')})" + headers["User-Agent"] = useragent + + if method == "GET": + req = requests.get + elif method == "POST": + req = requests.post + else: + raise HttpStatusError( + addon.getLocalizedString(32152) % method) + + try: + res = req(url, headers=headers) + except requests.exceptions.RequestException as error: + xbmc.log("Request Exception: %s" % str(error), xbmc.LOGERROR) + raise HttpStatusError(addon.getLocalizedString(32153)) + + if res.status_code == 200: + if res.encoding and res.encoding != "utf-8": + rv = res.text.encode(res.encoding).decode("utf-8") + else: + rv = res.text + + return rv, res.cookies + + else: + raise HttpStatusError(addon.getLocalizedString( + 32154) % (res.status_code, url)) \ No newline at end of file diff --git a/plugin.audio.bbcpodcasts/resources/lib/rssaddon/http_status_error.py b/plugin.audio.bbcpodcasts/resources/lib/rssaddon/http_status_error.py new file mode 100644 index 0000000000..ae3185f615 --- /dev/null +++ b/plugin.audio.bbcpodcasts/resources/lib/rssaddon/http_status_error.py @@ -0,0 +1,7 @@ +class HttpStatusError(Exception): + + message = "" + + def __init__(self, msg): + + self.message = msg \ No newline at end of file diff --git a/plugin.audio.bbcpodcasts/resources/settings.xml b/plugin.audio.bbcpodcasts/resources/settings.xml new file mode 100644 index 0000000000..5689ea2d35 --- /dev/null +++ b/plugin.audio.bbcpodcasts/resources/settings.xml @@ -0,0 +1,25 @@ + + +
+ + + + 0 + + false + + + 0 + 0 + + + + + + + + + + +
+
\ No newline at end of file diff --git a/plugin.audio.deutschlandfunk/addon.py b/plugin.audio.deutschlandfunk/addon.py new file mode 100644 index 0000000000..d8e4da48fa --- /dev/null +++ b/plugin.audio.deutschlandfunk/addon.py @@ -0,0 +1,8 @@ +from resources.lib.deutschlandfunk.deutschlandfunkaddon import DeutschlandfunkAddon + +import sys + +if __name__ == '__main__': + + deutschlandfunkAddon = DeutschlandfunkAddon(int(sys.argv[1])) + deutschlandfunkAddon.handle(sys.argv) diff --git a/plugin.audio.deutschlandfunk/addon.xml b/plugin.audio.deutschlandfunk/addon.xml new file mode 100644 index 0000000000..554f97efd8 --- /dev/null +++ b/plugin.audio.deutschlandfunk/addon.xml @@ -0,0 +1,66 @@ + + + + + + + + + + audio + + + Sender & Podcasts des Deutschlandfunks in KODI + Kodi Addon für Deutschlandfunk, Deutschlandfunk Kultur und +Deutschlandfunk Nova für Livestream und Podcasts + +Mit diesem Addon können Radioprogramme des Deutschlandfunks gehört werden. +Es können sowohl die Live Streams vom Deutschlandfunk, +Deutschlandfunk Kultur und Deutschlandfunk Nova als auch +sämtliche Podcasts der Sender gestreamt werden. + Der Author des Addons ist nicht verantwortlich für die Inhalte, die auf Ihr Gerät gestreamt werden. Insbesondere liegt es in der alleinigen Verantwortung und Pflicht des Addon-Nutzers sich davon zu überzeugen, dass die Urheberrechtsvereinbarungen der zum Streaming aufgerufenden Websites und Inhalte nicht verletzt werden. + de_DE + all + MIT + https://github.com/Heckie75/kodi-addon-deutschlandfunk + https://github.com/Heckie75/kodi-addon-deutschlandfunk/tree/master/plugin.audio.deutschlandfunk + +v2.0.8 (2023-11-26) +- Fixed URLs of live streams + +v2.0.7 (2023-03-05) +- Changed URL of Deutschlandfunk Kultur podcasts + +v2.0.6 (2023-02-02) +- Improved icon if item is added to favourites + +v2.0.5 (2022-07-04) +- Changed parser after changes on website +- Improved performance when loading rss feed with episodes + +v2.0.4 (2021-12-12) +- Changed URL for DRK to new location + +v2.0.3 (2021-11-15) +- Changed scraper for DLF and DRK again since previous version v2.0.2 didn't caught all contents + +v2.0.2 (2021-11-14) +- Changed scraper for DLF and DRK after changes on websites + +v2.0.1 (2021-06-27) +- Fixed incorrect navigation from specific podcast to station's podcasts menu when using Kodi Kore App +- Refactoring + +v2.0.0 (2021-05-13) +- Migration to Kodi 19 (Matrix) +- almost total rewrite + + + resources/assets/icon.png + resources/assets/fanart.png + resources/assets/screen1.png + resources/assets/screen2.png + resources/assets/screen3.png + + + diff --git a/plugin.audio.deutschlandfunk/resources/assets/fanart.png b/plugin.audio.deutschlandfunk/resources/assets/fanart.png new file mode 100644 index 0000000000..645b33b27a Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/fanart.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/icon.png b/plugin.audio.deutschlandfunk/resources/assets/icon.png new file mode 100644 index 0000000000..26c96fcc2d Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/icon.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/icon_dlf.png b/plugin.audio.deutschlandfunk/resources/assets/icon_dlf.png new file mode 100644 index 0000000000..eef26806f0 Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/icon_dlf.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/icon_dlf_rss.png b/plugin.audio.deutschlandfunk/resources/assets/icon_dlf_rss.png new file mode 100644 index 0000000000..3c23b8cdad Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/icon_dlf_rss.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/icon_drk.png b/plugin.audio.deutschlandfunk/resources/assets/icon_drk.png new file mode 100644 index 0000000000..b6721e6bfd Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/icon_drk.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/icon_drk_rss.png b/plugin.audio.deutschlandfunk/resources/assets/icon_drk_rss.png new file mode 100644 index 0000000000..6b3667e337 Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/icon_drk_rss.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/icon_nova.png b/plugin.audio.deutschlandfunk/resources/assets/icon_nova.png new file mode 100644 index 0000000000..51a780cb5b Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/icon_nova.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/icon_nova_rss.png b/plugin.audio.deutschlandfunk/resources/assets/icon_nova_rss.png new file mode 100644 index 0000000000..05a16a3108 Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/icon_nova_rss.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/screen1.png b/plugin.audio.deutschlandfunk/resources/assets/screen1.png new file mode 100644 index 0000000000..aa8680d938 Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/screen1.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/screen2.png b/plugin.audio.deutschlandfunk/resources/assets/screen2.png new file mode 100644 index 0000000000..d9724981b9 Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/screen2.png differ diff --git a/plugin.audio.deutschlandfunk/resources/assets/screen3.png b/plugin.audio.deutschlandfunk/resources/assets/screen3.png new file mode 100644 index 0000000000..237a236da6 Binary files /dev/null and b/plugin.audio.deutschlandfunk/resources/assets/screen3.png differ diff --git a/plugin.audio.deutschlandfunk/resources/language/resource.language.de_de/strings.po b/plugin.audio.deutschlandfunk/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..a199b1c921 --- /dev/null +++ b/plugin.audio.deutschlandfunk/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,38 @@ +# Kodi Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32101" +msgid "Most recent episode" +msgstr "aktuelle Sendung" + +msgctxt "#32151" +msgid "Connection Error" +msgstr "Verbindungsfehler" + +msgctxt "#32152" +msgid "HTTP Method %s not supported" +msgstr "HTTP Methode %s wird nicht unterstützt" + +msgctxt "#32153" +msgid "Request Exception: Pls. check URL and port. See logs for further details." +msgstr "Request Fehler: Prüfe URL und Port. Für weitere Details prüfe die Log-Datei." + +msgctxt "#32154" +msgid "Unexpected HTTP Status %i for %s" +msgstr "Unerwarteter HTTP Status %i für %s" + +msgctxt "#32155" +msgid "Unexpected content for podcast" +msgstr "Unerwarteter Inhalt für Podcast" diff --git a/plugin.audio.deutschlandfunk/resources/language/resource.language.en_gb/strings.po b/plugin.audio.deutschlandfunk/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..2e7af84773 --- /dev/null +++ b/plugin.audio.deutschlandfunk/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,38 @@ +# Kodi Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32101" +msgid "Most recent episode" +msgstr "" + +msgctxt "#32151" +msgid "Connection Error" +msgstr "" + +msgctxt "#32152" +msgid "HTTP Method %s not supported" +msgstr "" + +msgctxt "#32153" +msgid "Request Exception: Pls. check URL and port. See logs for further details." +msgstr "" + +msgctxt "#32154" +msgid "Unexpected HTTP Status %i for %s" +msgstr "" + +msgctxt "#32155" +msgid "Unexpected content for podcast" +msgstr "" diff --git a/plugin.audio.deutschlandfunk/resources/lib/deutschlandfunk/deutschlandfunkaddon.py b/plugin.audio.deutschlandfunk/resources/lib/deutschlandfunk/deutschlandfunkaddon.py new file mode 100644 index 0000000000..7829f01d2a --- /dev/null +++ b/plugin.audio.deutschlandfunk/resources/lib/deutschlandfunk/deutschlandfunkaddon.py @@ -0,0 +1,241 @@ +import json +import os + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +from bs4 import BeautifulSoup +from resources.lib.rssaddon.abstract_rss_addon import AbstractRssAddon +from resources.lib.rssaddon.http_client import http_request +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +class DeutschlandfunkAddon(AbstractRssAddon): + + __PLUGIN_ID__ = "plugin.audio.deutschlandfunk" + + URL_STREAM_DLF = "https://st01.sslstream.dlf.de/dlf/01/high/aac/stream.aac?aggregator=web" + URL_STREAM_DFK = "https://st02.sslstream.dlf.de/dlf/02/high/aac/stream.aac?aggregator=web" + URL_STREAM_NOVA = "https://st03.sslstream.dlf.de/dlf/03/high/aac/stream.aac?aggregator=web" + + URL_PODCASTS_DLF = "https://www.deutschlandfunk.de/podcasts" + URL_PODCASTS_DFK = "https://www.deutschlandfunkkultur.de/program-and-podcast" + URL_PODCASTS_NOVA = "https://www.deutschlandfunknova.de/podcasts" + + PATH_DLF = "dlf" + PATH_DLK = "dkultur" + PATH_NOVA = "nova" + PATH_PODCASTS = "podcasts" + + addon = xbmcaddon.Addon(id=__PLUGIN_ID__) + + def __init__(self, addon_handle): + + super().__init__(addon_handle) + + def _make_root_menu(self): + + nodes = [ + { + "path": DeutschlandfunkAddon.PATH_DLF, + "name": "Deutschlandfunk", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_dlf.png"), + "node": [] + }, + { + "path": DeutschlandfunkAddon.PATH_DLK, + "name": "Deutschlandfunk Kultur", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_drk.png"), + "node": [] + }, + { + "path": DeutschlandfunkAddon.PATH_NOVA, + "name": "Deutschlandfunk Nova", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_nova.png"), + "node": [] + } + ] + + for entry in nodes: + self.add_list_item(entry, "/") + + xbmcplugin.endOfDirectory(self.addon_handle, updateListing=False) + + def _make_station_menu(self, station): + + nodes = list() + if DeutschlandfunkAddon.PATH_DLF == station: + nodes.append({ + "path": "stream", + "name": "Deutschlandfunk", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_dlf.png"), + "stream_url": DeutschlandfunkAddon.URL_STREAM_DLF, + "type": "music", + "specialsort": "top" + }) + nodes.append({ + "path": DeutschlandfunkAddon.PATH_PODCASTS, + "name": "Podcasts", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_dlf_rss.png"), + "node": [] + }) + + elif DeutschlandfunkAddon.PATH_DLK == station: + nodes.append({ + "path": "stream", + "name": "Deutschlandfunk Kultur", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_drk.png"), + "stream_url": DeutschlandfunkAddon.URL_STREAM_DFK, + "type": "music", + "specialsort": "top" + }) + nodes.append({ + "path": DeutschlandfunkAddon.PATH_PODCASTS, + "name": "Podcasts", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_drk_rss.png"), + "node": [] + }) + + elif DeutschlandfunkAddon.PATH_NOVA == station: + nodes.append({ + "path": "stream", + "name": "Deutschlandfunk Nova", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_nova.png"), + "stream_url": DeutschlandfunkAddon.URL_STREAM_NOVA, + "type": "music", + "specialsort": "top" + }) + nodes.append({ + "path": DeutschlandfunkAddon.PATH_PODCASTS, + "name": "Podcasts", + "icon": os.path.join( + self.addon_dir, "resources", "assets", "icon_nova_rss.png"), + "node": [] + }) + + for entry in nodes: + self.add_list_item(entry, "/%s" % station) + + xbmcplugin.endOfDirectory(self.addon_handle, updateListing=False) + + def _parse_nova(self, path): + + _BASE_URL = "https://www.deutschlandfunknova.de/podcast/" + + # download html site with podcast overview + _data, _cookies = http_request(self.addon, self.URL_PODCASTS_NOVA) + + # parse site and read podcast meta data kindly provided as js + soup = BeautifulSoup(_data, 'html.parser') + _casts = soup.select('li.item') + + for _cast in _casts: + + _href = _cast.a.get("href") + _path = _href.replace("/podcasts/download/", "") + _img = _cast.img + + entry = { + "path": _path, + "name": _img.get("alt"), + "icon": _img.get("src"), + "params": [ + { + "rss": _BASE_URL + _path + } + ], + "node": [] + } + self.add_list_item(entry, path) + + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_LABEL) + + xbmcplugin.endOfDirectory(self.addon_handle, updateListing=False) + + def _parse_dlf(self, path, url): + + # download html site with podcast overview + _data, _cookies = http_request(self.addon, url) + + soup = BeautifulSoup(_data, "html.parser") + main = soup.find("main") + _script_js_client_queries = main.find_all( + "script", class_="js-client-queries") + + entries = list() + _img_src = None + + for _script in _script_js_client_queries: + + if not _script.has_attr("data-json"): + continue + + try: + _data_json = json.loads(_script["data-json"]) + if "value" not in _data_json or "__typename" not in _data_json["value"]: + continue + + if _data_json["value"]["__typename"] == "Image": + _img_src = _data_json["value"]["src"] + + elif _data_json["value"]["__typename"] == "Teaser" and "pathPodcast" in _data_json["value"]: + if not _data_json["value"]["pathPodcast"] or not _data_json["value"]["pathPodcast"].endswith(".xml"): + continue + + entry = { + "path": _data_json["value"]["sophoraId"], + "name": _data_json["value"]["title"], + "icon": _img_src, + "params": [ + { + "rss": _data_json["value"]["pathPodcast"] + } + ], + "node": [] + } + entries.append(entry) + + except: + _img_src = None + + uniq_entries = {entries[i]["params"][0]["rss"]: entries[i] for i in range(len(entries))} + uniq_entries = [uniq_entries[e] for e in uniq_entries] + uniq_entries.sort(key=lambda e: e["name"]) + + for entry in uniq_entries: + self.add_list_item(entry, path) + + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_LABEL) + + xbmcplugin.endOfDirectory(self.addon_handle, updateListing=False) + + def route(self, path, url_params): + + splitted_path = [n for n in path.split("/") if n != ""] + if len(splitted_path) == 2 and splitted_path[1] == DeutschlandfunkAddon.PATH_PODCASTS: + + if splitted_path[0] == DeutschlandfunkAddon.PATH_DLF: + self._parse_dlf(path, self.URL_PODCASTS_DLF) + + elif splitted_path[0] == DeutschlandfunkAddon.PATH_DLK: + self._parse_dlf(path, self.URL_PODCASTS_DFK) + + elif splitted_path[0] == DeutschlandfunkAddon.PATH_NOVA: + self._parse_nova(path) + + elif len(splitted_path) == 1 and splitted_path[0] in [DeutschlandfunkAddon.PATH_DLF, DeutschlandfunkAddon.PATH_DLK, DeutschlandfunkAddon.PATH_NOVA]: + self._make_station_menu(splitted_path[0]) + + else: + self._make_root_menu() diff --git a/plugin.audio.deutschlandfunk/resources/lib/rssaddon/abstract_rss_addon.py b/plugin.audio.deutschlandfunk/resources/lib/rssaddon/abstract_rss_addon.py new file mode 100644 index 0000000000..3525df1daa --- /dev/null +++ b/plugin.audio.deutschlandfunk/resources/lib/rssaddon/abstract_rss_addon.py @@ -0,0 +1,316 @@ +import base64 +import os +import re +import urllib.parse +from datetime import datetime +from io import StringIO +from xml.etree.ElementTree import iterparse + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +import xbmcvfs +from resources.lib.rssaddon.http_client import http_request +from resources.lib.rssaddon.http_status_error import HttpStatusError + +# see https://forum.kodi.tv/showthread.php?tid=112916 +_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", + "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + + +class AbstractRssAddon: + + addon = None + addon_handle = None + addon_dir = None + anchor_for_latest = True + + def __init__(self, addon_handle): + + self.addon = xbmcaddon.Addon() + self.addon_handle = addon_handle + self.addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + + def handle(self, argv: 'list[str]') -> None: + + path = urllib.parse.urlparse(argv[0]).path.replace("//", "/") + url_params = urllib.parse.parse_qs(argv[2][1:]) + + if not self.check_disclaimer(): + path = "/" + url_params = list() + + if "rss" in url_params: + url = self.decode_param(url_params["rss"][0]) + limit = int(self.decode_param( + url_params["limit"][0])) if "limit" in url_params else 0 + offset = int(self.decode_param( + url_params["offset"][0])) if "offset" in url_params else 0 + self.render_rss(path, url, limit=limit, offset=offset) + + elif "play_latest" in url_params: + url = self.decode_param(url_params["play_latest"][0]) + self.play_latest(url) + else: + self.route(path, url_params) + + def decode_param(self, encoded_param: str) -> str: + + return base64.urlsafe_b64decode(encoded_param).decode("utf-8") + + def check_disclaimer(self) -> bool: + + return True + + def route(self, path: str, url_params): + + pass + + def is_force_http(self) -> bool: + + return False + + def _load_rss(self, url: str) -> 'tuple[str,str,str,list[dict]]': + + def parse_rss_feed(xml: str) -> 'tuple[str,str,str,list[dict]]': + + path = list() + + title = None + description = "" + image = None + items = list() + + for event, elem in iterparse(StringIO(xml), ("start", "end")): + + if event == "start": + path.append(elem.tag) + + if path == ["rss", "channel", "item"]: + item = dict() + + elif event == "end": + + if path == ["rss", "channel"]: + pass + + elif path == ["rss", "channel", "title"] and elem.text: + title = elem.text.strip() + + elif path == ["rss", "channel", "description"] and elem.text: + description = elem.text.strip() + + elif path == ["rss", "channel", "image", "url"] and elem.text: + image = elem.text.strip() + + elif (path == ["rss", "channel", "{http://www.itunes.com/dtds/podcast-1.0.dtd}image"] + and "href" in elem.attrib and not image): + image = elem.attrib["href"] + + elif path == ["rss", "channel", "item", "title"] and elem.text: + item["name"] = elem.text.strip() + + elif path == ["rss", "channel", "item", "description"] and elem.text: + item["description"] = elem.text.strip() + + elif path == ["rss", "channel", "item", "enclosure"]: + item["stream_url"] = elem.attrib["url"] if not self.is_force_http( + ) else elem.attrib["url"].replace("https://", "http://") + item["type"] = "video" if elem.attrib["type"].split( + "/")[0] == "video" else "music" + + elif (path == ["rss", "channel", "item", "{http://www.itunes.com/dtds/podcast-1.0.dtd}image"] + and elem.attrib["href"]): + item["icon"] = elem.attrib["href"] + + elif path == ["rss", "channel", "item", "pubDate"] and elem.text: + _f = re.findall( + "(\d{1,2}) (\w{3}) (\d{4}) (\d{2}):(\d{2}):(\d{2})", elem.text) + + if _f: + _m = _MONTHS.index(_f[0][1]) + 1 + item["date"] = datetime(year=int(_f[0][2]), month=_m, day=int(_f[0][0]), hour=int( + _f[0][3]), minute=int(_f[0][4]), second=int(_f[0][5])) + + elif path == ["rss", "channel", "item", "{http://www.itunes.com/dtds/podcast-1.0.dtd}duration"] and elem.text: + try: + duration = 0 + for i, s in enumerate(reversed(elem.text.split(":"))): + duration += 60**i * int(s) + + item["duration"] = duration + + except: + pass + + elif path == ["rss", "channel", "item"]: + + if "description" not in item: + item["description"] = "" + + if "icon" not in item: + item["icon"] = image + + if "stream_url" in item and item["stream_url"]: + items.append(item) + + elem.clear() + path.pop() + + return title, description, image, items + + xml, cookies = http_request(self.addon, url) + + if not xml.startswith(" None: + + pass + + def _create_list_item(self, item: dict) -> xbmcgui.ListItem: + + li = xbmcgui.ListItem(label=item["name"]) + + if "description" in item: + li.setProperty("label2", item["description"]) + + if "stream_url" in item: + li.setPath(item["stream_url"]) + + if "type" in item: + infos = { + "title": item["name"] + } + + if item["type"] == "video": + infos["plot"] = item["description"] if "description" in item else "" + + if "duration" in item and item["duration"] >= 0: + infos["duration"] = item["duration"] + + li.setInfo(item["type"], infos) + + if "icon" in item and item["icon"]: + li.setArt({"thumb": item["icon"]}) + else: + addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + li.setArt({"icon": os.path.join( + addon_dir, "resources", "assets", "icon.png")} + ) + + if "date" in item and item["date"]: + if "setDateTime" in dir(li): # available since Kodi v20 + li.setDateTime(item["date"].strftime("%Y-%m-%dT%H:%M:%SZ")) + else: + pass + + if "specialsort" in item: + li.setProperty("SpecialSort", item["specialsort"]) + + return li + + def add_list_item(self, entry: dict, path: str) -> None: + + def _build_param_string(params: 'list[str]', current="") -> str: + + if params == None: + return current + + for obj in params: + for name in obj: + enc_value = base64.urlsafe_b64encode( + obj[name].encode("utf-8")) + current += "?" if len(current) == 0 else "&" + current += name + "=" + str(enc_value, "utf-8") + + return current + + if path == "/": + path = "" + + item_path = path + "/" + entry["path"] + + param_string = "" + if "params" in entry: + param_string = _build_param_string(entry["params"], + current=param_string) + + li = self._create_list_item(entry) + + if "stream_url" in entry: + url = entry["stream_url"] + + else: + url = "".join( + ["plugin://", self.addon.getAddonInfo("id"), item_path, param_string]) + + is_folder = "node" in entry + li.setProperty("IsPlayable", "false" if is_folder else "true") + + xbmcplugin.addDirectoryItem(handle=self.addon_handle, + listitem=li, + url=url, + isFolder=is_folder) + + def render_rss(self, path: str, url: str, limit=0, offset=0) -> None: + + try: + title, description, image, items = self._load_rss(url) + + except HttpStatusError as error: + xbmc.log("HTTP Status Error: %s, path=%s" % + (error.message, path), xbmc.LOGERROR) + xbmcgui.Dialog().notification(self.addon.getLocalizedString(32151), error.message) + + else: + if len(items) > 0 and self.anchor_for_latest: + entry = { + "path": "latest", + "name": "%s (%s)" % (title, self.addon.getLocalizedString(32101)), + "description": description, + "icon": image, + "date": datetime.now(), + "specialsort": "top", + "type": items[0]["type"], + "params": [ + { + "play_latest": url + } + ] + } + self.add_list_item(entry, path) + + li = None + for i, item in enumerate(items): + if i >= offset and (not limit or i < offset + limit): + li = self._create_list_item(item) + xbmcplugin.addDirectoryItem(handle=self.addon_handle, + listitem=li, + url=item["stream_url"], + isFolder=False) + + if li and "setDateTime" in dir(li): # available since Kodi v20 + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.endOfDirectory(self.addon_handle) + + def play_latest(self, url: str) -> None: + + try: + title, description, image, items = self._load_rss(url) + item = items[0] + li = self._create_list_item(item) + xbmcplugin.setResolvedUrl(self.addon_handle, True, li) + + except HttpStatusError as error: + + xbmcgui.Dialog().notification(self.addon.getLocalizedString(32151), error.message) diff --git a/plugin.audio.deutschlandfunk/resources/lib/rssaddon/http_client.py b/plugin.audio.deutschlandfunk/resources/lib/rssaddon/http_client.py new file mode 100644 index 0000000000..2f34fd4cf1 --- /dev/null +++ b/plugin.audio.deutschlandfunk/resources/lib/rssaddon/http_client.py @@ -0,0 +1,36 @@ +from resources.lib.rssaddon.http_status_error import HttpStatusError + +import requests + +import xbmc + +def http_request(addon, url, headers=dict(), method="GET"): + + useragent = f"{addon.getAddonInfo('id')}/{addon.getAddonInfo('version')} (Kodi/{xbmc.getInfoLabel('System.BuildVersionShort')})" + headers["User-Agent"] = useragent + + if method == "GET": + req = requests.get + elif method == "POST": + req = requests.post + else: + raise HttpStatusError( + addon.getLocalizedString(32152) % method) + + try: + res = req(url, headers=headers) + except requests.exceptions.RequestException as error: + xbmc.log("Request Exception: %s" % str(error), xbmc.LOGERROR) + raise HttpStatusError(addon.getLocalizedString(32153)) + + if res.status_code == 200: + if res.encoding and res.encoding != "utf-8": + rv = res.text.encode(res.encoding).decode("utf-8") + else: + rv = res.text + + return rv, res.cookies + + else: + raise HttpStatusError(addon.getLocalizedString( + 32154) % (res.status_code, url)) \ No newline at end of file diff --git a/plugin.audio.deutschlandfunk/resources/lib/rssaddon/http_status_error.py b/plugin.audio.deutschlandfunk/resources/lib/rssaddon/http_status_error.py new file mode 100644 index 0000000000..ae3185f615 --- /dev/null +++ b/plugin.audio.deutschlandfunk/resources/lib/rssaddon/http_status_error.py @@ -0,0 +1,7 @@ +class HttpStatusError(Exception): + + message = "" + + def __init__(self, msg): + + self.message = msg \ No newline at end of file diff --git a/plugin.audio.eco99music/LICENSE.txt b/plugin.audio.eco99music/LICENSE.txt new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/plugin.audio.eco99music/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugin.audio.eco99music/addon.py b/plugin.audio.eco99music/addon.py new file mode 100644 index 0000000000..5b83417a6f --- /dev/null +++ b/plugin.audio.eco99music/addon.py @@ -0,0 +1,164 @@ +# coding=utf-8 + +import sys +import os +import xbmcaddon +import xbmcgui +import xbmcplugin +import requests +import re +import xmltodict +from six.moves.urllib.parse import parse_qs, urlencode + + +def build_url(query): + """Build route url + + :param query: Dictionary to create URL for. + :type query: dict + :return: Complete route URL. + :rtype: str + """ + base_url = sys.argv[0] + return base_url + '?' + urlencode(query) + + +def get_rss(url): + """Download the source XML for given RSS URL using requests + and parse the page using xmltodict. + + :param url: URL of RSS page. + :type url: str + :return: Dictionary of parsed XML RSS page. + :rtype: dict + """ + return xmltodict.parse(requests.get(url).text) + + +def get_channels(): + """Extract channels from rss. + + :return: Return dictionary of received channels. + :rtype: dict + """ + rss = get_rss('http://eco99fm.maariv.co.il/RSS_MusicChannels_Index/') + channels = {} + index = 1 + + for item in rss["rss"]["channel"]["item"]: + channels.update({ + index: { + 'album_cover': + re.search("src='([^']+)'", item['description']).group(1), + 'title': + item['title'], + 'description': + item['itunes:summary'], + 'url': + build_url({ + 'mode': 'playlist', + 'url': item['link'] + }) + } + }) + index += 1 + return channels + + +def get_playlists(url): + """Get playlists of a channel. + + :param url: Channel rss url. + :type url: str + :return: Dictionary containing playlist items. + :rtype: dict + """ + rss = get_rss(url) + playlists = {} + index = 1 + for item in rss["rss"]["channel"]["item"]: + playlists.update({ + index: { + 'album_cover': + re.search("src='([^']+)'", item['description']).group(1), + 'title': + item['title'], + 'description': + item['itunes:summary'], + 'url': + build_url({ + 'mode': 'stream', + 'url': item['enclosure']['@url'] + }) + } + }) + index += 1 + return playlists + + +def build_menu(items, is_folder): + """Build menu control + + :param items: List of items, can be channels or playlist. + :type items: list + :param is_folder: If True the item is channel else a playlist. + :type is_folder: bool + """ + items_list = [] + + for item in items: + # create a list item using the song filename for the label + li = xbmcgui.ListItem(label=items[item]['title']) + # set the fanart to the album cover + if not is_folder: + li.setProperty('IsPlayable', 'true') + li.setProperty('PlotOutline', items[item]['description']) + li.setInfo( + 'video', { + 'title': items[item]['title'], + 'genre': 'Podcast', + 'plot': items[item]['description'] + }) + li.setArt({ + 'thumb': + items[item]['album_cover'], + 'poster': + items[item]['album_cover'], + 'fanart': + os.path.join(ADDON_FOLDER, 'resources', 'media', 'fanart.jpg') + }) + url = items[item]['url'] + items_list.append((url, li, is_folder)) + xbmcplugin.addDirectoryItems(ADDON_HANDLE, items_list, len(items_list)) + xbmcplugin.setContent(ADDON_HANDLE, 'songs') + xbmcplugin.endOfDirectory(ADDON_HANDLE) + + +def play(url): + """Play playlist by URL. + + :param url: URL of playlist. + :type url: str + """ + play_item = xbmcgui.ListItem(path=url) + xbmcplugin.setResolvedUrl(ADDON_HANDLE, True, listitem=play_item) + + +def main(): + """Main method.""" + args = parse_qs(sys.argv[2][1:]) + mode = args.get('mode', None) + if mode is None: + items = get_channels() + build_menu(items, True) + elif mode[0] == 'playlist': + items = get_playlists(args['url'][0]) + build_menu(items, False) + elif mode[0] == 'stream': + play(args['url'][0].replace('/playlist.m3u8', '')) + + +if __name__ == '__main__': + ADDON_FOLDER = xbmcaddon.Addon().getAddonInfo('path') + ADDON_HANDLE = int(sys.argv[1]) + main() diff --git a/plugin.audio.eco99music/addon.xml b/plugin.audio.eco99music/addon.xml new file mode 100644 index 0000000000..36e64995c7 --- /dev/null +++ b/plugin.audio.eco99music/addon.xml @@ -0,0 +1,42 @@ + + + + + + + + + + audio + + + אלפי פלייליסטים וסטים מוזיקליים, ערוכים ומוכנים להאזנה, ומותאמים לכל מצב, בכל מקום ובכל שעה + Thousands of premium music playlists for any mood, activity and time! + + ברוכים הבאים לתוסף eco99music מבית תחנת הרדיו eco99fm. מחכים לכם כאן אלפי פלייליסטים וסטים מוזיקליים, ערוכים ומוכנים להאזנה, ומותאמים לכל מצב, בכל מקום ובכל שעה. +תהנו, האזנה נעימה! +לתשומת לבכם- התוכן זמין ופעיל בישראל בלבד. + + + Welcome to eco99music, an innovative music application based on the consumer possibilities of the listener and browser, and adapts edited music playlists for the current and relevant status of each listener. The app offers the consumers +a unique and personalizes service, based on their location, activity, time of the day and their mood. +Enjoy! +… +FYI - our content is available only in Israel. + + This is an official ECO 99FM Kodi add-on + he + all + GPL-3.0-only + http://eco99fm.maariv.co.il/ + radio@eco99.fm + https://github.com/noamsto/plugin.audio.eco99music + + resources/media/icon.png + resources/media/fanart.jpg + resources/media/screenshot-01.jpg + resources/media/screenshot-02.jpg + resources/media/screenshot-03.jpg + + + diff --git a/plugin.audio.eco99music/changelog.txt b/plugin.audio.eco99music/changelog.txt new file mode 100644 index 0000000000..ea4823d54e --- /dev/null +++ b/plugin.audio.eco99music/changelog.txt @@ -0,0 +1,19 @@ +v0.0.1 +- Initial version + +v0.0.2 +- Use different link to play playlist + +v0.0.3 +- Update description & fanart + +v0.0.4 +- Fix the XML parsing errors + +v0.0.5 +- Fixed playlist not playing bug +- Ready for python3 + +v0.0.6 +- Bumped Python requirement to version 3.0.0 +- Kodi 19 compatible diff --git a/plugin.audio.eco99music/resources/media/fanart.jpg b/plugin.audio.eco99music/resources/media/fanart.jpg new file mode 100644 index 0000000000..7591546e23 Binary files /dev/null and b/plugin.audio.eco99music/resources/media/fanart.jpg differ diff --git a/plugin.audio.eco99music/resources/media/icon.png b/plugin.audio.eco99music/resources/media/icon.png new file mode 100644 index 0000000000..268fe64b63 Binary files /dev/null and b/plugin.audio.eco99music/resources/media/icon.png differ diff --git a/plugin.audio.eco99music/resources/media/screenshot-01.jpg b/plugin.audio.eco99music/resources/media/screenshot-01.jpg new file mode 100644 index 0000000000..55934930d0 Binary files /dev/null and b/plugin.audio.eco99music/resources/media/screenshot-01.jpg differ diff --git a/plugin.audio.eco99music/resources/media/screenshot-02.jpg b/plugin.audio.eco99music/resources/media/screenshot-02.jpg new file mode 100644 index 0000000000..4f928ef80e Binary files /dev/null and b/plugin.audio.eco99music/resources/media/screenshot-02.jpg differ diff --git a/plugin.audio.eco99music/resources/media/screenshot-03.jpg b/plugin.audio.eco99music/resources/media/screenshot-03.jpg new file mode 100644 index 0000000000..7635435d61 Binary files /dev/null and b/plugin.audio.eco99music/resources/media/screenshot-03.jpg differ diff --git a/plugin.audio.indigitube/LICENSE.txt b/plugin.audio.indigitube/LICENSE.txt new file mode 100644 index 0000000000..a625e2a375 --- /dev/null +++ b/plugin.audio.indigitube/LICENSE.txt @@ -0,0 +1,19 @@ +This plugin is Copyright (c) 2023 Simon Mollema. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugin.audio.indigitube/addon.xml b/plugin.audio.indigitube/addon.xml new file mode 100644 index 0000000000..1a3ccbf9d9 --- /dev/null +++ b/plugin.audio.indigitube/addon.xml @@ -0,0 +1,28 @@ + + + + + + + + audio + + + Unofficial plugin for indigiTUBE: the Australian national media platform by and for First Nations people. + MIT + indigiTUBE (www.indigitube.com.au) connects and supports the preservation of language and culture for our future generations. A living modern midden where technology and culture are woven together. indigiTUBE gathers First Nations stories from the desert to the sea, connecting and sharing culture from our extremely remote to urban regions; from our fresh new talent to archived histories. indigiTUBE is a digital meeting place for First Nations song, dance, language and lore; creating a unified space to share our evolving and living culture. It also live streams 27 different radio stations to hear what's going on around the country. The visually stunning media platform reflects the rich culture of our First Nations people, and the vibrant colours represent ochre, land and sea. + all + xbmc@molzy.com + https://github.com/molzy/plugin.audio.indigitube + v1.0.0 +- Initial release, with content from the homepage including radio, albums, videos and more + + + resources/icon.png + resources/fanart.jpg + resources/screenshots/home.jpg + resources/screenshots/radio.jpg + resources/screenshots/album.jpg + + + diff --git a/plugin.audio.indigitube/default.py b/plugin.audio.indigitube/default.py new file mode 100644 index 0000000000..521303d240 --- /dev/null +++ b/plugin.audio.indigitube/default.py @@ -0,0 +1,136 @@ +import sys, json, re +from urllib.request import Request, urlopen +from urllib.parse import parse_qs, urlencode + +import xbmcgui +import xbmcplugin +import xbmcaddon +import xbmc +from resources.lib.ListItems import ListItems + +try: + import StorageServer +except: + from resources.lib.cache import storageserverdummy as StorageServer +cache = StorageServer.StorageServer('plugin.audio.indigitube', 24) # (Your plugin name, Cache time in hours) + +USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' +CONTENT_QUERY = 'https://api.appbooks.com/content/_query/{}' +CONTENT_CHANNEL = 'https://api.appbooks.com/content/channel/{}' +CONTENT_ALBUM = 'https://api.appbooks.com/content/album/{}' +CONTENT_PAGE = 'https://api.appbooks.com/content/pageConfiguration/{}' + +PAGE_HOME = '5b5abc2bf6b4d90e6deeaabb' +QUERY_RADIO = '5d1aeac759dd785afe88ec0b' +CHANNEL_RADIO = '5b5ac73df6b4d90e6deeabd1' + +def urlopen_ua(url): + headers = { + 'User-Agent': USER_AGENT, + 'Authorization': 'Bearer $2a$10$x2Zy/TgIAOC0UUMi3NPKc.KY49e/ZLUJFOpBCNYAs8D72UUnlI526', + } + return urlopen(Request(url, headers=headers), timeout=5) + +def get_json(url): + return urlopen_ua(url).read().decode() + +def get_json_obj(url): + return json.loads(get_json(url)) + +def get_query_content(media_id): + return get_json_obj(CONTENT_QUERY.format(media_id)) + +def get_channel_content(media_id): + return get_json_obj(CONTENT_CHANNEL.format(media_id)) + +def get_album_content(media_id): + return get_json_obj(CONTENT_ALBUM.format(media_id)) + +def get_page_content(page_id): + return get_json_obj(CONTENT_PAGE.format(page_id)) + + +def build_main_menu(): + home_json = get_page_content(PAGE_HOME) + root_items = list_items.get_root_items(home_json) + if len(root_items) > 0: + xbmcplugin.setPluginCategory(addon_handle, home_json.get('title', '')) + xbmcplugin.addDirectoryItems(addon_handle, root_items, len(root_items)) + xbmcplugin.endOfDirectory(addon_handle) + +def build_query_list(query_id, title=''): + query_json = get_query_content(query_id) + query_items = list_items.get_query_items(query_json) + if len(query_items) > 0: + xbmcplugin.setPluginCategory(addon_handle, title) + xbmcplugin.addDirectoryItems(addon_handle, query_items, len(query_items)) + xbmcplugin.endOfDirectory(addon_handle) + +def build_channel_list(channel_id): + channel_json = get_channel_content(channel_id) + channel_data = channel_json.get('data', {}) + if len(channel_data.get('items', [])) > 1: + channel_items = list_items.get_channel_items(channel_json) + if len(channel_items) > 0: + xbmcplugin.setPluginCategory(addon_handle, channel_json.get('title', '')) + xbmcplugin.addDirectoryItems(addon_handle, channel_items, len(channel_items)) + xbmcplugin.endOfDirectory(addon_handle) + else: + query = channel_data.get('query', {}).get('_id') + return build_query_list(query, title=channel_json.get('title')) + +def build_song_list(album_id): + album_json = get_album_content(album_id) + album_items = list_items.get_track_items(album_json) + if len(album_items) > 0: + xbmcplugin.setPluginCategory(addon_handle, album_json.get('title', '')) + xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.setContent(addon_handle, 'songs') + xbmcplugin.addDirectoryItems(addon_handle, album_items, len(album_items)) + xbmcplugin.endOfDirectory(addon_handle) + +def play_song(url): + play_item = xbmcgui.ListItem(path=url) + xbmcplugin.setResolvedUrl(addon_handle, True, listitem=play_item) + + +def main(): + if addon.getSettingBool('first_run'): + xbmcgui.Dialog().textviewer(get_string(30098), get_string(30099)) + if not list_items.matrix: + explicit = xbmcgui.Dialog().yesno(get_string(30030), get_string(30032), defaultbutton=xbmcgui.DLG_YESNO_YES_BTN) + else: + explicit = xbmcgui.Dialog().yesno(get_string(30030), get_string(30032)) + # deceased = xbmcgui.Dialog().yesno(get_string(30040), get_string(30042), defaultbutton=xbmcgui.DLG_YESNO_YES_BTN) + addon.setSettingBool('allow_explicit', explicit) + # addon.setSettingBool('allow_deceased', deceased) + addon.setSettingBool('first_run', False) + args = parse_qs(sys.argv[2][1:]) + mode = args.get('mode', None) + if mode is None: + build_main_menu() + elif mode[0] == 'explicit': + xbmcgui.Dialog().notification(get_string(30033), get_string(30034)) + elif mode[0] == 'stream': + play_song(args.get('url', [''])[0]) + elif mode[0] == 'list_radio': + build_query_list(QUERY_RADIO) + elif mode[0] == 'list_query': + query_id = args.get('query_id', [''])[0] + build_query_list(query_id) + elif mode[0] == 'list_channel': + channel_id = args.get('channel_id', [''])[0] + build_channel_list(channel_id) + elif mode[0] == 'list_songs': + album_id = args.get('album_id', [''])[0] + build_song_list(album_id) + +def get_string(string_id): + return addon.getLocalizedString(string_id) + +if __name__ == '__main__': + xbmc.log("indigiTUBE plugin called: " + str(sys.argv), xbmc.LOGDEBUG) + addon = xbmcaddon.Addon() + list_items = ListItems(addon) + addon_handle = int(sys.argv[1]) + main() diff --git a/plugin.audio.indigitube/resources/fanart.jpg b/plugin.audio.indigitube/resources/fanart.jpg new file mode 100644 index 0000000000..083b617005 Binary files /dev/null and b/plugin.audio.indigitube/resources/fanart.jpg differ diff --git a/plugin.audio.indigitube/resources/icon.png b/plugin.audio.indigitube/resources/icon.png new file mode 100644 index 0000000000..92ce6a9cfa Binary files /dev/null and b/plugin.audio.indigitube/resources/icon.png differ diff --git a/plugin.audio.indigitube/resources/language/resource.language.en_gb/strings.po b/plugin.audio.indigitube/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..5aec25ed31 --- /dev/null +++ b/plugin.audio.indigitube/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,96 @@ +# Kodi Media Center language file +# Addon Name: Indigitube +# Addon id: plugin.audio.indigitube +# Addon Provider: Simon Mollema + +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings - General +msgctxt "#30001" +msgid "General" +msgstr "" + +msgctxt "#30011" +msgid "General settings for the indigiTUBE addon" +msgstr "" + +msgctxt "#30020" +msgid "Image Quality" +msgstr "" + +msgctxt "#30021" +msgid "Set the desired quality level for album art and band images" +msgstr "" + +msgctxt "#30022" +msgid "High" +msgstr "" + +msgctxt "#30023" +msgid "Medium" +msgstr "" + +msgctxt "#30024" +msgid "Low" +msgstr "" + +msgctxt "#30030" +msgid "Allow Explicit Language" +msgstr "" + +msgctxt "#30031" +msgid "When turned off, content containing explicit language will be filtered." +msgstr "" + +msgctxt "#30032" +msgid "WARNING: Some content contains explicit language.\nAllow explicit language?\nYour selection can be changed later in the add-on settings." +msgstr "" + +msgctxt "#30033" +msgid "Contains Explicit Language" +msgstr "" + +msgctxt "#30034" +msgid "Explicit language is disabled in the add-on settings." +msgstr "" + +msgctxt "#30040" +msgid "Allow Content Containing Deceased People" +msgstr "" + +msgctxt "#30041" +msgid "When turned off, content containing deceased people will be filtered." +msgstr "" + +msgctxt "#30032" +msgid "WARNING: Some content contains images, voices and names of deceased people.\nAllow this content?\nYour selection can be changed later in the add-on settings." +msgstr "" + +msgctxt "#30098" +msgid "Acknowledgement Of Country" +msgstr "" + +msgctxt "#30099" +msgid "indigiTUBE acknowledges our traditional custodians of country where we live and work.\n\nWe pay respect to Elders past, present and emerging, and acknowledge our continuous connection and contribution to land, sea and community.\n\nWARNING: This add-on contains images, voices and names of deceased people." +msgstr "" + +# GUI main menu +msgctxt "#30101" +msgid "Listen To Live Radio" +msgstr "" + +msgctxt "#30103" +msgid "Search" +msgstr "" diff --git a/plugin.audio.indigitube/resources/lib/ListItems.py b/plugin.audio.indigitube/resources/lib/ListItems.py new file mode 100644 index 0000000000..dc064558f2 --- /dev/null +++ b/plugin.audio.indigitube/resources/lib/ListItems.py @@ -0,0 +1,310 @@ +import sys, re, os +import xbmc +import xbmcgui +from urllib.parse import urlencode + +class ListItems: + INDIGITUBE_ACCESS_KEY = 'access_token=%242a%2410%24x2Zy%2FTgIAOC0UUMi3NPKc.KY49e%2FZLUJFOpBCNYAs8D72UUnlI526' + INDIGITUBE_ALBUM_URL = 'https://api.appbooks.com/content/album/{}?' + INDIGITUBE_ACCESS_KEY + INDIGITUBE_TRACK_URL = 'https://api.appbooks.com/get/{}?' + INDIGITUBE_ACCESS_KEY + INDIGITUBE_VIDEO_URL = 'https://api.appbooks.com/get/{}?variant=720&' + INDIGITUBE_ACCESS_KEY + INDIGITUBE_ALBUM_ART_URL = 'https://api.appbooks.com/get/{}/file/file.jpg?w={}&quality=90&' + INDIGITUBE_ACCESS_KEY + '&ext=.jpg' + QUERY_RADIO = '5b5ac73df6b4d90e6deeabd1' + NOWHERE = 'plugin://plugin.audio.indigitube/?mode=explicit' + + def __init__(self, addon): + self.matrix = '19.' in xbmc.getInfoLabel('System.BuildVersion') + self.addon = addon + self.allow_explicit = self.addon.getSettingBool('allow_explicit') + self.allow_deceased = self.addon.getSettingBool('allow_deceased') + self._respath = os.path.join(self.addon.getAddonInfo('path'), 'resources') + self.fanart = os.path.join(self._respath, 'fanart.jpg') + quality = self.addon.getSetting('image_quality') + self.quality = int(quality) if quality else 1 + + def _album_quality(self): + if self.quality == 0: + return '700' + if self.quality == 1: + return '350' + if self.quality == 2: + return '200' + + def _build_url(self, query): + base_url = sys.argv[0] + return base_url + '?' + urlencode(query) + + def get_item(self, item_json, args={}): + definition = item_json.get('definition', item_json.get('file', {}).get('definition')) + if definition == 'radioStation': + return self.get_radio_station_item(item_json) + elif definition == 'channel': + if item_json.get('_id') == self.QUERY_RADIO: + return self.get_channel_item(item_json, query=True) + else: + return self.get_channel_item(item_json) + elif definition == 'audioContent': + return self.get_track_item(item_json, args) + elif definition == 'videoContent': + return self.get_video_item(item_json) + elif definition == 'album': + return self.get_album_item(item_json) + + def get_radio_station_item(self, item_json): + item_data = item_json.get('data', {}) + title = item_json.get('title', '') + if not self.allow_deceased: + if item_data.get('deceasedContent', 'no') != "no": + return + artist = item_json.get('realms', [{}])[0].get('title') + url = item_data.get('feedSource') + desc = item_data.get('description') + textbody = re.compile(r'<[^>]+>').sub('', desc) + art_id = item_data.get('coverImage', '') + if not isinstance(art_id, str): + art_id = art_id.get('_id') + art_url = self.INDIGITUBE_ALBUM_ART_URL.format(art_id, self._album_quality()) + + if item_data.get('explicit'): + title += ' (Explicit)' + if not self.allow_explicit: + url = self.NOWHERE + if artist: + title = title + ' - ' + artist + li = xbmcgui.ListItem(label=title, offscreen=True) + if not self.matrix: + vi = li.getVideoInfoTag() + vi.setTitle(title) + vi.setPlot(textbody) + else: # Matrix v19.0 + vi = { + 'title': title, + 'plot': textbody, + } + li.setInfo('video', vi) + li.setArt({'thumb': art_url, 'fanart': self.fanart}) + li.setProperty('IsPlayable', 'true') + li.setPath(url) + return (url, li, False) + + def get_channel_item(self, item_json, query=False): + item_data = item_json.get('data', {}) + title = item_json.get('title', '') + if not self.allow_deceased: + if item_data.get('deceasedContent', 'no') != "no": + return + if query: + mode = 'list_query' + key_id = 'query_id' + item_id = item_json.get('data', {}).get('query') + else: + mode = 'list_channel' + key_id = 'channel_id' + item_id = item_json.get('_id') + desc = item_data.get('description', '') + textbody = re.compile(r'<[^>]+>').sub('', desc) + url = self._build_url({'mode': mode, key_id: item_id}) + + if item_data.get('allExplicit'): + title += ' (Explicit)' + if not self.allow_explicit: + url = self.NOWHERE + li = xbmcgui.ListItem(label=title, offscreen=True) + if not self.matrix: + vi = li.getVideoInfoTag() + vi.setTitle(title) + vi.setPlot(textbody) + else: # Matrix v19.0 + vi = { + 'title': title, + 'plot': textbody, + } + li.setInfo('video', vi) + li.setArt({'fanart': self.fanart}) + li.setPath(url) + return (url, li, True) + + def get_album_item(self, item_json): + item_data = item_json.get('data', {}) + if not self.allow_deceased: + if item_data.get('deceasedContent', 'no') != "no": + return + title = item_json.get('title', '') + artist = item_data.get('artist', '') + desc = item_data.get('description', '') + textbody = re.compile(r'<[^>]+>').sub('', desc) + art_id = item_data.get('coverImage', '') + if not isinstance(art_id, str): + art_id = art_id.get('_id') + art_url = self.INDIGITUBE_ALBUM_ART_URL.format(art_id, self._album_quality()) + + if item_data.get('allExplicit') or item_data.get('explicit'): + title += ' (Explicit)' + if artist: + title = title + ' - ' + artist + folder = False + + if len(item_data.get('items', [])) > 1: + url = self._build_url({'mode': 'list_songs', 'album_id': item_json.get('_id')}) + + folder = True + if item_data.get('allExplicit'): + if not self.allow_explicit: + folder = False + url = self.NOWHERE + li = xbmcgui.ListItem(label=title, offscreen=True) + else: + item = item_data.get('items', [])[0] + file = item.get('file', '') + if not isinstance(file, str): + file = file.get('_id') + url = self.INDIGITUBE_TRACK_URL.format(file) + + li = xbmcgui.ListItem(label=title, offscreen=True) + li.setProperty('IsPlayable', 'true') + if item_data.get('explicit'): + if not self.allow_explicit: + li.setProperty('IsPlayable', 'false') + url = self.NOWHERE + + li.setArt({'thumb': art_url, 'fanart': self.fanart}) + if not self.matrix: + vi = li.getVideoInfoTag() + vi.setTitle(title) + vi.setPlot(textbody) + else: # Matrix v19.0 + vi = { + 'title': title, + 'plot': textbody, + } + li.setInfo('video', vi) + li.setPath(url) + return (url, li, folder) + + def get_track_item(self, item_json, args): + title = item_json.get('title', '') + artist = item_json.get('artist', '') + file = item_json.get('file', '') + if not isinstance(file, str): + file = file.get('_id') + url = self.INDIGITUBE_TRACK_URL.format(file) + + if item_json.get('explicit'): + title += ' (Explicit)' + if not self.allow_explicit: + url = self.NOWHERE + li = xbmcgui.ListItem(label=title, offscreen=True) + # if not self.matrix: + if False: + mi = li.getMusicInfoTag() + mi.setTitle(title) + mi.setArtist(artist) + mi.setMediaType('song') + if args.get('album'): + mi.setAlbum(args.get('album')) + if args.get('album_artist'): + mi.setAlbumArtist(args.get('album_artist')) + # setTrack doesn't work in v20. Bypassed + mi.setTrack(args.get('track_number')) + else: # Matrix v19.0 + mi = { + 'title': title, + 'artist': artist, + 'mediatype': 'song', + 'album': args.get('album'), + 'albumartist': args.get('album_artist'), + 'tracknumber': args.get('track_number'), + } + li.setInfo('music', mi) + li.setArt({'thumb': args.get('art_url'), 'fanart': self.fanart}) + li.setProperty('IsPlayable', 'true') + li.setPath(url) + return (url, li, False) + + def get_video_item(self, item_json): + item_data = item_json.get('data', {}) + title = item_json.get('title', '') + duration = int(item_json.get('duration', 0)) + url = self.INDIGITUBE_VIDEO_URL.format(item_json.get('_id')) + desc = item_data.get('description', '') + textbody = re.compile(r'<[^>]+>').sub('', desc) + art_id = item_json.get('poster', '') + if not isinstance(art_id, str) and len(art_id) > 0: + art_id = art_id[0] + + if item_data.get('explicit'): + title += ' (Explicit)' + if not self.allow_explicit: + url = self.NOWHERE + li = xbmcgui.ListItem(label=title, offscreen=True) + if not self.matrix: + vi = li.getVideoInfoTag() + vi.setTitle(title) + vi.setDuration(duration) + if textbody: + vi.setPlot(textbody) + else: + vi = { + 'title': title, + 'duration': duration, + 'plot': textbody, + } + li.setInfo('video', vi) + if art_id: + art_url = self.INDIGITUBE_ALBUM_ART_URL.format(art_id, self._album_quality()) + li.setArt({'thumb': art_url, 'fanart': self.fanart}) + li.setProperty('IsPlayable', 'true') + if item_data.get('explicit'): + if not self.allow_explicit: + url = self.NOWHERE + li.setProperty('IsPlayable', 'false') + li.setPath(url) + return (url, li, False) + + + def get_root_items(self, page_json): + page_data = page_json.get('data', {}) + items = [] + for carousel in page_data.get('carousels', []): + item = self.get_item(carousel) + if item: + items.append(item) + return items + + def get_query_items(self, query_json): + items = [] + for station in query_json: + item = self.get_item(station) + if item: + items.append(item) + # items.sort(key=lambda x: x[1].getLabel()) + return items + + def get_channel_items(self, channel_json): + channel_data = channel_json.get('data', {}) + channel_items = channel_data.get('items', {}) + items = [] + for channel in channel_items: + if channel.get('item'): + item = self.get_item(channel.get('item')) + if item: + items.append(item) + return items + + def get_track_items(self, album_json): + items = [] + album_title = album_json.get('title', '') + album_artist = album_json.get('realms', [{}])[0].get('title', '') + album_data = album_json.get('data', {}) + art_id = album_data.get('coverImage', {}).get('_id') + album_art_url = self.INDIGITUBE_ALBUM_ART_URL.format(art_id, self._album_quality()) + args = { + 'track_number': 1, + 'album': album_title, + 'album_artist': album_artist, + 'art_url': album_art_url, + } + for track in album_data.get('items', []): + items.append(self.get_item(track, args)) + args['track_number'] += 1 + return items diff --git a/plugin.audio.indigitube/resources/screenshots/album.jpg b/plugin.audio.indigitube/resources/screenshots/album.jpg new file mode 100644 index 0000000000..8f2dd100d7 Binary files /dev/null and b/plugin.audio.indigitube/resources/screenshots/album.jpg differ diff --git a/plugin.audio.indigitube/resources/screenshots/home.jpg b/plugin.audio.indigitube/resources/screenshots/home.jpg new file mode 100644 index 0000000000..c5ccdaee77 Binary files /dev/null and b/plugin.audio.indigitube/resources/screenshots/home.jpg differ diff --git a/plugin.audio.indigitube/resources/screenshots/radio.jpg b/plugin.audio.indigitube/resources/screenshots/radio.jpg new file mode 100644 index 0000000000..0a6fd4366c Binary files /dev/null and b/plugin.audio.indigitube/resources/screenshots/radio.jpg differ diff --git a/plugin.audio.indigitube/resources/settings.xml b/plugin.audio.indigitube/resources/settings.xml new file mode 100644 index 0000000000..1f6da85d8c --- /dev/null +++ b/plugin.audio.indigitube/resources/settings.xml @@ -0,0 +1,36 @@ + + +
+ + + + 4 + true + + + 0 + true + + + + 4 + true + + + 0 + 1 + + + + + + + + + 30020 + + + + +
+
diff --git a/plugin.audio.kvartal/LICENSE.txt b/plugin.audio.kvartal/LICENSE.txt new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/plugin.audio.kvartal/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.audio.kvartal/README.md b/plugin.audio.kvartal/README.md new file mode 100644 index 0000000000..d18761d2e2 --- /dev/null +++ b/plugin.audio.kvartal/README.md @@ -0,0 +1,20 @@ +# Kvartal +Kodi add-on for listening to podcasts published by Kvartal: https://kvartal.se. + +## Screenshots + + + + + + + +
+ +## Disclaimer +This add-on is unofficial and its author has no affiliation with https://kvartal.se. + +## Contact +email: andrelofgren@hotmail.co.uk + + diff --git a/plugin.audio.kvartal/addon.py b/plugin.audio.kvartal/addon.py new file mode 100644 index 0000000000..7e07b45feb --- /dev/null +++ b/plugin.audio.kvartal/addon.py @@ -0,0 +1,5 @@ +from resources.lib import plugin + + +if __name__ == "__main__": + plugin.run() diff --git a/plugin.audio.kvartal/addon.xml b/plugin.audio.kvartal/addon.xml new file mode 100644 index 0000000000..9242c394b4 --- /dev/null +++ b/plugin.audio.kvartal/addon.xml @@ -0,0 +1,28 @@ + + + + + + + + + audio + + + Swedish online journal publishing podcasts and articles with a focus on public affair journalism. + Svensk nättidskrift som publicerar poddar och texter med samhällsjournalistisk inriktning. + This add-on is unofficial and is not supported nor endorsed by https://kvartal.se. + Detta är ett inofficiellt add-on oavhängig https://kvartal.se. + GPL-2.0-or-later + all + sv + + resources/icon.png + resources/fanart.jpg + resources/screenshot-01.jpg + resources/screenshot-02.jpg + resources/screenshot-03.jpg + resources/screenshot-04.jpg + + + diff --git a/plugin.audio.kvartal/resources/__init__.py b/plugin.audio.kvartal/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.kvartal/resources/fanart.jpg b/plugin.audio.kvartal/resources/fanart.jpg new file mode 100644 index 0000000000..316e50f7c9 Binary files /dev/null and b/plugin.audio.kvartal/resources/fanart.jpg differ diff --git a/plugin.audio.kvartal/resources/icon.png b/plugin.audio.kvartal/resources/icon.png new file mode 100644 index 0000000000..03a218d70e Binary files /dev/null and b/plugin.audio.kvartal/resources/icon.png differ diff --git a/plugin.audio.kvartal/resources/language/resource.language.en_gb/strings.po b/plugin.audio.kvartal/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..108e51def0 --- /dev/null +++ b/plugin.audio.kvartal/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,38 @@ + +# Kodi Media Center language file +# Addon Name: Kvartal +# Addon id: plugin.audio.kvartal +# Addon Provider: Ikosse +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_GB\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "The Swedish model" +msgstr "" + +msgctxt "#30001" +msgid "The Friday interview" +msgstr "" + +msgctxt "#30002" +msgid "Spoken essays" +msgstr "" + +msgctxt "#30003" +msgid "The weekly panel" +msgstr "" + +msgctxt "#30004" +msgid "About" +msgstr "" diff --git a/plugin.audio.kvartal/resources/language/resource.language.sv_se/strings.po b/plugin.audio.kvartal/resources/language/resource.language.sv_se/strings.po new file mode 100644 index 0000000000..59c032f828 --- /dev/null +++ b/plugin.audio.kvartal/resources/language/resource.language.sv_se/strings.po @@ -0,0 +1,38 @@ + +# Kodi Media Center language file +# Addon Name: Kvartal +# Addon id: plugin.audio.kvartal +# Addon Provider: Ikosse +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv_SE\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "The Swedish model" +msgstr "Den svenska modellen" + +msgctxt "#30001" +msgid "The Friday interview" +msgstr "Fredagsintervjun" + +msgctxt "#30002" +msgid "Spoken essays" +msgstr "Inlästa texter" + +msgctxt "#30003" +msgid "The weekly panel" +msgstr "Veckopanelen" + +msgctxt "#30004" +msgid "About" +msgstr "Om" diff --git a/plugin.audio.kvartal/resources/lib/__init__.py b/plugin.audio.kvartal/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.kvartal/resources/lib/api.py b/plugin.audio.kvartal/resources/lib/api.py new file mode 100644 index 0000000000..5e339caf8b --- /dev/null +++ b/plugin.audio.kvartal/resources/lib/api.py @@ -0,0 +1,44 @@ +import re +from resources.lib.kodiutils import AddonUtils +from resources.lib.webutils import WebScraper + + +class Kvartal(): + + addon_utils = AddonUtils() + shows = [{"name": addon_utils.localize(30000), "suburl": "den-svenska-modellen"}, + {"name": addon_utils.localize(30001), "suburl": "fredagsintervjun"}, + {"name": addon_utils.localize(30002), "suburl": "inlasta-essaer"}, + {"name": addon_utils.localize(30003), "suburl": "veckopanelen"}] + + def __init__(self): + self.scraper = WebScraper() + + def get_content(self, show_id): + base_url = "https://feeder.acast.com/api/v1/shows" + content_url = "{0}/{1}".format(base_url, self.shows[show_id]["suburl"]) + show = self.scraper.get_json(content_url) + for episode in show["episodes"]: + summary = self._extract_string_from_html(episode["summary"]) + item = {"label": episode["title"], + "summary": summary, + "date": episode["publishDate"].split("T")[0], + "media_url": episode["url"], + "image_url": show["image"]} + yield item + + def get_show_summary(self, show_id): + base_url = "https://feeder.acast.com/api/v1/shows" + content_url = "{0}/{1}".format(base_url, self.shows[show_id]["suburl"]) + show = self.scraper.get_json(content_url) + summary = self._extract_string_from_html(show["description"]) + return summary[:-2] + + def _extract_string_from_html(self, summary_html): + start = 0 + stop = summary_html.find(" ") - 1 + summary = summary_html[start:stop] + pattern = re.compile("<(.*?)>") + summary = pattern.sub("", summary).replace(" ", " ") + summary = re.sub(r" +", " ", summary) + return summary diff --git a/plugin.audio.kvartal/resources/lib/kodiutils.py b/plugin.audio.kvartal/resources/lib/kodiutils.py new file mode 100644 index 0000000000..a4f5c37e6b --- /dev/null +++ b/plugin.audio.kvartal/resources/lib/kodiutils.py @@ -0,0 +1,39 @@ +import os +import sys +import xbmc +import xbmcaddon +import xbmcvfs + + +class AddonUtils(): + + def __init__(self): + self.addon = xbmcaddon.Addon() + self.id = self.addon.getAddonInfo("id") + self.name = self.addon.getAddonInfo("name") + self.url = sys.argv[0] + self.handle = int(sys.argv[1]) + + self.path = xbmcvfs.translatePath(self.addon.getAddonInfo("path")) + self.profile = xbmcvfs.translatePath(self.addon.getAddonInfo("profile")) + self.resources = os.path.join(self.path, "resources") + self.media = os.path.join(self.resources, "media") + + def localize(self, *args): + if len(args) < 1: + raise ValueError("String id missing") + elif len(args) == 1: + string_id = args[0] + return self.addon.getLocalizedString(string_id) + else: + return [self.addon.getLocalizedString(string_id) for string_id in args] + + @staticmethod + def get_user_input(prompt_msg=""): + keyboard = xbmc.Keyboard("", prompt_msg) + keyboard.doModal() + if not keyboard.isConfirmed(): + return "" + + input_str = keyboard.getText() + return input_str diff --git a/plugin.audio.kvartal/resources/lib/menus.py b/plugin.audio.kvartal/resources/lib/menus.py new file mode 100644 index 0000000000..3cb5ef003e --- /dev/null +++ b/plugin.audio.kvartal/resources/lib/menus.py @@ -0,0 +1,95 @@ +import os +from xbmcgui import ListItem, Dialog +from xbmcplugin import addDirectoryItems, addSortMethod, endOfDirectory, \ + setResolvedUrl +from resources.lib.api import Kvartal +from resources.lib.kodiutils import AddonUtils + + +class MenuList(): + + def __init__(self): + self.kvartal = Kvartal() + self.addon_utils = AddonUtils() + + def _add_folder_item(self, items, title, url, icon_url=None, fanart_url=None, + isfolder=True, isplayable=False, context_menu_items=None, + offscreen=True): + + if fanart_url is None: + fanart_url = os.path.join(self.addon_utils.resources, "fanart.jpg") + + if icon_url is None: + icon_url = os.path.join(self.addon_utils.resources, "icon.png") + + list_item = ListItem(label=title, offscreen=offscreen) + list_item.setArt({"thumb": icon_url, "fanart": fanart_url}) + list_item.setInfo("music", {"title": title}) + + if isplayable: + list_item.setProperty("IsPlayable", "true") + else: + list_item.setProperty("IsPlayable", "false") + + if context_menu_items is not None: + list_item.addContextMenuItems(context_menu_items) + + items.append((url, list_item, isfolder)) + + def _end_folder(self, items, sort_methods=()): + addDirectoryItems(self.addon_utils.handle, items, totalItems=len(items)) + + for sort_method in sort_methods: + addSortMethod(self.addon_utils.handle, sort_method) + + endOfDirectory(self.addon_utils.handle) + + def root_menu(self): + items = [] + for (show_id, show) in enumerate(self.kvartal.shows): + url = "{0}?action=listshows&show_id={1}".format( + self.addon_utils.url, show_id) + context_url = "{0}?action={1}&show_id={2}".format( + self.addon_utils.url, "getshowsummary", show_id) + context_menu = [(self.addon_utils.localize(30004), + "RunPlugin({0})".format(context_url))] + icon_url = os.path.join(self.addon_utils.media, + show["suburl"] + ".png") + self._add_folder_item(items, show["name"], url, icon_url=icon_url, + context_menu_items=context_menu) + + self._end_folder(items) + + def content_menu(self, show_id): + episodes = self.kvartal.get_content(int(show_id)) + + items = [] + for episode in episodes: + url = "{0}?action=play&audio={1}".format(self.addon_utils.url, + episode["media_url"]) + title = "{0} ({1})".format(episode["label"], episode["date"]) + context_url = "{0}?action={1}&show_id={2}&episode_id={3}".format( + self.addon_utils.url, "getepisodesummary", show_id, + episode["media_url"]) + context_menu = [(self.addon_utils.localize(30004), + "RunPlugin({0})".format(context_url))] + self._add_folder_item(items, title, url, episode["image_url"], + isplayable=True, isfolder=False, context_menu_items=context_menu) + + self._end_folder(items) + + def view_show_summary(self, show_id): + summary = self.kvartal.get_show_summary(int(show_id)) + Dialog().textviewer(self.addon_utils.name, summary) + + def view_episode_summary(self, show_id, episode_id): + episodes = self.kvartal.get_content(int(show_id)) + + for episode in episodes: + if episode["media_url"] == episode_id: + Dialog().textviewer(self.addon_utils.name, episode["summary"]) + break + + def play_audio(self, path): + play_item = ListItem(path=path) + setResolvedUrl(self.addon_utils.handle, True, listitem=play_item) diff --git a/plugin.audio.kvartal/resources/lib/plugin.py b/plugin.audio.kvartal/resources/lib/plugin.py new file mode 100644 index 0000000000..0d2aabd571 --- /dev/null +++ b/plugin.audio.kvartal/resources/lib/plugin.py @@ -0,0 +1,20 @@ +import sys +from urllib.parse import parse_qsl +from resources.lib.menus import MenuList + + +def run(): + paramstring = sys.argv[2] + params = dict(parse_qsl(paramstring[1:])) + menu_list = MenuList() + if params: + if params["action"] == "listshows": + menu_list.content_menu(params["show_id"]) + elif params["action"] == "getshowsummary": + menu_list.view_show_summary(params["show_id"]) + elif params["action"] == "getepisodesummary": + menu_list.view_episode_summary(params["show_id"], params["episode_id"]) + elif params["action"] == "play": + menu_list.play_audio(params["audio"]) + else: + menu_list.root_menu() diff --git a/plugin.audio.kvartal/resources/lib/webutils.py b/plugin.audio.kvartal/resources/lib/webutils.py new file mode 100644 index 0000000000..54dfeb0c70 --- /dev/null +++ b/plugin.audio.kvartal/resources/lib/webutils.py @@ -0,0 +1,25 @@ +import requests +from bs4 import BeautifulSoup + + +class WebScraper(): + + def __init__(self): + self.session = requests.Session() + self.session.headers = { + "User-Agent": "kodi.tv", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip", + "DNT": "1", # Do Not Track Request Header + "Connection": "close" + } + + def get_html(self, url): + page_response = self.session.get(url) + html = BeautifulSoup(page_response.text, "html.parser") + return html + + def get_json(self, url): + page_response = self.session.get(url) + return page_response.json() diff --git a/plugin.audio.kvartal/resources/media/den-svenska-modellen.png b/plugin.audio.kvartal/resources/media/den-svenska-modellen.png new file mode 100644 index 0000000000..8bc735500e Binary files /dev/null and b/plugin.audio.kvartal/resources/media/den-svenska-modellen.png differ diff --git a/plugin.audio.kvartal/resources/media/fredagsintervjun.png b/plugin.audio.kvartal/resources/media/fredagsintervjun.png new file mode 100644 index 0000000000..d3639996d7 Binary files /dev/null and b/plugin.audio.kvartal/resources/media/fredagsintervjun.png differ diff --git a/plugin.audio.kvartal/resources/media/inlasta-essaer.png b/plugin.audio.kvartal/resources/media/inlasta-essaer.png new file mode 100644 index 0000000000..fbbae95006 Binary files /dev/null and b/plugin.audio.kvartal/resources/media/inlasta-essaer.png differ diff --git a/plugin.audio.kvartal/resources/media/veckopanelen.png b/plugin.audio.kvartal/resources/media/veckopanelen.png new file mode 100644 index 0000000000..d9624d5472 Binary files /dev/null and b/plugin.audio.kvartal/resources/media/veckopanelen.png differ diff --git a/plugin.audio.kvartal/resources/screenshot-01.jpg b/plugin.audio.kvartal/resources/screenshot-01.jpg new file mode 100644 index 0000000000..01172a63f9 Binary files /dev/null and b/plugin.audio.kvartal/resources/screenshot-01.jpg differ diff --git a/plugin.audio.kvartal/resources/screenshot-02.jpg b/plugin.audio.kvartal/resources/screenshot-02.jpg new file mode 100644 index 0000000000..0229095849 Binary files /dev/null and b/plugin.audio.kvartal/resources/screenshot-02.jpg differ diff --git a/plugin.audio.kvartal/resources/screenshot-03.jpg b/plugin.audio.kvartal/resources/screenshot-03.jpg new file mode 100644 index 0000000000..dafb31c3c2 Binary files /dev/null and b/plugin.audio.kvartal/resources/screenshot-03.jpg differ diff --git a/plugin.audio.kvartal/resources/screenshot-04.jpg b/plugin.audio.kvartal/resources/screenshot-04.jpg new file mode 100644 index 0000000000..b82299c3d5 Binary files /dev/null and b/plugin.audio.kvartal/resources/screenshot-04.jpg differ diff --git a/plugin.audio.kxmxpxtx.bandcamp/LICENSE b/plugin.audio.kxmxpxtx.bandcamp/LICENSE new file mode 100644 index 0000000000..5349361e15 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Virusmater + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugin.audio.kxmxpxtx.bandcamp/README.md b/plugin.audio.kxmxpxtx.bandcamp/README.md new file mode 100644 index 0000000000..8619971b78 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/README.md @@ -0,0 +1,41 @@ +# plugin.audio.kxmxpxtx.bandcamp +Bandcamp (https://bandcamp.com) on Kodi Media Player. Currently possible to get featured tracks from discovery and browse own collection. + +Thanks @Virusmater for creating this plugin! This is a continuation of [the original plugin](https://github.com/Virusmater/plugin.audio.kxmxpxtx.bandcamp). + +Uses [this python bandcamp wrapper](https://github.com/Virusmater/bandcamp_api) with some modifications. + +Support bands you like! + +# Screenshots +## Menu +![image](screenshot/menu.jpg) + +## Collection +Album view: + +![image](screenshot/collection-albums.jpg) + +Group by Artist: + +![image](screenshot/collection-artists.jpg) + +## Discover +Genres: + +![image](screenshot/discover-genres.jpg) + +Tracks within genre: + +![image](screenshot/discover.jpg) + +# Installation +You can install [the published add-on](https://kodi.tv/addons/matrix/plugin.audio.kxmxpxtx.bandcamp) through Kodi. + +Alternatively, you can download a zip file of this repository, and manually add it to Kodi. + +# todo +* add caches collection and fan\_id +* add possibility to listen to all bands and albums in the collection +* add setting for discovery playlist size +* add personal mix (e.g top punk-hardcore + new black metal) diff --git a/plugin.audio.kxmxpxtx.bandcamp/addon.xml b/plugin.audio.kxmxpxtx.bandcamp/addon.xml new file mode 100644 index 0000000000..46d45f0287 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/addon.xml @@ -0,0 +1,40 @@ + + + + + + + + + audio + + + Use Bandcamp to discover new music and browse your collection. + MIT + Bandcamp is an online record store and music community where passionate fans discover, connect with, and directly support the artists they love. Visit https://bandcamp.com to support your favourite bands! + all + xbmc@molzy.com + https://github.com/molzy/plugin.audio.kxmxpxtx.bandcamp + v0.5.1+matrix.1 (2024-08) + - Resolved 403 forbidden error when accessing bandcamp API + +v0.5.0+matrix.1 (2023-01) + - Added image quality settings and band fanart + - Changed default view for user collection and wishlist + - Now matches the user-sorted list of albums on the Bandcamp website + - Moved the previous view (grouping by artist) to a submenu + - Added folder icons to the main menu + - Add German and Dutch translations + - Updated provider, thanks @kxmxpxtx for creating this plugin! + + + icon.png + fanart.png + screenshot/menu.jpg + screenshot/discover-genres.jpg + screenshot/discover.jpg + screenshot/collection-albums.jpg + screenshot/collection-artists.jpg + + + diff --git a/plugin.audio.kxmxpxtx.bandcamp/default.py b/plugin.audio.kxmxpxtx.bandcamp/default.py new file mode 100644 index 0000000000..a480f1de51 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/default.py @@ -0,0 +1,179 @@ +# https://docs.python.org/2.7/ +import sys + +from future.standard_library import install_aliases + +install_aliases() +from future.utils import (PY3) + +if PY3: + from urllib.parse import parse_qs +else: + from urlparse import parse_qs + +import xbmcgui +import xbmcplugin +import xbmcaddon +import random +import xbmc +from resources.lib.bandcamp_api import bandcamp +from resources.lib.bandcamp_api.bandcamp import Band, Album +from resources.lib.kodi.ListItems import ListItems + +try: + import StorageServer +except: + from resources.lib.cache import storageserverdummy as StorageServer +cache = StorageServer.StorageServer('plugin.audio.kxmxpxtx.bandcamp', 24) # (Your plugin name, Cache time in hours) + + +def build_main_menu(): + root_items = list_items.get_root_items(username) + xbmcplugin.addDirectoryItems(addon_handle, root_items, len(root_items)) + xbmcplugin.endOfDirectory(addon_handle) + + +def build_band_list(bands, from_wishlist=False): + band_list = list_items.get_band_items(bands, from_wishlist) + xbmcplugin.addDirectoryItems(addon_handle, band_list, len(band_list)) + xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE) + xbmcplugin.endOfDirectory(addon_handle) + + +def build_album_list(albums, band=None, group_by_artist=False): + albums_list = list_items.get_album_items(albums, band, group_by_artist) + + xbmcplugin.addDirectoryItems(addon_handle, albums_list, len(albums_list)) + xbmcplugin.endOfDirectory(addon_handle) + + +def build_genre_list(): + genre_list = list_items.get_genre_items(cache.cacheFunction(bandcamp.get_genres)) + xbmcplugin.addDirectoryItems(addon_handle, genre_list, len(genre_list)) + xbmcplugin.endOfDirectory(addon_handle) + + +def build_subgenre_list(genre): + subgenre_list = list_items.get_subgenre_items(genre, cache.cacheFunction(bandcamp.get_subgenres)) + xbmcplugin.addDirectoryItems(addon_handle, subgenre_list, len(subgenre_list)) + xbmcplugin.endOfDirectory(addon_handle) + + +def build_song_list(band, album, tracks, autoplay=False): + track_list = list_items.get_track_items(band=band, album=album, tracks=tracks) + if autoplay: + ## Few hacks, check for more info: https://forum.kodi.tv/showthread.php?tid=354733&pid=2952379#pid2952379 + playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + xbmcplugin.setResolvedUrl(addon_handle, True, listitem=track_list[0][1]) + xbmc.sleep(2000) + for url, list_item, folder in track_list[1:]: + playlist.add(url, list_item) + else: + xbmcplugin.addDirectoryItems(addon_handle, track_list, len(track_list)) + xbmcplugin.setContent(addon_handle, 'songs') + xbmcplugin.endOfDirectory(addon_handle) + + +def build_search_result_list(items): + item_list = [] + for item in items: + if isinstance(item, Band): + item_list += list_items.get_band_items([item], from_search=True) + elif isinstance(item, Album): + item_list += list_items.get_album_items([item]) + xbmcplugin.addDirectoryItems(addon_handle, item_list, len(item_list)) + xbmcplugin.endOfDirectory(addon_handle) + + +def build_featured_list(bands): + for band in bands: + for album in bands[band]: + track_list = list_items.get_track_items(band=band, album=album, tracks=bands[band][album], to_album=True) + xbmcplugin.addDirectoryItems(addon_handle, track_list, len(track_list)) + xbmcplugin.setContent(addon_handle, 'songs') + xbmcplugin.endOfDirectory(addon_handle) + + +def play_song(url): + play_item = xbmcgui.ListItem(path=url) + xbmcplugin.setResolvedUrl(addon_handle, True, listitem=play_item) + + +def search(query): + build_search_result_list(bandcamp.search(query)) + + +def main(): + args = parse_qs(sys.argv[2][1:]) + mode = args.get('mode', None) + if mode is None: + build_main_menu() + elif mode[0] == 'stream': + play_song(args.get('url', [''])[0]) + elif mode[0] == 'list_discover': + build_genre_list() + elif mode[0] == 'list_collection': + build_album_list(bandcamp.get_collection(bandcamp.get_fan_id(), return_albums=True), group_by_artist=mode[0]) + elif mode[0] == 'list_collection_band': + build_band_list(bandcamp.get_collection(bandcamp.get_fan_id())) + elif mode[0] == 'list_wishlist': + build_album_list(bandcamp.get_wishlist(bandcamp.get_fan_id(), return_albums=True), group_by_artist=mode[0]) + elif mode[0] == 'list_wishlist_band': + build_band_list(bandcamp.get_wishlist(bandcamp.get_fan_id()), from_wishlist=True) + elif mode[0] == 'list_wishlist_band_albums': + bands = bandcamp.get_wishlist(bandcamp.get_fan_id()) + band, albums = bandcamp.get_band(args.get('band_id', [''])[0]) + build_album_list(bands[band], band) + elif mode[0] == 'list_search_albums': + band, albums = bandcamp.get_band(args.get('band_id', [''])[0]) + build_album_list(albums, band) + elif mode[0] == 'list_albums': + bands = bandcamp.get_collection(bandcamp.get_fan_id()) + band, albums = bandcamp.get_band(args.get('band_id', [''])[0]) + build_album_list(bands[band], band) + elif mode[0] == 'list_songs': + album_id = args.get('album_id', [''])[0] + item_type = args.get('item_type', [''])[0] + build_song_list(*bandcamp.get_album(album_id=album_id, item_type=item_type)) + elif mode[0] == 'list_subgenre': + genre = args.get('category', [''])[0] + build_subgenre_list(genre) + elif mode[0] == 'list_subgenre_songs': + genre = args.get('category', [''])[0] + subgenre = args.get('subcategory', None)[0] + slices = [] + if addon.getSetting('slice_top') == 'true': + slices.append('top') + if addon.getSetting('slice_new') == 'true': + slices.append('new') + if addon.getSetting('slice_rec') == 'true': + slices.append('rec') + discover_dict = {} + for slice in slices: + discover_dict.update(bandcamp.discover(genre, subgenre, slice)) + shuffle_list = list(discover_dict.items()) + random.shuffle(shuffle_list) + discover_dict = dict(shuffle_list) + build_featured_list(discover_dict) + elif mode[0] == 'search': + action = args.get('action', [''])[0] + query = args.get('query', [''])[0] + if action == 'new': + query = xbmcgui.Dialog().input(addon.getLocalizedString(30103)) + if query: + search(query) + elif mode[0] == 'url': + url = args.get('url', [''])[0] + build_song_list(*bandcamp.get_album_by_url(url), autoplay=True) + elif mode[0] == 'settings': + addon.openSettings() + + +if __name__ == '__main__': + xbmc.log('sys.argv:' + str(sys.argv), xbmc.LOGDEBUG) + addon = xbmcaddon.Addon() + list_items = ListItems(addon) + username = addon.getSetting('username') + bandcamp = bandcamp.Bandcamp(username) + addon_handle = int(sys.argv[1]) + main() diff --git a/plugin.audio.kxmxpxtx.bandcamp/fanart.png b/plugin.audio.kxmxpxtx.bandcamp/fanart.png new file mode 100644 index 0000000000..7d79665342 Binary files /dev/null and b/plugin.audio.kxmxpxtx.bandcamp/fanart.png differ diff --git a/plugin.audio.kxmxpxtx.bandcamp/icon.png b/plugin.audio.kxmxpxtx.bandcamp/icon.png new file mode 100644 index 0000000000..9cc5aeef8d Binary files /dev/null and b/plugin.audio.kxmxpxtx.bandcamp/icon.png differ diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/__init__.py b/plugin.audio.kxmxpxtx.bandcamp/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.de_de/strings.po b/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..dc7645d1ed --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,105 @@ +# Kodi Media Center language file +# Addon Name: Bandcamp +# Addon id: plugin.audio.kxmxpxtx.bandcamp +# Addon Provider: kxmxpxtx + +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: German (https://forum.kodi.tv/showthread.php?tid=354190&pid=2974663#pid2974663)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de_de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings - General +msgctxt "#30001" +msgid "General" +msgstr "Generell" + +msgctxt "#30002" +msgid "Username" +msgstr "Benutzername" + +msgctxt "#30003" +msgid "Discover" +msgstr "Entdecken" + +msgctxt "#30011" +msgid "Top" +msgstr "Top" + +msgctxt "#30012" +msgid "New" +msgstr "Neu" + +msgctxt "#30013" +msgid "Recommended" +msgstr "Empfohlen" + +msgctxt "#30014" +msgid "General settings for the Bandcamp addon" +msgstr "Allgemeine Einstellungen für den Bandcamp" + +msgctxt "#30016" +msgid "Choose what you will see in the Discover section" +msgstr "Wählen Sie, was Sie im Abschnitt Discover sehen werden" + +msgctxt "#30020" +msgid "Image Quality" +msgstr "Bildqualität" + +msgctxt "#30021" +msgid "Set the desired quality level for album art and band images" +msgstr "Stellen Sie das gewünschte Qualitätsniveau für Albumkunst ein" + +msgctxt "#30022" +msgid "High" +msgstr "Hoch" + +msgctxt "#30023" +msgid "Medium" +msgstr "Mittel" + +msgctxt "#30024" +msgid "Low" +msgstr "Niedrig" + +# GUI main menu +msgctxt "#30101" +msgid "Discover" +msgstr "Entdecken" + +msgctxt "#30102" +msgid "Collection" +msgstr "Kollektion" + +msgctxt "#30103" +msgid "Search" +msgstr "Suche" + +msgctxt "#30104" +msgid "Add your username to access the collection" +msgstr "Für Kollektionszugriff Benutzernamen eingeben" + +msgctxt "#30105" +msgid "Wishlist" +msgstr "Wunschliste" + +msgctxt "#30106" +msgid "Group by Artist" +msgstr "Sortieren Sie nach Künstler" + +# GUI - Discover +msgctxt "#30201" +msgid "All" +msgstr "Alle" + +msgctxt "#30202" +msgid "Go to the album" +msgstr "Gehe zum Album" diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.en_gb/strings.po b/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..d8821a3c21 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,105 @@ +# Kodi Media Center language file +# Addon Name: Bandcamp +# Addon id: plugin.audio.kxmxpxtx.bandcamp +# Addon Provider: kxmxpxtx + +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings - General +msgctxt "#30001" +msgid "General" +msgstr "" + +msgctxt "#30002" +msgid "Username" +msgstr "" + +msgctxt "#30003" +msgid "Discover" +msgstr "" + +msgctxt "#30011" +msgid "Top" +msgstr "" + +msgctxt "#30012" +msgid "New" +msgstr "" + +msgctxt "#30013" +msgid "Recommended" +msgstr "" + +msgctxt "#30014" +msgid "General settings for the Bandcamp addon" +msgstr "" + +msgctxt "#30016" +msgid "Choose what you will see in the Discover section" +msgstr "" + +msgctxt "#30020" +msgid "Image Quality" +msgstr "" + +msgctxt "#30021" +msgid "Set the desired quality level for album art and band images" +msgstr "" + +msgctxt "#30022" +msgid "High" +msgstr "" + +msgctxt "#30023" +msgid "Medium" +msgstr "" + +msgctxt "#30024" +msgid "Low" +msgstr "" + +# GUI main menu +msgctxt "#30101" +msgid "Discover" +msgstr "" + +msgctxt "#30102" +msgid "Collection" +msgstr "" + +msgctxt "#30103" +msgid "Search" +msgstr "" + +msgctxt "#30104" +msgid "Add your username to access the collection" +msgstr "" + +msgctxt "#30105" +msgid "Wishlist" +msgstr "" + +msgctxt "#30106" +msgid "Group by Artist" +msgstr "" + +# GUI - Discover +msgctxt "#30201" +msgid "All" +msgstr "" + +msgctxt "#30202" +msgid "Go to the album" +msgstr "" diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.nl_nl/strings.po b/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..5418d32bb3 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,105 @@ +# Kodi Media Center language file +# Addon Name: Bandcamp +# Addon id: plugin.audio.kxmxpxtx.bandcamp +# Addon Provider: kxmxpxtx + +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Dutch \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl_nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings - General +msgctxt "#30001" +msgid "General" +msgstr "Algemeen" + +msgctxt "#30002" +msgid "Username" +msgstr "Gebruikersnaam" + +msgctxt "#30003" +msgid "Discover" +msgstr "Ontdek" + +msgctxt "#30011" +msgid "Top" +msgstr "Bovenaan" + +msgctxt "#30012" +msgid "New" +msgstr "Nieuw" + +msgctxt "#30013" +msgid "Recommended" +msgstr "Aanbevolen" + +msgctxt "#30014" +msgid "General settings for the Bandcamp addon" +msgstr "Algemeene instellingen voor Bandcamp" + +msgctxt "#30016" +msgid "Choose what you will see in the Discover section" +msgstr "Kies wat je ziet in het Ontdek sectie" + +msgctxt "#30020" +msgid "Image Quality" +msgstr "Beeldkwaliteit" + +msgctxt "#30021" +msgid "Set the desired quality level for album art and band images" +msgstr "Kies het kwaliteitsniveau voor alle beelden" + +msgctxt "#30022" +msgid "High" +msgstr "Hoog" + +msgctxt "#30023" +msgid "Medium" +msgstr "Gemiddled" + +msgctxt "#30024" +msgid "Low" +msgstr "Laag" + +# GUI main menu +msgctxt "#30101" +msgid "Discover" +msgstr "Ontdek" + +msgctxt "#30102" +msgid "Collection" +msgstr "Verzameling" + +msgctxt "#30103" +msgid "Search" +msgstr "Zoek" + +msgctxt "#30104" +msgid "Add your username to access the collection" +msgstr "Voeg uw gebruikersnaam toe om de verzameling te bekijken" + +msgctxt "#30105" +msgid "Wishlist" +msgstr "Verlanglijst" + +msgctxt "#30106" +msgid "Group by Artist" +msgstr "Sorteer op groep" + +# GUI - Discover +msgctxt "#30201" +msgid "All" +msgstr "Alles" + +msgctxt "#30202" +msgid "Go to the album" +msgstr "Ga naar het album" diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/lib/__init__.py b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/lib/bandcamp_api/__init__.py b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/bandcamp_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/lib/bandcamp_api/bandcamp.py b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/bandcamp_api/bandcamp.py new file mode 100644 index 0000000000..c00ac2ba92 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/bandcamp_api/bandcamp.py @@ -0,0 +1,271 @@ +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +import json +import time + +from future.standard_library import install_aliases +from future.utils import (PY2) + +install_aliases() + +from urllib.parse import unquote, quote_plus +from urllib.request import Request, urlopen +from builtins import * +from html.parser import HTMLParser + +USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' + +def urlopen_ua(url, data=None): + return urlopen(Request(url, data=data, headers={'User-Agent': USER_AGENT}), timeout=5) + +def req(url, data=None): + body = data.encode() if data else None + return urlopen_ua(url, data=body).read().decode() + +class Band: + def __init__(self, band_id=None, band_name="", band_img=None): + self.band_name = band_name + self.band_id = str(band_id) + self.band_img = band_img + + def __eq__(self, other): + if type(other) is type(self): + return self.band_id == other.band_id + else: + return False + + def __hash__(self): + return hash(self.band_id) + + def get_art_img(self, quality=20): + if self.band_img: + return "https://f4.bcbits.com/img/{band_img}_{quality}.jpg".format(band_img=self.band_img, quality=quality) + + +class Album: + ALBUM_TYPE = "a" + TRACK_TYPE = "t" + + def __init__(self, album_id, album_name, art_id, item_type=ALBUM_TYPE, genre="", band=None): + self.album_name = album_name + self.art_id = art_id + self.album_id = album_id + self.item_type = item_type + self.genre = genre + self.band = band + + def get_art_img(self, quality=2): + if self.art_id: + return "https://f4.bcbits.com/img/a0{art_id}_{quality}.jpg".format(art_id=self.art_id, quality=quality) + + +class Track: + def __init__(self, track_name, file, duration, number=None): + self.track_name = track_name + self.file = file + self.duration = duration + self.number = number + + +class _DataBlobParser(HTMLParser): + data_blob = None + + def handle_starttag(self, tag, attrs): + for attr in attrs: + if attr[0] == "data-blob" and self.data_blob is None: + data_html = attr[1] + self.data_blob = json.loads(data_html) + + +class _PlayerDataParser(HTMLParser): + player_data = None + + def handle_data(self, data): + if "playerdata" in data: + end = data.index('};') + 1 + player_data = data[26:end] + self.player_data = json.loads(player_data) + + +class Bandcamp: + + def __init__(self, user_name): + self.data_blob = None + if user_name is None: + self.user_name = "" + else: + self.user_name = user_name + + @staticmethod + def discover(genre="all", sub_genre="any", slice="best", page=0): + url = "https://bandcamp.com/api/discover/3/get_web?g={genre}&t={sub_genre}&s={slice}&p={page}&f=all" \ + .format(genre=genre, sub_genre=sub_genre, slice=slice, page=page) + request = req(url) + items = json.loads(request)['items'] + discover_list = {} + for item in items: + track = Track(item['featured_track']['title'], item['featured_track']['file']['mp3-128'], + item['featured_track']['duration']) + album_genre = u'{genre} ({slice})'.format(genre=item['genre_text'], slice=slice) + band = Band(band_id=item['band_id'], band_name=item['secondary_text'], + band_img=item['bio_image']['image_id']) + album = Album(album_id=item['id'], album_name=item['primary_text'], art_id=item['art_id'], + genre=album_genre, item_type=item['type'], band=band) + discover_list[band] = {album: [track]} + return discover_list + + def get_fan_id(self): + return self._get_data_blob()['fan_data']['fan_id'] + + def get_genres(self): + return self._get_data_blob()['signup_params']['genres'] + + def get_subgenres(self): + return self._get_data_blob()['signup_params']['subgenres'] + + def get_collection(self, fan_id, count=1000, return_albums=False): + url = "https://bandcamp.com/api/fancollection/1/collection_items" + token = self._get_token() + body = '{{"fan_id": "{fan_id}", "older_than_token": "{token}", "count":"{count}"}}' \ + .format(fan_id=fan_id, token=token, count=count) + x = req(url, data=body) + items = json.loads(x)['items'] + bands = {} + albums = [] + for item in items: + band = Band(band_id=item['band_id'], band_name=item['band_name']) + album = Album(album_id=item['tralbum_id'], album_name=item['item_title'], + art_id=item['item_art_id'], item_type=item['tralbum_type'], + band=band) + if band not in bands: + bands[band] = {} + bands[band].update({album: [None]}) + albums.append(album) + if return_albums: + return albums + else: + return bands + + def get_wishlist(self, fan_id, count=1000, return_albums=False): + url = "https://bandcamp.com/api/fancollection/1/wishlist_items" + token = self._get_token() + body = '{{"fan_id": "{fan_id}", "older_than_token": "{token}", "count":"{count}"}}' \ + .format(fan_id=fan_id, token=token, count=count) + x = req(url, data=body) + items = json.loads(x)['items'] + bands = {} + albums = [] + for item in items: + band = Band(band_id=item['band_id'], band_name=item['band_name']) + album = Album(album_id=item['tralbum_id'], album_name=item['item_title'], + art_id=item['item_art_id'], item_type=item['tralbum_type'], + band=band) + if band not in bands: + bands[band] = {} + bands[band].update({album: [None]}) + albums.append(album) + if return_albums: + return albums + else: + return bands + + def get_album(self, album_id, item_type=Album.ALBUM_TYPE, band_id=1): + url = "https://bandcamp.com/api/mobile/24/tralbum_details" \ + "?band_id={band_id}&tralbum_type={item_type}&tralbum_id={album_id}" \ + .format(band_id=band_id, item_type=item_type, album_id=album_id) + request = req(url) + album_details = json.loads(request) + track_list = [] + for track in album_details['tracks']: + # sometimes not all tracks are available online + if track['streaming_url'] is not None: + track_list.append( + Track(track['title'], track['streaming_url']['mp3-128'], track['duration'], + number=track['track_num'])) + art_id = album_details['art_id'] + band = Band(band_name=album_details['band']['name'], band_id=album_details['band']['band_id'], + band_img=album_details['band']['image_id']) + album = Album(album_id, album_details['title'], art_id, band=band) + return band, album, track_list + + def get_album_legacy(self, album_id, item_type="album"): + url = "https://bandcamp.com/EmbeddedPlayer/{item_type}={album_id}" \ + .format(album_id=album_id, item_type=item_type) + content = req(url) + parser = _PlayerDataParser() + parser.feed(content) + player_data = parser.player_data + track_list = [] + for track in player_data['tracks']: + # sometimes not all tracks are available online + if track['file'] is not None: + track_list.append( + Track(track['title'], track['file']['mp3-128'], track['duration'], number=track['tracknum'] + 1)) + art_id = player_data['album_art_id'] + if item_type == "track": + art_id = track['art_id'] + band = Band(band_name=player_data['artist']) + album = Album(album_id, player_data['album_title'], art_id, band=band) + return band, album, track_list + + def get_album_by_url(self, url): + url = unquote(url) + request = req(url) + parser = _DataBlobParser() + parser.feed(request) + if '/album/' in url: + album_id = parser.data_blob['album_id'] + item_type = Album.ALBUM_TYPE + elif '/track/' in url: + album_id = parser.data_blob['track_id'] + item_type = Album.TRACK_TYPE + return self.get_album(album_id, item_type) + + def get_band(self, band_id): + url = "https://bandcamp.com/api/mobile/24/band_details" + body = '{{"band_id": "{band_id}"}}'.format(band_id=band_id) + request = req(url, data=body) + band_details = json.loads(request) + band = Band(band_id=band_details['id'], band_name=band_details['name'], + band_img=band_details['bio_image_id']) + albums = [] + for album in band_details['discography']: + albums.append(Album(album_id=album['item_id'], album_name=album['title'], + art_id=album['art_id'], item_type=album['item_type'][0], + band=band)) + return band, albums + + def search(self, query): + if PY2: + query = query.decode('utf-8') + url = "https://bandcamp.com/api/fuzzysearch/1/autocomplete?q={query}".format(query=quote_plus(query)) + request = req(url) + results = json.loads(request)['auto']['results'] + items = [] + for result in results: + if result['type'] == "b": + item = Band(band_id=result['id'], band_name=result['name'], + band_img=result['img']) + elif result['type'] == "a" or result['type'] == "t": + band = Band(band_id=result['band_id'], band_name=result['band_name']) + item = Album(album_id=result['id'], album_name=result['name'], + art_id=result['art_id'], item_type=result['type'], + band=band) + if item is not None: + items.append(item) + return items + + + @staticmethod + def _get_token(): + return str(int(time.time())) + "::FOO::" + + def _get_data_blob(self): + if self.data_blob is None: + url = "https://bandcamp.com/{user_name}".format(user_name=quote_plus(self.user_name)) + content = req(url) + parser = _DataBlobParser() + parser.feed(content) + self.data_blob = parser.data_blob + return self.data_blob diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/lib/cache/__init__.py b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/lib/cache/storageserverdummy.py b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/cache/storageserverdummy.py new file mode 100644 index 0000000000..77c5818ec1 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/cache/storageserverdummy.py @@ -0,0 +1,40 @@ +""" + StorageServer override + Version: 1.0 + + Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen + Copyright (C) 2019 anxdpanic + + This file is part of script.common.plugin.cache + + SPDX-License-Identifier: GPL-3.0-only + See LICENSES/GPL-3.0-only.txt for more information. +""" + + +class StorageServer: + def __init__(self, table, timeout=24): + pass + + def cacheFunction(self, funct=False, *args): + if funct: + return funct(*args) + return [] + + def set(self, name, data): + return "" + + def get(self, name): + return "" + + def setMulti(self, name, data): + return "" + + def getMulti(self, name, items): + return "" + + def lock(self, name): + return False + + def unlock(self, name): + return False diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/lib/kodi/ListItems.py b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/kodi/ListItems.py new file mode 100644 index 0000000000..9bfb6d70e0 --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/kodi/ListItems.py @@ -0,0 +1,150 @@ +import sys + +import xbmcgui +from future.standard_library import install_aliases + +install_aliases() +from urllib.parse import urlencode + + +class ListItems: + + def __init__(self, addon): + self.addon = addon + quality = self.addon.getSetting('image_quality') + self.quality = int(quality) if quality else 1 + + def _band_quality(self): + if self.quality == 0: + return 1 # full resolution + if self.quality == 1: + return 10 # 1200px wide + if self.quality == 2: + return 25 # 700px wide + + def _album_quality(self): + if self.quality == 0: + return 5 # 700px wide + if self.quality == 1: + return 2 # 350px wide + if self.quality == 2: + return 9 # 210px wide + + def _build_url(self, query): + base_url = sys.argv[0] + return base_url + '?' + urlencode(query) + + def get_root_items(self, username): + items = [] + # discover menu + li = xbmcgui.ListItem(label=self.addon.getLocalizedString(30101)) + li.setArt({'icon': 'DefaultMusicSources.png'}) + url = self._build_url({'mode': 'list_discover'}) + items.append((url, li, True)) + # collection menu + # don't add if not configured + if username == "": + li = xbmcgui.ListItem(label=self.addon.getLocalizedString(30104)) + li.setArt({'icon': 'DefaultAddonService.png'}) + url = self._build_url({'mode': 'settings'}) + items.append((url, li, True)) + else: + li = xbmcgui.ListItem(label=self.addon.getLocalizedString(30102)) + li.setArt({'icon': 'DefaultMusicAlbums.png'}) + url = self._build_url({'mode': 'list_collection'}) + items.append((url, li, True)) + + li = xbmcgui.ListItem(label=self.addon.getLocalizedString(30105)) + li.setArt({'icon': 'DefaultMusicRecentlyAdded.png'}) + url = self._build_url({'mode': 'list_wishlist'}) + items.append((url, li, True)) + # search + li = xbmcgui.ListItem(label=self.addon.getLocalizedString(30103)) + li.setArt({'icon': 'DefaultMusicSearch.png'}) + url = self._build_url({'mode': 'search', 'action': 'new'}) + items.append((url, li, True)) + return items + + def get_album_items(self, albums, band=None, group_by_artist=False): + items = [] + if group_by_artist: + li = xbmcgui.ListItem(label=self.addon.getLocalizedString(30106)) + li.setArt({'icon': 'DefaultMusicArtists.png'}) + url = self._build_url({'mode': group_by_artist + '_band'}) + items.append((url, li, True)) + for album in albums: + if band: + album_title = '{} - {}'.format(band.band_name, album.album_name) + elif album.band: + album_title = '{} - {}'.format(album.band.band_name, album.album_name) + else: + album_title = album.album_name + + li = xbmcgui.ListItem(label=album_title) + url = self._build_url({'mode': 'list_songs', 'album_id': album.album_id, 'item_type': album.item_type}) + band_art = band.get_art_img(quality=self._band_quality()) if band else None + album_art = album.get_art_img(quality=self._album_quality()) + li.setArt({'thumb': album_art, 'fanart': band_art if band_art else album_art}) + items.append((url, li, True)) + return items + + def get_genre_items(self, genres): + items = [] + li = xbmcgui.ListItem(label=self.addon.getLocalizedString(30201)) + url = self._build_url({'mode': 'list_subgenre_songs', 'category': 'all', 'subcategory': 'all'}) + items.append((url, li, True)) + for genre in genres: + li = xbmcgui.ListItem(label=genre['name']) + url = self._build_url({'mode': 'list_subgenre', 'category': genre['value']}) + items.append((url, li, True)) + return items + + def get_subgenre_items(self, genre, subgenres): + items = [] + li = xbmcgui.ListItem(label=self.addon.getLocalizedString(30201) + " " + genre) + url = self._build_url({'mode': 'list_subgenre_songs', 'category': genre, 'subcategory': 'all'}) + items.append((url, li, True)) + for subgenre in subgenres[genre]: + li = xbmcgui.ListItem(label=subgenre['name']) + url = self._build_url({'mode': 'list_subgenre_songs', 'category': genre, 'subcategory': subgenre['value']}) + items.append((url, li, True)) + return items + + def get_track_items(self, band, album, tracks, to_album=False): + items = [] + for track in tracks: + title = u"{band} - {track}".format(band=band.band_name, track=track.track_name) + li = xbmcgui.ListItem(label=title) + li.setInfo('music', {'duration': int(track.duration), 'album': album.album_name, 'genre': album.genre, + 'mediatype': 'song', 'tracknumber': track.number, 'title': track.track_name, + 'artist': band.band_name}) + band_art = band.get_art_img(quality=self._band_quality()) + album_art = album.get_art_img(quality=self._album_quality()) + li.setArt({'thumb': album_art, 'fanart': band_art if band_art else album_art}) + li.setProperty('IsPlayable', 'true') + url = self._build_url({'mode': 'stream', 'url': track.file, 'title': title}) + li.setPath(url) + if to_album: + album_url = self._build_url( + {'mode': 'list_songs', 'album_id': album.album_id, 'item_type': album.item_type}) + cmd = 'Container.Update({album_url})'.format(album_url=album_url) + commands = [(self.addon.getLocalizedString(30202), cmd)] + li.addContextMenuItems(commands) + items.append((url, li, False)) + return items + + def get_band_items(self, bands, from_wishlist=False, from_search=False): + items = [] + mode = 'list_albums' + if from_wishlist: + mode = 'list_wishlist_band_albums' + elif from_search: + mode = 'list_search_albums' + for band in bands: + li = xbmcgui.ListItem(label=band.band_name) + band_art = band.get_art_img(quality=self._band_quality()) + if band_art: + li.setArt({'thumb': band_art, 'fanart': band_art}) + url = self._build_url({'mode': mode, 'band_id': band.band_id}) + items.append((url, li, True)) + return items diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/lib/kodi/__init__.py b/plugin.audio.kxmxpxtx.bandcamp/resources/lib/kodi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.kxmxpxtx.bandcamp/resources/settings.xml b/plugin.audio.kxmxpxtx.bandcamp/resources/settings.xml new file mode 100644 index 0000000000..fc60fba6ed --- /dev/null +++ b/plugin.audio.kxmxpxtx.bandcamp/resources/settings.xml @@ -0,0 +1,52 @@ + + +
+ + + + 0 + + + true + + + 30002 + + + + 0 + 1 + + + + + + + + + 30020 + + + + + + + + 0 + true + + + + 0 + false + + + + 0 + false + + + + +
+
diff --git a/plugin.audio.kxmxpxtx.bandcamp/screenshot/collection-albums.jpg b/plugin.audio.kxmxpxtx.bandcamp/screenshot/collection-albums.jpg new file mode 100644 index 0000000000..c54eff1483 Binary files /dev/null and b/plugin.audio.kxmxpxtx.bandcamp/screenshot/collection-albums.jpg differ diff --git a/plugin.audio.kxmxpxtx.bandcamp/screenshot/collection-artists.jpg b/plugin.audio.kxmxpxtx.bandcamp/screenshot/collection-artists.jpg new file mode 100644 index 0000000000..542f16fc60 Binary files /dev/null and b/plugin.audio.kxmxpxtx.bandcamp/screenshot/collection-artists.jpg differ diff --git a/plugin.audio.kxmxpxtx.bandcamp/screenshot/discover-genres.jpg b/plugin.audio.kxmxpxtx.bandcamp/screenshot/discover-genres.jpg new file mode 100644 index 0000000000..a2b14cc969 Binary files /dev/null and b/plugin.audio.kxmxpxtx.bandcamp/screenshot/discover-genres.jpg differ diff --git a/plugin.audio.kxmxpxtx.bandcamp/screenshot/discover.jpg b/plugin.audio.kxmxpxtx.bandcamp/screenshot/discover.jpg new file mode 100644 index 0000000000..d944817cc8 Binary files /dev/null and b/plugin.audio.kxmxpxtx.bandcamp/screenshot/discover.jpg differ diff --git a/plugin.audio.kxmxpxtx.bandcamp/screenshot/menu.jpg b/plugin.audio.kxmxpxtx.bandcamp/screenshot/menu.jpg new file mode 100644 index 0000000000..b6e28d7531 Binary files /dev/null and b/plugin.audio.kxmxpxtx.bandcamp/screenshot/menu.jpg differ diff --git a/plugin.audio.mixcloud/LICENSE.txt b/plugin.audio.mixcloud/LICENSE.txt new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/plugin.audio.mixcloud/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugin.audio.mixcloud/README.md b/plugin.audio.mixcloud/README.md new file mode 100644 index 0000000000..a0fea6790d --- /dev/null +++ b/plugin.audio.mixcloud/README.md @@ -0,0 +1,46 @@ +# KODI Mixcloud Plugin + +### Developer: + - jackyNIX + +### Contributors: + - Bochi + - SilentException + - fleshgolem + - gordielachance + - understatement + - peat8 + - ronan-ln + +### Current version: + 3.0.2 + - Matrix + +### Features: + - Profile + - Followings + - Followers + - Favorites + - History + - Uploads + - Listen later + - Playlists + - Browse + - Categories + - Search + - Cloudcasts + - Users + - Search history + - Play cloudcasts + - Mixcloud resolver + - Offliberty resolver + - Mixcloud-Downloader resolver (broken) + - Thumbnails + - History + - Profile + - Local + - Localisation + - English + - Dutch + - French + - German \ No newline at end of file diff --git a/plugin.audio.mixcloud/addon.py b/plugin.audio.mixcloud/addon.py new file mode 100644 index 0000000000..a925d5640c --- /dev/null +++ b/plugin.audio.mixcloud/addon.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +''' +@author: jackyNIX + +Copyright (C) 2011-2020 jackyNIX + +This file is part of KODI Mixcloud Plugin. + +KODI Mixcloud Plugin 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. + +KODI Mixcloud Plugin 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 KODI Mixcloud Plugin. If not, see . +''' + +# from lib import run +from lib import run +run() \ No newline at end of file diff --git a/plugin.audio.mixcloud/addon.xml b/plugin.audio.mixcloud/addon.xml new file mode 100644 index 0000000000..4d8796f984 --- /dev/null +++ b/plugin.audio.mixcloud/addon.xml @@ -0,0 +1,37 @@ + + + + + + + audio + + + KODI plugin for Mixcloud + KODI plugin voor Mixcloud + KODI plugin pour Mixcloud + KODI plugin für Mixcloud + Mixcloud is re-thinking radio. Listen to great radio shows, Podcasts and DJ mix sets on-demand. + Mixcloud herdefinieerd radio. Luister naar uitstekende radioshows, podcasts en dj sets on demand. + Mixcloud redéfinit la radio. Écoutez les émissions radio, podcasts et mixes DJ sur demande. + Mixcloud erfindet Radio neu. Höre Radioshows, Podcasts und DJ Mixe wann immer Du willst. + + v3.0.2 (2020-10-07) + [fix] fixed local mixcloud resolver + + all + en + GPL-3.0-or-later + https://forum.kodi.tv/showthread.php?tid=116386 + https://www.mixcloud.com + https://github.com/jackyNIX/xbmc-mixcloud-plugin + + resources/icon.png + resources/fanart.jpg + + true + + diff --git a/plugin.audio.mixcloud/lib/__init__.py b/plugin.audio.mixcloud/lib/__init__.py new file mode 100644 index 0000000000..e7908d11ac --- /dev/null +++ b/plugin.audio.mixcloud/lib/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +''' +@author: jackyNIX + +Copyright (C) 2011-2020 jackyNIX + +This file is part of KODI Mixcloud Plugin. + +KODI Mixcloud Plugin 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. + +KODI Mixcloud Plugin 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 KODI Mixcloud Plugin. If not, see . +''' + + + +from .listbuilder import run \ No newline at end of file diff --git a/plugin.audio.mixcloud/lib/base.py b/plugin.audio.mixcloud/lib/base.py new file mode 100644 index 0000000000..db8e3c556c --- /dev/null +++ b/plugin.audio.mixcloud/lib/base.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- + +''' +@author: jackyNIX + +Copyright (C) 2011-2020 jackyNIX + +This file is part of KODI Mixcloud Plugin. + +KODI Mixcloud Plugin 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. + +KODI Mixcloud Plugin 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 KODI Mixcloud Plugin. If not, see . +''' + + + +from .utils import Utils +from .lang import Lang +import sys +import xbmc +import xbmcplugin +import xbmcgui +from enum import Enum + + + +class BuildResult(Enum): + ENDOFDIRECTORY_DONOTHING = 0 + ENDOFDIRECTORY_FAILED = 1 + ENDOFDIRECTORY_SUCCESS = 2 + + + +# super class +class BaseBuilder: + + def __init__(self): + plugin_args = Utils.getArguments() + self.plugin_handle = int(sys.argv[1]) + self.mode = plugin_args.get('mode', '') + self.key = plugin_args.get('key', '') + if plugin_args.get('offsetex', ''): + self.offset = [int(plugin_args.get('offset', '0')), int(plugin_args.get('offsetex', '0'))] + else: + self.offset = int(plugin_args.get('offset', '0')) + Utils.log('BaseBuilder.__init__(self = ' + self.__class__.__name__ + ', plugin_handle = ' + str(self.plugin_handle) + ', mode = ' + self.mode + ', key = ' + self.key + ', offset = ' + str(self.offset) + ')') + + def execute(self): + Utils.log('BaseBuilder.execute()') + ret = self.build() + if ret is not BuildResult.ENDOFDIRECTORY_DONOTHING: + xbmcplugin.endOfDirectory(handle = self.plugin_handle, succeeded = (ret is BuildResult.ENDOFDIRECTORY_SUCCESS)) + + # returns BuildResult + def build(self): + Utils.log('BaseBuilder.build()') + return BuildResult.ENDOFDIRECTORY_SUCCESS + + + +# super class for lists +class BaseListBuilder(BaseBuilder): + + def build(self): + Utils.log('BaseListBuilder.build()') + nextOffset = self.buildItems() + Utils.log('next offset: ' + str(nextOffset)) + nextOffsetEx = None + if isinstance(nextOffset, list): + nextOffsetEx = nextOffset[1] + nextOffset = nextOffset[0] + if (nextOffset and (nextOffset > 0)) or ((nextOffsetEx is not None) and (nextOffsetEx > 0)): + parameters = {'mode' : self.mode, 'key' : self.key, 'offset' : nextOffset} + if nextOffsetEx is not None: + parameters['offsetex'] = nextOffsetEx + self.addFolderItem({'title' : Lang.MORE}, parameters) + if nextOffset != -1: + return BuildResult.ENDOFDIRECTORY_SUCCESS + else: + return BuildResult.ENDOFDIRECTORY_FAILED + + # returns offset + def buildItems(self): + Utils.log('BaseListBuilder.buildItems()') + return 0 + + def addFolderItem(self, infolabels = {}, parameters = {}, img = '', contextmenuitems = []): + Utils.log('BaseListBuilder.addFolderItem(infolabels = ' + str(infolabels) + ', parameters = ' + str(parameters) + ', img = ' + img + ', contextmenuitems = ' + str(contextmenuitems) + ')') + + listitem = xbmcgui.ListItem(infolabels['title'], infolabels['title']) + listitem.setArt({'icon' : img, 'thumb' : img}) + listitem.setInfo('music', infolabels) + + if contextmenuitems: + listitem.addContextMenuItems(contextmenuitems) + + return xbmcplugin.addDirectoryItem(handle = self.plugin_handle, url = Utils.encodeArguments(parameters), listitem = listitem, isFolder = True) + + def addAudioItem(self, infolabels = {}, parameters = {}, img = '', contextmenuitems = [], total = 0): + Utils.log('BaseListBuilder.addAudioItem(infolabels = ' + str(infolabels) + ', parameters = ' + str(parameters) + ', img = ' + img + ', contextmenuitems = ' + str(contextmenuitems) + ', total = ' + str(total) + ')') + + listitem = xbmcgui.ListItem(infolabels['title'], infolabels['artist']) + listitem.setArt({'icon' : img, 'thumb' : img}) + listitem.setInfo('music', infolabels) + listitem.setProperty('IsPlayable', 'true') + + if contextmenuitems: + listitem.addContextMenuItems(contextmenuitems) + + xbmcplugin.addDirectoryItem(handle = self.plugin_handle, url = Utils.encodeArguments(parameters), listitem = listitem, isFolder = False, totalItems = total) + + def buildContextMenuItems(self, item): + contextMenuItems = [] + + if item.favorited == False: + contextMenuItems.append((Lang.ADD_TO_FAVORITES, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'post', 'key' : item.key + 'favorite/'}) + ')')) + elif item.favorited == True: + contextMenuItems.append((Lang.REMOVE_FROM_FAVORITES, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'delete', 'key' : item.key + 'favorite/'}) + ')')) + + if item.listenlater == False: + contextMenuItems.append((Lang.ADD_TO_LISTEN_LATER, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'post', 'key' : item.key + 'listen-later/'}) + ')')) + elif item.listenlater == True: + contextMenuItems.append((Lang.REMOVE_FROM_LISTEN_LATER, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'delete', 'key' : item.key + 'listen-later/'}) + ')')) + + userKey = item.user + if not userKey: + userKey = item.key + + if item.following == False: + contextMenuItems.append((Lang.ADD_TO_FOLLOWINGS, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'post', 'key' : userKey + 'follow/'}) + ')')) + elif item.following == True: + contextMenuItems.append((Lang.REMOVE_FROM_FOLLOWINGS, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'delete', 'key' : userKey + 'follow/'}) + ')')) + + # fake menu separator + # I do hope one day kodi will support menu separators + if len(contextMenuItems) > 0: + contextMenuItems.append(('----------------------------------------', '')) + + return contextMenuItems + + + +# super class for user queries +class QueryListBuilder(BaseListBuilder): + + def buildItems(self): + query = self.key + if not query: + keyboard = xbmc.Keyboard(query) + keyboard.doModal() + if keyboard.isConfirmed(): + query = keyboard.getText() + if query: + return self.buildQueryItems(query) + return -1 + + def buildQueryItems(self, query): + Utils.log('QueryListBuilder.buildQueryItems(' + query + ')') + return 0 + + + +# class for list data +class BaseList: + + def __init__(self): + self.items = [] + self.nextOffset = 0 + + def initTrackNumbers(self, offset): + index = offset + for item in self.items: + index += 1 + item.infolabels['tracknumber'] = index + item.infolabels['count'] = index + + def merge(self, baseLists = []): + listCount = len(baseLists) + Utils.log('merge lists: ' + str(listCount)) + maxItems = int(Utils.getSetting('page_limit')) + index = [] + count = [] + curItems = [] + for baseList in baseLists: + index.append(0) + count.append(len(baseList.items)) + curItems.append(None) + + mon = xbmc.Monitor() + for iMerged in range(maxItems): + # user aborted + if mon.abortRequested(): + break + + for iList in range(listCount): + if index[iList] < count[iList]: + curItems[iList] = baseLists[iList].items[index[iList]] + else: + curItems[iList] = None + + iAdd = -1 + for iList in range(listCount): + if curItems[iList]: + if (iAdd == -1) or ((not curItems[iAdd].timestamp) and (curItems[iList])) or ((curItems[iAdd].timestamp) and (curItems[iList].timestamp) and (curItems[iList].timestamp > curItems[iAdd].timestamp)): + iAdd = iList + + if iAdd != -1: + Utils.log('merge: ' + str(iMerged) + ' from ' + str(iAdd) + ' - ' + str(curItems[iAdd])) + self.items.append(curItems[iAdd]) + index[iAdd] = index[iAdd] + 1 + else: + break + + Utils.log('merged result: ' + str(len(self.items))) + Utils.log('nextoffset: ' + str(index)) + self.nextOffset = index + + # limit list + def trim(self): + maxItems = int(Utils.getSetting('page_limit')) + while len(self.items) > maxItems: + self.items.pop() + + + +class BaseListItem: + + def __init__(self): + self.key = None + self.user = None + self.image = None + self.timestamp = None + self.favorited = None + self.listenlater = None + self.following = None + self.infolabels = {} + + def setKey(self, sourceData, sourceKey): + if sourceKey in sourceData and sourceData[sourceKey]: + self.key = sourceData[sourceKey] + else: + self.key = None + return self.key + + def setUser(self, sourceData, sourceKey): + if sourceKey in sourceData and sourceData[sourceKey]: + self.user = sourceData[sourceKey] + else: + self.user = None + return self.user + + def setImage(self, sourceData, sourceKey): + if sourceKey in sourceData and sourceData[sourceKey]: + self.image = sourceData[sourceKey] + else: + self.image = None + return self.image + + def setTimestamp(self, sourceData, sourceKey): + if sourceKey in sourceData and sourceData[sourceKey]: + self.timestamp = sourceData[sourceKey] + else: + self.timestamp = None + return self.timestamp + + def setFavorited(self, sourceData, sourceKey): + if sourceKey in sourceData: + self.favorited = sourceData[sourceKey] + else: + self.favorited = None + return self.favorited + + def setListenLater(self, sourceData, sourceKey): + if sourceKey in sourceData: + self.listenlater = sourceData[sourceKey] + else: + self.listenlater = None + return self.listenlater + + def setFollowing(self, sourceData, sourceKey): + if sourceKey in sourceData: + self.following = sourceData[sourceKey] + else: + self.following = None + return self.following + + def __repr__(self): + return 'BaseListItem(key: ' + str(self.key) + ', user: ' + str(self.user) + ', image: ' + str(self.image) + ', timestamp: ' + str(self.timestamp) + ', favorited: ' + str(self.favorited) + ', listen-later: ' + str(self.listenlater) + ', following: ' + str(self.following) + ', infolabels: ' + str(self.infolabels) + ')' \ No newline at end of file diff --git a/plugin.audio.mixcloud/lib/history.py b/plugin.audio.mixcloud/lib/history.py new file mode 100644 index 0000000000..cb20753d95 --- /dev/null +++ b/plugin.audio.mixcloud/lib/history.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +''' +@author: jackyNIX + +Copyright (C) 2011-2020 jackyNIX + +This file is part of KODI Mixcloud Plugin. + +KODI Mixcloud Plugin 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. + +KODI Mixcloud Plugin 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 KODI Mixcloud Plugin. If not, see . +''' + + + +import os +import sys +import json +from datetime import datetime +import xbmc +import xbmcaddon +from .utils import Utils + + + +# variables +__addon__ = xbmcaddon.Addon('plugin.audio.mixcloud') +CACHED_HISTORY = {} + + + +class History: + + def __init__(self, name): + self.name = name + self.data = [] + self.readFile() + + def readFile(self): + starttime = datetime.now() + self.data = [] + filepath = xbmc.translatePath(__addon__.getAddonInfo('profile')) + self.name + '.json' + Utils.log('reading json file: ' + filepath) + try: + # read file + if os.path.exists(filepath): + with open(filepath, 'r') as text_file: + self.data = json.loads(text_file.read()) + self.trim() + elif __addon__.getSetting(self.name+'_list'): + # convert old 2.4.x settings + list_data = __addon__.getSetting(self.name + '_list').split(', ') + for list_entry in list_data: + json_entry = {} + list_fields = list_entry.split('=') + for list_field in list_fields: + if len(json_entry) == 0: + json_entry['key'] = list_field + elif len(json_entry) == 1: + json_entry['value'] = list_field + self.data.append(json_entry) + self.trim() + Utils.log('convert old 2.4.x settings: ' + self.name + ' -> ' + json.dumps(self.data)) + self.writeFile() + __addon__.setSetting(self.name + '_list', None) + + except Exception as e: + Utils.log('unable to read json file: ' + filepath, e) + elapsedtime = datetime.now() - starttime + Utils.log('read ' + str(len(self.data)) + ' items in ' + str(elapsedtime.seconds) + '.' + str(elapsedtime.microseconds) + ' seconds') + return self.data + + def writeFile(self): + filepath = xbmc.translatePath(__addon__.getAddonInfo('profile')) + self.name + '.json' + try: + with open(filepath, 'w+') as text_file: + text_file.write(json.dumps(self.data, indent = 4 * ' ')) + except Exception as e: + Utils.log('unable to write json file: ' + filepath, e) + + # add data and write file + def add(self, json_entry = {}): + try: + json_entry['timestamp'] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + self.data.insert(0, json_entry) + self.trim() + self.writeFile() + except Exception as e: + Utils.log('unable to add to json', e) + + # limit list + def trim(self): + json_max = 1 + if __addon__.getSetting(self.name + '_max'): + json_max = int(__addon__.getSetting(self.name + '_max')) + mon = xbmc.Monitor() + while len(self.data) > json_max: + # user aborted + if mon.abortRequested(): + break + + self.data.pop() + + # clear list + def clear(self): + Utils.log('clear json sfile') + self.data = [] + + @staticmethod + def getHistory(name): + history = CACHED_HISTORY.get(name) + if not history: + history = History(name) + CACHED_HISTORY[name] = history + return history \ No newline at end of file diff --git a/plugin.audio.mixcloud/lib/lang.py b/plugin.audio.mixcloud/lib/lang.py new file mode 100644 index 0000000000..cacca85877 --- /dev/null +++ b/plugin.audio.mixcloud/lib/lang.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +''' +@author: jackyNIX + +Copyright (C) 2011-2020 jackyNIX + +This file is part of KODI Mixcloud Plugin. + +KODI Mixcloud Plugin 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. + +KODI Mixcloud Plugin 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 KODI Mixcloud Plugin. If not, see . +''' + + + +import xbmcaddon + + + +__addon__ = xbmcaddon.Addon('plugin.audio.mixcloud') + + + +class Lang: + # main menu (301xx) + PROFILE = __addon__.getLocalizedString(30100) + FOLLOWINGS = __addon__.getLocalizedString(30101) + FOLLOWERS = __addon__.getLocalizedString(30102) + FAVORITES = __addon__.getLocalizedString(30103) + UPLOADS = __addon__.getLocalizedString(30104) + PLAYLISTS = __addon__.getLocalizedString(30105) + LISTEN_LATER = __addon__.getLocalizedString(30106) + CATEGORIES = __addon__.getLocalizedString(30107) + HISTORY = __addon__.getLocalizedString(30108) + SEARCH = __addon__.getLocalizedString(30109) + MORE = __addon__.getLocalizedString(30110) + + # search menu (302xx) + SEARCH_FOR_CLOUDCASTS = __addon__.getLocalizedString(30200) + SEARCH_FOR_USERS = __addon__.getLocalizedString(30201) + + # context menu items (303xx) + ADD_TO_FAVORITES = __addon__.getLocalizedString(30300) + REMOVE_FROM_FAVORITES = __addon__.getLocalizedString(30301) + ADD_TO_FOLLOWINGS = __addon__.getLocalizedString(30302) + REMOVE_FROM_FOLLOWINGS = __addon__.getLocalizedString(30303) + ADD_TO_LISTEN_LATER = __addon__.getLocalizedString(30304) + REMOVE_FROM_LISTEN_LATER = __addon__.getLocalizedString(30305) + + # others (304xx) + TOKEN_ERROR = __addon__.getLocalizedString(30400) + ENTER_OATH_CODE = __addon__.getLocalizedString(30401) + ASK_PROFILE_LOGOUT = __addon__.getLocalizedString(30402) + ASK_CLEAR_HISTORY = __addon__.getLocalizedString(30403) + NO_ACTIVE_RESOLVERS = __addon__.getLocalizedString(30404) + + # settings (309xx) \ No newline at end of file diff --git a/plugin.audio.mixcloud/lib/listbuilder.py b/plugin.audio.mixcloud/lib/listbuilder.py new file mode 100644 index 0000000000..523184f03a --- /dev/null +++ b/plugin.audio.mixcloud/lib/listbuilder.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- + +''' +@author: jackyNIX + +Copyright (C) 2011-2020 jackyNIX + +This file is part of KODI Mixcloud Plugin. + +KODI Mixcloud Plugin 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. + +KODI Mixcloud Plugin 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 KODI Mixcloud Plugin. If not, see . +''' + + + +import sys +import xbmc +import xbmcgui +import xbmcplugin +from datetime import datetime +from .mixcloud import MixcloudInterface +from .utils import Utils +from .history import History +from .resolver import ResolverBuilder +from .base import BaseBuilder, BaseListBuilder, QueryListBuilder, BaseList, BuildResult +from .lang import Lang + + + +# main menu +class MainBuilder(BaseListBuilder): + + def buildItems(self): + Utils.log('MainBuilder.buildItems()') + if MixcloudInterface().profileLoggedIn(): + self.addFolderItem({'title' : Lang.FOLLOWINGS}, {'mode' : 'playlists', 'key' : '/me/following/'}, Utils.getIcon('nav/kodi_highlight.png')) + self.addFolderItem({'title' : Lang.FOLLOWERS}, {'mode' : 'playlists', 'key' : '/me/followers/'}, Utils.getIcon('nav/kodi_highlight.png')) + self.addFolderItem({'title' : Lang.FAVORITES}, {'mode' : 'cloudcasts', 'key' : '/me/favorites/'}, Utils.getIcon('nav/kodi_favorites.png')) + self.addFolderItem({'title' : Lang.UPLOADS}, {'mode' : 'cloudcasts', 'key' : '/me/cloudcasts/'}, Utils.getIcon('nav/kodi_uploads.png')) + self.addFolderItem({'title' : Lang.PLAYLISTS}, {'mode' : 'playlists', 'key' : '/me/playlists/'}, Utils.getIcon('nav/kodi_playlists.png')) + self.addFolderItem({'title' : Lang.LISTEN_LATER}, {'mode' : 'cloudcasts', 'key' : '/me/listen-later/'}, Utils.getIcon('nav/kodi_listenlater.png')) + else: + self.addFolderItem({'title' : Lang.PROFILE}, {'mode' : 'profile', 'key' : 'login'}, Utils.getIcon('nav/kodi_profile.png')) + self.addFolderItem({'title' : Lang.CATEGORIES}, {'mode' : 'playlists', 'key' : '/categories/'}, Utils.getIcon('nav/kodi_categories.png')) + self.addFolderItem({'title' : Lang.HISTORY}, {'mode' : 'playhistory', 'offset' : 0, 'offsetex' : 0}, Utils.getIcon('nav/kodi_history.png')) + self.addFolderItem({'title' : Lang.SEARCH}, {'mode' : 'search'}, Utils.getIcon('nav/kodi_search.png')) + return 0 + + + +# cloudcasts menu +class CloudcastsBuilder(BaseListBuilder): + + def buildItems(self): + Utils.log('CloudcastsBuilder.buildItems()') + xbmcplugin.setContent(self.plugin_handle, 'songs') + cloudcasts = MixcloudInterface().getList(self.key, {'offset' : self.offset}) + mon = xbmc.Monitor() + for cloudcast in cloudcasts.items: + # user aborted + if mon.abortRequested(): + break + + contextMenuItems = self.buildContextMenuItems(cloudcast) + self.addAudioItem(cloudcast.infolabels, {'mode' : 'resolve', 'key' : cloudcast.key, 'user' : cloudcast.user}, cloudcast.image, contextMenuItems, len(cloudcasts.items)) + return cloudcasts.nextOffset + + + +# playlists menu +class PlaylistsBuilder(BaseListBuilder): + + def buildItems(self): + Utils.log('PlaylistsBuilder.buildItems()') + playlists = MixcloudInterface().getList(self.key, {'offset' : self.offset}) + mon = xbmc.Monitor() + for playlist in playlists.items: + # user aborted + if mon.abortRequested(): + break + + if playlist.image: + image = playlist.image + elif self.key == '/categories/': + image = Utils.getIcon('nav/kodi_categories.png') + elif self.key == '/me/playlists/': + image = Utils.getIcon('nav/kodi_playlists.png') + else: + image = '' + contextMenuItems = self.buildContextMenuItems(playlist) + self.addFolderItem(playlist.infolabels, {'mode' : 'cloudcasts', 'key' : playlist.key + 'cloudcasts/'}, image, contextMenuItems) + return playlists.nextOffset + + + +# play history menu (with profile listens) +class PlayHistoryBuilder(BaseListBuilder): + + def buildItems(self): + Utils.log('PlayHistoryBuilder.buildItems()') + xbmcplugin.setContent(self.plugin_handle, 'songs') + + cloudcasts = [] + playHistory = History.getHistory('play_history') + if playHistory: + cloudcasts.append(MixcloudInterface().getCloudcasts(playHistory.data, {'offset' : self.offset[0]})) + else: + cloudcasts.append(BaseList()) + if MixcloudInterface().profileLoggedIn(): + cloudcasts.append(MixcloudInterface().getList('/me/listens/', {'offset' : self.offset[1]})) + else: + cloudcasts.append(BaseList()) + + mergedCloudcasts = BaseList() + mergedCloudcasts.merge(cloudcasts) + mergedCloudcasts.initTrackNumbers(self.offset[0] + self.offset[1]) + if (cloudcasts[0].nextOffset + cloudcasts[1].nextOffset) > 0: + mergedCloudcasts.nextOffset[0] = self.offset[0] + mergedCloudcasts.nextOffset[0] + mergedCloudcasts.nextOffset[1] = self.offset[1] + mergedCloudcasts.nextOffset[1] + else: + mergedCloudcasts.nextOffset = [0, 0] + + mon = xbmc.Monitor() + for cloudcast in mergedCloudcasts.items: + # user aborted + if mon.abortRequested(): + break + + contextMenuItems = self.buildContextMenuItems(cloudcast) + self.addAudioItem(cloudcast.infolabels, {'mode' : 'resolve', 'key' : cloudcast.key, 'user' : cloudcast.user}, cloudcast.image, contextMenuItems, len(mergedCloudcasts.items)) + + return mergedCloudcasts.nextOffset + + + +# search menu +class SearchBuilder(BaseListBuilder): + + def buildItems(self): + self.addFolderItem({'title' : Lang.SEARCH_FOR_CLOUDCASTS}, {'mode' : 'searchcloudcast'}, Utils.getIcon('nav/kodi_search.png')) + self.addFolderItem({'title' : Lang.SEARCH_FOR_USERS}, {'mode' : 'searchuser'}, Utils.getIcon('nav/kodi_search.png')) + searchHistory = History.getHistory('search_history') + if searchHistory: + index = 0 + mon = xbmc.Monitor() + for keyitem in searchHistory.data: + # user aborted + if mon.abortRequested(): + break + + index += 1 + if index > self.offset: + if index <= self.offset + 10: + if keyitem['key'] == 'cloudcast': + self.addFolderItem({'title' : keyitem['value']}, {'mode' : 'searchcloudcast', 'key' : keyitem['value']}, Utils.getIcon('nav/kodi_playlists.png')) + elif keyitem['key'] == 'user': + self.addFolderItem({'title' : keyitem['value']}, {'mode' : 'searchuser', 'key' : keyitem['value']}, Utils.getIcon('nav/kodi_profile.png')) + else: + break + if index < len(searchHistory.data): + return index + return 0 + + + +# search cloudcast menu +class SearchCloudcastBuilder(QueryListBuilder): + + def buildQueryItems(self, query): + xbmcplugin.setContent(self.plugin_handle, 'songs') + cloudcasts = MixcloudInterface().getList('/search/', {'q' : query, 'type' : 'cloudcast', 'offset' : self.offset}) + mon = xbmc.Monitor() + for cloudcast in cloudcasts.items: + # user aborted + if mon.abortRequested(): + break + + contextMenuItems = self.buildContextMenuItems(cloudcast) + self.addAudioItem(cloudcast.infolabels, {'mode' : 'resolve', 'key' : cloudcast.key, 'user' : cloudcast.user}, cloudcast.image, contextMenuItems, len(cloudcasts.items)) + if not self.key: + searchHistory = History.getHistory('search_history') + if searchHistory: + searchHistory.add({'key' : 'cloudcast', 'value' : query}) + return cloudcasts.nextOffset + + + +# search user menu +class SearchUserBuilder(QueryListBuilder): + + def buildQueryItems(self, query): + users = MixcloudInterface().getList('/search/', {'q' : query, 'type' : 'user', 'offset' : self.offset}) + mon = xbmc.Monitor() + for user in users.items: + # user aborted + if mon.abortRequested(): + break + + contextMenuItems = self.buildContextMenuItems(user) + self.addFolderItem(user.infolabels, {'mode' : 'cloudcasts', 'key' : user.key + 'cloudcasts/'}, user.image, contextMenuItems) + if not self.key: + searchHistory = History.getHistory('search_history') + if searchHistory: + searchHistory.add({'key' : 'user', 'value' : query}) + return users.nextOffset + + + +# mixcloud profile builder +class MixcloudProfileBuilder(BaseBuilder): + + def build(self): + if (self.key == 'login') and (MixcloudInterface().profileLogin()): + return MainBuilder().build() + elif self.key == 'logout': + MixcloudInterface().profileLogout() + xbmc.executebuiltin('Container.Refresh') + return BuildResult.ENDOFDIRECTORY_DONOTHING + else: + return BuildResult.ENDOFDIRECTORY_FAILED + + + +# mixcloud post or delete builder +class MixcloudProfileActionBuilder(BaseBuilder): + + def build(self): + MixcloudInterface().profileAction(self.mode.upper(), self.key) + xbmc.executebuiltin('Container.Refresh') + return BuildResult.ENDOFDIRECTORY_DONOTHING + + + +# mixcloud post or delete builder +class ClearHistoryBuilder(BaseBuilder): + + def build(self): + if xbmcgui.Dialog().yesno('Mixcloud', Lang.ASK_CLEAR_HISTORY): + playHistory = History.getHistory('play_history') + playHistory.clear() + playHistory.writeFile() + + searchHistory = History.getHistory('search_history') + searchHistory.clear() + searchHistory.writeFile() + + xbmc.executebuiltin('Container.Refresh') + return BuildResult.ENDOFDIRECTORY_DONOTHING + + + +# mode/class switches +BUILDERS = { + '' : MainBuilder, + 'cloudcasts' : CloudcastsBuilder, + 'playlists' : PlaylistsBuilder, + 'playhistory' : PlayHistoryBuilder, + 'search' : SearchBuilder, + 'searchcloudcast' : SearchCloudcastBuilder, + 'searchuser' : SearchUserBuilder, + 'resolve' : ResolverBuilder, + 'profile' : MixcloudProfileBuilder, + 'post' : MixcloudProfileActionBuilder, + 'delete' : MixcloudProfileActionBuilder, + 'history' : ClearHistoryBuilder +} + +# main entry +def run(): + starttime = datetime.now() + Utils.log('##############################################################################################################################') + plugin_args = Utils.getArguments() + Utils.log('args: ' + str(plugin_args)) + + try: + BUILDERS.get(plugin_args.get('mode', ''), MainBuilder)().execute() + except Exception as e: + Utils.log('builder execute failed', e) + + elapsedtime = datetime.now() - starttime + Utils.log('executed in ' + str(elapsedtime.seconds) + '.' + str(elapsedtime.microseconds) + ' seconds') + + # version check + currentVersion = Utils.getVersion() + lastCheckedVersion = Utils.getSetting('last_checked_version') + if currentVersion != lastCheckedVersion: + xbmcgui.Dialog().ok('Mixcloud', Utils.getChangeLog()) + Utils.setSetting('last_checked_version', currentVersion) \ No newline at end of file diff --git a/plugin.audio.mixcloud/lib/mixcloud.py b/plugin.audio.mixcloud/lib/mixcloud.py new file mode 100644 index 0000000000..3bf12755ea --- /dev/null +++ b/plugin.audio.mixcloud/lib/mixcloud.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- + +''' +@author: jackyNIX + +Copyright (C) 2011-2020 jackyNIX + +This file is part of KODI Mixcloud Plugin. + +KODI Mixcloud Plugin 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. + +KODI Mixcloud Plugin 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 KODI Mixcloud Plugin. If not, see . +''' + + + +from urllib import parse, request +from .utils import Utils +from .base import BaseBuilder, BaseList, BaseListItem +from .lang import Lang +import json +import sys +import time +import xbmc +import xbmcgui + + + +STR_MIXCLOUD_API = 'https://api.mixcloud.com' +STR_CLIENTID= 'Vef7HWkSjCzEFvdhet' +STR_CLIENTSECRET= 'VK7hwemnZWBexDbnVZqXLapVbPK3FFYT' +STR_USERAGENT= 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0' +URL_REDIRECTURI= 'http://forum.kodi.tv/showthread.php?tid=116386' +URL_MIXCLOUD= 'https://www.mixcloud.com/' +URL_TOKEN= 'https://www.mixcloud.com/oauth/access_token' + +STR_THUMB_SIZES = { + 0 : 'small', # 25x25 + 1 : 'thumbnail', # 50x50 + 2 : 'medium', # 100x100 + 3 : 'large', # 300x300 + 4 : 'extra_large' # 600x600 +} + + + +class MixcloudInterface: + + def __init__(self): + self.accessToken = Utils.getSetting('access_token') + self.thumbSize = STR_THUMB_SIZES[int(Utils.getSetting('thumb_size'))] + + + + def getList(self, key = '', parameters = None): + Utils.log('getList(key = ' + key + ', parameters = ' + str(parameters) + ')') + mixcloudList = BaseList() + try: + url = STR_MIXCLOUD_API + key + offset = 0 + listLimit = int(Utils.getSetting('page_limit')) + if self.accessToken: + if parameters: + parameters['access_token'] = self.accessToken + else: + parameters = {'access_token' : self.accessToken} + if parameters: + parameters['limit'] = listLimit + if 'offset' in parameters and parameters['offset']: + offset = parameters['offset'] + else: + parameters = {'limit' : listLimit} + if parameters and len(parameters) > 0: + url = url + '?' + parse.urlencode(parameters) + Utils.log('getList(' + url + ')') + response = json.loads(request.urlopen(url).read()) + if 'data' in response and response['data'] : + data = response['data'] + mon = xbmc.Monitor() + for item in data: + # user aborted + if mon.abortRequested(): + break + + if (Utils.getSetting('ext_info') == 'true') and (listLimit == 10) and ('key' in item) and (item['key']): + mixcloudList.items.append(self.getCloudcast(item['key'], {})) + else: + mixcloudList.items.append(self.toListItem(item)) + if 'paging' in response and response['paging']: + paging = response['paging'] + if 'next' in paging and paging['next']: + mixcloudList.nextOffset = offset + listLimit + mixcloudList.initTrackNumbers(offset) + except Exception as e: + Utils.log('getList failed error', e) + return mixcloudList + + + + def getCloudcasts(self, keylist, parameters = {}): + mixcloudList = BaseList() + try: + offset = 0 + listLimit = int(Utils.getSetting('page_limit')) + index = 0 + if parameters and 'offset' in parameters and parameters['offset']: + offset = parameters['offset'] + mon = xbmc.Monitor() + for keyitem in keylist: + # user aborted + if mon.abortRequested(): + break + + if index >= offset: + if index < offset + listLimit: + mixcloudListItem = self.getCloudcast(keyitem['key'], {}) + if mixcloudListItem: + mixcloudListItem.setTimestamp(keyitem, 'timestamp') + mixcloudList.items.append(mixcloudListItem) + else: + index -= 1 + else: + break + index += 1 + if index < len(keylist): + mixcloudList.nextOffset = index + mixcloudList.initTrackNumbers(offset) + except Exception as e: + Utils.log('Get cloudcasts failed error: %s' % (sys.exc_info()[1]), e) + return mixcloudList + + + + def getCloudcast(self, key, parameters = {}): + try: + url = STR_MIXCLOUD_API + key + if self.accessToken: + if parameters: + parameters['access_token'] = self.accessToken + else: + parameters = {'access_token' : self.accessToken} + if parameters and (len(parameters) > 0): + url = url + '?' + parse.urlencode(parameters) + Utils.log('getCloudcast(' + url + ')') + response = json.loads(request.urlopen(url).read()) + return self.toListItem(response) + except Exception as e: + Utils.log('Get cloudcast failed error: %s' % (sys.exc_info()[1]), e) + return None + + + + def toListItem(self, data): + mixcloudListItem = BaseListItem() + if mixcloudListItem.setKey(data, 'key'): + Utils.copyValue(data, 'name', mixcloudListItem.infolabels, 'title') + if 'created_time' in data and data['created_time']: + created = data['created_time'] + structtime = time.strptime(created[0 : 10], '%Y-%m-%d') + mixcloudListItem.infolabels['year'] = int(time.strftime('%Y', structtime)) + mixcloudListItem.infolabels['date'] = time.strftime('%d.%m.%Y', structtime) + Utils.copyValue(data, 'audio_length', mixcloudListItem.infolabels, 'duration') + if 'user' in data and data['user']: + user = data['user'] + mixcloudListItem.setUser(user, 'key') + if not ('is_current_user' in user and user['is_current_user']): + mixcloudListItem.setFollowing(user, 'following') + Utils.copyValue(user, 'name', mixcloudListItem.infolabels, 'artist') + else: + if not ('is_current_user' in data and data['is_current_user']): + mixcloudListItem.setFollowing(data, 'following') + if 'pictures' in data and data['pictures']: + pictures = data['pictures'] + mixcloudListItem.setImage(pictures, self.thumbSize) + Utils.copyValue(data, 'description', mixcloudListItem.infolabels, 'comment') + if 'tags' in data and data['tags']: + tags = data['tags'] + genres = '' + for tag in tags: + if 'name' in tag and tag['name']: + genres = genres + tag['name'] + ' ' + if genres: + mixcloudListItem.infolabels['genre'] = genres.strip() + mixcloudListItem.setTimestamp(data, 'listen_time') + mixcloudListItem.setFavorited(data, 'favorited') + mixcloudListItem.setListenLater(data, 'is_listen_later') + Utils.log('toListItem(): ' + str(mixcloudListItem)) + return mixcloudListItem + + + + def profileLogout(self): + if xbmcgui.Dialog().yesno('Mixcloud', Lang.ASK_PROFILE_LOGOUT): + self.accessToken = '' + # setSetting('oath_code', '') + Utils.setSetting('access_token', '') + + + + def profileLoggedIn(self): + return self.accessToken != '' + + + + def profileLogin(self): + # ask for code if no token provided yet + if not self.accessToken: + Utils.log('No access token found') + ask = True + oathCode = Utils.getSetting('oath_code') + mon = xbmc.Monitor() + while ask: + # user aborted + if mon.abortRequested(): + break + + ask = xbmcgui.Dialog().yesno('Mixcloud', Lang.TOKEN_ERROR, Lang.ENTER_OATH_CODE) + if ask: + oathCode = Utils.getQuery(oathCode) + Utils.setSetting('oath_code', oathCode) + Utils.setSetting('access_token', '') + if oathCode != '': + try: + values = { + 'client_id' : STR_CLIENTID, + 'redirect_uri' : URL_REDIRECTURI, + 'client_secret' : STR_CLIENTSECRET, + 'code' : oathCode + } + headers = { + 'User-Agent' : STR_USERAGENT, + 'Referer' : URL_MIXCLOUD + } + postdata = parse.urlencode(values).encode('utf-8') + req = request.Request(URL_TOKEN, postdata, headers, URL_MIXCLOUD) + response = json.loads(request.urlopen(req).read().decode('utf-8')) + if 'access_token' in response and response['access_token'] : + Utils.log('Access_token received') + self.accessToken = response['access_token'] + Utils.setSetting('access_token', self.accessToken) + else: + Utils.log('No access_token received') + Utils.log(str(response)) + except Exception as e: + Utils.log('oath_code failed error=%s' % (sys.exc_info()[1]), e) + + ask=((oathCode!='') and (self.accessToken=='')) + + return self.accessToken != '' + + def profileAction(self, action, key): + Utils.log('profile action: ' + action + ' key: ' + key) + url = STR_MIXCLOUD_API + key + '?' + parse.urlencode({'access_token' : self.accessToken}) + Utils.log('url: ' + url) + req = request.Request(url, data = 'none'.encode('utf-8')) + req.get_method = lambda: action + response = request.urlopen(req).read().decode('utf-8') + data = json.loads(response) + info='' + if 'result' in data and data['result']: + result = data['result'] + if 'message' in result and result['message']: + info = result['message'] + if not(('success' in result) and (result['success'] == True)): + info = info + '\n\nFAILED!' + if info == '': + Utils.log(str(data)) + info = 'Unknown error occured.\n\n' + str(data) + xbmcgui.Dialog().ok('Mixcloud', info) \ No newline at end of file diff --git a/plugin.audio.mixcloud/lib/resolver.py b/plugin.audio.mixcloud/lib/resolver.py new file mode 100644 index 0000000000..07735056ea --- /dev/null +++ b/plugin.audio.mixcloud/lib/resolver.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- + +''' +@author: jackyNIX + +Copyright (C) 2011-2020 jackyNIX + +This file is part of KODI Mixcloud Plugin. + +KODI Mixcloud Plugin 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. + +KODI Mixcloud Plugin 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 KODI Mixcloud Plugin. If not, see . +''' + + + +from .utils import Utils +from .history import History +from .mixcloud import MixcloudInterface +from .base import BaseBuilder +from .lang import Lang +from urllib import request, parse +import xbmc +import xbmcgui +import xbmcplugin +import re +import sys +import json +import base64 +from itertools import cycle + + + +STR_USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0' + + + +class BaseResolver: + + def __init__(self, key): + self.key = key + + def resolve(self): + return '' + + + +class MixcloudResolver(BaseResolver): + + def resolve(self): + url = None + ck = 'https://www.mixcloud.com' + self.key + Utils.log('resolving cloudcast stream via mixcloud: ' + ck) + + try: + keysplit = self.key.split('/') + Utils.log('keysplit [empty, username, slug, empty] = %s' % (keysplit)) + + # get crsf token + csrf_token = None + response = request.urlopen('https://www.mixcloud.com') + headers = response.info() + for header in headers.get_all('Set-Cookie', []): + attributes = header.split('; ') + for attribute in attributes: + pair = attribute.split('=') + if pair[0] == 'csrftoken': + csrf_token = pair[1] + Utils.log('csrf_token = %s' % (csrf_token)) + + # create graphql + graphql = { + 'query' : 'query HeaderQuery(\n $lookup: CloudcastLookup!\n) {\n cloudcast: cloudcastLookup(lookup: $lookup) {\n id\n isExclusive\n ...PlayButton_cloudcast\n }\n}\n\nfragment PlayButton_cloudcast on Cloudcast {\n streamInfo {\n hlsUrl\n dashUrl\n url\n uuid\n }\n}\n', + 'variables' : { + 'lookup' : { + 'username' : keysplit[1], + 'slug' : keysplit[2] + } + } + } + Utils.log('graphql = %s' % (graphql)) + + # request graphql + postdata = json.dumps(graphql).encode() + headers = { + 'Referer' : 'https://www.mixcloud.com', + 'X-CSRFToken' : csrf_token, + 'Cookie' : 'csrftoken=' + csrf_token, + 'Content-Type' : 'application/json' + } + + req = request.Request('https://www.mixcloud.com/graphql', postdata, headers, 'https://www.mixcloud.com') + response = request.urlopen(req) + content = response.read() + json_content = json.loads(content) + Utils.log('response = %s' % (json_content)) + + # parse json + json_isexclusive=False + json_url=None + if 'data' in json_content and json_content['data']: + json_data = json_content['data'] + if 'cloudcast' in json_data and json_data['cloudcast']: + json_cloudcast = json_data['cloudcast'] + if 'isExclusive' in json_cloudcast and json_cloudcast['isExclusive']: + json_isexclusive = json_cloudcast['isExclusive'] + if 'streamInfo' in json_cloudcast and json_cloudcast['streamInfo']: + json_streaminfo = json_cloudcast['streamInfo'] + if 'url' in json_streaminfo and json_streaminfo['url']: + json_url = json_streaminfo['url'] + elif 'hlsUrl' in json_streaminfo and json_streaminfo['hlsUrl']: + json_url = json_streaminfo['hlsUrl'] + elif 'dashUrl' in json_streaminfo and json_streaminfo['dashUrl']: + json_url = json_streaminfo['dashUrl'] + + if json_url: + Utils.log('encoded url: ' + json_url) + decoded_url = base64.b64decode(json_url).decode('utf-8') + url = ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(decoded_url, cycle('IFYOUWANTTHEARTISTSTOGETPAIDDONOTDOWNLOADFROMMIXCLOUD'))) + Utils.log('url found: ' + url) + if not Utils.isValidURL(url): + Utils.log('invalid url') + url = None + elif json_isexclusive: + Utils.log('Cloudcast is exclusive') + else: + Utils.log('Unable to find url in json') + + except Exception as e: + Utils.log('Unable to resolve', e) + return url + + + +class MixcloudDownloaderResolver(BaseResolver): + + def resolve(self): + url = None + ck = 'https://www.mixcloud.com' + self.key + Utils.log('resolving cloudcast stream via mixcloud-downloader: ' + ck) + + try: + headers = { + 'User-Agent' : STR_USERAGENT, + 'Referer' : 'https://www.mixcloud-downloader.com/' + } + + values = { + 'url' : ck, + } + postdata = parse.urlencode(values).encode('utf-8') + req = request.Request('https://www.mixcloud-downloader.com/download/', postdata, headers, 'https://www.mixcloud-downloader.com/') + response = request.urlopen(req) + data = response.read().decode('utf-8') + + # first attempt + match = re.search(r'a class="btn btn-secondary btn-sm"(.*)', data, re.DOTALL) + if match: + match=re.search(r'href="(.*)"', match.group(1)) + if match: + url = match.group(1) + Utils.log('url found (1): ' + url) + if not Utils.isValidURL(url): + Utils.log('invalid url') + url = None + else: + Utils.log('Wrong response code (1)=%s len=%s' % (response.getcode(), len(data))) + + # second attempt + if not url: + match = re.search(r'URL from Mixcloud:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin.audio.podcasts/LICENSE.txt b/plugin.audio.podcasts/LICENSE.txt new file mode 100644 index 0000000000..98798f56b4 --- /dev/null +++ b/plugin.audio.podcasts/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Heckie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugin.audio.podcasts/addon.py b/plugin.audio.podcasts/addon.py new file mode 100644 index 0000000000..fbf9795127 --- /dev/null +++ b/plugin.audio.podcasts/addon.py @@ -0,0 +1,62 @@ +import sys + +from resources.lib.podcasts.actions.commit_gpodder import CommitGPodderAction +from resources.lib.podcasts.actions.commit_nextcloud import CommitNextcloudAction +from resources.lib.podcasts.actions.download_gpodder_subscriptions_action import DownloadGpodderSubscriptionsAction +from resources.lib.podcasts.actions.export_opml_action import ExportOpmlAction +from resources.lib.podcasts.actions.import_gpodder_subscriptions_action import ImportGPodderSubscriptionsAction +from resources.lib.podcasts.actions.import_opml_action import ImportOpmlAction +from resources.lib.podcasts.actions.search_fyyd_action import SearchFyydAction +from resources.lib.podcasts.actions.sync_nextcloud_subscriptions_action import SyncNextcloudSubscriptionsAction +from resources.lib.podcasts.actions.unassign_opml_action import UnassignOpmlAction +from resources.lib.podcasts.podcastsaddon import PodcastsAddon + +if __name__ == '__main__': + + if sys.argv[1] == "import_gpodder_subscriptions": + importGPodderSubscriptionsAction = ImportGPodderSubscriptionsAction() + importGPodderSubscriptionsAction.import_gpodder_subscriptions( + "True" == sys.argv[2]) + + elif sys.argv[1] == "download_gpodder_subscriptions": + downloadGpodderSubscriptionsAction = DownloadGpodderSubscriptionsAction() + downloadGpodderSubscriptionsAction.download_gpodder_subscriptions() + + elif sys.argv[1] == "import_opml": + importOpmlAction = ImportOpmlAction() + importOpmlAction.import_opml() + + elif sys.argv[1] == "unassign_opml": + unassignOpmlAction = UnassignOpmlAction() + unassignOpmlAction.unassign_opml() + + elif sys.argv[1] == "commit_gpodder": + commitGPodderAction = CommitGPodderAction() + commitGPodderAction.commit_gpodder() + + elif sys.argv[1] == "commit_nextcloud": + commitNextcloudAction = CommitNextcloudAction() + commitNextcloudAction.commit_nextcloud() + + elif sys.argv[1] == "sync_nextcloud_subscriptions": + syncNextcloudSubscriptionsAction = SyncNextcloudSubscriptionsAction() + syncNextcloudSubscriptionsAction.sync_nextcloud_subscriptions( + "True" == sys.argv[2], "True" == sys.argv[3]) + + elif sys.argv[1] == "export_to_nextcloud": + syncNextcloudSubscriptionsAction = SyncNextcloudSubscriptionsAction() + syncNextcloudSubscriptionsAction.export_to_nextcloud() + + elif sys.argv[1] == "export_opml": + exportOpmlAction = ExportOpmlAction() + exportOpmlAction.export_opml() + + elif sys.argv[1] == "search_fyyd": + searchFyydAction = SearchFyydAction() + searchFyydAction.search_fyyd() + + else: + podcastsAddon = PodcastsAddon(int(sys.argv[1])) + podcastsAddon.handle(sys.argv) + syncNextcloudSubscriptionsAction = SyncNextcloudSubscriptionsAction() + syncNextcloudSubscriptionsAction.check_for_updates() diff --git a/plugin.audio.podcasts/addon.xml b/plugin.audio.podcasts/addon.xml new file mode 100644 index 0000000000..0fdd88feb9 --- /dev/null +++ b/plugin.audio.podcasts/addon.xml @@ -0,0 +1,79 @@ + + + + + + + + + audio video + + + RSS Podcasts in KODI + RSS Podcasts in KODI + This addon allows to import your audio and video podcasts from gPodder.net, Nextcloud Gpoddersync or from OPML files. +You can make use of up to 10 categories in order to manage your podcasts. You can also directly enter the URLs of your podcasts +or attach up to 10 OPML files directly. + + Dieses Addon ermöglicht den Import Deiner Audio und Video Podcasts von gPodder.net, Nextcloud Gpoddersync oder aus OPML Dateien. +Du kannst Deine Podcasts in bis zu 10 Kategorien einsortieren. RSS Feeds können auch manuell gepflegt werden. Weiterhin können bis zu 10 OPML +Dateien direkt eingehängt werden. + + de_de + en_gb + all + MIT + https://github.com/Heckie75/kodi-addon-podcast + https://github.com/Heckie75/kodi-addon-podcast + +v2.3.2 (2024-01-21) +- Fixed bug #23: Addon crashes immediately after starting it (prob. MS Windows only) + +v2.3.1 (2023-10-02) +- Fixed bug #1, support network paths like smb:// + +v2.3.0 (2023-08-06) +- Added feature in order to search for podcasts in fyyd.de podcast directory +- Support for Nextcloud Gpoddersync +- Improved thumbnail if item is added to favourites +- Added help texts in settings +- refactoring + +v2.2.3 (2022-10-04) +- Fixed error if feed is empty (see issue #18) + +v2.2.2 (2022-05-26) +- Improved performance when loading rss feed with episodes (up to 50% faster on RPi 4, see issue #17) +- Added feature to limit episodes (see issue #17) + +v2.2.1 (2021-10-17) +- less restrictive when parsing RSS feeds with no XML processing instruction (reported issue #16) +- Forced UTF-8 encoding, reported problem starting with Kodi 19.2 (reported issue #15) + +v2.2.0 (2021-08-14) +- Added feauture in order to backup feeds as OPML file + +v2.1.1 (2021-08-13) +- Migration to new settings format +- Refactoring only. No new features +- Minor bugfixes + +v2.1.0 (2021-05-24) +- Added support for importing subscriptions from your gPodder account or from local files +- Added information like duration and plot for video podcasts +- many refactorings and bugfixes + + + resources/assets/icon.png + resources/assets/fanart.png + resources/assets/screenshot_01.png + resources/assets/screenshot_02.png + resources/assets/screenshot_03.png + resources/assets/screenshot_04.png + resources/assets/screenshot_05.png + resources/assets/screenshot_06.png + resources/assets/screenshot_07.png + resources/assets/screenshot_08.png + + + diff --git a/plugin.audio.podcasts/resources/assets/fanart.png b/plugin.audio.podcasts/resources/assets/fanart.png new file mode 100644 index 0000000000..4d8a0596c2 Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/fanart.png differ diff --git a/plugin.audio.podcasts/resources/assets/icon.png b/plugin.audio.podcasts/resources/assets/icon.png new file mode 100644 index 0000000000..6712d75c72 Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/icon.png differ diff --git a/plugin.audio.podcasts/resources/assets/notification.png b/plugin.audio.podcasts/resources/assets/notification.png new file mode 100644 index 0000000000..e9543c6922 Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/notification.png differ diff --git a/plugin.audio.podcasts/resources/assets/screenshot_01.png b/plugin.audio.podcasts/resources/assets/screenshot_01.png new file mode 100644 index 0000000000..d3304324a5 Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/screenshot_01.png differ diff --git a/plugin.audio.podcasts/resources/assets/screenshot_02.png b/plugin.audio.podcasts/resources/assets/screenshot_02.png new file mode 100644 index 0000000000..f623d2a1ba Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/screenshot_02.png differ diff --git a/plugin.audio.podcasts/resources/assets/screenshot_03.png b/plugin.audio.podcasts/resources/assets/screenshot_03.png new file mode 100644 index 0000000000..5efc691316 Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/screenshot_03.png differ diff --git a/plugin.audio.podcasts/resources/assets/screenshot_04.png b/plugin.audio.podcasts/resources/assets/screenshot_04.png new file mode 100644 index 0000000000..7574dea77b Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/screenshot_04.png differ diff --git a/plugin.audio.podcasts/resources/assets/screenshot_05.png b/plugin.audio.podcasts/resources/assets/screenshot_05.png new file mode 100644 index 0000000000..9bf4d5d942 Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/screenshot_05.png differ diff --git a/plugin.audio.podcasts/resources/assets/screenshot_06.png b/plugin.audio.podcasts/resources/assets/screenshot_06.png new file mode 100644 index 0000000000..081765dc78 Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/screenshot_06.png differ diff --git a/plugin.audio.podcasts/resources/assets/screenshot_07.png b/plugin.audio.podcasts/resources/assets/screenshot_07.png new file mode 100644 index 0000000000..cb163c47dd Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/screenshot_07.png differ diff --git a/plugin.audio.podcasts/resources/assets/screenshot_08.png b/plugin.audio.podcasts/resources/assets/screenshot_08.png new file mode 100644 index 0000000000..7a0a1d7a97 Binary files /dev/null and b/plugin.audio.podcasts/resources/assets/screenshot_08.png differ diff --git a/plugin.audio.podcasts/resources/language/resource.language.de_de/strings.po b/plugin.audio.podcasts/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..462f8bc77d --- /dev/null +++ b/plugin.audio.podcasts/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,542 @@ +# Kodi Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Group" +msgstr "Gruppe" + +msgctxt "#32001" +msgid "Group 1" +msgstr "Gruppe 1" + +msgctxt "#32002" +msgid "Group 2" +msgstr "Gruppe 2" + +msgctxt "#32003" +msgid "Group 3" +msgstr "Gruppe 3" + +msgctxt "#32004" +msgid "Group 4" +msgstr "Gruppe 4" + +msgctxt "#32005" +msgid "Group 5" +msgstr "Gruppe 5" + +msgctxt "#32006" +msgid "Group 6" +msgstr "Gruppe 6" + +msgctxt "#32007" +msgid "Group 7" +msgstr "Gruppe 7" + +msgctxt "#32008" +msgid "Group 8" +msgstr "Gruppe 8" + +msgctxt "#32009" +msgid "Group 9" +msgstr "Gruppe 9" + +msgctxt "#32010" +msgid "Group 10" +msgstr "Gruppe 10" + +msgctxt "#32011" +msgid "OPML File 1" +msgstr "OPML Datei 1" + +msgctxt "#32012" +msgid "OPML File 2" +msgstr "OPML Datei 2" + +msgctxt "#32013" +msgid "OPML File 3" +msgstr "OPML Datei 3" + +msgctxt "#32014" +msgid "OPML File 4" +msgstr "OPML Datei 4" + +msgctxt "#32015" +msgid "OPML File 5" +msgstr "OPML Datei 5" + +msgctxt "#32016" +msgid "OPML File 6" +msgstr "OPML Datei 6" + +msgctxt "#32017" +msgid "OPML File 7" +msgstr "OPML Datei 7" + +msgctxt "#32018" +msgid "OPML File 8" +msgstr "OPML Datei 8" + +msgctxt "#32019" +msgid "OPML File 9" +msgstr "OPML Datei 9" + +msgctxt "#32020" +msgid "OPML File 10" +msgstr "OPML Datei 10" + +msgctxt "#32021" +msgid "OPML Files" +msgstr "OPML Dateien" + +msgctxt "#32022" +msgid "URL" +msgstr "URL" + +msgctxt "#32023" +msgid "OPML File" +msgstr "OPML Datei" + +msgctxt "#32024" +msgid "vacancy / occupancy" +msgstr "frei / belegt" + +msgctxt "#32031" +msgid "Podcast 1" +msgstr "Podcast 1" + +msgctxt "#32032" +msgid "Podcast 2" +msgstr "Podcast 2" + +msgctxt "#32033" +msgid "Podcast 3" +msgstr "Podcast 3" + +msgctxt "#32034" +msgid "Podcast 4" +msgstr "Podcast 4" + +msgctxt "#32035" +msgid "Podcast 5" +msgstr "Podcast 5" + +msgctxt "#32036" +msgid "Podcast 6" +msgstr "Podcast 6" + +msgctxt "#32037" +msgid "Podcast 7" +msgstr "Podcast 7" + +msgctxt "#32038" +msgid "Podcast 8" +msgstr "Podcast 8" + +msgctxt "#32039" +msgid "Podcast 9" +msgstr "Podcast 9" + +msgctxt "#32040" +msgid "Podcast 10" +msgstr "Podcast 10" + +msgctxt "#32050" +msgid "Miscellaneous" +msgstr "Sonstiges" + +msgctxt "#32051" +msgid "Anchor for most recent podcast (good for bookmarking)" +msgstr "Anker für aktuelle Sendung (hilfreich zum Bookmarken)" + +msgctxt "#32052" +msgid "limit episodes" +msgstr "begrenze Episoden" + +msgctxt "#32053" +msgid "Untitled podcast from" +msgstr "Namenloser Podcast von" + +msgctxt "#32054" +msgid "no limit" +msgstr "keine Begrenzung" + +msgctxt "#32055" +msgid "30 episodes" +msgstr "30 Episoden" + +msgctxt "#32056" +msgid "60 episodes" +msgstr "60 Episoden" + +msgctxt "#32057" +msgid "120 episodes" +msgstr "120 Episoden" + +msgctxt "#32058" +msgid "180 episodes" +msgstr "180 Episoden" + +msgctxt "#32059" +msgid "365 episodes" +msgstr "365 Episoden" + +msgctxt "#32060" +msgid "Actions" +msgstr "Aktionen" + +msgctxt "#32061" +msgid "gPodder.net" +msgstr "gPodder.net" + +msgctxt "#32062" +msgid "Username" +msgstr "Benutzername" + +msgctxt "#32063" +msgid "Password" +msgstr "Passwort" + +msgctxt "#32064" +msgid "Import subscriptions to group" +msgstr "Subskriptionen in eine Gruppe laden" + +msgctxt "#32065" +msgid "Download subscriptions as OPML File" +msgstr "Subskriptionen als OPML Datei laden" + +msgctxt "#32066" +msgid "Import from OPML File to group" +msgstr "OPML Datei in eine Gruppe laden" + +msgctxt "#32067" +msgid "Hostname" +msgstr "Hostname" + +msgctxt "#32068" +msgid "Apply changes" +msgstr "Änderungen übernehmen" + +msgctxt "#32069" +msgid "Import new subscriptions to group" +msgstr "Neue Subskriptionen in eine Gruppe laden" + +msgctxt "#32070" +msgid "Select OPML file" +msgstr "Wähle OPML Datei für Import aus" + +msgctxt "#32071" +msgid "Select feeds" +msgstr "Wähle RSS Feeds für den Import aus" + +msgctxt "#32072" +msgid "No feeds selected" +msgstr "Keine Feeds ausgewählt" + +msgctxt "#32073" +msgid "You must select at least one feed" +msgstr "Du musst mindestens einen Feed auswählen" + +msgctxt "#32074" +msgid "Too many feeds selected" +msgstr "Zu viele Feeds ausgewählt" + +msgctxt "#32075" +msgid "Do not select more than %i feeds" +msgstr "Es dürfen nicht mehr als %i Feeds ausgewählt werden" + +msgctxt "#32076" +msgid "Select group for import" +msgstr "Wähle die Gruppe aus, in die importiert werden soll" + +msgctxt "#32077" +msgid "vacant slots" +msgstr "freie Plätze" + +msgctxt "#32078" +msgid "No vacant slots in group left" +msgstr "Keine freien Plätze in Gruppe verfügbar" + +msgctxt "#32079" +msgid "Attach OPML File?" +msgstr "OPML Datei zuweisen?" + +msgctxt "#32080" +msgid "Select folder for download" +msgstr "Ordner für Download auswählen" + +msgctxt "#32081" +msgid "Saving OPML file failed" +msgstr "Speichern fehlgeschlagen" + +msgctxt "#32082" +msgid "Saving OPML file failed for unknown reason" +msgstr "Speichern ist fehlgeschlagen" + +msgctxt "#32083" +msgid "Saved as" +msgstr "Gespeichert als" + +msgctxt "#32084" +msgid "Select a different group or disable slots in this group first" +msgstr "Wähle eine andere Gruppe oder deaktiviere Podcasts in dieser Gruppe um fortzufahren" + +msgctxt "#32085" +msgid "RSS Podcast" +msgstr "RSS Podcast" + +msgctxt "#32086" +msgid "Actions completed" +msgstr "Aktionen beendet" + +msgctxt "#32087" +msgid "Detach OPML files" +msgstr "OPML Dateien aushängen" + +msgctxt "#32088" +msgid "There are no feeds to select" +msgstr "Es gibt derzeit keine Feeds zur Auswahl" + +msgctxt "#32089" +msgid "Backup subscription as OPML" +msgstr "Feeds als OPML Datei sichern" + +msgctxt "#32090" +msgid "Select folder for backup" +msgstr "Wähle Verzeichnis für Sicherung" + +msgctxt "#32091" +msgid "Backup successful" +msgstr "Sicherung erfolgreich" + +msgctxt "#32092" +msgid "Backup failed" +msgstr "Sicherung gescheitert" + +msgctxt "#32093" +msgid "Provider" +msgstr "Provider" + +msgctxt "#32094" +msgid "Nextcloud Gpoddersync" +msgstr "Nextcloud Gpoddersync" + +msgctxt "#32095" +msgid "Authentification failed." +msgstr "Anmeldung fehlgeschlagen" + +msgctxt "#32096" +msgid "Automatic synchronization" +msgstr "Automatische Synchronisierung" + +msgctxt "#32097" +msgid "off" +msgstr "aus" + +msgctxt "#32098" +msgid "once per day" +msgstr "einmal pro Tag" + +msgctxt "#32099" +msgid "once per week" +msgstr "einmal pro Woche" + +msgctxt "#32101" +msgid "most recent episode" +msgstr "aktuelle Sendung" + +msgctxt "#32111" +msgid "Full synchronization" +msgstr "Komplette Synchronisation" + +msgctxt "#32112" +msgid "Synchronize now" +msgstr "Änderungen synchronisieren" + +msgctxt "#32113" +msgid "Synchronizing with Nextcloud..." +msgstr "Synchronisiere mit Nextcloud..." + +msgctxt "#32114" +msgid "Inspecting new feeds..." +msgstr "Prüfe neue Feeds..." + +msgctxt "#32115" +msgid "Select feeds to delete in Kodi?" +msgstr "Wähle Feeds zum Löschen in Kodi aus?" + +msgctxt "#32116" +msgid "Select feeds to add to group in Kodi?" +msgstr "Wähle Feeds zum Hinzufügen in eine Gruppe in Kodi aus?" + +msgctxt "#32117" +msgid "Synchronization completed" +msgstr "Synchronisation fertig" + +msgctxt "#32118" +msgid "Export to Nextcloud" +msgstr "Exportiere nach Nextcloud" + +msgctxt "#32119" +msgid "Select feeds to add in Nextcloud?" +msgstr "Wähle Feeds zum Hinzufügen in Nextcloud aus?" + +msgctxt "#32120" +msgid "Select feeds to delete in Nextcloud?" +msgstr "Wähle Feeds zum Löschen in Nextcloud aus?" + +msgctxt "#32121" +msgid "Search podcasts directories" +msgstr "Durchsuche Podcast Verzeichnisse" + +msgctxt "#32122" +msgid "Search fyyd.de" +msgstr "Durchsuche fyyd.de" + +msgctxt "#32123" +msgid "Enter search term in order to find podcast" +msgstr "Gebe Suchbegriff ein, um einen Podcast zu finden" + +msgctxt "#32124" +msgid "Search for %s" +msgstr "Suche nach %s" + +msgctxt "#32125" +msgid "No results for %s found" +msgstr "Keine Ergebnisse für %s" + +msgctxt "#32126" +msgid "Choose podcasts in order to subscribe" +msgstr "Wähle Podcasts zum Abonnieren aus" + +msgctxt "#32127" +msgid "Successfully subscribed to podcasts" +msgstr "Podcast erfolgreich abonniert" + +msgctxt "#32151" +msgid "Connection Error" +msgstr "Verbindungsfehler" + +msgctxt "#32152" +msgid "HTTP Method %s not supported" +msgstr "HTTP Methode %s wird nicht unterstützt" + +msgctxt "#32153" +msgid "Request Exception: Please check URL and port or see logs for further details" +msgstr "Request Fehler: Prüfe URL und Port. Für weitere Details prüfe die Log-Datei" + +msgctxt "#32154" +msgid "Unexpected HTTP Status %i for %s" +msgstr "Unerwarteter HTTP Status %i für %s" + +msgctxt "#32155" +msgid "Unexpected content for podcast" +msgstr "Unerwarteter Inhalt für Podcast" + +msgctxt "#32200" +msgid "Select a remote type if you want to synchronize your subscriptions between Kodi and other devices. Supported protocols are gPodder.net and Nextcloud Gpoddersync." +msgstr "Wählen einen Servertypen aus, wenn Du deine Abos zwischen Deinen Geräten synchronisieren möchtest. Unterstützte Protokolle sind gPodder.net und Nextcloud Gpoddersync." + +msgctxt "#32201" +msgid "Enter the hostname of your server, e.g. https://myserver.net:443" +msgstr "Gebe hier den Hostnamen deines Servers ein, z.B. https://myserver.net:443" + +msgctxt "#32202" +msgid "Enter your username in order to login to server." +msgstr "Gebe deinen Nutzernamen an, mit dem Du dich an dem Server anmeldest." + +msgctxt "#32203" +msgid "Enter your password in order to login to server. Note: Password is stored in plain text in settings file of this addon!" +msgstr "Gebe dein Passwort ein, mit dem Du dich an dem Server anmeldest. Hinweis: Das Passwort wird unverschlüsselt in Kodi gespeichert." + +msgctxt "#32204" +msgid "Apply changes in order to check if hostname, username and password work as expected." +msgstr "Übernehme Änderungen, um zu prüfen ob Hostname, Nutzername und Passwort funktionieren." + +msgctxt "#32205" +msgid "Select an update interval in order to get reminded if subscriptions has changed on your remote." +msgstr "Wähle ein Aktualisierungsinterval, um über Änderungen deiner Abos auf anderen Geräten erinnert zu werden." + +msgctxt "#32206" +msgid "Request all available subscriptions from gPodder.net and subscribe to them by assigning these to groups." +msgstr "Frage alle Abos von gPodder.net ab und abonniere diese, in dem Du diese Gruppen zuweist." + +msgctxt "#32207" +msgid "Request only subscriptions from gPodder.net that are not subscribed in Kodi yet and subscribe to them by assigning these to groups." +msgstr "Frage nur Abos von gPodder.net ab, die noch nicht in Kodi abonniert wurden, und abonniere diese, in dem Du diese Gruppen zuweist." + +msgctxt "#32208" +msgid "Request all available subscriptions from gPodder.net and download these as OPML file." +msgstr "Frage alle Abos von gPodder.net ab und speichere diese als OPML-Datei." + +msgctxt "#32209" +msgid "Synchronize all subscriptions with Nextcloud Gpoddersync. You can subscribe to new podcasts and will be asked to remove subscriptions in Kodi that has been deleted on other devices." +msgstr "Synchronisiere alle Abos über Nextcloud Gpoddersync. Du kannst neue Podcasts übernehmen und wirst gefragt, gelöschte Abos in Kodi zu löschen, die auf anderen Geräten entfernt wurden." + +msgctxt "#32210" +msgid "Synchronize only changes with Nextcloud Gpoddersync since last synchronization. You can subscribe to new podcasts and will be asked to remove subscriptions in Kodi that has been deleted on other devices." +msgstr "Synchronisiere nur Änderungen über Nextcloud Gpoddersync seit der letzten Synchronisation. Du kannst neue Podcasts übernehmen und wirst gefragt, gelöschte Abos in Kodi zu löschen, die auf anderen Geräten entfernt wurden." + +msgctxt "#32211" +msgid "Import subscriptions by loading an OPML file." +msgstr "Importiere Abos aus einer OMPL Datei." + +msgctxt "#32212" +msgid "Click here If you want to detach an OPML file. File won't be deleted." +msgstr "Klicke hier, um eine OPML Datei auszuhängen. Die Datei wird dabei nicht gelöscht." + +msgctxt "#32213" +msgid "Click here If you want to make a backup of your subscriptions. A OPML file will be written." +msgstr "Klicke hier, um eine Sicherung deiner Abos zu machen. Es wird eine OPML Datei geschrieben." + +msgctxt "#32214" +msgid "Slot in order to attach an OPML file." +msgstr "Platz, um eine OPML Datei einzuhängen." + +msgctxt "#32215" +msgid "Toggle switch in order to display or hide this group." +msgstr "Stelle den Schalter ein, um diese Gruppe ein- oder auszublenden." + +msgctxt "#32216" +msgid "Enter the displayname of this group." +msgstr "Gebe hier den Anzeigenamen der Gruppe ein." + +msgctxt "#32217" +msgid "Mark this slot as free or occupied. If it is marked as free you can use this slot in order to subscribe to other podcasts e.g. by synchronization with other devices." +msgstr "Markiere diesen Platz als frei oder belegt. Wenn der Platz frei ist, kann dieser für neue Abos verwendet werden, z.B. während der Synchronisation mit anderen Geräten." + +msgctxt "#32218" +msgid "Enter the displayname of this subscription." +msgstr "Gebe hier den Anzeigenamen des Abos ein." + +msgctxt "#32219" +msgid "Here you can enter or change the URL of the subscription manually." +msgstr "Hier kannst Du die URL des Abos manuell eingeben oder ändern." + +msgctxt "#32220" +msgid "Activate this button in order to display a solid list item on top of each subscription which represents the latest episode. This is very helpful if you want to bookmark latest episode by using Kodi's favorites menu." +msgstr "Aktiviere diesen Schalter, wenn Du am Anfang jeder Liste einen festen Eintrag für die aktuellste Episode haben möchtest. Dies ist hilfreich, um einen entsprechenden Eintrag im Favoritenmenü von Kodi hinzuzufügen." + +msgctxt "#32221" +msgid "Sometimes a subscription contains hundrets of episodes. Then you would probably like to limit number of episodes displayed in list." +msgstr "Manchmal haben Abos hunderte Episoden. In diesem Fall möchtest Du vielleicht die Liste der angezeigten Episoden begrenzen." + +msgctxt "#32222" +msgid "Export subscriptions in Kodi to Nextcloud. Active and inactive subscriptions will be taken into account." +msgstr "Exportiere Abos zu Nextcloud. Es werden aktive und deaktivierte Abos berücksichtigt." + +msgctxt "#32223" +msgid "Search fyyd.de for podcasts. You can subscribe to new podcasts by adding them to groups." +msgstr "Durchsuche fyyd.de nach Podcasts. Du kannst diese abonnieren und Gruppen zuordnen." diff --git a/plugin.audio.podcasts/resources/language/resource.language.en_gb/strings.po b/plugin.audio.podcasts/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..631cca4553 --- /dev/null +++ b/plugin.audio.podcasts/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,542 @@ +# Kodi Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Group" +msgstr "" + +msgctxt "#32001" +msgid "Group 1" +msgstr "" + +msgctxt "#32002" +msgid "Group 2" +msgstr "" + +msgctxt "#32003" +msgid "Group 3" +msgstr "" + +msgctxt "#32004" +msgid "Group 4" +msgstr "" + +msgctxt "#32005" +msgid "Group 5" +msgstr "" + +msgctxt "#32006" +msgid "Group 6" +msgstr "" + +msgctxt "#32007" +msgid "Group 7" +msgstr "" + +msgctxt "#32008" +msgid "Group 8" +msgstr "" + +msgctxt "#32009" +msgid "Group 9" +msgstr "" + +msgctxt "#32010" +msgid "Group 10" +msgstr "" + +msgctxt "#32011" +msgid "OPML File 1" +msgstr "" + +msgctxt "#32012" +msgid "OPML File 2" +msgstr "" + +msgctxt "#32013" +msgid "OPML File 3" +msgstr "" + +msgctxt "#32014" +msgid "OPML File 4" +msgstr "" + +msgctxt "#32015" +msgid "OPML File 5" +msgstr "" + +msgctxt "#32016" +msgid "OPML File 6" +msgstr "" + +msgctxt "#32017" +msgid "OPML File 7" +msgstr "" + +msgctxt "#32018" +msgid "OPML File 8" +msgstr "" + +msgctxt "#32019" +msgid "OPML File 9" +msgstr "" + +msgctxt "#32020" +msgid "OPML File 10" +msgstr "" + +msgctxt "#32021" +msgid "OPML Files" +msgstr "" + +msgctxt "#32022" +msgid "URL" +msgstr "" + +msgctxt "#32023" +msgid "OPML File" +msgstr "" + +msgctxt "#32024" +msgid "vacancy / occupancy" +msgstr "" + +msgctxt "#32031" +msgid "Podcast 1" +msgstr "" + +msgctxt "#32032" +msgid "Podcast 2" +msgstr "" + +msgctxt "#32033" +msgid "Podcast 3" +msgstr "" + +msgctxt "#32034" +msgid "Podcast 4" +msgstr "" + +msgctxt "#32035" +msgid "Podcast 5" +msgstr "" + +msgctxt "#32036" +msgid "Podcast 6" +msgstr "" + +msgctxt "#32037" +msgid "Podcast 7" +msgstr "" + +msgctxt "#32038" +msgid "Podcast 8" +msgstr "" + +msgctxt "#32039" +msgid "Podcast 9" +msgstr "" + +msgctxt "#32040" +msgid "Podcast 10" +msgstr "" + +msgctxt "#32050" +msgid "Miscellaneous" +msgstr "" + +msgctxt "#32051" +msgid "Anchor for most recent podcast (good for bookmarking)" +msgstr "" + +msgctxt "#32052" +msgid "limit episodes" +msgstr "" + +msgctxt "#32053" +msgid "Untitled podcast from" +msgstr "" + +msgctxt "#32054" +msgid "no limit" +msgstr "" + +msgctxt "#32055" +msgid "30 episodes" +msgstr "" + +msgctxt "#32056" +msgid "60 episodes" +msgstr "" + +msgctxt "#32057" +msgid "120 episodes" +msgstr "" + +msgctxt "#32058" +msgid "180 episodes" +msgstr "" + +msgctxt "#32059" +msgid "365 episodes" +msgstr "" + +msgctxt "#32060" +msgid "Actions" +msgstr "" + +msgctxt "#32061" +msgid "gPodder.net" +msgstr "" + +msgctxt "#32062" +msgid "Username" +msgstr "" + +msgctxt "#32063" +msgid "Password" +msgstr "" + +msgctxt "#32064" +msgid "Import subscriptions to group" +msgstr "" + +msgctxt "#32065" +msgid "Download subscriptions as OPML File" +msgstr "" + +msgctxt "#32066" +msgid "Import from OPML File to group" +msgstr "" + +msgctxt "#32067" +msgid "Hostname" +msgstr "" + +msgctxt "#32068" +msgid "Apply changes" +msgstr "" + +msgctxt "#32069" +msgid "Import new subscriptions to group" +msgstr "" + +msgctxt "#32070" +msgid "Select OPML file" +msgstr "" + +msgctxt "#32071" +msgid "Select feeds" +msgstr "" + +msgctxt "#32072" +msgid "No feeds selected" +msgstr "" + +msgctxt "#32073" +msgid "You must select at least one feed" +msgstr "" + +msgctxt "#32074" +msgid "Too many feeds selected" +msgstr "" + +msgctxt "#32075" +msgid "Do not select more than %i feeds" +msgstr "" + +msgctxt "#32076" +msgid "Select group for import" +msgstr "" + +msgctxt "#32077" +msgid "vacant slots" +msgstr "" + +msgctxt "#32078" +msgid "No vacant slots in group left" +msgstr "" + +msgctxt "#32079" +msgid "Attach OPML File?" +msgstr "" + +msgctxt "#32080" +msgid "Select folder for download" +msgstr "" + +msgctxt "#32081" +msgid "Saving OPML file failed" +msgstr "" + +msgctxt "#32082" +msgid "Saving OPML file failed for unknown reason" +msgstr "" + +msgctxt "#32083" +msgid "Saved as" +msgstr "" + +msgctxt "#32084" +msgid "Select a different group or disable slots in this group first" +msgstr "" + +msgctxt "#32085" +msgid "RSS Podcast" +msgstr "" + +msgctxt "#32086" +msgid "Actions completed" +msgstr "" + +msgctxt "#32087" +msgid "Detach OPML files" +msgstr "" + +msgctxt "#32088" +msgid "There are no feeds to select" +msgstr "" + +msgctxt "#32089" +msgid "Backup subscription as OPML" +msgstr "" + +msgctxt "#32090" +msgid "Select folder for backup" +msgstr "" + +msgctxt "#32091" +msgid "Backup successful" +msgstr "" + +msgctxt "#32092" +msgid "Backup failed" +msgstr "" + +msgctxt "#32093" +msgid "Provider" +msgstr "" + +msgctxt "#32094" +msgid "Nextcloud Gpoddersync" +msgstr "" + +msgctxt "#32095" +msgid "Authentification failed." +msgstr "" + +msgctxt "#32096" +msgid "Automatic synchronization" +msgstr "" + +msgctxt "#32097" +msgid "off" +msgstr "" + +msgctxt "#32098" +msgid "once per day" +msgstr "" + +msgctxt "#32099" +msgid "once per week" +msgstr "" + +msgctxt "#32101" +msgid "most recent episode" +msgstr "" + +msgctxt "#32111" +msgid "Full synchronization" +msgstr "" + +msgctxt "#32112" +msgid "Synchronize now" +msgstr "" + +msgctxt "#32113" +msgid "Synchronizing with Nextcloud..." +msgstr "" + +msgctxt "#32114" +msgid "Inspecting new feeds..." +msgstr "" + +msgctxt "#32115" +msgid "Select feeds to delete in Kodi?" +msgstr "" + +msgctxt "#32116" +msgid "Select feeds to add to group in Kodi?" +msgstr "" + +msgctxt "#32117" +msgid "Synchronization completed" +msgstr "" + +msgctxt "#32118" +msgid "Export to Nextcloud" +msgstr "" + +msgctxt "#32119" +msgid "Select feeds to add in Nextcloud?" +msgstr "" + +msgctxt "#32120" +msgid "Select feeds to delete in Nextcloud?" +msgstr "" + +msgctxt "#32121" +msgid "Search podcasts directories" +msgstr "" + +msgctxt "#32122" +msgid "Search fyyd.de" +msgstr "" + +msgctxt "#32123" +msgid "Enter search term in order to find podcast" +msgstr "" + +msgctxt "#32124" +msgid "Search for %s" +msgstr "" + +msgctxt "#32125" +msgid "No results for %s found" +msgstr "" + +msgctxt "#32126" +msgid "Choose podcasts in order to subscribe" +msgstr "" + +msgctxt "#32127" +msgid "Successfully subscribed to podcasts" +msgstr "" + +msgctxt "#32151" +msgid "Connection Error" +msgstr "" + +msgctxt "#32152" +msgid "HTTP Method %s not supported" +msgstr "" + +msgctxt "#32153" +msgid "Request Exception: Please check URL and port or see logs for further details" +msgstr "" + +msgctxt "#32154" +msgid "Unexpected HTTP Status %i for %s" +msgstr "" + +msgctxt "#32155" +msgid "Unexpected content for podcast" +msgstr "" + +msgctxt "#32200" +msgid "Select a remote type if you want to synchronize your subscriptions between Kodi and other devices. Supported protocols are gPodder.net and Nextcloud Gpoddersync." +msgstr "" + +msgctxt "#32201" +msgid "Enter the hostname of your server, e.g. https://myserver.net:443" +msgstr "" + +msgctxt "#32202" +msgid "Enter your username in order to login to server." +msgstr "" + +msgctxt "#32203" +msgid "Enter your password in order to login to server. Note: Password is stored in plain text in settings file of this addon!" +msgstr "" + +msgctxt "#32204" +msgid "Apply changes in order to check if hostname, username and password work as expected." +msgstr "" + +msgctxt "#32205" +msgid "Select an update interval in order to get reminded if subscriptions has changed on your remote." +msgstr "" + +msgctxt "#32206" +msgid "Request all available subscriptions from gPodder.net and subscribe to them by assigning these to groups." +msgstr "" + +msgctxt "#32207" +msgid "Request only subscriptions from gPodder.net that are not subscribed in Kodi yet and subscribe to them by assigning these to groups." +msgstr "" + +msgctxt "#32208" +msgid "Request all available subscriptions from gPodder.net and download these as OPML file." +msgstr "" + +msgctxt "#32209" +msgid "Synchronize all subscriptions with Nextcloud Gpoddersync. You can subscribe to new podcasts and will be asked to remove subscriptions in Kodi that has been deleted on other devices." +msgstr "" + +msgctxt "#32210" +msgid "Synchronize only changes with Nextcloud Gpoddersync since last synchronization. You can subscribe to new podcasts and will be asked to remove subscriptions in Kodi that has been deleted on other devices." +msgstr "" + +msgctxt "#32211" +msgid "Import subscriptions by loading an OPML file." +msgstr "" + +msgctxt "#32212" +msgid "Click here If you want to detach an OPML file. File won't be deleted." +msgstr "" + +msgctxt "#32213" +msgid "Click here If you want to make a backup of your subscriptions. A OPML file will be written." +msgstr "" + +msgctxt "#32214" +msgid "Slot in order to attach an OPML file." +msgstr "Platz, um eine OPML Datei einzuhängen." + +msgctxt "#32215" +msgid "Toggle switch in order to display or hide this group." +msgstr "" + +msgctxt "#32216" +msgid "Enter the displayname of this group." +msgstr "" + +msgctxt "#32217" +msgid "Mark this slot as free or occupied. If it is marked as free you can use this slot in order to subscribe to other podcasts e.g. by synchronization with other devices." +msgstr "" + +msgctxt "#32218" +msgid "Enter the displayname of this subscription." +msgstr "" + +msgctxt "#32219" +msgid "Here you can enter or change the URL of the subscription manually." +msgstr "" + +msgctxt "#32220" +msgid "Activate this button in order to display a solid list item on top of each subscription which represents the latest episode. This is very helpful if you want to bookmark latest episode by using Kodi's favorites menu." +msgstr "" + +msgctxt "#32221" +msgid "Sometimes a subscription contains hundrets of episodes. Then you would probably like to limit number of episodes displayed in list." +msgstr "" + +msgctxt "#32222" +msgid "Export subscriptions in Kodi to Nextcloud." +msgstr "" + +msgctxt "#32223" +msgid "Search fyyd.de for podcasts. You can subscribe to new podcasts by adding them to groups." +msgstr "" diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/action.py new file mode 100644 index 0000000000..f4aaa72f5c --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/action.py @@ -0,0 +1,60 @@ +import xbmcaddon +import xbmcgui +import xbmcvfs + + +class Action: + + addon = None + addon_dir = None + anchor_for_latest = True + + _GROUPS = 10 + _ENTRIES = 10 + + def __init__(self) -> None: + + self.addon = xbmcaddon.Addon() + self.addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + + def _select_target_group(self) -> 'tuple[int,int]': + + names = list() + freeslots = list() + for g in range(self._GROUPS): + free = sum("false" == self.addon.getSetting( + "group_%i_rss_%i_enable" % (g, r)) for r in range(self._ENTRIES)) + + freeslots.append(free) + + names.append("%s %i: %s (%i %s)" % + ( + self.addon.getLocalizedString(32000), + g + 1, + self.addon.getSetting("group_%i_name" % g), + free, + self.addon.getLocalizedString(32077) + )) + + selected = xbmcgui.Dialog().select(self.addon.getLocalizedString(32076), names) + if selected > -1 and freeslots[selected] == 0: + xbmcgui.Dialog().ok(heading=self.addon.getLocalizedString(32078), + message=self.addon.getLocalizedString(32084)) + return -1, 0 + + elif selected == -1: + return -1, 0 + + else: + return selected, freeslots[selected] + + def _select_target_opml_slot(self, heading: str, multi=False): + + selection = list() + for g in range(self._GROUPS): + filename = self.addon.getSetting("opml_file_%i" % g) + selection.append("%s %i%s" % (self.addon.getLocalizedString( + 32023), g + 1, ": %s" % filename if filename else "")) + + dialog = xbmcgui.Dialog().multiselect if multi else xbmcgui.Dialog().select + return dialog(heading, selection) diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/commit_gpodder.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/commit_gpodder.py new file mode 100644 index 0000000000..57fd87e5a9 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/commit_gpodder.py @@ -0,0 +1,24 @@ +import xbmc +import xbmcaddon +import xbmcgui +from resources.lib.podcasts.actions.action import Action +from resources.lib.podcasts.gpodder import GPodder +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +class CommitGPodderAction(Action): + + def commit_gpodder(self) -> None: + + try: + host = self.addon.getSetting("gpodder_hostname") + user = self.addon.getSetting("gpodder_username") + password = self.addon.getSetting("gpodder_password") + + gPodder = GPodder(self.addon, host, user) + gPodder.login(password) + + except HttpStatusError as error: + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32151), error.message) + + xbmcaddon.Addon().openSettings() diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/commit_nextcloud.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/commit_nextcloud.py new file mode 100644 index 0000000000..2d60aacd7a --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/commit_nextcloud.py @@ -0,0 +1,27 @@ +import xbmc +import xbmcaddon +import xbmcgui +from resources.lib.podcasts.actions.action import Action +from resources.lib.podcasts.nextcloud import Nextcloud +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +class CommitNextcloudAction(Action): + + def commit_nextcloud(self) -> None: + + try: + host = self.addon.getSetting("nextcloud_hostname") + user = self.addon.getSetting("nextcloud_username") + password = self.addon.getSetting("nextcloud_password") + + nextcloud = Nextcloud(self.addon, host, user, password) + response = nextcloud.request_subscriptions() + if "timestamp" not in response: + raise HttpStatusError() + + except HttpStatusError as error: + xbmc.log(str(error), xbmc.LOGERROR) + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32151), error.message) + + xbmcaddon.Addon().openSettings() diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/download_gpodder_subscriptions_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/download_gpodder_subscriptions_action.py new file mode 100644 index 0000000000..bd1bfb104e --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/download_gpodder_subscriptions_action.py @@ -0,0 +1,75 @@ +import re + +import xbmcgui +import xbmcvfs +import xmltodict +from resources.lib.podcasts.actions.action import Action +from resources.lib.podcasts.gpodder import GPodder +from resources.lib.podcasts.util import get_asset_path +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +class DownloadGpodderSubscriptionsAction(Action): + + def download_gpodder_subscriptions(self) -> None: + + # Step 1: download subscriptions from gPodder + try: + host = self.addon.getSetting("gpodder_hostname") + user = self.addon.getSetting("gpodder_username") + password = self.addon.getSetting("gpodder_password") + + gPodder = GPodder(self.addon, host, user) + sessionid = gPodder.login(password) + + opml_data = gPodder.request_subscriptions(sessionid) + + except HttpStatusError as error: + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32151), error.message) + return + + # Step 2: Save file in folder + path, filename = self._save_opml_file(opml_data) + if not path: + return + + # Success + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32085), message="%s %s" % (self.addon.getLocalizedString(32083), filename), icon=get_asset_path("notification.png")) + + # Step 3: Select target opml slot + slot = self._select_target_opml_slot( + self.addon.getLocalizedString(32079)) + if slot == -1: + return + + self.addon.setSetting("opml_file_%i" % slot, path) + + # Success + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32085), message=self.addon.getLocalizedString(32086), icon=get_asset_path("notification.png")) + + def _save_opml_file(self, data: str) -> 'tuple[str,str]': + + opml = xmltodict.parse(data) + filename = "%s.opml" % re.sub( + "[^A-Za-z0-9']", " ", opml["opml"]["head"]["title"]) + + path = xbmcgui.Dialog().browse( + type=3, heading=self.addon.getLocalizedString(32080), shares="") + + if not path: + return None, None + + try: + fullpath = "%s%s" % (path, filename) + with xbmcvfs.File(fullpath, 'w') as _file: + _file.write(data) + + return fullpath, filename + + except: + xbmcgui.Dialog().ok(heading=self.addon.getLocalizedString( + 32081), message=self.addon.getLocalizedString(32082)) + + return None, None diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/export_opml_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/export_opml_action.py new file mode 100644 index 0000000000..0cb0a11696 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/export_opml_action.py @@ -0,0 +1,72 @@ +from datetime import datetime + +import xbmcgui +import xbmcvfs +from resources.lib.podcasts.actions.opml_action import OpmlAction +from resources.lib.podcasts.util import get_asset_path + + +class ExportOpmlAction(OpmlAction): + + def _write_opml_file(self, path: str) -> bool: + + def _get_rfc822_date(_dt: datetime) -> str: + + _month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + _day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + + return "%s, %i %s %i %s +0000" % (_day[int(_dt.strftime("%w"))], _dt.day, _month[_dt.month - 1], _dt.year, _dt.strftime("%H:%M:%S")) + + def _escape(str: str) -> str: + str = str.replace("&", "&") + str = str.replace("<", "<") + str = str.replace(">", ">") + str = str.replace("\"", """) + return str + + outlines = list() + for g in range(self._GROUPS): + if self.addon.getSetting("group_%i_enable" % g) == "true": + _group = _escape(self.addon.getSetting("group_%i_name" % g)) + for e in range(self._ENTRIES): + if self.addon.getSetting("group_%i_rss_%i_enable" % (g, e)) == "true": + _url = _escape(self.addon.getSetting( + "group_%i_rss_%i_url" % (g, e))) + _name = _escape(self.addon.getSetting( + "group_%i_rss_%i_name" % (g, e))) + if _url and _name: + outlines.append( + "" % (_url, _group, _name, _name)) + + title = self.addon.getAddonInfo("name") + created = _get_rfc822_date(datetime.now()) + + _xml = "\n%s%s%s" % ( + title, created, "".join(outlines)) + + try: + with xbmcvfs.File("%s%s.opml" % (path, title), 'w') as _file: + _file.write(_xml) + + return True + + except: + return False + + def export_opml(self) -> None: + + # Step 1: Select folder + path = xbmcgui.Dialog().browse(type=3, heading=self.addon.getLocalizedString( + 32090), shares="") + if not path: + return + + # Step 2: Write file + if self._write_opml_file(path): + # Success + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32091), message=self.addon.getLocalizedString(32086), icon=get_asset_path("notification.png")) + else: + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32092), message=self.addon.getLocalizedString(32086), icon=get_asset_path("notification.png")) diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/import_gpodder_subscriptions_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/import_gpodder_subscriptions_action.py new file mode 100644 index 0000000000..8579cdbf7d --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/import_gpodder_subscriptions_action.py @@ -0,0 +1,69 @@ +import xbmcgui +from resources.lib.podcasts.actions.opml_action import OpmlAction +from resources.lib.podcasts.gpodder import GPodder +from resources.lib.podcasts.opml_file import parse_opml +from resources.lib.podcasts.util import get_asset_path +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +class ImportGPodderSubscriptionsAction(OpmlAction): + + def __init__(self) -> None: + super().__init__() + + def _query_subscriptions_from_gpodder(self) -> 'tuple[str,list]': + + try: + host = self.addon.getSetting("gpodder_hostname") + user = self.addon.getSetting("gpodder_username") + password = self.addon.getSetting("gpodder_password") + + gPodder = GPodder(self.addon, host, user) + sessionid = gPodder.login(password) + subscriptions = gPodder.request_subscriptions(sessionid) + name, entries = parse_opml(subscriptions) + return name, entries + + except HttpStatusError as error: + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32151), error.message) + return None, None + + def import_gpodder_subscriptions(self, only_new_ones=False) -> None: + + def _filter_new_ones(entries): + _known_urls = list() + for g in range(self._GROUPS): + if self.addon.getSetting("group_%i_enable" % g) == "true": + for e in range(self._ENTRIES): + if self.addon.getSetting("group_%i_rss_%i_enable" % (g, e)) == "true": + _known_urls.append(self.addon.getSetting( + "group_%i_rss_%i_url" % (g, e))) + + return [e for e in entries if "params" in e and len(e["params"]) == 1 and "rss" in e["params"][0] and e["params"][0]["rss"] not in _known_urls] + + # Step 1: query subscriptions from gPodder + name, entries = self._query_subscriptions_from_gpodder() + if only_new_ones: + entries = _filter_new_ones(entries) + + if len(entries) == 0: + xbmcgui.Dialog().ok( + self.addon.getLocalizedString(32071), self.addon.getLocalizedString(32088)) + return + + # Step 2: Select target group + group, freeslots = self._select_target_group() + if group == -1: + return + + # Step 3: Select feeds + feeds = self._select_feeds(name, entries, freeslots) + if feeds == None: + return + + # Step 4: Apply to group + self._apply_to_group(entries, group, feeds) + + # Success + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32085), message=self.addon.getLocalizedString(32086), icon=get_asset_path("notification.png")) diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/import_opml_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/import_opml_action.py new file mode 100644 index 0000000000..c7e2b2e257 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/import_opml_action.py @@ -0,0 +1,49 @@ +import xbmc +import xbmcgui +from resources.lib.podcasts.actions.opml_action import OpmlAction +from resources.lib.podcasts.opml_file import open_opml_file, parse_opml +from resources.lib.podcasts.util import get_asset_path + + +class ImportOpmlAction(OpmlAction): + + def __init__(self) -> None: + super().__init__() + + def import_opml(self) -> None: + + # Step 1: Select target group + group, freeslots = self._select_target_group() + if group == -1: + return + + # Step 2: Select file + name, entries = self._select_opml_file() + if name == None: + return + + # Step 3: Select feeds + feeds = self._select_feeds(name, entries, freeslots) + if feeds == None: + return + + # Step 4: Confirm + self._apply_to_group(entries, group, feeds) + + # Success + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32085), message=self.addon.getLocalizedString(32086), icon=get_asset_path("notification.png")) + + def _select_opml_file(self) -> 'tuple[str,list]': + + path = xbmcgui.Dialog().browse( + type=1, heading=self.addon.getLocalizedString(32070), shares="", mask=".xml|.opml") + if path == "": + return None, None + + try: + return parse_opml(open_opml_file(path)) + + except: + xbmc.log("Cannot read opml file %s" % path, xbmc.LOGERROR) + return None, None diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/opml_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/opml_action.py new file mode 100644 index 0000000000..c0e5da11f5 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/opml_action.py @@ -0,0 +1,82 @@ +import xbmcgui +from resources.lib.podcasts.actions.action import Action + + +class OpmlAction(Action): + + def _select_target_group(self) -> 'tuple[int,int]': + + names = list() + freeslots = list() + for g in range(self._GROUPS): + free = sum("false" == self.addon.getSetting( + "group_%i_rss_%i_enable" % (g, r)) for r in range(self._ENTRIES)) + + freeslots.append(free) + + names.append("%s %i: %s (%i %s)" % + ( + self.addon.getLocalizedString(32000), + g + 1, + self.addon.getSetting("group_%i_name" % g), + free, + self.addon.getLocalizedString(32077) + )) + + selected = xbmcgui.Dialog().select(self.addon.getLocalizedString(32076), names) + if selected > -1 and freeslots[selected] == 0: + xbmcgui.Dialog().ok(heading=self.addon.getLocalizedString(32078), + message=self.addon.getLocalizedString(32084)) + return -1, 0 + + elif selected == -1: + return -1, 0 + + else: + return selected, freeslots[selected] + + def _select_feeds(self, name: str, entries: 'list[dict]', freeslots: 'list[int]') -> 'list[int]': + + selection = [e["name"] + for e in entries if "params" in e and len(e["params"]) == 1 and "rss" in e["params"][0]] + + if len(selection) == 0: + xbmcgui.Dialog().ok( + self.addon.getLocalizedString(32071), self.addon.getLocalizedString(32088)) + return None + + ok = False + while not ok: + feeds = xbmcgui.Dialog().multiselect( + self.addon.getLocalizedString(32071), selection) + if feeds == None: + ok = True + elif len(feeds) == 0: + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32072), + self.addon.getLocalizedString(32073)) + elif len(feeds) > freeslots: + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32074), + self.addon.getLocalizedString(32075) % freeslots) + else: + ok = True + + return feeds + + def _apply_to_group(self, entries: dict, group: int, feeds: 'list[int]') -> None: + + self.addon.setSetting("group_%i_enable" % group, "True") + + i, j = 0, 0 + while (i < self._ENTRIES): + + if j < len(feeds) and "false" == self.addon.getSetting("group_%i_rss_%i_enable" % (group, i)): + self.addon.setSetting("group_%i_rss_%i_enable" % + (group, i), "True") + self.addon.setSetting("group_%i_rss_%i_name" % + (group, i), entries[feeds[j]]["name"]) + self.addon.setSetting("group_%i_rss_%i_url" % ( + group, i), entries[feeds[j]]["params"][0]["rss"]) + self.addon.setSetting("group_%i_rss_%i_icon" % (group, i), "") + j += 1 + + i += 1 diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/remote_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/remote_action.py new file mode 100644 index 0000000000..c79f0ce8f5 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/remote_action.py @@ -0,0 +1,61 @@ +import xbmcgui +from resources.lib.podcasts.actions.opml_action import OpmlAction +from resources.lib.podcasts.util import get_asset_path + + +class RemoteAction(OpmlAction): + + def __init__(self) -> None: + super().__init__() + + def subscribe_feeds(self, feeds: 'list[dict]') -> 'tuple[list[dict], bool]': + + items: 'list[xbmcgui.ListItem]' = list() + for f in feeds: + li = xbmcgui.ListItem( + label=f["title"], label2=f["subtitle"], path=f["url"]) + li.setArt({"thumb": f["icon"]}) + items.append(li) + + selected_feeds = xbmcgui.Dialog().multiselect( + heading=self.addon.getLocalizedString(32126), options=items, useDetails=True) + + if selected_feeds == None: + return [], True + + elif not selected_feeds: + return feeds, False + + group, freeslots = self._select_target_group() + if group == -1: + return feeds, False + + elif freeslots < len(selected_feeds): + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32074), + self.addon.getLocalizedString(32075) % freeslots) + return feeds, False + + else: + self.addon.setSetting("group_%i_enable" % group, "True") + i, j = 0, 0 + while (i < self._ENTRIES): + if j < len(selected_feeds) and not self.addon.getSettingBool("group_%i_rss_%i_enable" % (group, i)): + self.addon.setSettingBool( + "group_%i_rss_%i_enable" % (group, i), True) + self.addon.setSetting("group_%i_rss_%i_name" % ( + group, i), items[selected_feeds[j]].getLabel()) + self.addon.setSetting("group_%i_rss_%i_url" % ( + group, i), items[selected_feeds[j]].getPath()) + self.addon.setSetting( + "group_%i_rss_%i_icon" % (group, i), items[selected_feeds[j]].getArt("thumb")) + j += 1 + + i += 1 + + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString(32085), + message=self.addon.getLocalizedString(32127), icon=get_asset_path(asset="notification.png")) + + feeds = [f for i, f in enumerate( + feeds) if i not in selected_feeds] + + return feeds, False diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/search_fyyd_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/search_fyyd_action.py new file mode 100644 index 0000000000..359fab9d9c --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/search_fyyd_action.py @@ -0,0 +1,54 @@ +import xbmc +import xbmcgui +from resources.lib.podcasts.actions.remote_action import RemoteAction +from resources.lib.podcasts.fyyd import Fyyd +from resources.lib.podcasts.util import get_asset_path +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +class SearchFyydAction(RemoteAction): + + def _query_fyyd(self, term: str) -> dict: + + try: + fyydClient = Fyyd(self.addon) + response = fyydClient.search_podcasts(term=term) + if response["msg"] == "ok": + return [ + { + "title": d["title"], + "subtitle": d["subtitle"], + "url": d["xmlURL"], + "icon": d["imgURL"] + } for d in response["data"] if d["status"] == 200 + ] + else: + return None + + except HttpStatusError as error: + xbmc.log(str(error), xbmc.LOGERROR) + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32151), error.message) + return None + + def search_fyyd(self) -> None: + + while True: + term = xbmcgui.Dialog().input(heading=self.addon.getLocalizedString(32123)) + if not term: + break + + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString(32122), + message=self.addon.getLocalizedString(32124) % term, icon=get_asset_path(asset="notification.png")) + + feeds = self._query_fyyd(term=term) + if feeds == None: + return + + elif not feeds: + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString(32122), + message=self.addon.getLocalizedString(32125) % term, icon=get_asset_path(asset="notification.png")) + continue + + feeds, abort = self.subscribe_feeds(feeds=feeds) + if abort: + break diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/sync_nextcloud_subscriptions_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/sync_nextcloud_subscriptions_action.py new file mode 100644 index 0000000000..6dfcb9a6dc --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/sync_nextcloud_subscriptions_action.py @@ -0,0 +1,260 @@ +import json +import time + +import xbmc +import xbmcaddon +import xbmcgui +from resources.lib.podcasts.actions.remote_action import RemoteAction +from resources.lib.podcasts.nextcloud import Nextcloud +from resources.lib.podcasts.podcastsaddon import PodcastsAddon +from resources.lib.podcasts.util import get_asset_path +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +class SyncNextcloudSubscriptionsAction(RemoteAction): + + def _query_subscriptions_from_nextcloud(self, full: bool = True) -> dict: + + try: + host = self.addon.getSetting("nextcloud_hostname") + user = self.addon.getSetting("nextcloud_username") + password = self.addon.getSetting("nextcloud_password") + nextcloud = Nextcloud(self.addon, host, user, password) + + timestamp = self.addon.getSettingInt("nextcloud_sync_timestamp") + response = nextcloud.request_subscriptions( + timestamp=0 if full else timestamp) + self.addon.setSettingInt( + "nextcloud_sync_timestamp", int(time.time())) + + return response + + except HttpStatusError as error: + xbmc.log(str(error), xbmc.LOGERROR) + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32151), error.message) + + def _change_subscription_in_nextcloud(self, add: 'list[str]', remove: 'list[str]', timestamp: int = None) -> dict: + + try: + host = self.addon.getSetting("nextcloud_hostname") + user = self.addon.getSetting("nextcloud_username") + password = self.addon.getSetting("nextcloud_password") + nextcloud = Nextcloud(self.addon, host, user, password) + + payload = { + "add": add, + "remove": remove, + "timestamp": timestamp if timestamp is not None else int(time.time()) + } + + xbmc.log(json.dumps(payload), xbmc.LOGINFO) + response = nextcloud.change_subscriptions(payload=payload) + + return response + + except HttpStatusError as error: + xbmc.log(str(error), xbmc.LOGERROR) + xbmcgui.Dialog().ok(self.addon.getLocalizedString(32151), error.message) + + def _get_current_feeds(self) -> 'tuple[dict[dict],dict[dict]]': + _active_feeds = dict() + _inactive_feeds = dict() + for g in range(self._GROUPS): + for e in range(self._ENTRIES): + name = self.addon.getSetting("group_%i_rss_%i_name" % (g, e)) + url = self.addon.getSetting("group_%i_rss_%i_url" % (g, e)) + icon = self.addon.getSetting("group_%i_rss_%i_icon" % (g, e)) + group = self.addon.getSetting("group_%i_name" % g) + if name and url: + if self.addon.getSettingBool("group_%i_enable" % g) and self.addon.getSettingBool("group_%i_rss_%i_enable" % (g, e)): + _active_feeds[url] = { + "title": name, + "icon": icon, + "group": group, + "url": url + } + else: + _inactive_feeds[url] = { + "title": name, + "icon": icon, + "group": group, + "url": url + } + + return _active_feeds, _inactive_feeds + + def _deactivate_feed_by_url(self, url: str) -> bool: + + found = False + for g in range(self._GROUPS): + for e in range(self._ENTRIES): + if self.addon.getSetting("group_%i_rss_%i_url" % (g, e)) == url: + self.addon.setSettingBool( + "group_%i_rss_%i_enable" % (g, e), False) + found = True + + return found + + def sync_nextcloud_subscriptions(self, full: bool = True, opensettings: bool = False) -> None: + + def _inspect_feeds_to_add(candicates_to_add: 'dict[dict]') -> 'list[dict]': + + if not candicates_to_add: + return list() + + progress = xbmcgui.DialogProgress() + progress.create(heading=self.addon.getLocalizedString(32114)) + + podcastsAddon = PodcastsAddon(self.addon) + feeds = list() + for i, url in enumerate(candicates_to_add): + try: + progress.update(percent=int( + i / len(candicates_to_add) * 100), message=url) + title, description, icon, items = podcastsAddon.load_rss( + url) + feeds.append({ + "title": title, + "icon": icon, + "subtitle": description, + "url": url + } + ) + if progress.iscanceled(): + return None + except: + xbmc.log("Unable to load rssfeed %s" % + url, xbmc.LOGWARNING) + + progress.close() + return feeds + + def _handle_candicates_to_add(candicates_to_add: 'dict[dict]') -> bool: + + feeds = _inspect_feeds_to_add(candicates_to_add) + while feeds: + feeds, abort = self.subscribe_feeds(feeds=feeds) + if abort: + return False + + return True + + def _handle_candicates_to_delete(candicates_to_delete: 'dict[dict]') -> bool: + + if not candicates_to_delete: + return True + + items: 'list[xbmcgui.ListItem]' = list() + for url in candicates_to_delete: + entry = candicates_to_delete[url] + li = xbmcgui.ListItem(label=entry["title"], path=url) + + if "group" in entry and entry["group"]: + li.setLabel2("%s: %s" % ( + self.addon.getLocalizedString(32000), entry["group"])) + + if "icon" in entry and entry["icon"]: + li.setArt({"thumb": entry["icon"]}) + else: + li.setArt({"thumb": get_asset_path("notification.png")}) + + items.append(li) + + selection = xbmcgui.Dialog().multiselect( + heading=self.addon.getLocalizedString(32115), options=items, useDetails=True) + + if selection == None: + return False + + for i in selection: + self._deactivate_feed_by_url(items[i].getPath()) + + return True + + current_active_feeds, current_inactive_feeds = self._get_current_feeds() + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32094), message=self.addon.getLocalizedString(32113), icon=get_asset_path("notification.png")) + response = self._query_subscriptions_from_nextcloud(full) + + candicates_to_add = { + url: None for url in response["add"] if url not in current_active_feeds} + candicates_to_delete = { + url: current_active_feeds[url] for url in response["remove"] if url in current_active_feeds + } + if full: + for current in current_active_feeds: + if current not in response["add"]: + candicates_to_delete[current] = current_active_feeds[current] + + if not _handle_candicates_to_add(candicates_to_add): + return + + if not _handle_candicates_to_delete(candicates_to_delete): + return + + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32094), message=self.addon.getLocalizedString(32117), icon=get_asset_path("notification.png")) + + if opensettings: + xbmcaddon.Addon().openSettings() + + def export_to_nextcloud(self) -> None: + + def _handle_candicates(candidates: 'dict[dict]', heading: str) -> 'list[str]': + + if not candidates: + return [] + + items: 'list[xbmcgui.ListItem]' = list() + for url in candidates: + entry = candidates[url] + li = xbmcgui.ListItem(label=entry["title"], label2="%s: %s" % ( + self.addon.getLocalizedString(32000), entry["group"]), path=url) + if entry["icon"]: + li.setArt({"thumb": entry["icon"]}) + else: + li.setArt({"thumb": get_asset_path("notification.png")}) + + items.append(li) + + selection = xbmcgui.Dialog().multiselect( + heading=heading, options=items, useDetails=True) + + if selection == None: + return None + + return [items[i].getPath() for i in selection] + + current_active_feeds, current_inactive_feeds = self._get_current_feeds() + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32094), message=self.addon.getLocalizedString(32113), icon=get_asset_path("notification.png")) + response = self._query_subscriptions_from_nextcloud() + + candicates_to_add = { + url: current_active_feeds[url] for url in current_active_feeds if url not in response["add"]} + candicates_to_delete = { + url: current_inactive_feeds[url] for url in current_inactive_feeds if url in response["add"] and url not in current_active_feeds} + + _to_add = _handle_candicates( + candicates_to_add, heading=self.addon.getLocalizedString(32119)) + if _to_add == None: + return + + _to_delete = _handle_candicates( + candicates_to_delete, heading=self.addon.getLocalizedString(32120)) + if _to_delete == None: + return + + self._change_subscription_in_nextcloud( + add=_to_add, remove=_to_delete, timestamp=0) + + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32094), message=self.addon.getLocalizedString(32117), icon=get_asset_path("notification.png")) + + def check_for_updates(self) -> None: + + if self.addon.getSettingInt("remote_type") == 2 and self.addon.getSetting("nextcloud_hostname") not in ["https://", ""]: + if time.time() - self.addon.getSettingInt("nextcloud_sync_interval") * 86400 > self.addon.getSettingInt("nextcloud_sync_timestamp"): + syncNextcloudSubscriptionsAction = SyncNextcloudSubscriptionsAction() + syncNextcloudSubscriptionsAction.sync_nextcloud_subscriptions( + False, False) diff --git a/plugin.audio.podcasts/resources/lib/podcasts/actions/unassign_opml_action.py b/plugin.audio.podcasts/resources/lib/podcasts/actions/unassign_opml_action.py new file mode 100644 index 0000000000..e2b3da310f --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/actions/unassign_opml_action.py @@ -0,0 +1,33 @@ +import xbmcgui +from resources.lib.podcasts.actions.action import Action +from resources.lib.podcasts.util import get_asset_path + + +class UnassignOpmlAction(Action): + + def unassign_opml(self) -> None: + + # Step 1: Select slots + slots = self._select_target_opml_slot( + self.addon.getLocalizedString(32087), multi=True) + if slots == None or len(slots) == 0: + return + + # Step 2: empty slots + for slot in slots: + self.addon.setSetting("opml_file_%i" % slot, " ") + + # Success + xbmcgui.Dialog().notification(heading=self.addon.getLocalizedString( + 32085), message=self.addon.getLocalizedString(32086), icon=get_asset_path("notification.png")) + + def _select_target_opml_slot(self, heading: str, multi=False): + + selection = list() + for g in range(self._GROUPS): + filename = self.addon.getSetting("opml_file_%i" % g) + selection.append("%s %i%s" % (self.addon.getLocalizedString( + 32023), g + 1, ": %s" % filename if filename else "")) + + dialog = xbmcgui.Dialog().multiselect if multi else xbmcgui.Dialog().select + return dialog(heading, selection) diff --git a/plugin.audio.podcasts/resources/lib/podcasts/fyyd.py b/plugin.audio.podcasts/resources/lib/podcasts/fyyd.py new file mode 100644 index 0000000000..44b21ca02e --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/fyyd.py @@ -0,0 +1,25 @@ +import json +import urllib.parse + +import xbmcaddon +from resources.lib.rssaddon.http_client import http_request + + +class Fyyd: + + _FYYD_API = { + "search": "https://api.fyyd.de/0.2/search/podcast?term=%s" + } + + _addon = None + + def __init__(self, addon: xbmcaddon.Addon) -> None: + + self._addon = addon + + def search_podcasts(self, term: str) -> dict: + + response, cookies = http_request(self._addon, + self._FYYD_API["search"] % urllib.parse.quote(term)) + + return json.loads(response) diff --git a/plugin.audio.podcasts/resources/lib/podcasts/gpodder.py b/plugin.audio.podcasts/resources/lib/podcasts/gpodder.py new file mode 100644 index 0000000000..15169149ac --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/gpodder.py @@ -0,0 +1,48 @@ +from resources.lib.rssaddon.http_status_error import HttpStatusError +from resources.lib.rssaddon.http_client import http_request +import base64 + +import xbmcaddon + +class GPodder: + + _GPODDER_API = { + "login": "%s/api/2/auth/%s/login.json", + "subscriptions": "%s/subscriptions/%s.%s" + } + + _addon = None + _host = None + _user = None + + def __init__(self, addon: xbmcaddon.Addon, host: str, user: str) -> None: + + self._addon = addon + self._host = host + self._user = user + + def login(self, password: str) -> str: + auth_string = "%s:%s" % (self._user, password) + b64auth = { + "Authorization": "Basic %s" % base64.urlsafe_b64encode(auth_string.encode("utf-8")).decode("utf-8") + } + response, cookies = http_request(self._addon, + self._GPODDER_API["login"] % (self._host, + self._user), b64auth, "POST") + + if "sessionid" not in cookies: + raise HttpStatusError("Invalid session. Check credentials") + + return cookies["sessionid"] + + def request_subscriptions(self, sessionid: str) -> str: + + session_cookie = { + "Cookie": "%s=%s" % ("sessionid", sessionid) + } + response, cookies = http_request(self._addon, + self._GPODDER_API["subscriptions"] % (self._host, + self._user, + "opml"), session_cookie) + + return response diff --git a/plugin.audio.podcasts/resources/lib/podcasts/nextcloud.py b/plugin.audio.podcasts/resources/lib/podcasts/nextcloud.py new file mode 100644 index 0000000000..87e4415abd --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/nextcloud.py @@ -0,0 +1,50 @@ +import base64 +import json + +import xbmcaddon +from resources.lib.rssaddon.http_client import http_request +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +class Nextcloud: + + _NEXTCLOUD_API = { + "status": "%s/status.php", + "subscriptions": "%s/index.php/apps/gpoddersync/subscriptions?since=%i", + "subscription_change": "%s/index.php/apps/gpoddersync/subscription_change/create" + } + + _addon = None + _host = None + _user = None + _password = None + + def __init__(self, addon: xbmcaddon.Addon, host: str, user: str, password: str) -> None: + + self._addon = addon + self._host = host + self._user = user + self._password = password + + def _get_auth(self) -> dict: + + auth_string = "%s:%s" % (self._user, self._password) + return { + "Authorization": "Basic %s" % base64.urlsafe_b64encode(auth_string.encode("utf-8")).decode("utf-8") + } + + def request_subscriptions(self, timestamp: int = 0) -> dict: + + response, cookies = http_request(self._addon, + self._NEXTCLOUD_API["subscriptions"] % (self._host, timestamp), self._get_auth()) + + return json.loads(response) + + def change_subscriptions(self, payload: dict) -> None: + + headers = self._get_auth() + headers["Content-type"] = "application/json" + response, cookies = http_request( + self._addon, self._NEXTCLOUD_API["subscription_change"] % self._host, method="POST", headers=headers, data=json.dumps(payload)) + + return json.loads(response) diff --git a/plugin.audio.podcasts/resources/lib/podcasts/opml_file.py b/plugin.audio.podcasts/resources/lib/podcasts/opml_file.py new file mode 100644 index 0000000000..6c7e483bcb --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/opml_file.py @@ -0,0 +1,65 @@ +import re + +import xbmcaddon +import xbmcvfs +import xmltodict + + +def parse_opml(data: str, limit=0) -> 'tuple[str,list[dict]]': + + def parse_outlines_from_opml(outline): + + if type(outline) is not list: + outline = [outline] + + entries = [] + for i, o in enumerate(outline): + name = o["@title"] if "@title" in o else o["@text"] + if not name and "@xmlUrl" in o: + m = re.match( + "^https?:\/\/([^\/]+).*\/?.*\/([^\/]+)\/?$", o["@xmlUrl"]) + if m: + name = "%s %s...%s" % (xbmcaddon.Addon().getLocalizedString( + 32053), m.groups()[0][:20], m.groups()[1][-40:]) + + entry = { + "path": str(i), + "name": name, + "node": [] + } + + if "@type" in o and o["@type"] == "rss" and "@xmlUrl" in o: + entry["params"] = [{ + "rss": o["@xmlUrl"], + "limit": str(limit) + }] + entries.append(entry) + + elif "outline" in o: + entry["node"] = parse_outlines_from_opml( + o["outline"]) + entries.append(entry) + + return entries + + opml_data = xmltodict.parse(data) + + if "opml" in opml_data and "head" in opml_data["opml"] and "title" in opml_data["opml"]["head"]: + title = opml_data["opml"]["head"]["title"] + + else: + title = "" + + if "opml" in opml_data and "body" in opml_data["opml"] and "outline" in opml_data["opml"]["body"]: + entries = parse_outlines_from_opml( + opml_data["opml"]["body"]["outline"]) + else: + entries = [] + + return title, entries + + +def open_opml_file(path: str) -> str: + + with xbmcvfs.File(path, 'r') as _opml_file: + return _opml_file.read() diff --git a/plugin.audio.podcasts/resources/lib/podcasts/podcastsaddon.py b/plugin.audio.podcasts/resources/lib/podcasts/podcastsaddon.py new file mode 100644 index 0000000000..bb96d7a71b --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/podcastsaddon.py @@ -0,0 +1,153 @@ +from datetime import datetime + +import xbmc +import xbmcplugin +import xbmcvfs +from resources.lib.podcasts.opml_file import open_opml_file, parse_opml +from resources.lib.rssaddon.abstract_rss_addon import AbstractRssAddon + +GROUPS = 10 +ENTRIES = 10 + + +class PodcastsAddon(AbstractRssAddon): + + def __init__(self, addon_handle) -> None: + + super().__init__(addon_handle) + self.anchor_for_latest = "true" == self.addon.getSetting("anchor") + + def load_rss(self, url) -> 'tuple[str,str,str,list[dict]]': + + return self._load_rss(url) + + def on_rss_loaded(self, url: str, title: str, description: str, image: str, items: 'list[dict]'): + + # update image + for g in range(GROUPS): + + if self.addon.getSetting("group_%i_enable" % g) == "false": + continue + + for e in range(ENTRIES): + + if self.addon.getSetting("group_%i_rss_%i_enable" % (g, e)) == "false": + continue + + elif url == self.addon.getSetting("group_%i_rss_%i_url" % (g, e)): + self.addon.setSetting( + "group_%i_rss_%i_icon" % (g, e), image) + + def _browse(self, dir_structure: str, path: str, updateListing=False) -> None: + + def _get_node_by_path(path: str) -> dict: + + if path == "/": + return dir_structure[0] + + tokens = path.split("/")[1:] + node = dir_structure[0] + + while len(tokens) > 0: + path = tokens.pop(0) + for n in node["node"]: + if n["path"] == path: + node = n + break + + return node + + node = _get_node_by_path(path) + for entry in node["node"]: + self.add_list_item(entry, path) + + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_FULLPATH) + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_LABEL) + + xbmcplugin.endOfDirectory( + self.addon_handle, updateListing=updateListing) + + def _build_dir_structure(self) -> None: + + groups = [] + + limit = self.addon.getSettingInt("limit_episodes") + + # opml files / podcasts lists + for g in range(GROUPS): + + if self.addon.getSetting("opml_file_%i" % g) == "": + continue + + path = xbmcvfs.translatePath( + self.addon.getSetting("opml_file_%i" % g)) + try: + name, nodes = parse_opml(open_opml_file(path), limit=limit) + groups.append({ + "path": "opml-%i" % g, + "name": name, + "node": nodes + }) + + except: + xbmc.log("Cannot read opml file %s" % path, xbmc.LOGERROR) + + # rss feeds from addon + for g in range(GROUPS): + + if self.addon.getSetting("group_%i_enable" % g) == "false": + continue + + entries = [] + for e in range(ENTRIES): + + if self.addon.getSetting("group_%i_rss_%i_enable" % (g, e)) == "false": + continue + + icon = self.addon.getSetting("group_%i_rss_%i_icon" + % (g, e)) + + entries += [{ + "path": "%i" % e, + "name": self.addon.getSetting("group_%i_rss_%i_name" + % (g, e)), + "params": [ + { + "rss": self.addon.getSetting("group_%i_rss_%i_url" % (g, e)), + "limit": str(limit) + } + ], + "icon": icon, + "node": [] + }] + + groups += [{ + "path": "pod-%i" % g, + "name": self.addon.getSetting("group_%i_name" % g), + "node": entries + }] + + return [ + { # root + "path": "", + "node": groups + } + ] + + def route(self, path) -> None: + + _dir_structure = self._build_dir_structure() + self._browse(dir_structure=_dir_structure, path=path) + + def build_plot(self, item) -> str: + + plot = list() + if "description" in item: + plot.append(item["description"]) + + if "date" in item: + plot.append(datetime.strftime(item["date"], "%Y-%m-%d %H:%M")) + + return "\n".join(plot) diff --git a/plugin.audio.podcasts/resources/lib/podcasts/util.py b/plugin.audio.podcasts/resources/lib/podcasts/util.py new file mode 100644 index 0000000000..99d67afaa3 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/podcasts/util.py @@ -0,0 +1,12 @@ +import os + +import xbmcaddon +import xbmcvfs + + +def get_asset_path(asset: str) -> str: + + addon = xbmcaddon.Addon() + return os.path.join(xbmcvfs.translatePath(addon.getAddonInfo('path')), + "resources", + "assets", asset) diff --git a/plugin.audio.podcasts/resources/lib/rssaddon/abstract_rss_addon.py b/plugin.audio.podcasts/resources/lib/rssaddon/abstract_rss_addon.py new file mode 100644 index 0000000000..6d5ddf7969 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/rssaddon/abstract_rss_addon.py @@ -0,0 +1,335 @@ +import base64 +import os +import re +import urllib.parse +from datetime import datetime +from io import StringIO +from xml.etree.ElementTree import iterparse + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +import xbmcvfs +from resources.lib.rssaddon.http_client import http_request +from resources.lib.rssaddon.http_status_error import HttpStatusError + +# see https://forum.kodi.tv/showthread.php?tid=112916 +_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", + "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + + +class AbstractRssAddon: + + addon = None + addon_handle = None + addon_dir = None + anchor_for_latest = True + + def __init__(self, addon_handle): + + self.addon = xbmcaddon.Addon() + self.addon_handle = addon_handle + self.addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + + self.params = dict() + + def handle(self, argv: 'list[str]') -> None: + + path = urllib.parse.urlparse(argv[0]).path.replace("//", "/") + url_params = urllib.parse.parse_qs(argv[2][1:]) + + if not self.check_disclaimer(): + self.route("/", dict()) + return + + self.params = {key: self.decode_param( + url_params[key][0]) for key in url_params} + + if "rss" in self.params: + url = self.params["rss"] + limit = int(self.params["limit"] + ) if "limit" in self.params else 0 + offset = int(self.params["offset"] + ) if "offset" in self.params else 0 + self.render_rss(path, url, limit=limit, offset=offset) + + elif "play_latest" in self.params: + url = self.params["play_latest"] + self.play_latest(url) + else: + self.route(path) + + def decode_param(self, value: str) -> str: + + try: + return base64.urlsafe_b64decode(value).decode("utf-8") + except: + return value + + def check_disclaimer(self) -> bool: + + return True + + def route(self, path: str): + + pass + + def is_force_http(self) -> bool: + + return False + + def _load_rss(self, url: str) -> 'tuple[str,str,str,list[dict]]': + + def parse_rss_feed(xml: str) -> 'tuple[str,str,str,list[dict]]': + + path = list() + + title = None + description = "" + image = None + items = list() + + for event, elem in iterparse(StringIO(xml), ("start", "end")): + + if event == "start": + path.append(elem.tag) + + if path == ["rss", "channel", "item"]: + item = dict() + + elif event == "end": + + if path == ["rss", "channel"]: + pass + + elif path == ["rss", "channel", "title"] and elem.text: + title = elem.text.strip() + + elif path == ["rss", "channel", "description"] and elem.text: + description = elem.text.strip() + + elif path == ["rss", "channel", "image", "url"] and elem.text: + image = elem.text.strip() + + elif (path == ["rss", "channel", "{http://www.itunes.com/dtds/podcast-1.0.dtd}image"] + and "href" in elem.attrib and not image): + image = elem.attrib["href"] + + elif path == ["rss", "channel", "item", "title"] and elem.text: + item["name"] = elem.text.strip() + + elif path == ["rss", "channel", "item", "description"] and elem.text: + item["description"] = elem.text.strip() + + elif path == ["rss", "channel", "item", "enclosure"]: + item["stream_url"] = elem.attrib["url"] if not self.is_force_http( + ) else elem.attrib["url"].replace("https://", "http://") + item["type"] = "video" if elem.attrib["type"].split( + "/")[0] == "video" else "music" + + elif (path == ["rss", "channel", "item", "{http://www.itunes.com/dtds/podcast-1.0.dtd}image"] + and elem.attrib["href"]): + item["icon"] = elem.attrib["href"] + + elif path == ["rss", "channel", "item", "pubDate"] and elem.text: + _f = re.findall( + "(\d{1,2}) (\w{3}) (\d{4}) (\d{2}):(\d{2}):(\d{2})", elem.text) + + if _f: + _m = _MONTHS.index(_f[0][1]) + 1 + item["date"] = datetime(year=int(_f[0][2]), month=_m, day=int(_f[0][0]), hour=int( + _f[0][3]), minute=int(_f[0][4]), second=int(_f[0][5])) + + elif path == ["rss", "channel", "item", "{http://www.itunes.com/dtds/podcast-1.0.dtd}duration"] and elem.text: + try: + duration = 0 + for i, s in enumerate(reversed(elem.text.split(":"))): + duration += 60**i * int(s) + + item["duration"] = duration + + except: + pass + + elif path == ["rss", "channel", "item"]: + + if "description" not in item: + item["description"] = "" + + if "icon" not in item: + item["icon"] = image + + if "stream_url" in item and item["stream_url"]: + items.append(item) + + elem.clear() + path.pop() + + return title, description, image, items + + xml, cookies = http_request(self.addon, url) + + if not xml.startswith(" None: + + pass + + def build_label(self, item) -> str: + + return item["name"] + + def build_plot(self, item) -> str: + + return item["description"] if "description" in item else "" + + def build_url(self, item) -> str: + + return item["stream_url"] + + def _create_list_item(self, item: dict) -> xbmcgui.ListItem: + + li = xbmcgui.ListItem(label=self.build_label(item)) + + if "description" in item: + li.setProperty("label2", item["description"]) + + if "stream_url" in item: + li.setPath(self.build_url(item)) + + if "type" in item: + infos = { + "title": self.build_label(item) + } + + if item["type"] == "video": + infos["plot"] = self.build_plot(item) + + if "duration" in item and item["duration"] >= 0: + infos["duration"] = item["duration"] + + li.setInfo(item["type"], infos) + + if "icon" in item and item["icon"]: + li.setArt({"thumb": item["icon"]}) + else: + addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + li.setArt({"icon": os.path.join( + addon_dir, "resources", "assets", "icon.png")} + ) + + if "date" in item and item["date"]: + if "setDateTime" in dir(li): # available since Kodi v20 + li.setDateTime(item["date"].strftime("%Y-%m-%dT%H:%M:%SZ")) + else: + pass + + if "specialsort" in item: + li.setProperty("SpecialSort", item["specialsort"]) + + return li + + def add_list_item(self, entry: dict, path: str) -> None: + + def _build_param_string(params: 'list[str]', current="") -> str: + + if params == None: + return current + + for obj in params: + for name in obj: + enc_value = base64.urlsafe_b64encode( + obj[name].encode("utf-8")) + current += "?" if len(current) == 0 else "&" + current += name + "=" + str(enc_value, "utf-8") + + return current + + if path == "/": + path = "" + + item_path = path + "/" + entry["path"] + + param_string = "" + if "params" in entry: + param_string = _build_param_string(entry["params"], + current=param_string) + + li = self._create_list_item(entry) + + if "stream_url" in entry: + url = self.build_url(entry) + + else: + url = "".join( + ["plugin://", self.addon.getAddonInfo("id"), item_path, param_string]) + + is_folder = "node" in entry + li.setProperty("IsPlayable", "false" if is_folder else "true") + + xbmcplugin.addDirectoryItem(handle=self.addon_handle, + listitem=li, + url=url, + isFolder=is_folder) + + def render_rss(self, path: str, url: str, limit=0, offset=0) -> None: + + try: + title, description, image, items = self._load_rss(url) + + except HttpStatusError as error: + xbmc.log("HTTP Status Error: %s, path=%s" % + (error.message, path), xbmc.LOGERROR) + xbmcgui.Dialog().notification(self.addon.getLocalizedString(32151), error.message) + + else: + if len(items) > 0 and self.anchor_for_latest: + entry = { + "path": "latest", + "name": "%s (%s)" % (title, self.addon.getLocalizedString(32101)), + "description": description, + "icon": image, + "specialsort": "top", + "type": items[0]["type"], + "params": [ + { + "play_latest": url + } + ] + } + self.add_list_item(entry, path) + + li = None + for i, item in enumerate(items): + if i >= offset and (not limit or i < offset + limit): + li = self._create_list_item(item) + xbmcplugin.addDirectoryItem(handle=self.addon_handle, + listitem=li, + url=self.build_url(item), + isFolder=False) + + if li and "setDateTime" in dir(li): # available since Kodi v20 + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.endOfDirectory(self.addon_handle) + + def play_latest(self, url: str) -> None: + + try: + title, description, image, items = self._load_rss(url) + item = items[0] + li = self._create_list_item(item) + xbmcplugin.setResolvedUrl(self.addon_handle, True, li) + + except HttpStatusError as error: + + xbmcgui.Dialog().notification(self.addon.getLocalizedString(32151), error.message) diff --git a/plugin.audio.podcasts/resources/lib/rssaddon/http_client.py b/plugin.audio.podcasts/resources/lib/rssaddon/http_client.py new file mode 100644 index 0000000000..29dfb9a427 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/rssaddon/http_client.py @@ -0,0 +1,39 @@ +import requests +import xbmc +from resources.lib.rssaddon.http_status_error import HttpStatusError + + +def http_request(addon, url, headers=dict(), method="GET", data=None): + + useragent = f"{addon.getAddonInfo('id')}/{addon.getAddonInfo('version')} (Kodi/{xbmc.getInfoLabel('System.BuildVersionShort')})" + headers["User-Agent"] = useragent + + if method == "GET": + req = requests.get + elif method == "POST": + req = requests.post + else: + raise HttpStatusError( + addon.getLocalizedString(32152) % method) + + try: + if method == "POST" and data: + res = req(url, headers=headers, data=data) + else: + res = req(url, headers=headers) + + except requests.exceptions.RequestException as error: + xbmc.log("Request Exception: %s" % str(error), xbmc.LOGERROR) + raise HttpStatusError(addon.getLocalizedString(32153)) + + if res.status_code == 200: + if res.encoding and res.encoding != "utf-8": + rv = res.text.encode(res.encoding).decode("utf-8") + else: + rv = res.text + + return rv, res.cookies + + else: + raise HttpStatusError(addon.getLocalizedString( + 32154) % (res.status_code, url)) \ No newline at end of file diff --git a/plugin.audio.podcasts/resources/lib/rssaddon/http_status_error.py b/plugin.audio.podcasts/resources/lib/rssaddon/http_status_error.py new file mode 100644 index 0000000000..ae3185f615 --- /dev/null +++ b/plugin.audio.podcasts/resources/lib/rssaddon/http_status_error.py @@ -0,0 +1,7 @@ +class HttpStatusError(Exception): + + message = "" + + def __init__(self, msg): + + self.message = msg \ No newline at end of file diff --git a/plugin.audio.podcasts/resources/settings.xml b/plugin.audio.podcasts/resources/settings.xml new file mode 100644 index 0000000000..0b98f75e79 --- /dev/null +++ b/plugin.audio.podcasts/resources/settings.xml @@ -0,0 +1,5531 @@ + + +
+ + + 0 + + 0 + + true + + + RunScript(plugin.audio.podcasts,search_fyyd) + true + + + + + + 0 + + true + + + 32061 + + false + + 1 + + + + 0 + + true + + + 32094 + + false + + 2 + + + + 0 + + true + + + RunScript(plugin.audio.podcasts,import_gpodder_subscriptions,False) + + + 1 + + + + 1 + + true + + + RunScript(plugin.audio.podcasts,import_gpodder_subscriptions,True) + + + 1 + + + + 2 + + true + + + RunScript(plugin.audio.podcasts,download_gpodder_subscriptions) + + + 1 + + + + 0 + + true + + + RunScript(plugin.audio.podcasts,sync_nextcloud_subscriptions,True,True) + true + + + 2 + + + + 1 + + true + + + RunScript(plugin.audio.podcasts,sync_nextcloud_subscriptions,False,True) + true + + + 2 + + + + + true + + + RunScript(plugin.audio.podcasts,export_to_nextcloud) + + + 2 + + + + + 1 + + 0 + + true + + + RunScript(plugin.audio.podcasts,import_opml) + + + + 0 + + true + + + RunScript(plugin.audio.podcasts,unassign_opml) + + + + 2 + + true + + + RunScript(plugin.audio.podcasts,export_opml) + + + + + + + + 0 + 0 + + + + + + + + + + + 0 + https://gpodder.net:443 + + true + + + 32067 + + + 1 + + + + 0 + + + true + + + 32062 + + + 1 + + + + 0 + + + true + + + 32063 + true + + + 1 + + + + 0 + RunScript(plugin.audio.podcasts,commit_gpodder) + + true + + + 1 + + + + 0 + https:// + + true + + + 32067 + + + 2 + + + + 0 + + + true + + + 32062 + + + 2 + + + + 0 + + + true + + + 32063 + true + + + 2 + + + + 1 + 0 + + + + + + + + + + 2 + + + + 0 + 0 + + true + + + 32094 + + false + + + 0 + RunScript(plugin.audio.podcasts,commit_nextcloud) + + true + + + 2 + + + + + + + + 0 + + + false + true + .xml|.opml + + + 32011 + + + + 0 + + + false + true + .xml|.opml + + + 32011 + + + + 0 + + + false + true + .xml|.opml + + + 32012 + + + + 0 + + + false + true + .xml|.opml + + + 32014 + + + + 0 + + + false + true + .xml|.opml + + + 32015 + + + + 0 + + + false + true + .xml|.opml + + + 32016 + + + + 0 + + + false + true + .xml|.opml + + + 32017 + + + + 0 + + + false + true + .xml|.opml + + + 32018 + + + + 0 + + + false + true + .xml|.opml + + + 32019 + + + + 0 + + + false + true + .xml|.opml + + + 32020 + + + + + + + + 0 + false + + + + 0 + News + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + Sports + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + Economie & Commerce + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + History, Politics & Society + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + Science & Education + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + Entertainment + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + Arts & Culture + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + Movies & Music + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + Games & hobbies + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 0 + Technology + + true + + + 556 + + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + 0 + false + + + True + + + + 0 + + + true + + + 556 + + + True + True + + + + 0 + + + true + + + 32022 + + + True + True + + + + 0 + + + true + + + 19282 + + False + + + + + + + 0 + false + + + + 1 + 0 + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/plugin.audio.pureradio/LICENSE.txt b/plugin.audio.pureradio/LICENSE.txt new file mode 100644 index 0000000000..0d481260fe --- /dev/null +++ b/plugin.audio.pureradio/LICENSE.txt @@ -0,0 +1,282 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------- diff --git a/plugin.audio.pureradio/README.md b/plugin.audio.pureradio/README.md new file mode 100644 index 0000000000..605dce051d --- /dev/null +++ b/plugin.audio.pureradio/README.md @@ -0,0 +1,5 @@ +### PureRadio Audio Plugin. + +This plugin enables the playback of the PureRadio audio stream, which can be set in different qualities using different bitrates. Also a selection of podcast, hosted on Spreaker, are available for instant playback. + +Visit us at PureRadio (http://www.pureradio.one) or Facebook (https://facebook/pureradio.one). diff --git a/plugin.audio.pureradio/addon.xml b/plugin.audio.pureradio/addon.xml new file mode 100644 index 0000000000..9763f1fe18 --- /dev/null +++ b/plugin.audio.pureradio/addon.xml @@ -0,0 +1,41 @@ + + + + + + + audio + + + all + Pureradio.One stream and podcast + Pureradio.One stream en podcasts + Pureradio.One is an internet radiostation which plays music from the 60's till today, commercial free and in great quality streaming and podcasts. + Pureradio.One is een internet radiostation met een brede mix van de beste hits, wereldwijd te volgen via een van onze streams en podcast. + GPL-3.0-or-later + https://github.com/bdommel/plugin.audio.pureradio + en + + resources/icon.png + resources/fanart.jpg + + + Version 2.0.0 (01-OCT-2020) + [*] Python3 used for matrix + [*] Spreaker podcast section optimalisation + Version 3.0.0 (01-NOV-2020) + [*] Rewrite based on current Kodi guidelines + [*] Removed screenshots + Version 3.0.1 (02-NOV-2020) + [*] Removed static strings from API + [*] Dynamic Streaming and podcast urls + [*] Replaced eval function for static function calls + Version 3.0.2 (04-NOV-2020) + [*] Commit to matrix branch + Version 3.0.3 (08-10-2021) + [*] made a single commit + Version 3.0.4 (13-10-2021) + [*] stream and podcast urls from settings.xml + + + diff --git a/plugin.audio.pureradio/default.py b/plugin.audio.pureradio/default.py new file mode 100644 index 0000000000..745d5c0367 --- /dev/null +++ b/plugin.audio.pureradio/default.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +import resources.lib.addon as addon + +# Runs the add-on from here. +if __name__ == "__main__": + addon.run() diff --git a/plugin.audio.pureradio/resources/fanart.jpg b/plugin.audio.pureradio/resources/fanart.jpg new file mode 100644 index 0000000000..cea216bbda Binary files /dev/null and b/plugin.audio.pureradio/resources/fanart.jpg differ diff --git a/plugin.audio.pureradio/resources/icon.png b/plugin.audio.pureradio/resources/icon.png new file mode 100644 index 0000000000..3e4e72c4c7 Binary files /dev/null and b/plugin.audio.pureradio/resources/icon.png differ diff --git a/plugin.audio.pureradio/resources/language/resource.language.en_gb/strings.po b/plugin.audio.pureradio/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..d345048b30 --- /dev/null +++ b/plugin.audio.pureradio/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,27 @@ +# Kodi Media Center language file +# Addon Name: PureRadio.One +# Addon id: plugin.audio.pureradio +# Addon Provider: cubitus + +msgid "" +msgstr "" + +msgctxt "#30001" +msgid "Play Stream" +msgstr "" + +msgctxt "#30002" +msgid "Podcasts" +msgstr "" + +msgctxt "#30100" +msgid "General" +msgstr "" + +msgctxt "#30101" +msgid "Play stream automatically" +msgstr "" + +msgctxt "#30102" +msgid "Show Podcast Images (bandwidth)" +msgstr "" diff --git a/plugin.audio.pureradio/resources/language/resource.language.nl_nl/strings.po b/plugin.audio.pureradio/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..cbe922845f --- /dev/null +++ b/plugin.audio.pureradio/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,27 @@ +# Kodi Media Center language file +# Addon Name: PureRadio.One +# Addon id: plugin.audio.pureradio +# Addon Provider: cubitus + +msgid "" +msgstr "" + +msgctxt "#30001" +msgid "Play Stream" +msgstr "Afspelen" + +msgctxt "#30002" +msgid "Podcasts" +msgstr "Podcasts" + +msgctxt "#30100" +msgid "General" +msgstr "Algemeen" + +msgctxt "#30101" +msgid "Play stream automatically" +msgstr "Automatisch afspelen" + +msgctxt "#30102" +msgid "Podcast Images" +msgstr "Podcast afbeeldingen gebruiken (kost bandbreedte)" diff --git a/plugin.audio.pureradio/resources/lib/addon.py b/plugin.audio.pureradio/resources/lib/addon.py new file mode 100644 index 0000000000..68660e301e --- /dev/null +++ b/plugin.audio.pureradio/resources/lib/addon.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +''' + 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 resources.lib.plugin import Plugin +import resources.lib.api as api + +plugin_id = 'plugin.audio.pureradio' + +p = Plugin(plugin_id) + +settings = p.get_plugin_settings() +translation = p.get_plugin_translation() + +localized_strings = { + 'Play' : 30001, + 'Podcasts' : 30002, +} + +# Entry point +def run(): + """This function is the entry point to the add-on. + It gets the add-on parameters and call the 'action' function. + """ + p.log("pureradio.run") + # Get the params + params = p.get_plugin_parms() + action = params.get("action", '') + # changed this to static function calls + if (action == 'play_audio'): + play_audio(params) + elif (action == 'retrieve_podcasts'): + retrieve_podcasts(params) + else: + create_index(params) # default menu + +# Main menu +def create_index(params): + """This function creates the main menu.""" + main_menu = [ + {'thumbnail': '', + 'info': {'type':'Audio', + 'title': get_located_string("Play")}, + 'path': p.get_plugin_path(action='play_audio', url=settings.getSetting('stream_url')), + 'IsPlayable': True, }, + {'thumbnail': '', + 'info': {'title': get_located_string("Podcasts")}, + 'path': p.get_plugin_path(action='retrieve_podcasts', url=settings.getSetting('podcast_url')), + 'IsPlayable': False, } + ] + p.add_items(main_menu) + +def retrieve_podcasts(params): + """This function retrieves the podcasts on spreaker.""" + p.log("pureradio.retrieve_podcasts "+repr(params)) + url = params.get("url") + if url: + create_podcast_items(url) + +def create_podcast_items(url): + """This function creates podcast items""" + root = api.retrieve_podcasts(url) + # builds up menu item + main_menu = [ {'thumbnail': podcast_image(item), + 'info': {'type':'Audio', 'title': item[0].text}, + 'path': p.get_plugin_path(action='play_audio', url=item[4].attrib['url']), + 'IsPlayable': True, } for item in root.iter('item')] + # add items to media playlist + p.add_items(main_menu) + +def podcast_image(item): + """This function return the image location if show images is set.""" + image = '' + if (settings.getSetting('podcast_images')=='true'): + image = item[8].attrib['href'] + p.log("pureradio.podcast_image "+ image) + return image + +def play_audio(params): + """This function plays the audio source.""" + p.log("pureradio.play_audio "+repr(params)) + url = params.get("url") + if url: + return p.play_resolved_url(url) + else: + p.showWarning(get_located_string('Audio not located')) + +def get_located_string(string_name): + """This function returns the localized string if it is available.""" + return translation(localized_strings.get(string_name)) or string_name if string_name in localized_strings else string_name diff --git a/plugin.audio.pureradio/resources/lib/api.py b/plugin.audio.pureradio/resources/lib/api.py new file mode 100644 index 0000000000..c96db4c053 --- /dev/null +++ b/plugin.audio.pureradio/resources/lib/api.py @@ -0,0 +1,8 @@ +# _*_ coding: utf-8 _*_ +import json +import urllib.request as request +from xml.etree import ElementTree as ET + +def retrieve_podcasts(url): + """ This function return a dictonary of podcast items """ + return ET.fromstring(request.urlopen(url=url, timeout=20).read().decode('utf-8')) diff --git a/plugin.audio.pureradio/resources/lib/plugin.py b/plugin.audio.pureradio/resources/lib/plugin.py new file mode 100644 index 0000000000..42241ac7eb --- /dev/null +++ b/plugin.audio.pureradio/resources/lib/plugin.py @@ -0,0 +1,102 @@ +# _*_ coding: utf-8 _*_ + +import sys, re, os +from urllib.parse import urlencode, quote_plus, unquote_plus +import xbmcplugin, xbmcaddon, xbmcgui, xbmcaddon, xbmc + +class Plugin(): + + def __init__(self, plugin_id='', show_thumb_as_fanart=False): + self.pluginpath = sys.argv[0] + self.pluginhandle = int(sys.argv[1]) + self.pluginparams = sys.argv[2] + self.plugin_id = plugin_id + self.plugin_type = 'Audio' if 'audio' in plugin_id else 'Music' + self.debug_enable = False # The debug logs are disabled by default. + # addon functions + self.plugin_settings = xbmcaddon.Addon(id=self.plugin_id) + self.translation = self.plugin_settings.getLocalizedString + self.root_path = self.plugin_settings.getAddonInfo('path') + + def get_plugin_settings(self): + """Getter method for settings method""" + return self.plugin_settings + + def get_plugin_translation(self): + """Getter method for translation method""" + return self.translation + + def get_system_language(self): + """Getter method for language method""" + return xbmc.getLanguage() + + def set_debug_mode(self, debug_flag=""): + """ debug mode, see init section """ + self.debug_enable = debug_flag in ("true", True) + + def log(self, message): + """Logs the messages into the main XBMC log file""" + if self.debug_enable: + try: + xbmc.log(msg=message, level=xbmc.LOGINFO) + except: + xbmc.log('%s: log this line is not possible due to encoding string problems' % self.plugin_id, level=xbmc.LOGINFO) + + def _log(self, message): + """ This method is privated and only called from other methods within the class""" + if self.debug_enable: + try: + xbmc.log(msg=message, level=xbmc.LOGINFO) + except: + xbmc.log('%s: _log this line is not possible due to encoding string problems' % self.plugin_id, level=xbmc.LOGINFO) + + def get_plugin_parms(self): + """This method gets all the parameters passed to the plugin from KODI API and retuns a dictionary""" + params = sys.argv[2] + pattern_params = re.compile('[?&]([^=&]+)=?([^&]*)') + options = dict((parameter, unquote_plus(value)) for (parameter, value) in pattern_params.findall(params)) + self._log("get_plugin_parms " + repr(options)) + return options + + def get_plugin_path(self, **kwars): + """Returns the add-on path URL encoded along with all its parameters.""" + return sys.argv[0] + '?' + urlencode(kwars) + + def get_url_decoded(self, url): + """Returns the URL decoded.""" + self._log('get_url_decoded URL: "%s"' % url) + return unquote_plus(url) + + + def get_url_encoded(self, url): + """Returns the URL encoded.""" + self._log('get_url_encoded URL: "%s"' % url) + return quote_plus(url) + + def add_items(self, items, updateListing=False): + """Adds the list of items (links and folders) to the add-on media list.""" + item_list = [] + for item in items: + link_item = xbmcgui.ListItem(item.get('info').get('title')) + thumbnailImage = item.get('thumbnail', '') + if thumbnailImage: + link_item.setArt({ 'thumb': thumbnailImage }) + if item.get('IsPlayable', False): + link_item.setProperty('IsPlayable', 'true') + link_item.setLabel(item.get('label', item.get('info').get('title'))) + link_item.setLabel2(item.get('label', item.get('info').get('title'))) + link_item.setInfo(type = self.plugin_type, infoLabels = item.get('info')) + item_list.append((item.get('path'), link_item, not item.get('IsPlayable', False))) + xbmcplugin.addDirectoryItems(self.pluginhandle, item_list, len(item_list)) + xbmcplugin.endOfDirectory(self.pluginhandle, succeeded=True, updateListing=updateListing, cacheToDisc=True) + + def showWarning(self, message): + """Shows a popup window in the XBMC GUI for 5 seconds""" + self._log("showWarning message: %s" % message) + xbmcgui.Dialog().notification(self.plugin_id, message, xbmcgui.NOTIFICATION_INFO, 6000) + + def play_resolved_url(self, url = ""): + """Plays the media file pointed by the URL passed as argument.""" + self._log("play_resolved_url pluginhandle = [%s] url = [%s]" % (self.pluginhandle, url)) + listitem = xbmcgui.ListItem(path=url) + return xbmcplugin.setResolvedUrl(self.pluginhandle, True, listitem) diff --git a/plugin.audio.pureradio/resources/settings.xml b/plugin.audio.pureradio/resources/settings.xml new file mode 100644 index 0000000000..5c766e29e9 --- /dev/null +++ b/plugin.audio.pureradio/resources/settings.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugin.audio.radio_de/LICENSE.txt b/plugin.audio.radio_de/LICENSE.txt new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/plugin.audio.radio_de/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.audio.radio_de/addon.py b/plugin.audio.radio_de/addon.py new file mode 100644 index 0000000000..3f8076081a --- /dev/null +++ b/plugin.audio.radio_de/addon.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +''' + * Copyright (C) 2019- enen92 (enen92@kodi.tv) + * Copyright (C) 2012-2019 Tristan Fischer (sphere@dersphere.de) + * This file is part of plugin.audio.radio_de + * + * SPDX-License-Identifier: GPL-2.0-only + * See LICENSE.txt for more information. +''' + +from resources.lib import plugin + +plugin.run() diff --git a/plugin.audio.radio_de/addon.xml b/plugin.audio.radio_de/addon.xml new file mode 100644 index 0000000000..11f0ca35c1 --- /dev/null +++ b/plugin.audio.radio_de/addon.xml @@ -0,0 +1,84 @@ + + + + + + + audio + + + all + https://www.radio.de + https://github.com/XBMC-Addons/plugin.audio.radio_de + https://forum.kodi.tv/showthread.php?tid=119362 + GPL-2.0-only + The plugin isn't compatible with the current API of radio.de + v3.0.9+matrix.1 (31/12/2021) + [new] Sync translations + + + resources/art/icon.png + resources/art/fanart.jpg + resources/art/screenshot-1.png + resources/art/screenshot-2.png + resources/art/screenshot-3.png + resources/art/screenshot-4.png + + accedeix a mes de 30000 emissores de radio + Få adgang til >30000 radioudsendelser + Zugang zu mehr als 30000 Radiosendern + Πρόσβαση σε πάνω από 30000 εκπομπές + Access >30000 radio broadcasts + Access >30000 radio broadcasts + Access >30000 radio broadcasts + Escuche más de 30000 emisoras de radio + Accede a >30000 transmisiones de radio + Accéder à > 30000 radiodiffusions + Ecoutez plus de 30000 stations de radio + Escoite máis de 30000 emisoras de radio + גישה ליותר מ־30000 שידורי רדיו + Pristup >30000 radio emitiranja + Hozzáférés 30000-nél is több rádióadáshoz + Akses >30000 siaran radio + Accedi a più di 30000 trasmissioni radio + >3만개의 라디오 방송국 연결 + Prieiga> 30000 radijo transliacijų + Toegang tot >30000 radio uitzendingen + Dostęp do ponad 30000 stacji radiowych + Acesse>30000 broadcasts rádios + Aceda a mais de 30000 transmissões de rádio + Доступ к >30000 радиостанциям + Pristupujte k vyše 30000 vysielaniam + Få tillgång till >30000 radiosändningar + 30000 க்கும் மேல் வானொலி ஒளிபரப்புகளை அணுகவும் + 访问大于7000个电台广播 + Plugin de música per escoltar més de 30000 emissores de ràdio internacionals des rad.io, radio.de i radio.fr[CR]Característiques actuals:[CR]- Traduccions a l'anglès, alemany i francès[CR]- Cercador d'emissores per ubicació, gènere, tema, país, ciutat o idioma[CR]- Cercador d'emissores[CR]- 115 gèneres, 59 temes, 94 països, 1010 ciutats, 63 idiomes + Musikplugin som giver adgang til over 30000 internationale radioudsendelser fra rad.io, radio.de, radio.fr, radio.pt og radio.es[CR]Nuværende funktioner[CR]- Oversat til engelsk, tysk og fransk[CR]- Gennemse stationer efter lokalitet, genre, emne, land, by og sprog[CR]- Søg efter stationer[CR]- 115 genrer, 59 emner, 94 lande, 1010 byer og 63 sprog + Das Radio im Internet mit über 30000 Radiosendern, Internetradiostationen, gratis Streams und Podcasts live von rad.io, radio.de und radio.fr.[CR]Features[CR]- Auf deutsch, englisch und französisch übersetzt[CR]- Sortiert nach: in der Nähe, Genre, Thema, Land und Sprache[CR]- nach Sendern suchen[CR]- 115 Genre, 59 Themen, 94 Länder, 1010 Städte, 63 Sprachen + Μουσικό plugin για πρόσβαση σε περισσότερους από 30000 διεθνείς ραδιοφωνικές εκπομπές από τα rad.io, radio.de και radio.fr[CR]Υποστηρίζονται[CR]- Μεταφράσεις σε Αγγλικά, Γερμανικά και Γαλλικά[CR]- Περιήγηση σταθμών ανά τοποθεσία, είδος, θέμα, χώρα, πόλη και γλώσσα[CR]- Αναζήτηση σταθμών[CR]- 115 είδη, 59 θέματα, 94 χώρες, 1010 πόλεις, 63 γλώσσες + Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages + Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german and french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages + Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de and radio.fr[CR]Currently features[CR]- English, german and french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages + Complemento de música para escuchar más de 30000 emisoras de radio de todo el mundo desde rad.io, radio.de, radio.fr, radio.pt y radio.es.[CR]Funciones:[CR]- Traducciones al inglés, alemán, francés, portugués y español.[CR]- Buscador de emisoras por ubicación, género, tema, país, ciudad o idioma.[CR]- Buscador de emisoras por nombre.[CR]- 73 géneros, 103 temas, 174 países y multitud de ciudades e idiomas. + Complemento de música para acceder a más de 30000 transmisiones de radio internacionales de rad.io, radio.de, radio.fr, radio.pt y radio.es[CR]Características actuales[CR]- Traducido a inglés, alemán, francés y español[CR]- Explora estaciones por ubicación, género, tópico, país, ciudad e idioma[CR]Busca estaciones[CR]- 115 géneros, 59 tópicos, 94 países, 1010, ciudades, 63 idiomas + Plugiciel de musique pour accéder à plus de 30000 radiodiffusions internationales de rad.io, radio.de et radio.fr[CR]Offre présentement [CR]- Traductions en anglais, allemand et français[CR]- Parcourir des stations par lieux, genres, sujets, pays, villes et langues[CR]- Rechercher des stations[CR]- 115 genres, 59 sujets, 94 pays, 1 010 villes, 63 langues. + Plugin musical pour écouter plus de 30000 stations de radio internationales de rad.io, radio.de et radio.fr[CR]Fonctionnalités[CR]- Traduit en Anglais, Allemand et Français[CR]- Parcourir les stations par genre, thème, pays, ville et langue[CR]- Recherche de stations[CR]- 115 genres, 59 thèmes, 94 pays, 1010 villes, 63 langues + Engadido de música para escoitar máis de 30000 emisoras de radio internacionais de rad.io, radio.de e radio.fr.[CR]Características:[CR]- Traducións ao inglés, alemán, francés e castelán.[CR]- Busca de emisoras por localización, xénero, tema, país, cidade ou idioma.[CR]- Busca de emisoras.[CR]- 115 xéneros, 59 temas, 94 países, 1010 cidades, 63 idiomas. + הרחבת מוזיקה המספקת גישה ליותר מ־30000 שידורי רדיו בינלאומים מ־rad.io, radio.de ו־radio.fr[CR]מאפיינים נוכחיים[CR]- שפות אנגלית, גרמנית וצרפתית[CR]- עיון לפי מיקום, סגנון, נושא, מדינה, עיר ושפה[CR]- חיפוש תחנות[CR]- 115 סגנונות, 59 נושאים, 94 מדינות 1010 ערים, 63 שפות + Glazbeni dodatak za pristup 30000 međunarodnih radio stanica na rad.io, radio.de i radio.fr[CR]Trenutne značajke[CR]- engleski, njemački i francuski prijevodi[CR]- Pregledavajte stanice prema lokaciji, žanru, temi, zemlji, gradu i jeziku[CR]- Pretraživanje stanica[CR]- 115 žanrova, 59 tema, 94 zemalja, 1010 gradova, 63 jezika + Beépülő modul több, mint 30000 nemzetközi rádióadás eléréséhez a rad.io, radio.de és radio.fr oldalakról[CR]Jelenlegi tulajdonságok[CR]- Angol, német, francia és magyar nyelv[CR]- Állomások böngészése hely, műfaj, téma, ország, város és nyelv szerint[CR]- Állomások keresése[CR]- 115 műfaj, 59 téma, 94 ország, 1010 város, 63 nyelv + Plugin musik untuk mengakses lebih dari 30000 siaran radio internasional dari rad.io, radio.de dan radio.fr[CR]Fitur saat ini[CR]- Terjemahan Inggris, Jerman dan Perancis[CR]- Jelajah stasiun berdasarkan lokasi, tema, topik, negara, kota dan bahasa[CR]- Cari stasiun[CR]- 115 tema, 59 topik, 94 negara, 1010 kota, 63 bahasa + Plugin per accedere a oltre 30000 trasmissioni internazionali radiofoniche da rad.io, radio.de e radio.fr[CR]Caratteristiche[CR]- Supporta inglese, tedesco e francese[CR]- Sfoglia le stazioni per località, genere, argomento, paese, citta e lingua[CR]- Ricerca delle stazioni[CR]- 115 generi, 59 argomenti, 94 paesi, 1010 città e 63 lingue + rad.io, radio.de, radio.fr, radio.pt 및 radio.es에서 3만개 이상의 국제 라디오 방송에 액세스할 수 있는 음악 플러그인[CR]현재 기능[CR]- 영어, 독일어, 프랑스어 번역[CR]- 방송국 검색 위치, 장르, 주제, 국가, 도시 및 언어별[CR]- 방송국 검색[CR]- 115개 장르, 59개 주제, 94개 국가, 1010개 도시, 63개 언어 + Muzikos įskiepio prieiga prie daugiau nei 30000 tarptautinių radijo transliacijų iš rad.io, radio.de ir radio.fr [CR] Šiuo metu programoja išversta į [Cr] - anglų, vokiečių ir prancūzų kalbas [CR] - Naršyti stotis pagal: vietovę, žanrą, temą, šalį, miestą ir/ar kalbą [CR] - Ieškoti radio stočių pagal:[Cr] - 115 žanrai, 59 temos, 94 šalys, 1010 miestų, 63 kalbos + muziek plug-in om toegang te krijgen tot over 30000 internationale radiouitzendingen van rad.io, radio.de en radio.fr[CR]Huidige functies[CR]- engels, duits en frans vertaalt[CR]- Zoek naar stations vanuit locatie, genre, onderwerp, land, plaats en taal[CR]- zoek voor stations[CR]- 115 genres. 59 onderwerpen, 94 landen. 1010 plaatsen, 63 talen + Wtyczka muzyczna dająca dostęp do ponad 30000 międzynarodowych stacji radiowych z[CR]rad.io, radio.de, daio.fr[CR]Obecnie w językach[CR]- Angielski, Niemiecki i Francuski[CR]- Szukaj stacji według lokacji, gatunku, tematu, kraju, miasta czy języka[CR]- 115 gatunków, 59 tematów, 94 kraje, 1010 miasta, 63 języki + Plugin de música para acessar mais de 30000 emissoras de rádio internacionais de rad.io, radio.de, radio.fr, radio.pt e radio.es [CR] Principais características [CR] - Inglês, Alemão e Francês traduzido [CR] - Procure estações por localização, gênero, tópico, país , cidade e linguagem [CR] - Procure estações [CR] - 115 gêneros, 59 temas, 94 países, 1010 cidades, 63 idiomas + Plugin de música para aceder a mais de 30000 transmissões de rádio internacionais de rad.io, radio.de, radio.fr, radio.pt e radio.es[CR]Actualmente, disponibiliza:[CR]- Traduções em vários idiomas[CR]- Navegar estações por localização, género, tópico, país, cidade e idioma[CR]- Procurar por estações[CR]- 115 géneros, 59 tópicos, 94 países, 1010 cidades, 63 idiomas + Музыкальный плагин для доступа к более чем 30000 международным радиостанциям с rad.io, radio.de, radio.fr, radio.pt и radio.es[CR]Текущие возможности[CR]- Английский, немецкий, французский переводы[CR]- Просмотр станций по расположению, жанру, теме, стране, городу и языку[CR]- Поиск станций[CR]- 115 жанров, 59 тем, 94 страны, 1010 городов, 63 языка + Hudobný doplnok na prístup k vyše 30000 medzinárodným rádio vysielaniam z rad.io, radio.de a radio.fr&#10;Momentálne podporuje:&#10;- anglické, nemecké a francúzske preklady&#10;- prehliadanie staníc podľa umiestnenia, žánru, témy, krajiny, mesta a jazyka&#10;- vyhľadávanie staníc&#10;- 115 žánrov, 59 tém, 94 krajín, 1010 miest, 63 jazykov + Musiktillägg som ger åtkomst till över 30000 internationella radiosändningar från rad.io, radio.de och radio.fr[CR]Nuvarande funktioner[CR]- Engelsk, tysk, och fransk översättning[]- Bläddra efter stationer baserat på genre, ämne, land, stad och språk[CR]- Sök efter stationer[CR]-115 genres, 59 ämnen, 94 länder, 1010 städer, 63 språk + இசை துணை பயன் rad.io, radio.de மற்றும் radio.fr இருந்து 30000 க்கும் மேல் வானொலி ஒளிபரப்புகளை அணுக உதவி செய்யும்[CR]தற்போதைய அம்சங்கள்[CR]- ஆங்கிலம், ஜெர்மன் மற்றும் பிரெஞ்சு மொழியாக்கம்[CR]- இருப்பிடம், வகை, தலைப்பு, நாடு, நகரம் மற்றும் மொழி கொண்டு நிலையங்களை உலாவ முடியும்[CR]- நிலையங்களை தேடுதல்[CR]- 115 வகைகள், 59 தலைப்புகள், 94 நாடுகள், 1010 நகரங்கள், 63 மொழிகள் + 访问rad.io、radio.de和radio.fr上超过7000个国际电台广播的音频插件[CR]现有功能[CR]- 英语、德语和法语支持[CR]- 按地点、类别、主题、国家、城市和语言浏览站点[CR]- 搜索站点[CR]- 115个类别、59个主题、94个国家、1010城市、63种语言 + + diff --git a/plugin.audio.radio_de/changelog.txt b/plugin.audio.radio_de/changelog.txt new file mode 100644 index 0000000000..52147a851c --- /dev/null +++ b/plugin.audio.radio_de/changelog.txt @@ -0,0 +1,130 @@ +v3.0.9+matrix.1 (31/12/2021) + - [new] Sync translations + +v3.0.8+matrix.1 (24/11/2021) + - [new] Sync translations + - [fix] Initialize empty context menu items + +v3.0.7+matrix.1 (3/10/2021) + - [new] New fanart without .de + - [new] Move art to the resources folder + - [fix] Fix playback and additions to the my_stations list if the stream url cannot be obtained + - [fix] hot reload of lists after addition/removal from the mystations table + +v3.0.4+matrix.0 (27/9/2020) + - [new] Add stationname property + +v3.0.3+matrix.6 (1/5/2020) + - [fix] Custom stations if the url is not a playlist + - [new] Spanish website radio.es and improved translations + - [new] screenshot support for addon-website + +v3.0.2+matrix.1 (1/5/2020) + - [fix] Custom stations if the url is not a playlist + +v3.0.1+matrix.1 (13/4/2020) + - [new] Use internal playlist resolver also in custom stations + +v3.0.0+matrix.1 (12/4/2020) + - [new] Automated submissions to matrix + - [new] Python3 only version + - [new] New language layout and other matrix fixes + - [fix] Playlist based stations + - [fix] Ratings + +v2.4.2 (9/2/2020) + - [add] sort methods for categories + - [add] setting to prefer http connections over https if http streams are available + - [fix] use sys.version_info[0] for compatibility with python2.6 + +v2.4.1 (31/12/2019) + - [fix] Custom radio tracks + +v2.4.0 (29/12/2019) + - [new] Use radio API v2 + - [fix] Remove dead code + - [new] Add radio.pt + - [new] Page listings + - [new] Better quality logos + - [new] Hide fanart setting + +v2.3.5 (25/12/2019) + - [fix] Artwork not available in player + +2.3.2 (07.06.2017) + - fixed playback + +2.3.1 (15.02.2015) + - updated translations + +2.3.0 (29.11.2013) + - added playback for asx playlists (e.g. BBC) + - added blayback for xml playlists + - improved station search + +2.2.1 (11.08.2013) + - updated translations + - added new addon.xml tags + +2.2.0 (11.03.2013) + - better station logos + - better add-on icon + - changed to xbmcswift2 v2.4.0 + - small code improvements + - use random server on playlists (instead first one) + - update translations + +2.1.3 (26.12.2012) + - better station logos + - fix unicode error + - change to xbmcswift 1.3.0 + +2.0.2 (16.12.2012) + - fix no genre available (resulted in script error) + +2.0.1 (03.11.2012) + - added migration code to convert old my_stations to new system + - fixed unicode in custom station title + - Added spanish translation (thx to Patrizia) + - Added french translation (thx to stombi) + +2.0.0 (unreleased) + - Code Rewrite + - Possibility to add custom (user defined) Stations + - Thumbnail View (You can disable in the add-on settings) + - Change to the xbmcswift2 framework + - New Icon + - Possibilty to add Stations to the XBMC Favorites + +1.1.1 (Unreleased) + - Added spanish translation (thx to Patrizia) + +1.1.0 (13.07.2012) + - New My Station logic (listing should be much faster after initial migration) + - Small fixes + +1.0.7 (unreleased) + - Fixed: error in language_guessing + - Fixed: routes (fix xbmcswift-xbox usecase) + +1.0.6 (05.03.2012) + - Fixed: error if a station was added to the list of mystations but has become unavailable + - Improved error catching + - Improved: Show bitrate in "kbit/s", not "B" + +1.0.5 (26.02.2012) + - Fixed .m3u playlists with empty lines (thx to Malte_Schroeder) + +1.0.4 (18.02.2012) + - Fixed python <2.5 error + - Added french translation (thx to stombi) + +1.0.3 (07.02.2012) + - added workaround for streams with .m3u and .pls files + - changed: show full context menu + +1.0.2 (29.01.2012) + - Fix error with UTF-8 characters in search string + +1.0.1 (13.01.2012) + - Initial Release \ No newline at end of file diff --git a/plugin.audio.radio_de/resources/art/fanart.jpg b/plugin.audio.radio_de/resources/art/fanart.jpg new file mode 100644 index 0000000000..1e3be67481 Binary files /dev/null and b/plugin.audio.radio_de/resources/art/fanart.jpg differ diff --git a/plugin.audio.radio_de/resources/art/icon.png b/plugin.audio.radio_de/resources/art/icon.png new file mode 100644 index 0000000000..86e190f2fa Binary files /dev/null and b/plugin.audio.radio_de/resources/art/icon.png differ diff --git a/plugin.audio.radio_de/resources/art/screenshot-1.png b/plugin.audio.radio_de/resources/art/screenshot-1.png new file mode 100644 index 0000000000..088289b76b Binary files /dev/null and b/plugin.audio.radio_de/resources/art/screenshot-1.png differ diff --git a/plugin.audio.radio_de/resources/art/screenshot-2.png b/plugin.audio.radio_de/resources/art/screenshot-2.png new file mode 100644 index 0000000000..e18c00198f Binary files /dev/null and b/plugin.audio.radio_de/resources/art/screenshot-2.png differ diff --git a/plugin.audio.radio_de/resources/art/screenshot-3.png b/plugin.audio.radio_de/resources/art/screenshot-3.png new file mode 100644 index 0000000000..e4526f2461 Binary files /dev/null and b/plugin.audio.radio_de/resources/art/screenshot-3.png differ diff --git a/plugin.audio.radio_de/resources/art/screenshot-4.png b/plugin.audio.radio_de/resources/art/screenshot-4.png new file mode 100644 index 0000000000..18e3eba1b3 Binary files /dev/null and b/plugin.audio.radio_de/resources/art/screenshot-4.png differ diff --git a/plugin.audio.radio_de/resources/language/resource.language.af_za/strings.po b/plugin.audio.radio_de/resources/language/resource.language.af_za/strings.po new file mode 100644 index 0000000000..29baded0cb --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.af_za/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Afrikaans (South Africa) \n" +"Language: af_za\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Taal" + +msgctxt "#30301" +msgid "English" +msgstr "Engels" + +msgctxt "#30302" +msgid "German" +msgstr "Duits" + +msgctxt "#30303" +msgid "French" +msgstr "Frans" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Algemeen" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Versteek ondersteunerkuns" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.am_et/strings.po b/plugin.audio.radio_de/resources/language/resource.language.am_et/strings.po new file mode 100644 index 0000000000..a088c08065 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.am_et/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Amharic (Ethiopia) \n" +"Language: am_et\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "ቋንቋ" + +msgctxt "#30301" +msgid "English" +msgstr "English" + +msgctxt "#30302" +msgid "German" +msgstr "German" + +msgctxt "#30303" +msgid "French" +msgstr "French" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ar_sa/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ar_sa/strings.po new file mode 100644 index 0000000000..78d3c6d32b --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ar_sa/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Arabic (Saudi Arabia) \n" +"Language: ar_sa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "الاستعراض حسب النوع" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "المحطات الخاصة بي" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "اللغة" + +msgctxt "#30301" +msgid "English" +msgstr "الإنجليزية" + +msgctxt "#30302" +msgid "German" +msgstr "الجرمانية" + +msgctxt "#30303" +msgid "French" +msgstr "الفرنسية" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "الاضافة الى 'محطاتي المفضلة'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "الازالة من قائمة محطاتي المفضلة" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "خطاء بالشبكة" + +msgctxt "#30601" +msgid "General" +msgstr "" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ast_es/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ast_es/strings.po new file mode 100644 index 0000000000..35c1458fb9 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ast_es/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Asturian (Spain) \n" +"Language: ast_es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Llingua" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "títulu" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Xeneral" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.az_az/strings.po b/plugin.audio.radio_de/resources/language/resource.language.az_az/strings.po new file mode 100644 index 0000000000..b930e4eebd --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.az_az/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Azerbaijani \n" +"Language: az_az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Dil" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Ümumi" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.be_by/strings.po b/plugin.audio.radio_de/resources/language/resource.language.be_by/strings.po new file mode 100644 index 0000000000..1e07f8f871 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.be_by/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Belarusian \n" +"Language: be_by\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Мова" + +msgctxt "#30301" +msgid "English" +msgstr "English" + +msgctxt "#30302" +msgid "German" +msgstr "German" + +msgctxt "#30303" +msgid "French" +msgstr "French" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "Network Error" + +msgctxt "#30601" +msgid "General" +msgstr "" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.bg_bg/strings.po b/plugin.audio.radio_de/resources/language/resource.language.bg_bg/strings.po new file mode 100644 index 0000000000..4d3a426a83 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.bg_bg/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Bulgarian \n" +"Language: bg_bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Търси по жанр" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Търси по държава" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Моите станции" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Език" + +msgctxt "#30301" +msgid "English" +msgstr "Английски" + +msgctxt "#30302" +msgid "German" +msgstr "Немски" + +msgctxt "#30303" +msgid "French" +msgstr "Френски" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Превключвай винаги на изглед 'Миниатюри'" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Добави в 'Моите станции'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Премахни от 'Моите станции'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "Мрежова грешка" + +msgctxt "#30601" +msgid "General" +msgstr "Основни" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Скрий фанарта" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.bs_ba/strings.po b/plugin.audio.radio_de/resources/language/resource.language.bs_ba/strings.po new file mode 100644 index 0000000000..ce6b1b2696 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.bs_ba/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Bosnian (Bosnia and Herzegovina) \n" +"Language: bs_ba\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Jezik" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Opšte" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ca_es/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ca_es/strings.po new file mode 100644 index 0000000000..aac24bb74e --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ca_es/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Catalan (Spain) \n" +"Language: ca_es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "accedeix a mes de 30000 emissores de radio" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Plugin de música per escoltar més de 30000 emissores de ràdio internacionals des rad.io, radio.de i radio.fr[CR]Característiques actuals:[CR]- Traduccions a l'anglès, alemany i francès[CR]- Cercador d'emissores per ubicació, gènere, tema, país, ciutat o idioma[CR]- Cercador d'emissores[CR]- 115 gèneres, 59 temes, 94 països, 1010 ciutats, 63 idiomes" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Recomanacions de l'editorial" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top de Emissores" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Buscar per Genere" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Buscar per Tema" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Cerca per país" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "buscar per ciutat" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Buscar per Idioma" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Emissores Locals" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Les meves emissores" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Cerca per Emissora" + +msgctxt "#30300" +msgid "Language" +msgstr "Idioma" + +msgctxt "#30301" +msgid "English" +msgstr "Anglès" + +msgctxt "#30302" +msgid "German" +msgstr "Alemany" + +msgctxt "#30303" +msgid "French" +msgstr "Francès" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Portuguès" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Castellà" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Força la vista a Miniatura" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "afageix a 'Les meves Emissores'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Elimina de 'Les meves Emissores'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Edita una emisora Personalitzada" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "siusplau entra %s" + +msgctxt "#30501" +msgid "title" +msgstr "titol" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "ruta de la miniatura" + +msgctxt "#30503" +msgid "stream url" +msgstr "Url de l'Stream" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Afageix una emissora personalitzada..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Error de xarxa" + +msgctxt "#30601" +msgid "General" +msgstr "General" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Amaga fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "El més popular" + +msgctxt "#30604" +msgid "A-Z" +msgstr "De la A a la Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Pàgina (%s/%s) | Següent >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Per països" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Preferir transmissions HTTP en comptes d'HTTPS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.cs_cz/strings.po b/plugin.audio.radio_de/resources/language/resource.language.cs_cz/strings.po new file mode 100644 index 0000000000..827cd1f28c --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.cs_cz/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Czech \n" +"Language: cs_cz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Jazyk" + +msgctxt "#30301" +msgid "English" +msgstr "Angličtina" + +msgctxt "#30302" +msgid "German" +msgstr "Němčina" + +msgctxt "#30303" +msgid "French" +msgstr "Francouzština" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Obecné" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Skrýt fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.cy_gb/strings.po b/plugin.audio.radio_de/resources/language/resource.language.cy_gb/strings.po new file mode 100644 index 0000000000..1617d3a4f5 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.cy_gb/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Welsh (United Kingdom) \n" +"Language: cy_gb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Iaith" + +msgctxt "#30301" +msgid "English" +msgstr "Saesneg" + +msgctxt "#30302" +msgid "German" +msgstr "Almaeneg" + +msgctxt "#30303" +msgid "French" +msgstr "Ffrangeg" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "teitl" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Cyffredinol" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Cuddio Celf Selogion" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.da_dk/strings.po b/plugin.audio.radio_de/resources/language/resource.language.da_dk/strings.po new file mode 100644 index 0000000000..b3df6d3fbc --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.da_dk/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-11-30 10:13+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Danish \n" +"Language: da_dk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.9.1\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Få adgang til >30000 radioudsendelser" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Musikplugin som giver adgang til over 30000 internationale radioudsendelser fra rad.io, radio.de, radio.fr, radio.pt og radio.es[CR]Nuværende funktioner[CR]- Oversat til engelsk, tysk og fransk[CR]- Gennemse stationer efter lokalitet, genre, emne, land, by og sprog[CR]- Søg efter stationer[CR]- 115 genrer, 59 emner, 94 lande, 1010 byer og 63 sprog" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Redaktørerne anbefaler" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top stationer" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Gennemse efter genre" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Gennemse efter tema" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Gennemse efter land" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Gennemse efter by" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Gennemse efter sprog" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Lokale stationer" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mine stationer" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Søg efter station" + +msgctxt "#30300" +msgid "Language" +msgstr "Sprog" + +msgctxt "#30301" +msgid "English" +msgstr "Engelsk" + +msgctxt "#30302" +msgid "German" +msgstr "Tysk" + +msgctxt "#30303" +msgid "French" +msgstr "Fransk" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Portugisisk" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Spansk" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Tving miniaturebillede-visning" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Føj til 'Mine Stationer'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Fjern fra 'Mine Stationer'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Rediger brugerdefineret station" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Indtast venligst %s" + +msgctxt "#30501" +msgid "title" +msgstr "titel" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "webadresse eller sti til miniaturebillede" + +msgctxt "#30503" +msgid "stream url" +msgstr "webadresse til stream" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Tilføj brugerdefineret station..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Netværksfejl" + +msgctxt "#30601" +msgid "General" +msgstr "Generel" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Skjul fankunst" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Mest populære" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Side (%s/%s) | Næste >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Efter land" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Foretræk HTTP-streams frem for HTTPS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "Kan ikke hente url til radiostream" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "Føjet til mine stationer!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "Fjernet fra mine stationer!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.de_de/strings.po b/plugin.audio.radio_de/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..83a218fd3b --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-08 01:37+0000\n" +"Last-Translator: Kai Sommerfeld \n" +"Language-Team: German \n" +"Language: de_de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Zugang zu mehr als 30000 Radiosendern" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Das Radio im Internet mit über 30000 Radiosendern, Internetradiostationen, gratis Streams und Podcasts live von rad.io, radio.de und radio.fr.[CR]Features[CR]- Auf deutsch, englisch und französisch übersetzt[CR]- Sortiert nach: in der Nähe, Genre, Thema, Land und Sprache[CR]- nach Sendern suchen[CR]- 115 Genre, 59 Themen, 94 Länder, 1010 Städte, 63 Sprachen" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Tipps der Redaktion" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Beliebteste Sender" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Nach Musikrichtung" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Nach Thema" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Nach Land" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Nach Stadt" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Nach Sprache" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Sender in der Nähe" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Meine Sender" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Nach Sender suchen" + +msgctxt "#30300" +msgid "Language" +msgstr "Sprache" + +msgctxt "#30301" +msgid "English" +msgstr "Englisch" + +msgctxt "#30302" +msgid "German" +msgstr "Deutsch" + +msgctxt "#30303" +msgid "French" +msgstr "Französisch" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Portugiesisch" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Spanisch" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "ViewMode „Vorschaubild“ erzwingen" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Zu „Meine Sender“ hinzufügen" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Aus „Meine Sender“ entfernen" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Benutzerdefinierten Sender bearbeiten" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Bitte %s eingeben" + +msgctxt "#30501" +msgid "title" +msgstr "Titel" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "Vorschaubild-URL oder -Pfad" + +msgctxt "#30503" +msgid "stream url" +msgstr "Stream-URL" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Benutzerdefinierten Sender hinzufügen ..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Netzwerkfehler" + +msgctxt "#30601" +msgid "General" +msgstr "Allgemein" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Fanart verbergen" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Am beliebtesten" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A bis Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Seite (%s / %s) | Nächste >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Nach Land" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "HTTPS-Streams gegenüber HTTP-Streams bevorzugen" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "URL des Radio-Streams kann nicht ermittelt werden" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "Erfolgreich zu „Meine Sender“ hinzugefügt!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "Erfolgreich aus „Meine Sender“ entfernt!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.el_gr/strings.po b/plugin.audio.radio_de/resources/language/resource.language.el_gr/strings.po new file mode 100644 index 0000000000..416af587fc --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.el_gr/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-12-14 15:13+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Greek \n" +"Language: el_gr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.9.1\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Πρόσβαση σε πάνω από 30000 εκπομπές" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Μουσικό plugin για πρόσβαση σε περισσότερους από 30000 διεθνείς ραδιοφωνικές εκπομπές από τα rad.io, radio.de και radio.fr[CR]Υποστηρίζονται[CR]- Μεταφράσεις σε Αγγλικά, Γερμανικά και Γαλλικά[CR]- Περιήγηση σταθμών ανά τοποθεσία, είδος, θέμα, χώρα, πόλη και γλώσσα[CR]- Αναζήτηση σταθμών[CR]- 115 είδη, 59 θέματα, 94 χώρες, 1010 πόλεις, 63 γλώσσες" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Προτάσεις Συντακτών" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Κορυφαίοι Σταθμοί" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Περιήγηση ανά είδος" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Περιήγηση ανά θέμα" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Περιήγηση ανά χώρα" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Περιήγηση ανά πόλη" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Περιήγηση ανά γλώσσα" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Τοπικοί Σταθμοί" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Οι Σταθμοί μου" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Αναζήτηση σταθμού" + +msgctxt "#30300" +msgid "Language" +msgstr "Γλώσσα" + +msgctxt "#30301" +msgid "English" +msgstr "Αγγλικά" + +msgctxt "#30302" +msgid "German" +msgstr "Γερμανικά" + +msgctxt "#30303" +msgid "French" +msgstr "Γαλλικά" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Πορτογαλικά" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Ισπανικά" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Οι 'Μικρογραφίες' σαν Τύπος Προβολής" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Προσθήκη στους 'Σταθμούς μου'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Αφαίρεση από τους 'Σταθμούς μου'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Αλλαγή προσαρμοσμένου Σταθμού" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Εισάγετε %s" + +msgctxt "#30501" +msgid "title" +msgstr "τίτλο" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "url ή διαδρομή μικρογραφίας" + +msgctxt "#30503" +msgid "stream url" +msgstr "url ροής" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Προσθήκη προσαρμοσμένου Σταθμού..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Σφάλμα Δικτύου" + +msgctxt "#30601" +msgid "General" +msgstr "Γενικά" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Απόκρυψη fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Τα πιο δημοφιλή" + +msgctxt "#30604" +msgid "A-Z" +msgstr "από το Α ως το Ζ" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "Ανά χώρα" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.en_au/strings.po b/plugin.audio.radio_de/resources/language/resource.language.en_au/strings.po new file mode 100644 index 0000000000..2c5785519f --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.en_au/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: English (Australia) \n" +"Language: en_au\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Language" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.en_gb/strings.po b/plugin.audio.radio_de/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..a3516bd581 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,169 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_GB\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.en_nz/strings.po b/plugin.audio.radio_de/resources/language/resource.language.en_nz/strings.po new file mode 100644 index 0000000000..1120e7d45e --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.en_nz/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: English (New Zealand) \n" +"Language: en_nz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Access >30000 radio broadcasts" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german and french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Editorials Recommendations" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top Stations" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Browse by genre" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Browse by topic" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Browse by country" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Browse by city" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Browse by language" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Local Stations" + +msgctxt "#30108" +msgid "My Stations" +msgstr "My Stations" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Search for station" + +msgctxt "#30300" +msgid "Language" +msgstr "Language" + +msgctxt "#30301" +msgid "English" +msgstr "English" + +msgctxt "#30302" +msgid "German" +msgstr "German" + +msgctxt "#30303" +msgid "French" +msgstr "French" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Force ViewMode to Thumbnail" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Add to 'My Stations'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Remove from 'My Stations'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Edit custom Station" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Please enter %s" + +msgctxt "#30501" +msgid "title" +msgstr "title" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "thumbnail url or path" + +msgctxt "#30503" +msgid "stream url" +msgstr "stream url" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Add custom Station..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Network Error" + +msgctxt "#30601" +msgid "General" +msgstr "" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.en_us/strings.po b/plugin.audio.radio_de/resources/language/resource.language.en_us/strings.po new file mode 100644 index 0000000000..d2cb20235b --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.en_us/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: English (United States) \n" +"Language: en_us\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Access >30000 radio broadcasts" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de and radio.fr[CR]Currently features[CR]- English, german and french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Editorials Recommendations" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top Stations" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Browse by genre" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Browse by topic" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Browse by country" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Browse by city" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Browse by language" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Local Stations" + +msgctxt "#30108" +msgid "My Stations" +msgstr "My Stations" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Search for station" + +msgctxt "#30300" +msgid "Language" +msgstr "Language" + +msgctxt "#30301" +msgid "English" +msgstr "English" + +msgctxt "#30302" +msgid "German" +msgstr "German" + +msgctxt "#30303" +msgid "French" +msgstr "French" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Force ViewMode to Thumbnail" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Add to 'My Stations'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Remove from 'My Stations'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Edit custom Station" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Please enter %s" + +msgctxt "#30501" +msgid "title" +msgstr "title" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "thumbnail url or path" + +msgctxt "#30503" +msgid "stream url" +msgstr "stream url" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Add custom Station..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Network Error" + +msgctxt "#30601" +msgid "General" +msgstr "" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.eo/strings.po b/plugin.audio.radio_de/resources/language/resource.language.eo/strings.po new file mode 100644 index 0000000000..fa741c3a1f --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.eo/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Esperanto \n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Lingvo" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "nomo" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Generalo" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.es_ar/strings.po b/plugin.audio.radio_de/resources/language/resource.language.es_ar/strings.po new file mode 100644 index 0000000000..506a8c1eec --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.es_ar/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Spanish (Argentina) \n" +"Language: es_ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Idioma" + +msgctxt "#30301" +msgid "English" +msgstr "Inglés" + +msgctxt "#30302" +msgid "German" +msgstr "Alemán" + +msgctxt "#30303" +msgid "French" +msgstr "Francés" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "título" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "General" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Ocultar fanarts" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.es_es/strings.po b/plugin.audio.radio_de/resources/language/resource.language.es_es/strings.po new file mode 100644 index 0000000000..d7ec54fb5b --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-12-14 15:13+0000\n" +"Last-Translator: Edson Armando \n" +"Language-Team: Spanish (Spain) \n" +"Language: es_es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.9.1\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Escuche más de 30000 emisoras de radio" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Complemento de música para escuchar más de 30000 emisoras de radio de todo el mundo desde rad.io, radio.de, radio.fr, radio.pt y radio.es.[CR]Funciones:[CR]- Traducciones al inglés, alemán, francés, portugués y español.[CR]- Buscador de emisoras por ubicación, género, tema, país, ciudad o idioma.[CR]- Buscador de emisoras por nombre.[CR]- 73 géneros, 103 temas, 174 países y multitud de ciudades e idiomas." + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Emisoras sugeridas por los editores" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Las mejores emisoras" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Buscar por género" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Buscar por tema" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Buscar por país" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Buscar por ciudad" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Buscar por idioma" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Emisoras cercanas" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mis emisoras" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Buscar por nombre" + +msgctxt "#30300" +msgid "Language" +msgstr "Idioma" + +msgctxt "#30301" +msgid "English" +msgstr "Inglés" + +msgctxt "#30302" +msgid "German" +msgstr "Alemán" + +msgctxt "#30303" +msgid "French" +msgstr "Francés" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Portugués" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Español" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Forzar el modo vista en miniatura" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Añadir a 'Mis emisoras'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Eliminar de 'Mis emisoras'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Editar emisora personalizada" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Escriba %s" + +msgctxt "#30501" +msgid "title" +msgstr "el nombre" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "la URL o ruta del icono" + +msgctxt "#30503" +msgid "stream url" +msgstr "la URL de la transmisión" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Añadir emisora personalizada..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Error de conexión" + +msgctxt "#30601" +msgid "General" +msgstr "General" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Ocultar fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Las más populares" + +msgctxt "#30604" +msgid "A-Z" +msgstr "De la A a la Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Página %s de %s | Siguiente >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Por país" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Preferir protocolo HTTP a HTTS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "No se ha podido obtener la url del stream de radio" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "¡Añadido correctamente a Mis estaciones!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "¡Eliminado correctamente de Mis estaciones!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.es_mx/strings.po b/plugin.audio.radio_de/resources/language/resource.language.es_mx/strings.po new file mode 100644 index 0000000000..ba25c8a624 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.es_mx/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-12-14 15:13+0000\n" +"Last-Translator: Edson Armando \n" +"Language-Team: Spanish (Mexico) \n" +"Language: es_mx\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.9.1\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Accede a >30000 transmisiones de radio" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Complemento de música para acceder a más de 30000 transmisiones de radio internacionales de rad.io, radio.de, radio.fr, radio.pt y radio.es[CR]Características actuales[CR]- Traducido a inglés, alemán, francés y español[CR]- Explora estaciones por ubicación, género, tópico, país, ciudad e idioma[CR]Busca estaciones[CR]- 115 géneros, 59 tópicos, 94 países, 1010, ciudades, 63 idiomas" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Recomendaciones de los editores" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top estaciones" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Navegar por genero" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Explorar por tópico" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Explorar por país" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Explorar por ciudad" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Explorar por idioma" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Estaciones locales" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mis Estaciones" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Buscar estación" + +msgctxt "#30300" +msgid "Language" +msgstr "Idioma" + +msgctxt "#30301" +msgid "English" +msgstr "Inglés" + +msgctxt "#30302" +msgid "German" +msgstr "Alemán" + +msgctxt "#30303" +msgid "French" +msgstr "Francés" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Portugués" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Español" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Forzar Modo de Vista a Miniatura" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Agregar a 'Mis Estaciones'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Remover de 'Mis Estaciones'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Editar estación personalizada" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Por favor ingresa %s" + +msgctxt "#30501" +msgid "title" +msgstr "título" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "ruta o url a miniatura" + +msgctxt "#30503" +msgid "stream url" +msgstr "url de transmisión" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Añadir estación personalizada..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Error de red" + +msgctxt "#30601" +msgid "General" +msgstr "General" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Esconder fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Más popular" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Página (%s/%s) | Siguiente >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Por país" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Preferir conexiones HTTP sobre HTTS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "No se pudo obtener la url de la transmisión de radio" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "¡Añadido exitosamente a Mis estaciones!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "¡Eliminado exitosamente de Mis estaciones!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.et_ee/strings.po b/plugin.audio.radio_de/resources/language/resource.language.et_ee/strings.po new file mode 100644 index 0000000000..0fca32e11d --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.et_ee/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Estonian \n" +"Language: et_ee\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Keel" + +msgctxt "#30301" +msgid "English" +msgstr "Inglise" + +msgctxt "#30302" +msgid "German" +msgstr "Saksa" + +msgctxt "#30303" +msgid "French" +msgstr "Prantsuse" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "pealkiri" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Üldine" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Peida fännipildid" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.eu_es/strings.po b/plugin.audio.radio_de/resources/language/resource.language.eu_es/strings.po new file mode 100644 index 0000000000..656dbcc42d --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.eu_es/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: Basque (Spain) \n" +"Language: eu_es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Hizkuntza" + +msgctxt "#30301" +msgid "English" +msgstr "Ingelesa" + +msgctxt "#30302" +msgid "German" +msgstr "Alemana" + +msgctxt "#30303" +msgid "French" +msgstr "Frantsesa" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "titulua" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Orokorra" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Ezkutatu fanart-a" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.fa_af/strings.po b/plugin.audio.radio_de/resources/language/resource.language.fa_af/strings.po new file mode 100644 index 0000000000..4c55d5274f --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.fa_af/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Persian (Afghanistan) \n" +"Language: fa_af\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "زبان" + +msgctxt "#30301" +msgid "English" +msgstr "انگلیسی" + +msgctxt "#30302" +msgid "German" +msgstr "آلمانی" + +msgctxt "#30303" +msgid "French" +msgstr "فرانسوی" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "عمومی" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.fa_ir/strings.po b/plugin.audio.radio_de/resources/language/resource.language.fa_ir/strings.po new file mode 100644 index 0000000000..e306a0ade7 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.fa_ir/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Persian (Iran) \n" +"Language: fa_ir\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "زبان" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "عمومی" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "مخفی کردن Fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.fi_fi/strings.po b/plugin.audio.radio_de/resources/language/resource.language.fi_fi/strings.po new file mode 100644 index 0000000000..7344b3102d --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.fi_fi/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: Finnish \n" +"Language: fi_fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Selaa lajityypeittäin" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Selaa maittain" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Kieli" + +msgctxt "#30301" +msgid "English" +msgstr "Englanti" + +msgctxt "#30302" +msgid "German" +msgstr "Saksa" + +msgctxt "#30303" +msgid "French" +msgstr "Ranska" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "nimi" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "Verkkovirhe" + +msgctxt "#30601" +msgid "General" +msgstr "Yleiset" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Piilota fanitaide" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.fo_fo/strings.po b/plugin.audio.radio_de/resources/language/resource.language.fo_fo/strings.po new file mode 100644 index 0000000000..c97c27220c --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.fo_fo/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Faroese \n" +"Language: fo_fo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Mál" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "heiti" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Vanligt" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Goym fjepparatilfar" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.fr_ca/strings.po b/plugin.audio.radio_de/resources/language/resource.language.fr_ca/strings.po new file mode 100644 index 0000000000..833bfaf31a --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.fr_ca/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: French (Canada) \n" +"Language: fr_ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Accéder à > 30000 radiodiffusions" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Plugiciel de musique pour accéder à plus de 30000 radiodiffusions internationales de rad.io, radio.de et radio.fr[CR]Offre présentement [CR]- Traductions en anglais, allemand et français[CR]- Parcourir des stations par lieux, genres, sujets, pays, villes et langues[CR]- Rechercher des stations[CR]- 115 genres, 59 sujets, 94 pays, 1 010 villes, 63 langues." + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Recommandations des éditoriaux" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Les 1res stations" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Parcourir par genre" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Parcourir par sujet" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Parcourir par pays" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Parcourir par ville" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Parcourir par langue" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Stations locales" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mes stations" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Rechercher des stations" + +msgctxt "#30300" +msgid "Language" +msgstr "Langue" + +msgctxt "#30301" +msgid "English" +msgstr "Anglais" + +msgctxt "#30302" +msgid "German" +msgstr "Allemand" + +msgctxt "#30303" +msgid "French" +msgstr "Français" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Forcer le mode de visualisation en imagette" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Ajouter à « Mes stations »" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Enlever de « Mes stations »" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Modifier une station personnalisée" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Veuillez saisir %s" + +msgctxt "#30501" +msgid "title" +msgstr "titre" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "URL ou chemin de l'imagette" + +msgctxt "#30503" +msgid "stream url" +msgstr "URL du flux" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Ajouter une station personnalisée..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Erreur de réseau" + +msgctxt "#30601" +msgid "General" +msgstr "Général" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Cacher le fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.fr_fr/strings.po b/plugin.audio.radio_de/resources/language/resource.language.fr_fr/strings.po new file mode 100644 index 0000000000..648b745386 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.fr_fr/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: French (France) \n" +"Language: fr_fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Ecoutez plus de 30000 stations de radio" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Plugin musical pour écouter plus de 30000 stations de radio internationales de rad.io, radio.de et radio.fr[CR]Fonctionnalités[CR]- Traduit en Anglais, Allemand et Français[CR]- Parcourir les stations par genre, thème, pays, ville et langue[CR]- Recherche de stations[CR]- 115 genres, 59 thèmes, 94 pays, 1010 villes, 63 langues" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Recommandations éditoriales" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top stations" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Parcourir par Genre" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Parcourir par Thème" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Parcourir par Pays" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Parcourir par Ville" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Parcourir par Langue" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Radios dans votre région" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mes stations" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Rechercher" + +msgctxt "#30300" +msgid "Language" +msgstr "Langue" + +msgctxt "#30301" +msgid "English" +msgstr "Anglais" + +msgctxt "#30302" +msgid "German" +msgstr "Allemand" + +msgctxt "#30303" +msgid "French" +msgstr "Français" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Forcer la vue Vignette" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Ajouter à 'Mes stations'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Supprimer de 'Mes stations'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Modifier la station personnalisée" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Veuillez entrer %s" + +msgctxt "#30501" +msgid "title" +msgstr "le titre" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "l'url de la vignette ou le chemin" + +msgctxt "#30503" +msgid "stream url" +msgstr "l'url du flux" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Ajouter une station personnalisée..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Erreur réseau" + +msgctxt "#30601" +msgid "General" +msgstr "Général" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Sans fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.gl_es/strings.po b/plugin.audio.radio_de/resources/language/resource.language.gl_es/strings.po new file mode 100644 index 0000000000..23a342ab44 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.gl_es/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Galician (Spain) \n" +"Language: gl_es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Escoite máis de 30000 emisoras de radio" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Engadido de música para escoitar máis de 30000 emisoras de radio internacionais de rad.io, radio.de e radio.fr.[CR]Características:[CR]- Traducións ao inglés, alemán, francés e castelán.[CR]- Busca de emisoras por localización, xénero, tema, país, cidade ou idioma.[CR]- Busca de emisoras.[CR]- 115 xéneros, 59 temas, 94 países, 1010 cidades, 63 idiomas." + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Emisoras suxeridas polos editores" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Mellores Emisoras" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Buscar por xénero" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Buscar por tema" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Buscar por país" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Buscar por nome" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Buscar por idioma" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Emisoras locais" + +msgctxt "#30108" +msgid "My Stations" +msgstr "As miñas Emisoras" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Buscar por emisora" + +msgctxt "#30300" +msgid "Language" +msgstr "Idioma" + +msgctxt "#30301" +msgid "English" +msgstr "Inglés" + +msgctxt "#30302" +msgid "German" +msgstr "Alemán" + +msgctxt "#30303" +msgid "French" +msgstr "Francés" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Forzar o Modo de Vista a miniatura" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Engadir a 'As miñas Emisoras'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Eliminar de 'As miñas Emisoras'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Editar Emisora personalizada" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Por favor introduza %s" + +msgctxt "#30501" +msgid "title" +msgstr "título" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "url ou ruta da miniatura" + +msgctxt "#30503" +msgid "stream url" +msgstr "url do fluxo" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Engadir Emisora personalizada..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Erro de rede" + +msgctxt "#30601" +msgid "General" +msgstr "Xeral" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Agochar cartel" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.he_il/strings.po b/plugin.audio.radio_de/resources/language/resource.language.he_il/strings.po new file mode 100644 index 0000000000..e33b4b7dff --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.he_il/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Hebrew (Israel) \n" +"Language: he_il\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "גישה ליותר מ־30000 שידורי רדיו" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "הרחבת מוזיקה המספקת גישה ליותר מ־30000 שידורי רדיו בינלאומים מ־rad.io, radio.de ו־radio.fr[CR]מאפיינים נוכחיים[CR]- שפות אנגלית, גרמנית וצרפתית[CR]- עיון לפי מיקום, סגנון, נושא, מדינה, עיר ושפה[CR]- חיפוש תחנות[CR]- 115 סגנונות, 59 נושאים, 94 מדינות 1010 ערים, 63 שפות" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "המלצות העורכים" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "התחנות הגדולות" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "עיון לפי סגנון" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "עיון לפי נושא" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "עיון לפי מדינה" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "עיון לפי עיר" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "עיון לפי שפה" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "תחנות מקומיות" + +msgctxt "#30108" +msgid "My Stations" +msgstr "התחנות שלי" + +msgctxt "#30200" +msgid "Search for station" +msgstr "חיפוש תחנה" + +msgctxt "#30300" +msgid "Language" +msgstr "שפה" + +msgctxt "#30301" +msgid "English" +msgstr "אנגלית" + +msgctxt "#30302" +msgid "German" +msgstr "גרמנית" + +msgctxt "#30303" +msgid "French" +msgstr "צרפתית" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "פורטוגלית" + +msgctxt "#30305" +msgid "Spanish" +msgstr "ספרדית" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "אלץ תצוגת תמונה ממוזערת" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "הוספה לתחנות שלי" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "הסרה מהתחנות שלי" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "עריכת תחנה מותאמת" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "יש להזין %s" + +msgctxt "#30501" +msgid "title" +msgstr "כותרת" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "כתובת או נתיב תמונה ממוזערת" + +msgctxt "#30503" +msgid "stream url" +msgstr "כתובת זרימה" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "הוספת תחנה מותאמת…" + +msgctxt "#30600" +msgid "Network Error" +msgstr "שגיאת רשת" + +msgctxt "#30601" +msgid "General" +msgstr "כללי" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "הסתר פאנארט" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "הנפוצים ביותר" + +msgctxt "#30604" +msgid "A-Z" +msgstr "א-ת" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "עמוד (%s/%s) | הבא >>" + +msgctxt "#30606" +msgid "By country" +msgstr "לפי מדינה" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "העדפת תזרימי HTTP על פני HTTPS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.hi_in/strings.po b/plugin.audio.radio_de/resources/language/resource.language.hi_in/strings.po new file mode 100644 index 0000000000..cafe5c86f0 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.hi_in/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Hindi (India) \n" +"Language: hi_in\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "बाशा" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "सामान्य" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.hr_hr/strings.po b/plugin.audio.radio_de/resources/language/resource.language.hr_hr/strings.po new file mode 100644 index 0000000000..1f87c0bc51 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.hr_hr/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Croatian \n" +"Language: hr_hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Pristup >30000 radio emitiranja" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Glazbeni dodatak za pristup 30000 međunarodnih radio stanica na rad.io, radio.de i radio.fr[CR]Trenutne značajke[CR]- engleski, njemački i francuski prijevodi[CR]- Pregledavajte stanice prema lokaciji, žanru, temi, zemlji, gradu i jeziku[CR]- Pretraživanje stanica[CR]- 115 žanrova, 59 tema, 94 zemalja, 1010 gradova, 63 jezika" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Preporuke uredništva" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top stanica" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Pregled po žanru" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Pregledaj po temi" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Pregled po državi" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Pregledaj po gradu" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Pregledaj po jeziku" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Lokalne stanice" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Moje stanice" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Pretraži stanice" + +msgctxt "#30300" +msgid "Language" +msgstr "Jezik" + +msgctxt "#30301" +msgid "English" +msgstr "Engleski" + +msgctxt "#30302" +msgid "German" +msgstr "Njemački" + +msgctxt "#30303" +msgid "French" +msgstr "Francuski" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Prisili način prikaza u 'Minijature'" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Dodaj u 'Moje stanice'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Ukloni iz 'Moje stanice'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Uredi prilagođenu stanicu" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Upišite %s" + +msgctxt "#30501" +msgid "title" +msgstr "naslov" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "URL minijature ili putanju" + +msgctxt "#30503" +msgid "stream url" +msgstr "URL streama" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Dodaj prilagođenu stanicu..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Greška mreže" + +msgctxt "#30601" +msgid "General" +msgstr "Općenito" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Sakrij sliku omota" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.hu_hu/strings.po b/plugin.audio.radio_de/resources/language/resource.language.hu_hu/strings.po new file mode 100644 index 0000000000..853da20471 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.hu_hu/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Hungarian \n" +"Language: hu_hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Hozzáférés 30000-nél is több rádióadáshoz" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Beépülő modul több, mint 30000 nemzetközi rádióadás eléréséhez a rad.io, radio.de és radio.fr oldalakról[CR]Jelenlegi tulajdonságok[CR]- Angol, német, francia és magyar nyelv[CR]- Állomások böngészése hely, műfaj, téma, ország, város és nyelv szerint[CR]- Állomások keresése[CR]- 115 műfaj, 59 téma, 94 ország, 1010 város, 63 nyelv" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Szerkesztők ajánlata" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Legjobb állomás" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Műfaj szerint" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Téma szerint" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Ország szerint" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Város szerint" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Nyelv szerint" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Helyi adók" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Saját adók" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Adó keresése" + +msgctxt "#30300" +msgid "Language" +msgstr "Nyelv" + +msgctxt "#30301" +msgid "English" +msgstr "Angol" + +msgctxt "#30302" +msgid "German" +msgstr "Német" + +msgctxt "#30303" +msgid "French" +msgstr "Francia" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Kis képek nézet kényszerítése" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Hozzáadás a 'Saját adók'-hoz" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Eltávolítás a 'Saját adók'-ból" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Egyéni adó beállítása" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Adja meg a %s-t" + +msgctxt "#30501" +msgid "title" +msgstr "cím" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "kis kép URL vagy útvonal" + +msgctxt "#30503" +msgid "stream url" +msgstr "folyam URL" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Egyéni állomás hozzáadása..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Hálózati hiba" + +msgctxt "#30601" +msgid "General" +msgstr "Általános" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Fanart elrejtése" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.hy_am/strings.po b/plugin.audio.radio_de/resources/language/resource.language.hy_am/strings.po new file mode 100644 index 0000000000..50a878580f --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.hy_am/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Armenian \n" +"Language: hy_am\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Լեզու" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Գլխավոր" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.id_id/strings.po b/plugin.audio.radio_de/resources/language/resource.language.id_id/strings.po new file mode 100644 index 0000000000..e7b244110d --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.id_id/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Indonesian \n" +"Language: id_id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Akses >30000 siaran radio" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Plugin musik untuk mengakses lebih dari 30000 siaran radio internasional dari rad.io, radio.de dan radio.fr[CR]Fitur saat ini[CR]- Terjemahan Inggris, Jerman dan Perancis[CR]- Jelajah stasiun berdasarkan lokasi, tema, topik, negara, kota dan bahasa[CR]- Cari stasiun[CR]- 115 tema, 59 topik, 94 negara, 1010 kota, 63 bahasa" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Rekomendasi Editor" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Stasiun Teratas" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Jelajahi menurut tema" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Jelajahi menurut topik" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Jelajahi menurut negara" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Jelajahi menurut kota" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Jelajahi menurut bahasa" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Stasiun Lokal" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Stasiun Saya" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Cari stasiun" + +msgctxt "#30300" +msgid "Language" +msgstr "Bahasa" + +msgctxt "#30301" +msgid "English" +msgstr "Inggris" + +msgctxt "#30302" +msgid "German" +msgstr "Jerman" + +msgctxt "#30303" +msgid "French" +msgstr "Perancis" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Paksakan ViewMode ke Gambar Kecil" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Tambahkan ke 'Stasiun Saya'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Hapus dari 'Stasiun Saya'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Edit Stasiun Lain" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Silakan masukkan %s" + +msgctxt "#30501" +msgid "title" +msgstr "judul" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "lokasi atau url gambar kecil" + +msgctxt "#30503" +msgid "stream url" +msgstr "url stream" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Tambah Stasiun Lain..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Kesalahan Jaringan" + +msgctxt "#30601" +msgid "General" +msgstr "Umum" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Sembunyikan fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.is_is/strings.po b/plugin.audio.radio_de/resources/language/resource.language.is_is/strings.po new file mode 100644 index 0000000000..71bcc067f3 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.is_is/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Icelandic \n" +"Language: is_is\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Tungumál" + +msgctxt "#30301" +msgid "English" +msgstr "Enska" + +msgctxt "#30302" +msgid "German" +msgstr "Þýska" + +msgctxt "#30303" +msgid "French" +msgstr "Franska" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "titill" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "Net Villa" + +msgctxt "#30601" +msgid "General" +msgstr "Almennt" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Fela aðdáendamyndir" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.it_it/strings.po b/plugin.audio.radio_de/resources/language/resource.language.it_it/strings.po new file mode 100644 index 0000000000..07d8b15225 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.it_it/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Matteo Ghetta \n" +"Language-Team: Italian \n" +"Language: it_it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Accedi a più di 30000 trasmissioni radio" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Plugin per accedere a oltre 30000 trasmissioni internazionali radiofoniche da rad.io, radio.de e radio.fr[CR]Caratteristiche[CR]- Supporta inglese, tedesco e francese[CR]- Sfoglia le stazioni per località, genere, argomento, paese, citta e lingua[CR]- Ricerca delle stazioni[CR]- 115 generi, 59 argomenti, 94 paesi, 1010 città e 63 lingue" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Consigli dagli editori" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Stazioni 100" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Sfoglia per genere" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Sfoglia per argomento" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Sfoglia per paese" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Sfoglia per città" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Sfoglia per lingua" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Stazioni locali" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Le mie stazioni" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Cerca una stazione" + +msgctxt "#30300" +msgid "Language" +msgstr "Lingua" + +msgctxt "#30301" +msgid "English" +msgstr "Inglese" + +msgctxt "#30302" +msgid "German" +msgstr "Tedesco" + +msgctxt "#30303" +msgid "French" +msgstr "Francese" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Portoghese" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Spagnolo" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Forza visuale in visualizzazione Anteprime" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Aggiungi alle mie stazioni" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Rimuovi dalle mie stazioni" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Modica la stazione personalizzata" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Inserisci %s" + +msgctxt "#30501" +msgid "title" +msgstr "titolo" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "url o percorso dell'anteprima" + +msgctxt "#30503" +msgid "stream url" +msgstr "url per lo streaming" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Inserisci una stazione personalizzata..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Errore di rete" + +msgctxt "#30601" +msgid "General" +msgstr "Generale" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Nascondi fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Più popolari" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Pagina (%s/%s) | Prossima >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Per Paese" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Preferisci HTTP rispetto a HTTS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ja_jp/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ja_jp/strings.po new file mode 100644 index 0000000000..4b83088ce3 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ja_jp/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Japanese \n" +"Language: ja_jp\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "言語" + +msgctxt "#30301" +msgid "English" +msgstr "英語" + +msgctxt "#30302" +msgid "German" +msgstr "ドイツ語" + +msgctxt "#30303" +msgid "French" +msgstr "フランス語" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "一般" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "ファンアートを隠す" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ko_kr/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ko_kr/strings.po new file mode 100644 index 0000000000..c59d19d833 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ko_kr/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-12-23 16:13+0000\n" +"Last-Translator: Minho Park \n" +"Language-Team: Korean \n" +"Language: ko_kr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.10.1\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr ">3만개의 라디오 방송국 연결" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "rad.io, radio.de, radio.fr, radio.pt 및 radio.es에서 3만개 이상의 국제 라디오 방송에 액세스할 수 있는 음악 플러그인[CR]현재 기능[CR]- 영어, 독일어, 프랑스어 번역[CR]- 방송국 검색 위치, 장르, 주제, 국가, 도시 및 언어별[CR]- 방송국 검색[CR]- 115개 장르, 59개 주제, 94개 국가, 1010개 도시, 63개 언어" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "편집 권장 사항" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "최고 방송국" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "장르로 탐색" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "주제별로 찾아보기" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "국가별 탐색" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "도시로 찾아보기" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "언어로 찾아보기" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "지역 방송국" + +msgctxt "#30108" +msgid "My Stations" +msgstr "내 방송국" + +msgctxt "#30200" +msgid "Search for station" +msgstr "방송국 검색" + +msgctxt "#30300" +msgid "Language" +msgstr "언어" + +msgctxt "#30301" +msgid "English" +msgstr "영어" + +msgctxt "#30302" +msgid "German" +msgstr "독일어" + +msgctxt "#30303" +msgid "French" +msgstr "프랑스어" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "포르투갈어" + +msgctxt "#30305" +msgid "Spanish" +msgstr "스페인어" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "썸네일 보기" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "'내 방송국'에 추가" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "'내 방송국'에서 제거" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "사용자 지정 방송국 편집" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "%s을(를) 입력하십시오" + +msgctxt "#30501" +msgid "title" +msgstr "제목" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "썸네일 URL 또는 경로" + +msgctxt "#30503" +msgid "stream url" +msgstr "스트림 URL" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "사용자 지정 방송국 추가..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "네트워크 오류" + +msgctxt "#30601" +msgid "General" +msgstr "일반" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "팬아트 숨기기" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "가장 인기있는" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "페이지(%s/%s) | 다음 >>" + +msgctxt "#30606" +msgid "By country" +msgstr "국가별" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "HTTPS보다 HTTP 스트림 선호" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "라디오 스트림 URL을 가져올 수 없음" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "나의 방송국에 성공적으로 추가되었습니다!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "나의 방송국에서 성공적으로 제거되었습니다!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.lt_lt/strings.po b/plugin.audio.radio_de/resources/language/resource.language.lt_lt/strings.po new file mode 100644 index 0000000000..f84b391e0f --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.lt_lt/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-12-14 15:13+0000\n" +"Last-Translator: Raimondas Dužinskas \n" +"Language-Team: Lithuanian \n" +"Language: lt_lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Weblate 4.9.1\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Prieiga> 30000 radijo transliacijų" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Muzikos įskiepio prieiga prie daugiau nei 30000 tarptautinių radijo transliacijų iš rad.io, radio.de ir radio.fr [CR] Šiuo metu programoja išversta į [Cr] - anglų, vokiečių ir prancūzų kalbas [CR] - Naršyti stotis pagal: vietovę, žanrą, temą, šalį, miestą ir/ar kalbą [CR] - Ieškoti radio stočių pagal:[Cr] - 115 žanrai, 59 temos, 94 šalys, 1010 miestų, 63 kalbos" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Redakcijos rekomendacijos" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top stotys" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Naršyti pagal žanrą" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Naršyti pagal temą" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Naršyti pagal šalį" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Naršyti pagal miestą" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Naršyti pagal kalbą" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Vietinės stotys" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mano radio stotys" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Ieškoti radio stoties" + +msgctxt "#30300" +msgid "Language" +msgstr "Kalba" + +msgctxt "#30301" +msgid "English" +msgstr "Anglų" + +msgctxt "#30302" +msgid "German" +msgstr "Vokiečių" + +msgctxt "#30303" +msgid "French" +msgstr "Prancūzų" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Portugalų" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Ispanų" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Versti ViewMode į miniatiūrą" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Pridėti į 'Mano stotys'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Pašalinti iš 'Mano stotys'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Redaguoti savo stotis" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Prašome įvesti %s" + +msgctxt "#30501" +msgid "title" +msgstr "antraštė" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "miniatiūrų url arba kelias" + +msgctxt "#30503" +msgid "stream url" +msgstr "url srautas" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Pridėti pasirinktas stotis ..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Tinklo klaida" + +msgctxt "#30601" +msgid "General" +msgstr "Pagrindinis" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Slėpti fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Populiariausi" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Puslapis (%s/%s) | Kitas >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Pagal šalį" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Pirmenybę teikite HTTP srautams, o ne HTTPS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "Nepavyko gauti radijo srauto URL" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "Sėkmingai pridėta prie mano stočių!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "Sėkmingai pašalintas iš mano stočių!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.lv_lv/strings.po b/plugin.audio.radio_de/resources/language/resource.language.lv_lv/strings.po new file mode 100644 index 0000000000..a779d0122a --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.lv_lv/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Latvian \n" +"Language: lv_lv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Valoda" + +msgctxt "#30301" +msgid "English" +msgstr "angļu" + +msgctxt "#30302" +msgid "German" +msgstr "vācu" + +msgctxt "#30303" +msgid "French" +msgstr "franču" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "nosaukums" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "Tīkla kļūda" + +msgctxt "#30601" +msgid "General" +msgstr "Vispārīgi" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Slēpt fanumākslu" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.mk_mk/strings.po b/plugin.audio.radio_de/resources/language/resource.language.mk_mk/strings.po new file mode 100644 index 0000000000..98a92e927a --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.mk_mk/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Macedonian \n" +"Language: mk_mk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Јазик" + +msgctxt "#30301" +msgid "English" +msgstr "Англиски" + +msgctxt "#30302" +msgid "German" +msgstr "Германски" + +msgctxt "#30303" +msgid "French" +msgstr "Француски" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Општо" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Сокриј слика" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ml_in/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ml_in/strings.po new file mode 100644 index 0000000000..00f746a599 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ml_in/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Malayalam (India) \n" +"Language: ml_in\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "ഭാഷ" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "പോതുവായത്" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.mn_mn/strings.po b/plugin.audio.radio_de/resources/language/resource.language.mn_mn/strings.po new file mode 100644 index 0000000000..92a7a4ee8e --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.mn_mn/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Mongolian \n" +"Language: mn_mn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Хэл" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Ерөнхий" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ms_my/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ms_my/strings.po new file mode 100644 index 0000000000..cacc712641 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ms_my/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Malay \n" +"Language: ms_my\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Bahasa" + +msgctxt "#30301" +msgid "English" +msgstr "Inggeris" + +msgctxt "#30302" +msgid "German" +msgstr "Jerman" + +msgctxt "#30303" +msgid "French" +msgstr "Perancis" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "tajuk" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Am" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Sembunyi Seni Peminat" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.mt_mt/strings.po b/plugin.audio.radio_de/resources/language/resource.language.mt_mt/strings.po new file mode 100644 index 0000000000..cbc257077b --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.mt_mt/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: Maltese \n" +"Language: mt_mt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Lingwa" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "titlu" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Ġenerali" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.my_mm/strings.po b/plugin.audio.radio_de/resources/language/resource.language.my_mm/strings.po new file mode 100644 index 0000000000..18ca83a49a --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.my_mm/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Burmese \n" +"Language: my_mm\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "ဘာသာစကား" + +msgctxt "#30301" +msgid "English" +msgstr "အင်္ဂလိပ်" + +msgctxt "#30302" +msgid "German" +msgstr "ဂျာမန်" + +msgctxt "#30303" +msgid "French" +msgstr "ပြင်သစ်" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "ယေဘုယျ" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Fanart ကိုဖျောက်ထားမည်" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.nb_no/strings.po b/plugin.audio.radio_de/resources/language/resource.language.nb_no/strings.po new file mode 100644 index 0000000000..7b7283d664 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.nb_no/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Norwegian Bokmål \n" +"Language: nb_no\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Vis etter sjanger" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Vis etter land" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mine stasjoner" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Språk" + +msgctxt "#30301" +msgid "English" +msgstr "Engelsk" + +msgctxt "#30302" +msgid "German" +msgstr "Tysk" + +msgctxt "#30303" +msgid "French" +msgstr "Fransk" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Tving visningsmodus til 'Miniatyrbilder'" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Legg til i 'Mine stasjoner'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Fjern fra 'Mine stasjoner'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "tittel" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "Nettverksfeil" + +msgctxt "#30601" +msgid "General" +msgstr "Generelt" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Skjul fankunst" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.nl_nl/strings.po b/plugin.audio.radio_de/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..d9edd29472 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Dutch \n" +"Language: nl_nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Toegang tot >30000 radio uitzendingen" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "muziek plug-in om toegang te krijgen tot over 30000 internationale radiouitzendingen van rad.io, radio.de en radio.fr[CR]Huidige functies[CR]- engels, duits en frans vertaalt[CR]- Zoek naar stations vanuit locatie, genre, onderwerp, land, plaats en taal[CR]- zoek voor stations[CR]- 115 genres. 59 onderwerpen, 94 landen. 1010 plaatsen, 63 talen" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Redactionele aanbevelingen" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top zenders" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Blader op genre" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Blader bij onderwerp" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Blader op land" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Blader bij stad" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Blader bij taal" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Lokale zenders" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mijn zenders" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Zoek naar zender" + +msgctxt "#30300" +msgid "Language" +msgstr "Taal" + +msgctxt "#30301" +msgid "English" +msgstr "Engels" + +msgctxt "#30302" +msgid "German" +msgstr "Duits" + +msgctxt "#30303" +msgid "French" +msgstr "Frans" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Video OSD: Toon thumbnail" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "voeg toe aan ''mijn zenders''" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Verwijder uit ''Mijn zenders''" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Wijzig aangepaste zender" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "voer %s in a.u.b." + +msgctxt "#30501" +msgid "title" +msgstr "Titel" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "Miniatuur url of locatie" + +msgctxt "#30503" +msgid "stream url" +msgstr "Stream url" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "voeg aangepaste zender toe..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Netwerk Fout" + +msgctxt "#30601" +msgid "General" +msgstr "Algemeen" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Fanart verbergen" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.pl_pl/strings.po b/plugin.audio.radio_de/resources/language/resource.language.pl_pl/strings.po new file mode 100644 index 0000000000..4fad045df3 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.pl_pl/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-08 01:37+0000\n" +"Last-Translator: Marek Adamski \n" +"Language-Team: Polish \n" +"Language: pl_pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Dostęp do ponad 30000 stacji radiowych" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Wtyczka muzyczna dająca dostęp do ponad 30000 międzynarodowych stacji radiowych z[CR]rad.io, radio.de, daio.fr[CR]Obecnie w językach[CR]- Angielski, Niemiecki i Francuski[CR]- Szukaj stacji według lokacji, gatunku, tematu, kraju, miasta czy języka[CR]- 115 gatunków, 59 tematów, 94 kraje, 1010 miasta, 63 języki" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Rekomendacje redakcyjne" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top stacji" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Przeglądaj według gatunku" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Przeglądaj według tematu" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Przeglądaj według kraju" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Przeglądaj według miasta" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Przeglądaj według języka" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Lokalne stacje" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Moje stacje" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Szukaj stacji" + +msgctxt "#30300" +msgid "Language" +msgstr "Język" + +msgctxt "#30301" +msgid "English" +msgstr "angielski" + +msgctxt "#30302" +msgid "German" +msgstr "niemiecki" + +msgctxt "#30303" +msgid "French" +msgstr "francuski" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "portugalski" + +msgctxt "#30305" +msgid "Spanish" +msgstr "hiszpański" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Wymuszaj widok Miniatury" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Dodaj do 'Moich stacji'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Usuń z 'Moich stacji'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Edytuj niestandardową stację" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Proszę wpisz %s" + +msgctxt "#30501" +msgid "title" +msgstr "tytuł" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "ścieżka lub adres miniatury" + +msgctxt "#30503" +msgid "stream url" +msgstr "url strumienia" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Dodaj niestandardową stację..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Błąd sieci" + +msgctxt "#30601" +msgid "General" +msgstr "Ogólne" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Ukrywaj fototapety" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Najbardziej popularne" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Strona (%s/%s) | Dalej >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Według kraju" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Preferuj strumienie HTTP zamiast HTTS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "Nie można uzyskać adresu URL strumienia radiowego" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "Pomyślnie dodano do moich stacji!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "Pomyślnie usunięto z moich stacji!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.pt_br/strings.po b/plugin.audio.radio_de/resources/language/resource.language.pt_br/strings.po new file mode 100644 index 0000000000..55dfd09e05 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.pt_br/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-12-06 01:13+0000\n" +"Last-Translator: Fabio \n" +"Language-Team: Portuguese (Brazil) \n" +"Language: pt_br\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.9.1\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Acesse>30000 broadcasts rádios" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Plugin de música para acessar mais de 30000 emissoras de rádio internacionais de rad.io, radio.de, radio.fr, radio.pt e radio.es [CR] Principais características [CR] - Inglês, Alemão e Francês traduzido [CR] - Procure estações por localização, gênero, tópico, país , cidade e linguagem [CR] - Procure estações [CR] - 115 gêneros, 59 temas, 94 países, 1010 cidades, 63 idiomas" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Recomendadas pelos Editores" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Estações Top" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Navegar por gênero" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Navegar por tópico" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Navegar por pais" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Navegar por cidade" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Navegar por linguagem" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Estações Locais" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Minhas Estações" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Procurar por estação" + +msgctxt "#30300" +msgid "Language" +msgstr "Idioma" + +msgctxt "#30301" +msgid "English" +msgstr "Inglês" + +msgctxt "#30302" +msgid "German" +msgstr "Alemão" + +msgctxt "#30303" +msgid "French" +msgstr "Francês" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Português" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Espanhol" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Forçar modo de visualização para Miniatura" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Adicionar para 'Minhas Estações'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Remover de 'Minhas Estações'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Editar Estação Customizada" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Por favor entre %s" + +msgctxt "#30501" +msgid "title" +msgstr "título" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "url da miniatura ou caminho" + +msgctxt "#30503" +msgid "stream url" +msgstr "Url transmissão" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Adicionar Estação Customizada..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Erro Rede" + +msgctxt "#30601" +msgid "General" +msgstr "Geral" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Ocultar fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Mais Populares" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Página (%s/%s) | Próxima >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Por país" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Preferir streams HTTP ao invés de HTTPS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "Não foi possível obter o url do stream da rádio" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "Adicionado com sucesso às minhas estações!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "Removido com sucesso das minhas estações!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.pt_pt/strings.po b/plugin.audio.radio_de/resources/language/resource.language.pt_pt/strings.po new file mode 100644 index 0000000000..43e414cbad --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.pt_pt/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Portuguese (Portugal) \n" +"Language: pt_pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Aceda a mais de 30000 transmissões de rádio" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Plugin de música para aceder a mais de 30000 transmissões de rádio internacionais de rad.io, radio.de, radio.fr, radio.pt e radio.es[CR]Actualmente, disponibiliza:[CR]- Traduções em vários idiomas[CR]- Navegar estações por localização, género, tópico, país, cidade e idioma[CR]- Procurar por estações[CR]- 115 géneros, 59 tópicos, 94 países, 1010 cidades, 63 idiomas" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Recomendações do Editor" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top Estações" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Navegar por género" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Navegar por tópico" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Navegar por país" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Navegar por cidade" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Navegar por idioma" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Estações Locais" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Minhas Estações" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Procurar por estação" + +msgctxt "#30300" +msgid "Language" +msgstr "Idioma" + +msgctxt "#30301" +msgid "English" +msgstr "Inglês" + +msgctxt "#30302" +msgid "German" +msgstr "Alemão" + +msgctxt "#30303" +msgid "French" +msgstr "Francês" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Forçar Vista para Miniatura" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Adicionar a 'Minhas Estações'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Remover de 'Minhas Estações'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Editar estação personalizada" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Por favor, introduza %s" + +msgctxt "#30501" +msgid "title" +msgstr "título" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "url ou localização da miniatura" + +msgctxt "#30503" +msgid "stream url" +msgstr "url da transmissão" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Adicionar estação personalizada..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Erro de Rede" + +msgctxt "#30601" +msgid "General" +msgstr "Geral" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Ocultar fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Mais populares" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Página (%s/%s) | Próxima >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Por país" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Preferir streams HTTP ao inves de HTTPS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "Incapaz de determinar o endereço da stream de rádio" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "Adicionado com sucesso à lista de estações!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "Removido com sucesso da lista de estações!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ro_ro/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ro_ro/strings.po new file mode 100644 index 0000000000..88c772205c --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ro_ro/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Romanian \n" +"Language: ro_ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Limbă" + +msgctxt "#30301" +msgid "English" +msgstr "Engleză" + +msgctxt "#30302" +msgid "German" +msgstr "Germană" + +msgctxt "#30303" +msgid "French" +msgstr "Franceză" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Impune vizualizarea în mod Miniaturi" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "nume" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "General" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Ascunde decorul" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Cel mai popular" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ru_ru/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ru_ru/strings.po new file mode 100644 index 0000000000..ea83b47dc7 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ru_ru/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-11-04 17:30+0000\n" +"Last-Translator: vdkbsd \n" +"Language-Team: Russian \n" +"Language: ru_ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.8.1\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Доступ к >30000 радиостанциям" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Музыкальный плагин для доступа к более чем 30000 международным радиостанциям с rad.io, radio.de, radio.fr, radio.pt и radio.es[CR]Текущие возможности[CR]- Английский, немецкий, французский переводы[CR]- Просмотр станций по расположению, жанру, теме, стране, городу и языку[CR]- Поиск станций[CR]- 115 жанров, 59 тем, 94 страны, 1010 городов, 63 языка" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Рекоменованные" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Топ станций" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Просмотр по стилю" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Просмотр по теме" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Просмотр по стране" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Просмотр по городу" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Просмотр по языку" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Местные станции" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Мои станции" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Поиск станции" + +msgctxt "#30300" +msgid "Language" +msgstr "Язык" + +msgctxt "#30301" +msgid "English" +msgstr "Английский" + +msgctxt "#30302" +msgid "German" +msgstr "Немецкий" + +msgctxt "#30303" +msgid "French" +msgstr "Французский" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Португальский" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Испанский" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Перевести режим просмотра к иконкам" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Добавить в 'Мои станции'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Убрать из 'Моих станций'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Ввод пользовательской станции" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Пожалуйста, введите %s" + +msgctxt "#30501" +msgid "title" +msgstr "заголовок" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "url или путь к лого" + +msgctxt "#30503" +msgid "stream url" +msgstr "url потока" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Добавить пользовательскую станцию..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Ошибка сети" + +msgctxt "#30601" +msgid "General" +msgstr "Общие" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Скрыть фанарт" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Самое популярное" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Страница (%s/%s) | Следующая >>" + +msgctxt "#30606" +msgid "By country" +msgstr "По странам" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Предпочитать HTTP перед HTTS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "Невозможно получить url потока радио" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "Успешно добавлен в мои станции!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "Успешно удалено с моих станций!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.sk_sk/strings.po b/plugin.audio.radio_de/resources/language/resource.language.sk_sk/strings.po new file mode 100644 index 0000000000..3ac7fe2257 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.sk_sk/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Slovak \n" +"Language: sk_sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Pristupujte k vyše 30000 vysielaniam" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Hudobný doplnok na prístup k vyše 30000 medzinárodným rádio vysielaniam z rad.io, radio.de a radio.fr Momentálne podporuje: - anglické, nemecké a francúzske preklady - prehliadanie staníc podľa umiestnenia, žánru, témy, krajiny, mesta a jazyka - vyhľadávanie staníc - 115 žánrov, 59 tém, 94 krajín, 1010 miest, 63 jazykov" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Redakcia odporúča" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Top staníc" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Prehliadať podľa žánra" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Prehliadať podľa témy" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Prehliadať podľa krajiny" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Prehliadať podľa mesta" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Prehliadať podľa jazyka" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Lokálne stanice" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Moje stanice" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Hľadať stanice" + +msgctxt "#30300" +msgid "Language" +msgstr "Jazyk" + +msgctxt "#30301" +msgid "English" +msgstr "Angličtina" + +msgctxt "#30302" +msgid "German" +msgstr "Nemčina" + +msgctxt "#30303" +msgid "French" +msgstr "Francúzština" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Vynútiť zobrazenie náhľadov" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Pridať do 'Moje stanice'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Odstrániť z 'Moje stanice'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Upraviť vlastné stanice" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Prosím zadajte %s" + +msgctxt "#30501" +msgid "title" +msgstr "Titul" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "URL alebo cesta k náhľadu" + +msgctxt "#30503" +msgid "stream url" +msgstr "URL streamu" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Pridať vlastné stanice..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Sieťová chyba" + +msgctxt "#30601" +msgid "General" +msgstr "Hlavné" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Skryť fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.sl_si/strings.po b/plugin.audio.radio_de/resources/language/resource.language.sl_si/strings.po new file mode 100644 index 0000000000..6dcca3fb7f --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.sl_si/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: Slovenian \n" +"Language: sl_si\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Prebrskaj glede na zvrst" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Moje postaje" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Jezik" + +msgctxt "#30301" +msgid "English" +msgstr "English" + +msgctxt "#30302" +msgid "German" +msgstr "German" + +msgctxt "#30303" +msgid "French" +msgstr "French" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Dodaj med »Moje postaje«" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Odstrani iz »Mojih postaj«" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "naslov" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "Omrežna napaka" + +msgctxt "#30601" +msgid "General" +msgstr "Splošno" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Skrij grafike" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.sq_al/strings.po b/plugin.audio.radio_de/resources/language/resource.language.sq_al/strings.po new file mode 100644 index 0000000000..7fcca94033 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.sq_al/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Albanian \n" +"Language: sq_al\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Gjuha" + +msgctxt "#30301" +msgid "English" +msgstr "Anglisht" + +msgctxt "#30302" +msgid "German" +msgstr "Gjermanisht" + +msgctxt "#30303" +msgid "French" +msgstr "Frangjisht" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "titulli" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Të përgjithshëm" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Fsheh fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.sr_rs/strings.po b/plugin.audio.radio_de/resources/language/resource.language.sr_rs/strings.po new file mode 100644 index 0000000000..039fdae422 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.sr_rs/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Serbian \n" +"Language: sr_rs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Језик" + +msgctxt "#30301" +msgid "English" +msgstr "Енглески" + +msgctxt "#30302" +msgid "German" +msgstr "Немачки" + +msgctxt "#30303" +msgid "French" +msgstr "Француски" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Опште" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Сакриј слику" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.sr_rs@latin/strings.po b/plugin.audio.radio_de/resources/language/resource.language.sr_rs@latin/strings.po new file mode 100644 index 0000000000..cc05c58d12 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.sr_rs@latin/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Serbian (latin) \n" +"Language: sr_rs@latin\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Jezik" + +msgctxt "#30301" +msgid "English" +msgstr "Engleski" + +msgctxt "#30302" +msgid "German" +msgstr "Nemački" + +msgctxt "#30303" +msgid "French" +msgstr "Francuski" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "naslov" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Opšte" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Sakrij sliku" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.sv_se/strings.po b/plugin.audio.radio_de/resources/language/resource.language.sv_se/strings.po new file mode 100644 index 0000000000..6c5dfe3bdf --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.sv_se/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Sopor \n" +"Language-Team: Swedish \n" +"Language: sv_se\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "Få tillgång till >30000 radiosändningar" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "Musiktillägg som ger åtkomst till över 30000 internationella radiosändningar från rad.io, radio.de och radio.fr[CR]Nuvarande funktioner[CR]- Engelsk, tysk, och fransk översättning[]- Bläddra efter stationer baserat på genre, ämne, land, stad och språk[CR]- Sök efter stationer[CR]-115 genres, 59 ämnen, 94 länder, 1010 städer, 63 språk" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Redaktionella rekommendationer" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "Toppstationer" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Bläddra efter genre" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Bläddra efter ämne" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Bläddra efter land" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Bläddra efter stad" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Bläddra efter språk" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Lokala stationer" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Mina stationer" + +msgctxt "#30200" +msgid "Search for station" +msgstr "Sök efter station" + +msgctxt "#30300" +msgid "Language" +msgstr "Språk" + +msgctxt "#30301" +msgid "English" +msgstr "Engelska" + +msgctxt "#30302" +msgid "German" +msgstr "Tyska" + +msgctxt "#30303" +msgid "French" +msgstr "Franska" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "Portugisiska" + +msgctxt "#30305" +msgid "Spanish" +msgstr "Spanska" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Tvinga visningsläge till miniatyrer" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Lägg till i 'Mina stationer'" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Ta bort från 'Mina stationer'" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "Redigera anpassad station" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "Ange %s" + +msgctxt "#30501" +msgid "title" +msgstr "titel" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "miniatyr-url eller sökväg" + +msgctxt "#30503" +msgid "stream url" +msgstr "ström-url" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Lägg till anpassad station..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Nätverksfel" + +msgctxt "#30601" +msgid "General" +msgstr "Allmänt" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Dölj fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "Mest populär" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "Sida (%s/%s) | Nästa >>" + +msgctxt "#30606" +msgid "By country" +msgstr "Efter land" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "Föredra HTTP-strömmar framför HTTPS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.ta_in/strings.po b/plugin.audio.radio_de/resources/language/resource.language.ta_in/strings.po new file mode 100644 index 0000000000..55f3fa6755 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.ta_in/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: Tamil (India) \n" +"Language: ta_in\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "30000 க்கும் மேல் வானொலி ஒளிபரப்புகளை அணுகவும்" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "இசை துணை பயன் rad.io, radio.de மற்றும் radio.fr இருந்து 30000 க்கும் மேல் வானொலி ஒளிபரப்புகளை அணுக உதவி செய்யும்[CR]தற்போதைய அம்சங்கள்[CR]- ஆங்கிலம், ஜெர்மன் மற்றும் பிரெஞ்சு மொழியாக்கம்[CR]- இருப்பிடம், வகை, தலைப்பு, நாடு, நகரம் மற்றும் மொழி கொண்டு நிலையங்களை உலாவ முடியும்[CR]- நிலையங்களை தேடுதல்[CR]- 115 வகைகள், 59 தலைப்புகள், 94 நாடுகள், 1010 நகரங்கள், 63 மொழிகள்" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "ஆசிரியர் பரிந்துரைகள்" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "உச்சி நிலையங்கள்" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "வகைபடி உலாவு" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "தலைப்புப்படி உலாவு" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "நாடுபடி உலாவு" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "நகரபடிஉலாவு" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "மொழிபடி உலாவு" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "உள்ளூர் நிலையங்கள்" + +msgctxt "#30108" +msgid "My Stations" +msgstr "என்னுடைய நிலையங்கள்" + +msgctxt "#30200" +msgid "Search for station" +msgstr "நிலையத்தை தேடுக" + +msgctxt "#30300" +msgid "Language" +msgstr "மொழி" + +msgctxt "#30301" +msgid "English" +msgstr "ஆங்கிலம்" + +msgctxt "#30302" +msgid "German" +msgstr "ஜேர்மன்" + +msgctxt "#30303" +msgid "French" +msgstr "பிரஞ்சு" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "பார்க்கும் முறையை சிறு படத்திற்கு கட்டாயபடுத்தவும்" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "'என்னுடைய நிலையங்கள்' பட்டியலில் சேர்க்கவும்" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "'என்னுடைய நிலையங்கள்' பட்டியலில் இருந்து நீக்கவும்" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "தனிப்பயன் நிலையத்தை திருத்தவும்" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "%s உள்ளிடவும்" + +msgctxt "#30501" +msgid "title" +msgstr "தலைப்பு" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "குறும்பட url அல்லது பாதை" + +msgctxt "#30503" +msgid "stream url" +msgstr "ஒலிப்பேழை url" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "தனிப்பயன் நிலையத்தை சேர்க்கவும்..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "பிணைய பிழை" + +msgctxt "#30601" +msgid "General" +msgstr "பொதுவானது" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "விசிறிபடத்தை மறை" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.th_th/strings.po b/plugin.audio.radio_de/resources/language/resource.language.th_th/strings.po new file mode 100644 index 0000000000..b3e586d24a --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.th_th/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Thai \n" +"Language: th_th\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "ภาษา" + +msgctxt "#30301" +msgid "English" +msgstr "อังกฤษ" + +msgctxt "#30302" +msgid "German" +msgstr "เยอรมัน" + +msgctxt "#30303" +msgid "French" +msgstr "ฝร่งเศส" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "ทั่วไป" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "ซ่อน แฟนอาร์ต" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.tr_tr/strings.po b/plugin.audio.radio_de/resources/language/resource.language.tr_tr/strings.po new file mode 100644 index 0000000000..818fea54da --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.tr_tr/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Turkish \n" +"Language: tr_tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "Editörlerin Önerileri" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "En iyi İstasyon" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "Tarza göre gözat" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "Konuya Göre Ara" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "Şehire Göre Tara" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "Ülkeye Göre Ara" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "Dile Göre Ara" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "Yerel İstasyonlar" + +msgctxt "#30108" +msgid "My Stations" +msgstr "Benim İstasyonlarım" + +msgctxt "#30200" +msgid "Search for station" +msgstr "İstasyona Göre Ara" + +msgctxt "#30300" +msgid "Language" +msgstr "Dil" + +msgctxt "#30301" +msgid "English" +msgstr "İngilizce" + +msgctxt "#30302" +msgid "German" +msgstr "Almanca" + +msgctxt "#30303" +msgid "French" +msgstr "Fransızca" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "Küçük Resim Görünüm Moduna Zorla" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "Benim İstasyonlarım'a Ekle" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "Benim İstasyonlarım'dan Kaldır" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "İstasyonu Özelleştir" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "başlık" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "önizleme adresi veya dizini" + +msgctxt "#30503" +msgid "stream url" +msgstr "akış adresi" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "Özel istasyon ekle..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "Ağ Hatası" + +msgctxt "#30601" +msgid "General" +msgstr "Genel" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Fanart'ı gizle" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.uk_ua/strings.po b/plugin.audio.radio_de/resources/language/resource.language.uk_ua/strings.po new file mode 100644 index 0000000000..8ed77ba4e9 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.uk_ua/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Ukrainian \n" +"Language: uk_ua\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Мова" + +msgctxt "#30301" +msgid "English" +msgstr "Англійська" + +msgctxt "#30302" +msgid "German" +msgstr "Німецька" + +msgctxt "#30303" +msgid "French" +msgstr "Французька" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Загальні" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Приховати фанарт" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.uz_uz/strings.po b/plugin.audio.radio_de/resources/language/resource.language.uz_uz/strings.po new file mode 100644 index 0000000000..b98c75e60e --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.uz_uz/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Uzbek \n" +"Language: uz_uz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Til" + +msgctxt "#30301" +msgid "English" +msgstr "" + +msgctxt "#30302" +msgid "German" +msgstr "" + +msgctxt "#30303" +msgid "French" +msgstr "" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "sarlavha" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Umumiy" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.vi_vn/strings.po b/plugin.audio.radio_de/resources/language/resource.language.vi_vn/strings.po new file mode 100644 index 0000000000..d3712c9a43 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.vi_vn/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: Vietnamese \n" +"Language: vi_vn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "Ngôn ngữ" + +msgctxt "#30301" +msgid "English" +msgstr "English" + +msgctxt "#30302" +msgid "German" +msgstr "German" + +msgctxt "#30303" +msgid "French" +msgstr "French" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "Tổng Quan" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "Ẩn fanart" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/language/resource.language.zh_cn/strings.po b/plugin.audio.radio_de/resources/language/resource.language.zh_cn/strings.po new file mode 100644 index 0000000000..33ff088771 --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.zh_cn/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-25 08:41+0000\n" +"Last-Translator: taxigps \n" +"Language-Team: Chinese (China) \n" +"Language: zh_cn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "访问大于7000个电台广播" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "访问rad.io、radio.de和radio.fr上超过7000个国际电台广播的音频插件[CR]现有功能[CR]- 英语、德语和法语支持[CR]- 按地点、类别、主题、国家、城市和语言浏览站点[CR]- 搜索站点[CR]- 115个类别、59个主题、94个国家、1010城市、63种语言" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "编辑推荐" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "强站点" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "按类别浏览" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "按主题浏览" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "按国家浏览" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "按城市浏览" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "按语言浏览" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "本地站点" + +msgctxt "#30108" +msgid "My Stations" +msgstr "我的电台" + +msgctxt "#30200" +msgid "Search for station" +msgstr "搜索站点" + +msgctxt "#30300" +msgid "Language" +msgstr "语言" + +msgctxt "#30301" +msgid "English" +msgstr "英语" + +msgctxt "#30302" +msgid "German" +msgstr "德语" + +msgctxt "#30303" +msgid "French" +msgstr "法语" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "葡萄牙语" + +msgctxt "#30305" +msgid "Spanish" +msgstr "西班牙语" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "视图模式强制为“缩略图”" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "加入“我的电台”" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "从“我的电台”删除" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "编辑自定义电台" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "请输入%s" + +msgctxt "#30501" +msgid "title" +msgstr "标题" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "缩略图url或目录" + +msgctxt "#30503" +msgid "stream url" +msgstr "流媒体url" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "加入自定义电台..." + +msgctxt "#30600" +msgid "Network Error" +msgstr "网络错误" + +msgctxt "#30601" +msgid "General" +msgstr "常规" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "隐藏同人画" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "最热门" + +msgctxt "#30604" +msgid "A-Z" +msgstr "A-Z" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "页(%s/%s)| 下一页 >>" + +msgctxt "#30606" +msgid "By country" +msgstr "按国家" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "首选 HTTP 流而非 HTTS" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "无法获取广播流 url" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "成功添加到我的电台!" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "成功地从我的电台中删除!" diff --git a/plugin.audio.radio_de/resources/language/resource.language.zh_tw/strings.po b/plugin.audio.radio_de/resources/language/resource.language.zh_tw/strings.po new file mode 100644 index 0000000000..790c3fc69c --- /dev/null +++ b/plugin.audio.radio_de/resources/language/resource.language.zh_tw/strings.po @@ -0,0 +1,170 @@ +# Kodi Media Center language file +# Addon Name: Radio +# Addon id: plugin.audio.radio_de +# Addon Provider: Tristan Fischer, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-03 12:30+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Chinese (Taiwan) \n" +"Language: zh_tw\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Access >30000 radio broadcasts" +msgstr "" + +msgctxt "Addon Description" +msgid "Music plugin to access over 30000 international radio broadcasts from rad.io, radio.de, radio.fr, radio.pt and radio.es[CR]Currently features[CR]- English, german, french translated[CR]- Browse stations by location, genre, topic, country, city and language[CR]- Search for stations[CR]- 115 genres, 59 topics, 94 countrys, 1010 citys, 63 languages" +msgstr "" + +msgctxt "#30100" +msgid "Editorials Recommendations" +msgstr "" + +msgctxt "#30101" +msgid "Top Stations" +msgstr "" + +msgctxt "#30102" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30103" +msgid "Browse by topic" +msgstr "" + +msgctxt "#30104" +msgid "Browse by country" +msgstr "" + +msgctxt "#30105" +msgid "Browse by city" +msgstr "" + +msgctxt "#30106" +msgid "Browse by language" +msgstr "" + +msgctxt "#30107" +msgid "Local Stations" +msgstr "" + +msgctxt "#30108" +msgid "My Stations" +msgstr "" + +msgctxt "#30200" +msgid "Search for station" +msgstr "" + +msgctxt "#30300" +msgid "Language" +msgstr "語言" + +msgctxt "#30301" +msgid "English" +msgstr "英文" + +msgctxt "#30302" +msgid "German" +msgstr "德文" + +msgctxt "#30303" +msgid "French" +msgstr "法文" + +msgctxt "#30304" +msgid "Portuguese" +msgstr "" + +msgctxt "#30305" +msgid "Spanish" +msgstr "" + +msgctxt "#30310" +msgid "Force ViewMode to Thumbnail" +msgstr "" + +msgctxt "#30400" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30401" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30402" +msgid "Edit custom Station" +msgstr "" + +msgctxt "#30500" +msgid "Please enter %s" +msgstr "" + +msgctxt "#30501" +msgid "title" +msgstr "" + +msgctxt "#30502" +msgid "thumbnail url or path" +msgstr "" + +msgctxt "#30503" +msgid "stream url" +msgstr "" + +msgctxt "#30504" +msgid "Add custom Station..." +msgstr "" + +msgctxt "#30600" +msgid "Network Error" +msgstr "" + +msgctxt "#30601" +msgid "General" +msgstr "一般設定" + +msgctxt "#30602" +msgid "Hide fanart" +msgstr "隱藏相關圖片" + +msgctxt "#30603" +msgid "Most Popular" +msgstr "最受歡迎" + +msgctxt "#30604" +msgid "A-Z" +msgstr "" + +msgctxt "#30605" +msgid "Page (%s/%s) | Next >>" +msgstr "" + +msgctxt "#30606" +msgid "By country" +msgstr "" + +msgctxt "#30607" +msgid "Prefer HTTP streams over HTTS" +msgstr "" + +msgctxt "#30608" +msgid "Unable to get radio stream url" +msgstr "" + +msgctxt "#30609" +msgid "Sucessfully added to my stations!" +msgstr "" + +msgctxt "#30610" +msgid "Sucessfully removed from my stations!" +msgstr "" diff --git a/plugin.audio.radio_de/resources/lib/api.py b/plugin.audio.radio_de/resources/lib/api.py new file mode 100644 index 0000000000..f0d0dec8f9 --- /dev/null +++ b/plugin.audio.radio_de/resources/lib/api.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' + * Copyright (C) 2019- enen92 (enen92@kodi.tv) + * Copyright (C) 2012-2019 Tristan Fischer (sphere@dersphere.de) + * This file is part of plugin.audio.radio_de + * + * SPDX-License-Identifier: GPL-2.0-only + * See LICENSE.txt for more information. +''' + +import json +import sys +import random +import xbmc + +from urllib.parse import urlencode +from urllib.request import urlopen, Request, HTTPError, URLError + + +class RadioApiError(Exception): + pass + + +class RadioApi(): + + MAIN_URLS = { + 'english': 'http://api.rad.io/info', + 'german': 'http://api.radio.de/info', + 'french': 'http://api.radio.fr/info', + 'portuguese': 'http://api.radio.pt/info', + 'spanish': 'http://api.radio.es/info', + } + + USER_AGENT = 'XBMC Addon Radio' + + PLAYLIST_PREFIXES = ('m3u', 'pls', 'asx', 'xml') + + def __init__(self, language='english', user_agent=USER_AGENT): + self.set_language(language) + self.user_agent = user_agent + + def set_language(self, language): + if not language in RadioApi.MAIN_URLS.keys(): + raise ValueError('Invalid language') + self.api_url = RadioApi.MAIN_URLS[language] + + def get_genres(self): + self.log('get_genres started') + path = 'v2/search/getgenres' + return self.__api_call(path) + + def get_topics(self): + self.log('get_topics started') + path = 'v2/search/gettopics' + return self.__api_call(path) + + def get_languages(self): + self.log('get_topics started') + path = 'v2/search/getlanguages' + return self.__api_call(path) + + def get_countries(self): + self.log('get_countries started') + path = 'v2/search/getcountries' + return self.__api_call(path) + + def get_cities(self, country=None): + self.log('get_cities_by_country started with country = %s' % country) + path = 'v2/search/getcities' + if country: + param = { + 'country': country + } + return self.__api_call(path, param) + else: + return self.__api_call(path) + + def get_recommendation_stations(self): + self.log('get_recommendation_stations started') + path = 'v2/search/editorstips' + return self.__format_stations_v2(self.__api_call(path)) + + def get_stations_by_genre(self, genre, sorttype, sizeperpage, pageindex): + self.log(('get_stations_by_genre started with genre=%s, ' + 'sorttype=%s, sizeperpage=%s, pageindex=%s') % ( + genre, sorttype, sizeperpage, pageindex)) + path = 'v2/search/stationsbygenre' + param = { + 'genre': genre, + 'sorttype': sorttype, + 'sizeperpage': sizeperpage, + 'pageindex': pageindex + } + response = self.__api_call(path, param) + if not response.get('categories'): + raise ValueError('Bad category_type') + return response.get('numberPages'), self.__format_stations_v2(response.get('categories')[0].get('matches')) + + def get_station_by_station_id(self, station_id, resolve_playlists=True, force_http=False): + self.log('get_station_by_station_id started with station_id=%s' + % station_id) + path = 'v2/search/station' + param = {'station': str(station_id)} + station = self.__api_call(path, param) + + streams = station.get('streamUrls') + + if streams: + station['streamUrl'] = streams[0].get('streamUrl') + + if force_http: + for stream in streams: + if "http://" in stream.get('streamUrl'): + station['streamUrl'] = stream['streamUrl'] + break + + if not station.get('streamUrl'): + self.log('Unable to detect a playable stream for station') + return None + + if resolve_playlists and self.__check_paylist(station['streamUrl']): + station['streamUrl'] = self.__resolve_playlist(station) + stations = (station, ) + return self.__format_stations_v2(stations)[0] + + def internal_resolver(self, station, ): + if station.get('is_custom', False): + stream_url = station['stream_url'] + else: + stream_url = station['streamUrl'] + + if self.__check_paylist(stream_url): + return self.__resolve_playlist(station) + else: + return stream_url + + def get_top_stations(self, sizeperpage, pageindex): + self.log(('get_top_stations started with ' + 'sizeperpage=%s, pageindex=%s') % ( + sizeperpage, pageindex)) + path = 'v2/search/topstations' + param = { + 'sizeperpage': sizeperpage, + 'pageindex': pageindex + } + response = self.__api_call(path, param) + if not response.get('categories'): + raise ValueError('Bad category_type') + return response.get('numberPages'), self.__format_stations_v2(response.get('categories')[0].get('matches')) + + def get_stations_by_country(self, country, sorttype, sizeperpage, pageindex): + self.log(('get_stations_by_country started with country=%s, ' + 'sorttype=%s, sizeperpage=%s, pageindex=%s') % ( + country, sorttype, sizeperpage, pageindex)) + path = 'v2/search/stationsbycountry' + param = { + 'country': country, + 'sorttype': sorttype, + 'sizeperpage': sizeperpage, + 'pageindex': pageindex + } + response = self.__api_call(path, param) + if not response.get('categories'): + raise ValueError('Bad category_type') + return response.get('numberPages'), self.__format_stations_v2(response.get('categories')[0].get('matches')) + + def get_stations_by_city(self, city, sorttype, sizeperpage, pageindex): + self.log(('get_stations_by_city started with city=%s, ' + 'sorttype=%s, sizeperpage=%s, pageindex=%s') % ( + city, sorttype, sizeperpage, pageindex)) + path = 'v2/search/stationsbycity' + param = { + 'city': city, + 'sorttype': sorttype, + 'sizeperpage': sizeperpage, + 'pageindex': pageindex + } + response = self.__api_call(path, param) + if not response.get('categories'): + raise ValueError('Bad category_type') + return response.get('numberPages'), self.__format_stations_v2(response.get('categories')[0].get('matches')) + + def get_stations_by_topic(self, topic, sorttype, sizeperpage, pageindex): + self.log(('get_stations_by_topic started with topic=%s, ' + 'sorttype=%s, sizeperpage=%s, pageindex=%s') % ( + topic, sorttype, sizeperpage, pageindex)) + path = 'v2/search/stationsbytopic' + param = { + 'topic': topic, + 'sorttype': sorttype, + 'sizeperpage': sizeperpage, + 'pageindex': pageindex + } + response = self.__api_call(path, param) + if not response.get('categories'): + raise ValueError('Bad category_type') + return response.get('numberPages'), self.__format_stations_v2(response.get('categories')[0].get('matches')) + + def get_stations_by_language(self, language, sorttype, sizeperpage, pageindex): + self.log(('get_stations_by_language started with language=%s, ' + 'sorttype=%s, sizeperpage=%s, pageindex=%s') % ( + language, sorttype, sizeperpage, pageindex)) + path = 'v2/search/stationsbylanguage' + param = { + 'language': language, + 'sorttype': sorttype, + 'sizeperpage': sizeperpage, + 'pageindex': pageindex + } + response = self.__api_call(path, param) + if not response.get('categories'): + raise ValueError('Bad category_type') + return response.get('numberPages'), self.__format_stations_v2(response.get('categories')[0].get('matches')) + + def get_stations_nearby(self, sizeperpage, pageindex): + self.log(('get_stations_nearby started with, ' + 'sizeperpage=%s, pageindex=%s') % (sizeperpage, pageindex)) + path = 'v2/search/localstations' + param = { + 'sizeperpage': sizeperpage, + 'pageindex': pageindex + } + response = self.__api_call(path, param) + if not response.get('categories'): + raise ValueError('Bad category_type') + return response.get('numberPages'), self.__format_stations_v2(response.get('categories')[0].get('matches')) + + def search_stations_by_string(self, search_string, sizeperpage, pageindex): + self.log('search_stations_by_string started with search_string=%s' + % search_string) + path = 'v2/search/stations' + param = { + 'query': search_string, + 'sizeperpage': sizeperpage, + 'pageindex': pageindex + } + response = self.__api_call(path, param) + if not response.get('categories'): + raise ValueError('Bad category_type') + return response.get('numberPages'), self.__format_stations_v2(response.get('categories')[0].get('matches')) + + def __api_call(self, path, param=None): + self.log('__api_call started with path=%s, param=%s' + % (path, param)) + url = '%s/%s' % (self.api_url, path) + if param: + url += '?%s' % urlencode(param) + + response = self.__urlopen(url) + json_data = json.loads(response) + return json_data + + def __resolve_playlist(self, station): + self.log('__resolve_playlist started with station=%s' + % station['id']) + servers = [] + + # Check if it is a custom station + if station.get('is_custom', False): + stream_url = station['stream_url'] + else: + stream_url = station['streamUrl'] + + if stream_url.lower().endswith('m3u'): + response = self.__urlopen(stream_url) + self.log('__resolve_playlist found .m3u file') + servers = [ + l for l in response.splitlines() + if l.strip() and not l.strip().startswith(self.__versioned_string('#')) + ] + elif stream_url.lower().endswith('pls'): + response = self.__urlopen(stream_url) + self.log('__resolve_playlist found .pls file') + servers = [ + l.split(self.__versioned_string('='))[1] for l in response.splitlines() + if l.lower().startswith(self.__versioned_string('file')) + ] + elif stream_url.lower().endswith('asx'): + response = self.__urlopen(stream_url) + self.log('__resolve_playlist found .asx file') + servers = [ + l.split(self.__versioned_string('href="'))[1].split('"')[0] + for l in response.splitlines() if self.__versioned_string('href') in l + ] + elif stream_url.lower().endswith('xml'): + self.log('__resolve_playlist found .xml file') + servers = [ + stream_url['streamUrl'] + for stream_url in station.get('streamUrls', []) + if 'streamUrl' in stream_url + ] + if servers: + self.log('__resolve_playlist found %d servers' % len(servers)) + return random.choice(servers) + return stream_url + + def __follow_redirect(self, url): + self.log('__follow_redirect probing url=%s' % url) + req = Request(url) + req.add_header('User-Agent', self.user_agent) + response = urlopen(req) + return response.geturl() + + def __urlopen(self, url): + self.log('__urlopen opening url=%s' % url) + req = Request(url) + req.add_header('User-Agent', self.user_agent) + try: + response = urlopen(req).read() + except HTTPError as error: + self.log('__urlopen HTTPError: %s' % error) + raise RadioApiError('HTTPError: %s' % error) + except URLError as error: + self.log('__urlopen URLError: %s' % error) + raise RadioApiError('URLError: %s' % error) + return response + + @staticmethod + def __format_stations_v2(stations): + formated_stations = [] + for station in stations: + thumbnail = ( + station.get('logo300x300') or + station.get('logo175x175') or + station.get('logo100x100') or + station.get('logo44x44') + ) + + try: + genre = [g['value'] for g in station.get('genres')] + except: + genre = [g for g in station.get('genres')] + + try: + description = station.get('description')['value'] if station.get('description') else '' + except: + description = station.get('description') + + try: + name = station['name']['value'] if station.get('name') else '' + except: + name = station.get('name') + + formated_stations.append({ + 'name': name, + 'thumbnail': thumbnail, + 'rating': station.get('rank', ''), + 'genre': ','.join(genre), + 'mediatype': 'song', + 'id': station['id'], + 'current_track': station.get('nowPlaying', ''), + 'stream_url': station.get('streamUrl', ''), + 'description': description + }) + return formated_stations + + @staticmethod + def __check_paylist(stream_url): + for prefix in RadioApi.PLAYLIST_PREFIXES: + if stream_url.lower().endswith(prefix): + return True + return False + + @staticmethod + def __check_redirect(stream_url): + if 'addrad.io' in stream_url: + return True + if '.nsv' in stream_url: + return True + return False + + @staticmethod + def __versioned_string(string): + return bytearray(string, 'utf-8') + + @staticmethod + def log(text): + xbmc.log('RadioApi: %s' % repr(text)) diff --git a/plugin.audio.radio_de/resources/lib/plugin.py b/plugin.audio.radio_de/resources/lib/plugin.py new file mode 100644 index 0000000000..42e6fde872 --- /dev/null +++ b/plugin.audio.radio_de/resources/lib/plugin.py @@ -0,0 +1,663 @@ +#!/usr/bin/env python +''' + * Copyright (C) 2019- enen92 (enen92@kodi.tv) + * Copyright (C) 2012-2019 Tristan Fischer (sphere@dersphere.de) + * This file is part of plugin.audio.radio_de + * + * SPDX-License-Identifier: GPL-2.0-only + * See LICENSE.txt for more information. +''' + +from xbmcswift2 import Plugin, xbmc, listitem +from resources.lib.api import RadioApi, RadioApiError + +STRINGS = { + 'editorials_recommendations': 30100, + 'top_stations': 30101, + 'browse_by_genre': 30102, + 'browse_by_topic': 30103, + 'browse_by_country': 30104, + 'browse_by_city': 30105, + 'browse_by_language': 30106, + 'local_stations': 30107, + 'my_stations': 30108, + 'search_for_station': 30200, + 'add_to_my_stations': 30400, + 'remove_from_my_stations': 30401, + 'edit_custom_station': 30402, + 'please_enter': 30500, + 'name': 30501, + 'thumbnail': 30502, + 'stream_url': 30503, + 'add_custom': 30504, + 'most_popular': 30603, + 'az': 30604, + 'next_page': 30605, + 'by_country': 30606, + 'error_stream': 30608, + 'station_add_success': 30609, + 'station_rm_success': 30610 +} + +SORT_TYPES = { + 'popular': 'RANK', + 'az': 'STATION_NAME' +} + +STATIONS_PER_PAGE = 50 + +plugin = Plugin() +radio_api = RadioApi() +my_stations = plugin.get_storage('my_stations.json', file_format='json') + + +@plugin.route('/') +def show_root_menu(): + items = ( + {'label': _('local_stations'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_local_stations', page=1), + 'offscreen': True + }, + {'label': _('editorials_recommendations'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_recommendation_stations'), + 'offscreen': True + }, + {'label': _('top_stations'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_top_stations', page=1), + 'offscreen': True + }, + {'label': _('browse_by_genre'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_genres'), + 'offscreen': True + }, + {'label': _('browse_by_topic'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_topics'), + 'offscreen': True + }, + {'label': _('browse_by_country'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_countries'), + 'offscreen': True + }, + {'label': _('browse_by_city'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_cities_submenu'), + 'offscreen': True + }, + {'label': _('browse_by_language'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_languages'), + 'offscreen': True + }, + {'label': _('search_for_station'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('search'), + 'offscreen': True + }, + {'label': _('my_stations'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for('show_my_stations'), + 'offscreen': True + }, + ) + return plugin.finish(items) + + +@plugin.route('/stations/local/') +def show_local_stations(page=1): + total_pages, stations = radio_api.get_stations_nearby(STATIONS_PER_PAGE, page) + + next_page = None + if int(page) < (total_pages): + next_page = { + 'url': plugin.url_for( + 'show_local_stations', + page = int(page) + 1), + 'page': page, + 'total_pages': total_pages, + 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'offscreen': True + } + + return __add_stations(stations, browse_more=next_page) + + +@plugin.route('/stations/recommended') +def show_recommendation_stations(): + stations = radio_api.get_recommendation_stations() + return __add_stations(stations) + + +@plugin.route('/stations/top/') +def show_top_stations(page=1): + total_pages, stations = radio_api.get_top_stations(STATIONS_PER_PAGE, page) + next_page = None + if int(page) < (total_pages): + next_page = { + 'url': plugin.url_for( + 'show_top_stations', + page = int(page) + 1), + 'page': page, + 'total_pages': total_pages, + 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'offscreen': True + } + return __add_stations(stations, browse_more=next_page) + + +@plugin.route('/stations/search/') +def search(): + query = plugin.keyboard(heading=_('search_for_station')) + if query: + url = plugin.url_for('search_result', search_string=query, page=1) + plugin.redirect(url) + + +@plugin.route('/stations/search//') +def search_result(search_string, page): + total_pages, stations = radio_api.search_stations_by_string(search_string, STATIONS_PER_PAGE, page) + next_page = None + if int(page) < (total_pages): + next_page = { + 'url': plugin.url_for( + 'search_result', + search_string = search_string, + page = int(page) + 1), + 'page': page, + 'total_pages': total_pages, + 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'offscreen': True + } + return __add_stations(stations, browse_more=next_page) + + +@plugin.route('/stations/my/') +def show_my_stations(): + stations = my_stations.values() + return __add_stations(stations, add_custom=True) + + +@plugin.route('/stations/my/custom/') +def custom_my_station(station_id): + if station_id == 'new': + station = {} + else: + stations = my_stations.values() + station = [s for s in stations if s['id'] == station_id][0] + for param in ('name', 'thumbnail', 'stream_url'): + heading = _('please_enter') % _(param) + station[param] = plugin.keyboard(station.get(param, ''), heading) or '' + station_name = station.get('name', 'custom') + station_id = station_name + station['id'] = station_id + station['is_custom'] = '1' + if station_id: + my_stations[station_id] = station + url = plugin.url_for('show_my_stations') + plugin.redirect(url) + + +@plugin.route('/stations/my/add/') +def add_to_my_stations(station_id): + station = radio_api.get_station_by_station_id(station_id) + if station: + my_stations[station_id] = station + my_stations.sync() + plugin.notify("Radio", _('station_add_success'), image=plugin.icon) + plugin.refresh_container() + else: + plugin.notify("Radio", _('error_stream'), image=plugin.icon) + + +@plugin.route('/stations/my/del/') +def del_from_my_stations(station_id): + if station_id in my_stations: + del my_stations[station_id] + my_stations.sync() + plugin.notify("Radio", _('station_rm_success'), image=plugin.icon) + plugin.refresh_container() + + +@plugin.route('/stations/genres') +def show_genres(): + genres = radio_api.get_genres() + items = [] + for genre in genres: + items.append({ + 'label': genre["systemEnglish"], + 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for( + 'show_popular_and_az', + category='genres', + value=genre["systemEnglish"] + ), + 'offscreen': True + }) + finish_kwargs = { + 'sort_methods': [ + ('LABEL', '%X'), + ], + } + return plugin.finish(items, **finish_kwargs) + +@plugin.route('/stations/topics') +def show_topics(): + topics = radio_api.get_topics() + items = [] + for topic in topics: + items.append({ + 'label': topic["systemEnglish"], + 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for( + 'show_popular_and_az', + category='topics', + value=topic["systemEnglish"] + ), + 'offscreen': True + }) + finish_kwargs = { + 'sort_methods': [ + ('LABEL', '%X'), + ], + } + return plugin.finish(items, **finish_kwargs) + +@plugin.route('/stations/countries') +def show_countries(): + countries = radio_api.get_countries() + items = [] + for country in countries: + items.append({ + 'label': country["systemEnglish"], + 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for( + 'show_popular_and_az', + category='countries', + value=country["systemEnglish"] + ), + 'offscreen': True + }) + finish_kwargs = { + 'sort_methods': [ + ('LABEL', '%X'), + ], + } + return plugin.finish(items, **finish_kwargs) + +@plugin.route('/menu/languages') +def show_languages(): + languages = radio_api.get_languages() + items = [] + for lang in languages: + items.append({ + 'label': lang["systemEnglish"], + 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for( + 'show_popular_and_az', + category='languages', + value=lang["systemEnglish"] + ), + 'offscreen': True + }) + finish_kwargs = { + 'sort_methods': [ + ('LABEL', '%X'), + ], + } + return plugin.finish(items, **finish_kwargs) + +@plugin.route('/menu/cities') +def show_cities_submenu(): + items = ( + {'label': _('by_country'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for( + 'show_cities_list', + option = 'country'), + 'offscreen': True + }, + {'label': _('az'), 'icon': plugin.icon, + 'fanart': __get_plugin_fanart(), + 'path': plugin.url_for( + 'show_cities_list', + option = 'az'), + 'offscreen': True + } + ) + finish_kwargs = { + 'sort_methods': [ + ('LABEL', '%X'), + ], + } + return plugin.finish(items, **finish_kwargs) + +@plugin.route('/menu/cities/select/
  • ([^<]*?)
  • ' + + menu_level = [] + for url, channel in l.find_multiple(html_channels, channel_pattern): + menu_item = { + 'action' : 'program_list', + 'title' : channel, + 'args' : root_url + url, + } + menu_level.append(menu_item) + + return menu_level + + +def get_create_index(): + """This function gets the the first level index menu.""" + + main_url = root_url + 'sanedac#/enr/atracala/'[::-1] + + menu_patterns = ( + ( 'menu_direct', 'href="([^"]*?)".*?([Rr]adio [Ee]n [Dd]irecto)'), + ) + + buffer_url = l.carga_web(main_url) + menu_entries = get_channels_menu(buffer_url) + + for action, pattern in menu_patterns: + url, title = l.find_first(buffer_url, pattern) or ('', '') + menu_item = { + 'action' : action, + 'title' : l.clean_title(title), + 'args' : root_url + url, + } + menu_entries.append(menu_item) + + return menu_entries + + +def get_program_list(menu_url, all_programmes_flag=False, localized=lambda x: x): + """This function makes programmes list data structure for all the program sections.""" + + program_pattern = '
    [^<]*?
    ' + suffix = 'zaSP=ldom'[::-1] + channel_pattern = '([^<]*?)' + page_num_pattern = 'pbq=([0-9]+)' + page_url_pattern = 'class="%s">.*?> %s (%s/%s)' % ( + localized('Next page'), + next_page_num, + last_page_num + ), + 'action' : 'program_list', + } + program_list.append(program_entry) + + return { 'program_list': program_list, 'reset_cache': reset_cache } + + +def get_audio_list(program_url, localized=lambda x: x): + """This function makes the emissions list data structure for all the programmes.""" + + audio_section_sep = '> %s (%s/%s)' % (localized('Next page'), next_page_num, last_page_num), + 'action' : 'audio_list', + 'IsPlayable' : False + } + audio_list.append(audio_entry) + + return { 'audio_list': audio_list, 'reset_cache': reset_cache } + + +def get_direct_channels(): + """This function makes the direct channels menu.""" + + direct_url = '8u3m.niam_s%_enr/enr/cesevtr/ten.deziamaka.maertsevilevtr//:sptth'[::-1] + + channel_list = ( + ( 'Radio Nacional', 'r1'), + ( 'Radio Clásica', 'r2'), + ( 'Radio 3', 'r3'), + ( 'Ràdio 4', 'r4'), + ( 'Radio 5', 'r5_madrid'), + ( 'Radio Exterior', 're'), + ) + + menu_entries = [] + for channel, playlist in channel_list: + menu_item = { + 'action' : 'play_audio', + 'title' : channel, + 'url' : direct_url % playlist, + } + menu_entries.append(menu_item) + + return menu_entries + + +def get_playable_url(url): + """This function gets the stream url for direct channels.""" + + playable_url_pattern = '(http[^#]+)' + + buffer_url = l.carga_web(url) + stream_url = l.find_first(buffer_url, playable_url_pattern) + l.log('get_playable_url has found this URL for direct playback. url: "%s"' % stream_url) + + return stream_url + + +def get_playable_search_url(url): + """This function gets the media url from the search url link.""" + + playable_url_pattern = '> %s (%d)' % (localized('Next page'), next_page_num), + 'action' : 'search_list', + 'IsPlayable' : False + } + search_list.append(search_entry) + + return { 'search_list': search_list, 'reset_cache': reset_cache } + + diff --git a/plugin.audio.rne/resources/settings.xml b/plugin.audio.rne/resources/settings.xml new file mode 100644 index 0000000000..728dfe2d49 --- /dev/null +++ b/plugin.audio.rne/resources/settings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/plugin.audio.shoutcast/LICENSE.txt b/plugin.audio.shoutcast/LICENSE.txt new file mode 100644 index 0000000000..03b0f8f452 --- /dev/null +++ b/plugin.audio.shoutcast/LICENSE.txt @@ -0,0 +1,281 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------- diff --git a/plugin.audio.shoutcast/addon.py b/plugin.audio.shoutcast/addon.py new file mode 100644 index 0000000000..5f81c1ac87 --- /dev/null +++ b/plugin.audio.shoutcast/addon.py @@ -0,0 +1,4 @@ +from resources.lib import plugin + +if __name__ == "__main__": + plugin.run() \ No newline at end of file diff --git a/plugin.audio.shoutcast/addon.xml b/plugin.audio.shoutcast/addon.xml new file mode 100644 index 0000000000..5c9e06b4f4 --- /dev/null +++ b/plugin.audio.shoutcast/addon.xml @@ -0,0 +1,99 @@ + + + + + + + + + audio + + + all + https://www.shoutcast.com/ + https://github.com/XBMC-Addons/plugin.audio.shoutcast + https://forum.kodi.tv/showthread.php?tid=143678 + GPL-2.0-only + وجود اكثر من 50.000 محطة اذاعية مجانية على الانترنت + more than 50.000 free internet radio stations + повече от 50 000 безплатни интернет радиостанции + Més de 50.000 estacions de ràdio per Internet gratuïtes + mere end 50.000 gratis netradiostationer + mehr als 50.000 kostenlose Internet Radio Sender + Πάνω από 50.000 δωρεάν διαδικτυακοί ραδιοφωνικοί σταθμοί + more than 50.000 free internet radio stations + more than 50.000 free internet radio stations + more than 50.000 free internet radio stations + Más de 50.000 emisoras de radio por Internet gratuitas + mas de 50.000 estaciones de radio gratuitas + plus de 50.000 radios internet gratuites + plus de 50 000 stations de radio Internet gratuites + máis de 50.000 emisoras de radio por internet de balde + יותר מ־50.000 תחנות רדיו אינטרנטיות חינמיות + više od 50.000 besplatnih Internet radio stanica + Több mint 50.000 ingyenes internet rádió + lebih dari 50.000 stasiun radio internet gratisan + più di 50.000 stazioni radio libere su internet + 50,000개 이상의 무료 인터넷 라디오 방송 청취 + daugiau kaip 50,000 nemokamų internetinių radijo stočių + Meer dan 50.000 vrije internet radio stations + mer enn 50.000 gratis radiokanaler fra internett + Ponad 50.000 darmowych internetowych stacji radiowych + mais de 50.000 estações de rádio da net grátis + mais de 50.000 estações de rádio gratuitas on-line + более 50 000 свободных интернет радиостанций + Viac než 50.000 voľných internetových rádio staníc + več kot 50000 brezplačnih internetnih radijskih postaj + mer än 50.000 fria internetradiostationer + 50.000 மேற்பட்ட இலவச இணைய வானொலி நிலையங்கள் + 50.000 den Fazla Ücretsiz Internet Radyo Istasyonu + 超过50.000免费网络电台 + With this Add-on you can browse more than 50.000 free internet radio stations. Current Features:[CR]- Top 500 Stations[CR]- Browse by genre (and subgenre if enabled in settings)[CR]- Search Station by name[CR]- Search Stations by current playing track[CR]- You can manage a "My Stations"-list[CR]- bitrate and amount of listeners visible and sortable[CR]- 500 Stations on each Page (can be changed in the settings)[CR]- uses cache (24h genre, 1h station listings) + С това допълнение, можете да слушате над 50.000 достъпни и свободни радиостанции. В момента плъгина позволява:[CR] - Преглед на Топ 500 станции[CR] - Търсене по жанр (и поджанрове, ако е определено в настройките)[CR] - Търсене на станции по име[CR] - Търсене на подобни станции по текущо възпроизвежданата музика[CR] - Управление на списъка "Моите станции"[CR] - Преглеждане, сортиране по битрейт и количество слушатели[CR] - 500 станции на всяка страница (може да се промени в настройките)[CR] - Ползване на кеширане (стилове на 24 часа, списъци на станции на 1 час) + Amb aquest Add-on poden escoltar més de 50.000 emissores de ràdio per Internet gratuïtes. Característiques: [CR]- Les 500 millors emissores. [CR]- Cerca per gènere (i per subgèneres si s'habilita en la configuració). [CR]- Cerca d'emissores per nom. [CR]- Recerca d'emissores que reprodueixen una cançó. [CR]- Possibilitat d'administrar un directori amb "Els meus emissores". [CR]- Directoris ordenats que mostren bitrate i quantitat d'oients. [CR]- 500 emissores a cada pàgina (la quantitat pot canviar en la configuració). [CR]- Ús de memòria cau (24h gènere, 1h directori d'emissores). + Med denne addon kan du udforske mere end 50.000 gratis internetradiostationer. Nuværende funktioner:[CR]- Top 500 Stationer[CR]- Gennemse efter genre (og undergenre hvis det er aktiveret i indstillinger)[CR]- Søg efter navn på Station[CR]- Søg efter Station ud fra nummeret der spiller[CR]- Du kan administrere en "My Stations"-liste[CR]- bitrate og antal lyttere vises og kan sorteres[CR]- 500 Stationer på hver side (kan ændres i indstillinger)[CR]- bruger cache (24 timer for genre, 1 time for lister) + Dieses Add-on ermöglicht dir Zugriff auf über 50.000 kostenlose Internet Radio Sender.[CR]Aktuelle Features:[CR]- Top 500 Sender[CR]- Nach Genre browsen[CR]- Sender nach Name suchen[CR]- Sender nach aktuellem Track suchen[CR]- Sender per Kontext Menu in die "Meine Sender"-Liste kopieren[CR]- Anzeige der Bitrate und Anzahl Höhrer[CR]- 500 Sender pro Seite[CR]- Schnell durch lokalen Cache + Με αυτό το Πρόσθετο μπορείτε να περιηγηθείτε ανάμεσα σε περισσότερους από 50.000 δωρεάν διαδικτυακούς ραδιοφωνικούς σταθμούς. Τρέχουσες λειτουργίες:[CR]- Κορυφαίοι 500 Σταθμοί[CR]- Περιήγηση ανά είδος (και υπο-είδος αν είναι ενεργό στις ρυθμίσεις)[CR]- Αναζήτηση Σταθμού με βάση το όνομα[CR]- Αναζήτηση Σταθμών με βάση το τρέχον τραγούδι[CR]- Μπορείτε να διαχειριστείτε μία λίστα "Σταθμών μου"[CR]- Το bitrate και το πλήθος των ακροατών είναι ορατά και η ταξινόμηση μπορεί να γίνει σύμφωνα με αυτά[CR]- 500 Σταθμοί σε κάθε Σελίδα (μπορεί να αλλάξει στις ρυθμίσεις)[CR]- Χρήση λανθάνουσας μνήμης (είδος 24 ωρών, καταχωρίσεις σταθμών 1 ώρας) + With this Add-on you can browse more than 50.000 free internet radio stations. Current Features:[CR]- Top 500 Stations[CR]- Browse by genre (and subgenre if enabled in settings)[CR]- Search Station by name[CR]- Search Stations by current playing track[CR]- You can manage a "My Stations"-list[CR]- bitrate and amount of listeners visible and sortable[CR]- 500 Stations on each Page (can be changed in the settings)[CR]- uses cache (24h genre, 1h station listings) + With this Add-on you can browse more than 50.000 free internet radio stations. Current Features:[CR]- Top 500 Stations[CR]- Browse by genre (and subgenre if enabled in settings)[CR]- Search Station by name[CR]- Search Stations by current playing track[CR]- You can manage a "My Stations"-list[CR]- bitrate and amount of listeners visible and sortable[CR]- 500 Stations on each Page (can be changed in the settings)[CR]- uses cache (24h genre, 1h station listings) + With this Add-on you can browse more than 50.000 free internet radio stations. Current Features:[CR]- Top 500 Stations[CR]- Browse by genre (and subgenre if enabled in settings)[CR]- Search Station by name[CR]- Search Stations by current playing track[CR]- You can manage a "My Stations"-list[CR]- bitrate and amount of listeners visible and sortable[CR]- 500 Stations on each Page (can be changed in the settings)[CR]- uses cache (24h genre, 1h station listings) + Con este Add-on pueden escucharse más de 50.000 emisoras de radio por Internet gratuitas. Características:[CR]- Las 500 mejores emisoras.[CR]- Búsqueda por género (y por subgéneros si se habilita en la configuración).[CR]- Búsqueda de emisoras por nombre.[CR]- Búsqueda de emisoras que reproducen una canción.[CR]- Posibilidad de administrar un directorio con "Mis emisoras".[CR]- Directorios ordenados que muestran bitrate y cantidad de oyentes.[CR]- 500 emisoras en cada página (la cantidad puede cambiarse en la configuración).[CR]- Uso de caché (24h género, 1h directorio de emisoras). + Con este complemento usted puede navegar por mas de 50.000 estaciones de redio de internet gratuitas. Caracteristicas Actuales:[CR]- Mejores 500 Estaciones[CR]- Navegar por genero (y subgenero si lo habilita en los ajustes)[CR]- Busqueda de Estacion por nombre[CR]- Busqueda de Estaciones por la pista actual en reproduccion[CR]- Usted puede administrar una lista"Mis Estaciones"[CR]- tasa de bits y cantidad de oyentes visibles y ordenable[CR]- 500 Estaciones en cada Pagina (puede ser cambiado en los ajustes)[CR]- utiliza cache (24h genero, 1h lista de estaciones) + Avec cette extension vous pouvez accéder à plus de 50.000 station radio internet gratuite. Possibilités actuelles :[CR]- Top 500 stations[CR]- Naviguer par genre (et sous-genre si le réglage est activé)[CR]- Rechercher une station par nom[CR]- Rechercher une station jouant la piste en cours[CR]- Création d'une liste de stations favorites[CR]- Bitrate et nombre d'auditeurs visibles et triables[CR]- 500 stations par page (modifiable dans les réglages)[CR]- Utilisation d'un cache (24h par genre, 1h par station) + Avec cet addiciel vous pouvez parcourir plus de 50 000 stations de radio Internet gratuites. Caractéristiques actuelles :[CR] - Les 500 1res stations[CR] - Parcourir par genre (et sous-genre si activé dans les paramètres)[CR] - Rechercher les stations par nom[CR] - Rechercher les stations par piste en cours de lecture[CR] - Vous pouvez gérer une liste « Mes stations »[CR] - Débit binaire et nombre d'auditeurs visible et triable[CR] - 500 stations par page (peut être modifié dans les paramètres)[CR] - Utilise un cache (24 h pour les genres, 1h pour les listes de stations). + Grazas a este complemento pode buscar máis de 50.000 emisoras de radio por internet de balde. Características:[CR]- Mellores 500 Emisoras[CR]- Buscar por xénero (e subxénero se esta habilitado na configuración)[CR]- Buscar Emisoras por nome[CR]- Buscar Emisoras pola pista en reprodución actual[CR]- Pode xestionar a listaxe "As miñas Emisoras"[CR]- taxa de bits e número de ointes visíbeis e ordenábel[CR]- 500 Emisoras en cada páxina (pódese trocar na configuración)[CR]- usar a caché (24h para xénero, 1h para listaxe de emisoras) + בעזרת הרחבה זו ניתן לעיין ביותר מ-50.000 תחנות רדיו אינטרנטיות חינמיות. כולל:[CR]- 500 התחנות הגדולות ביותר[CR]- עיון לפי סגנון (ותת סגנון אם מאופשר בהגדרות)[CR]- חיפוש תחנה לפי שם[CR]- חיפוש תחנות לפי רצועה המנוגנת כעת[CR]- ניתן לנהל רשימת "התחנות שלי"[CR]- מוצג קצב נתונים וכמות מאזינים וניתן למיין לפיהם[CR]- 500 תחנות בכל עמוד (ניתן לשינוי בהגדרות)[CR]- נעשה שימוש בזיכרון מטמון (24 שע' לסגנונות,1 שע' לרשימת התחנות) + Sa ovim dodatkom možete pregledati više od 50.000 besplatnih Internet radio stanica. Trenutne značajke:[CR]- Top 500 stanica[CR]- Pregledajte prema žanru (i podžanru ako je omogućeno u postavkama)[CR]- Pretraživajte stanice prema nazivu[CR]- Pretraživajte stanice prema trenutnoj reprodukciji pjesme[CR]- Možete upravljati "Moje stanice"-list[CR]- brzina prijenosa i broj slušatelja je vidljiv i prilagodljiv[CR]- 500 stanica na svakoj stranici (mogu se promijeniti u postavkama)[CR]- koristi se predmemorija (24 sata za žanr, 1 sat slušanje stanice) + Ezzel a kiegészítővel választhatsz több mint 50.000 ingyenes rádió adó közül. Jelenlegi szolgáltatások:[CR]- Top 500 állomás[CR]-Műfajonkénti (és alműfajonkénti) keresés[CR]- Név szerinti állomáskeresés[CR]- Állomáskeresés az éppen játszott szám alapján[CR]- Az "én állomásaim" lista kezelése[CR]- Minőség és hallgatottság szerinti rendezés[CR]- 500 adó minden oldalon (a szám beállítható)[CR]- Gyorsítótár használata (24óra a műfajokra, 1óra az állomásokra) + Dengan addon ini Anda dapat menjelajahi lebih dari 50.000 stasiun radio gratisan. Fitur saat ini:[CR]- 500 Stasiun Teratas[CR]- Jelajahi berdasarkan tema (dan subtema jika diaktifkan di pengaturan)[CR]- Cari Stasiun berdasarkan nama[CR]- Cari Stasiun berdasarkan trek yang diputar[CR]- Anda dapat mengelola daftar "Stasiun Saya"[CR]- bitrate dan jumlah pendengar terlihat dan dapat diurutkan[CR]- 500 Stasiun setiap halaman (dapat diubah di pengaturan)[CR]- menggunakan cache (24 jam tema, 1 jam daftar stasiun) + Con questo Add-on puoi sfogliare più di 50.000 stazioni radio libere su internet. Caratteristiche attuali:[CR]- Migliori 500 stazioni[CR]- Sfoglia per genere (e sottogenere se abilitato nelle impostazioni)[CR]- Cerca le stazioni per nome[CR]- Cerca le stazioni per traccia in esecuzione[CR]- Puoi gestire un lista "Mie Stazioni"[CR]- bitrate e quantità di ascoltatori visibile e riordinabile[CR]- 500 Stazioni in ogni pagina (può essere modificato nelle impostazioni)[CR]- cache utilizzata (24h genere, 1h ascolto) + 이 애드온으로 50,000개 이상의 무료 인터넷 라디오 방송을 탐색할 수 있습니다. 현재 기능:[CR]- Top 500 방송국[CR]- 장르(설정에서 활성화하면 하위 장르)로 탐색[CR]- 방송국명으로 검색[CR]- 현재 트랙을 방송중인 방송국 검색[CR]- "내 방송국" 목록 관리[CR]- 비트레이트와 청취자수 표시와 정렬[CR]- 페이지당 500 방송국 표시 (설정에서 변경 가능)[CR]- 캐시 사용 (장르 24시간, 방송국 리스트 1시간) + Su šiuo priedu galite naršyti daugiau kaip 50,000 nemokamų internetinių radijo stočių. Dabartinės funkcijos:[CR]- 500 geriausių stočių[CR]- Naršyti pagal žanrą (ir porūšį, jei įjungta nustatymuose)[CR]- Ieškoti stoties pagal pavadinimą[CR]- Ieškoti stočių pagal dabartinį atkuriamą takelį[CR]- Jūs galite valdyti "Mano radijo stotys" sąrašą[CR]- galima matyti pralaidumą ir klausytojų skaičių bei pagal juos rūšiuoti[CR]- 500 stočių kiekviename puslapyje (galima pakeisti nustatymuose)[CR]- Naudoja talpyklą (24h žanro, 1h stoties sąrašai) + Met deze Add-on die kunt u door meer dan 50.000 vrije internet radiostations bladeren. Huidige functies: [CR]- Top 500 Stations [CR]- bladeren door genre (en subgenre indien ingeschakeld in instellingen) [CR] - Zoek Station op naam [CR] - Zoek Stations door huidig afspelende track [CR]- u kunt een "Mijn Stations" lijst beheren[CR]- bitrate en hoeveelheid luisteraars zichtbaar en sorteerbaar [CR]- 500 Stations op elke pagina (kan gewijzigd worden in de instellingen) [CR]-gebruikte cache (24u genre, 1 uur station luisteren) + Med denne utvidelsen kan du finne mer enn 50.000 radiokanaler helt gratis fra internett. Nåværende funksjoner:[CR]- Topp 500 stasjoner[CR]- Bla gjennom etter sjanger (og undersjangre om aktivert i innstillinger)[CR]- Søk etter stasjon med navn[CR]- Søk etter stasjoner etter hva som spilles nå[CR]- Du kan lage din egen "Mine stasjoner" -liste[CR]- Kvalitet og antall lyttere vises og kan filtreres[CR]- 500 stasjoner på hver side (kan endres i innstillinger)[CR]- Bruker mellomlagring (24 timer for sjanger, 1 time for stasjonslister) + Dzięki tej wtyczce możesz przeglądać w ponad 50.000 darmowych stacjach radiowych. Aktualne funkcje:[CR]- Top 500 stacji[CR]- przeglądaj według gatunków (oraz subgatunków jeżeli włączone)[CR]- szukaj stacji po nazwie[CR]- szukaj stacji po granym utworze[CR]- zarządzaj listą Moje stacje[CR]- sortowanie oraz widoczność liczby słuchaczy i bitrate[CR]- 500 stacji na stronie (możliwa edycja)[CR]- pamięć podręczna (24h gatunki, 1h lista stacji) + Com este add-on, pode escolher entre mais de 50.000 estações de rádio da net grátis. Funcionalidades:[CR]- Top 500 Rádios[CR]- Procure por Género (e sub-género, se activo nas preferências)[CR]- Procurar Rádio por nome[CR]- Procurar Rádios por faixa em reprodução[CR]- Pode gerir uma lista de "Minhas Rádios"[CR]- bitrate e quantidade de ouvintes visíveis e ordenáveis[CR]- 500 Rádios em cada Página (pode ser alterado nas preferências)[CR]- usar cache (24h para género, 1h para lista das rádios) + Com este Add-on, você pode navegar em mais de 50.000 estações de rádio on-line grátis. Características atuais:[CR]- As 500 Melhores Estações[CR]- Procurar por gênero (e subgênero se habilitado nas configurações)[CR]- Pesquisar estações por nome[CR]- Pesquisar estações pela faixa atual[CR]- Você pode gerenciar uma lista de "Minhas Estações"[CR]- bitrate e quantidade de ouvintes visíveis e ordenáveis[CR]- 500 estações por página (pode ser alterado nas configurações)[CR]- usa cache (24h para gêneros, 1h para listagem de estações) + С этим дополнением Вы сможете слушать более 50 тысяч доступных свободных радиостанций. На данный момент плагин позволяет:[CR]- Просматривать Top 500 станций[CR]- Искать по стилю (и подстилю, если задано в настройках)[CR]- Искать станции по имени[CR]- Искать похожие станции по текущей воспроизводимой музыке[CR]- Управление списком "Мои станции"[CR]- Просматривать, сортировать по битрейту и количеству слушателей[CR]- 500 станций на каждой странице (может быть изменено в настройках)[CR]- Использование кеширования (стили на 24 часа, списки станций на 1 час) + S týmto doplnok môžete prehliadať vyše 50.000 voľných internetových rádio staníc. Momentálne podporuje: +- Top 500 staníc +- Prehliadanie podľa žánru (a pod-žánru, ak povolené v nastaveniach) +- hľadanie stanice podľa mena +- hľadanie stanice podľa práve hranej skladby +- Môžete spravovať zoznam "Moje stanice" +- zobrazenie a zoradenie podľa dátového toku a počtu poslucháčov +- 500 staníc na každej strane (môže byť zmenené v nastaveniach) +- používa medzi-pamäť (24h pre žánre, 1h pre zoznam staníc) + Med detta tillägg kan du bläddra bland mer än 50.000 fria internetradiostationer. Nuvarande funktioner:[CR]- Topp 500 stationer[CR]- Bläddra efter genre (och undergenre, om det är påslaget i inställningar)[CR]- Sök station efter namn[CR]- Sök stationer som spelar låt[CR]- Du kan hantera "Mina stationer"-lista[CR]- bitrate och antal lyssnare är synligt och sorterbart[CR]- 500 stationer på varje sida (kan ändras i inställningar)[CR]- använder buffert (24 genre, 1 timmes stationlista) + இந்த துணை பயன் மூலம் நீங்கள் 50.000 மேற்பட்ட இலவச இணைய வானொலி நிலையங்களை உலாவ முடியும். தற்போதய சிறப்பியல்:[CR]- உச்சி 500 நிலையங்கள்[CR]- வகைபடி உலாவு (துணை வகைப்படி சேர்க்கவும் - அமைப்புகளில் தேர்வு செய்யவும்)[CR]- பெயர்படி நிலையத்தனி தேடுக[CR]- பெயர்படி நிலையத்தனி தேடுக[CR]- "என்னுடைய நிலையங்கள்" பட்டியலை நீங்கள் நிர்வகிக்க முடியும்[CR]- பிட்வீதம் மற்றும் கேட்போர் அளவுகளை பார்க்கவும் மற்றும் வரிசைபடுத்தவும்[CR]- ஒரு பக்கத்தில் 500 நிலையங்கள் (அமைப்புகளில் மாற்ற இயலும்)[CR]- தேக்கத்தை பயன்படுத்தவும் (24 மணிநேர வகை, 1 மணிநேர நிலைய பட்டியல்) + 你可以使用本插件浏览超过50.000免费网络电台。当前功能:[CR]- 500强电台[CR]- 按类别浏览(可设置子类)[CR]- 按名字搜索电台[CR]- 按当前播放曲目搜索电台[CR]- 管理“我的电台”-列表[CR]- 按比特率和收听人数排序[CR]- 每页500个电台(可设置)[CR]- 使用缓存(24小时类别,1小时站点列表) + + 2.5.2+matrix.1 (07/05/2021) + - Updated translations from Weblate + + + icon.png + fanart.jpg + + + \ No newline at end of file diff --git a/plugin.audio.shoutcast/changelog.txt b/plugin.audio.shoutcast/changelog.txt new file mode 100644 index 0000000000..c954484804 --- /dev/null +++ b/plugin.audio.shoutcast/changelog.txt @@ -0,0 +1,53 @@ +2.5.2+matrix.1 (07/05/2021) + Updated translations from Weblate + + 2.5.1+matrix.0 (27/09/2020) + - Add stationname property (matrix only) + +2.5.0+matrix.1 (12.4.2020) - matrix edition + - Remove python2 support + - Remove simplejson + - New language folder layout + - Automated submissions to matrix branch + - Addon.xml cosmetics + +2.4.1 (29.3.2020) + - Fix requests + +2.4.0 (5.12.2019) + - Python3 compatibility for matrix + - Dependency bump + +2.3.0 (15.02.2015) + - fixed playback of some station (all should work now again) + - updated translations + +2.2.0 (23.04.2013) + - fixed unicode related error (Top 500 was broken) + - added bitrate filter + - updated translations + +2.1.1 (09.03.2013) + - updated translations + - migrated to xbmcswift2 v2.4.0 + +2.1.0 (30.01.2013) + - Rename Add-on to "SHOUTcast" to match guidelines + - Changed to xbmcswift2 v1.3 + - Fix Sorting + - updated translations + +2.0.1 (02.11.2012) + - added method and setting for auto choose server (don't ask for playlist entries) + +2.0.0 (unreleased) + - Initial Release + - Features: + Top 500 Stations + Browse by genre (and subgenre if enabled in settings) + Search Station by name + Search Stations by current playing track + You can manage a "My Stations"-list + bitrate and amount of listeners visible and sortable + 500 Stations on each Page (can be changed in the settings) + uses cache (24h genre, 1h station listings) \ No newline at end of file diff --git a/plugin.audio.shoutcast/fanart.jpg b/plugin.audio.shoutcast/fanart.jpg new file mode 100644 index 0000000000..c34dfd585c Binary files /dev/null and b/plugin.audio.shoutcast/fanart.jpg differ diff --git a/plugin.audio.shoutcast/icon.png b/plugin.audio.shoutcast/icon.png new file mode 100644 index 0000000000..71e992a2ef Binary files /dev/null and b/plugin.audio.shoutcast/icon.png differ diff --git a/plugin.audio.shoutcast/resources/__init__.py b/plugin.audio.shoutcast/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.shoutcast/resources/language/resource.language.af_za/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.af_za/strings.po new file mode 100644 index 0000000000..dbd5fc56ef --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.af_za/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: af_za\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.am_et/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.am_et/strings.po new file mode 100644 index 0000000000..d461c3c6d8 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.am_et/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: am_et\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ar_sa/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ar_sa/strings.po new file mode 100644 index 0000000000..08d120493d --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ar_sa/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-05-07 15:36+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Arabic (Saudi Arabia) \n" +"Language: ar_sa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" +"X-Generator: Weblate 4.6.1\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "جميع الانواع الفرعية" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "افضل 500 محطة" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "الاستعراض حسب النوع" + +msgctxt "#30003" +msgid "My Stations" +msgstr "المحطات الخاصة بي" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "البحث عن المحطة من خلال الأسم" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "الاضافة الى 'محطاتي المفضلة'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "الازالة من قائمة محطاتي المفضلة" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "اضهار الفئات الفرعية" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "عرض معدل البت في العنوان" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "اختيار ملقم خدمة عشوائي ( بدون سؤال)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "ترشيح المحطات من خلال الحد الادنى لمعدل البت" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "الحد الأدنى لمعدل البت" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 كيلو/بيت" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 كيلو/بيت" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 كيلو/بيت" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 كيلو/بيت" + +msgctxt "#30200" +msgid "Network Error" +msgstr "خطاء بالشبكة" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ast_es/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ast_es/strings.po new file mode 100644 index 0000000000..87d51e482a --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ast_es/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ast_es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.az_az/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.az_az/strings.po new file mode 100644 index 0000000000..4a890572e2 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.az_az/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: az_az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.be_by/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.be_by/strings.po new file mode 100644 index 0000000000..9be763d5c5 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.be_by/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-05-07 15:36+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Belarusian \n" +"Language: be_by\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Weblate 4.6.1\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimum Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Network Error" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.bg_bg/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.bg_bg/strings.po new file mode 100644 index 0000000000..f0f041f2d5 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.bg_bg/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: bg_BG\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Всички поджанрове" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Топ 500 станции" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Търси по жанр" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Моите станции" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Търси станция по име" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Търси станция по текущо слушаната песен" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Добави в 'Моите станции'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Премахни от 'Моите станции'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Покажи поджанрове" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Брой станции в списъка" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Покажи битрейта в заглавието" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Избери случаен сървър (Не питай)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Филтрирай станциите по минимален битрейт" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Минимален битрейт" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 кбит/с" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 кбит/с" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 кбит/с" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 кбит/с" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Мрежова грешка" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.bs_ba/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.bs_ba/strings.po new file mode 100644 index 0000000000..6f1a0e5594 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.bs_ba/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: bs_ba\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ca_es/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ca_es/strings.po new file mode 100644 index 0000000000..128fde090d --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ca_es/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: ca_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "tots els subgèneres" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 de Emissores" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Buscar per Genere" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Les meves emissores" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Cerca emissora per Nom" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Cerca de pista actual de la emissora en reproducció" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "afageix a 'Les meves Emissores'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Elimina de 'Les meves Emissores'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Mostrar els subgèneres" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Num de Emissores a la llista" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Mostra el Bitrate en el titol" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Tria un servidor aleatori (sense preguntar)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrar les Emissores per minim de Bitrate" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minim de Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Error de xarxa" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.cs_cz/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.cs_cz/strings.po new file mode 100644 index 0000000000..eb777ced0a --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.cs_cz/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: cs_cz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.cy_gb/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.cy_gb/strings.po new file mode 100644 index 0000000000..f26ffd0230 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.cy_gb/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: cy_gb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=(n==0) ? 0 : (n==1) ? 1 : (n==2) ? 2 : " +"(n==3) ? 3 :(n==6) ? 4 : 5;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.da_dk/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.da_dk/strings.po new file mode 100644 index 0000000000..5088ad9d1d --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.da_dk/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-05-07 15:36+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Danish \n" +"Language: da_dk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.6.1\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Alle undergenrer" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 Stationer" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Gennemse efter genre" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mine Stationer" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Søg efter navn på Station" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Søg efter Station ud fra nuværende nummer" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Føj til 'Mine Stationer'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Fjern fra 'Mine Stationer'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Vis Undergenrer" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Nummerer stationer på liste" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Vis Bitrate i Titel" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Vælg en tilfældig server (Spørg ikke)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrer stationerne med minimum bitrate" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimum Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Netværksfejl" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.de_de/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..2c4ea9e51e --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Alle Untergenre" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 Sender" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Nach Genre" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Meine Sender" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Sender Suche nach Name" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Suche Sender nach aktuellem Track" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Zu 'Meine Sender' hinzufügen" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Von 'Meine Sender' entfernen" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Untergenres anzeigen" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Anzahl Podcasts" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Bitrate in Title einfügen" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Wähle zufälligen Server (nicht fragen)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Sender nach Bitrate filtern" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Mindest-Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Netzwerkfehler" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.el_gr/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.el_gr/strings.po new file mode 100644 index 0000000000..2ba19a6b37 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.el_gr/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: el_GR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Όλα τα υπο-είδη" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Κορυφαίοι 500 Σταθμοί" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Περιήγηση ανά είδος" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Οι Σταθμοί μου" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Αναζήτηση Σταθμού με βάση το όνομα" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Αναζήτηση Σταθμού Τρέχοντος Τραγουδιού" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Προσθήκη στους 'Σταθμούς μου'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Αφαίρεση από τους 'Σταθμούς μου'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Προβολή Υπο-Ειδών" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Αριθμός σταθμών στη λίστα" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Προβολή Bitrate στον Τίτλο" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Επιλογή τυχαίου διακομιστή (Χωρίς ερώτηση)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Φιλτράρισμα σταθμών κατά ελάχιστο Bitrate" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Ελάχιστο Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Σφάλμα Δικτύου" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.en_au/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.en_au/strings.po new file mode 100644 index 0000000000..45046e25ad --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.en_au/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_au\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.en_gb/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..56264dad8b --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_GB\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" + diff --git a/plugin.audio.shoutcast/resources/language/resource.language.en_nz/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.en_nz/strings.po new file mode 100644 index 0000000000..10d8f29a51 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.en_nz/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: en_NZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "All subgenres" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 Stations" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Browse by genre" + +msgctxt "#30003" +msgid "My Stations" +msgstr "My Stations" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Search Station by name" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Search Current Track playing Station" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Add to 'My Stations'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Remove from 'My Stations'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Show Subgenres" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Num stations in list" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Show Bitrate in Title" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Choose a random server (Don't ask)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimum Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Network Error" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.en_us/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.en_us/strings.po new file mode 100644 index 0000000000..235a736fda --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.en_us/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "All subgenres" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 Stations" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Browse by genre" + +msgctxt "#30003" +msgid "My Stations" +msgstr "My Stations" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Search Station by name" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Search Current Track playing Station" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Add to 'My Stations'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Remove from 'My Stations'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Show Subgenres" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Num stations in list" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Show Bitrate in Title" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Choose a random server (Don't ask)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filter stations by minimum Bitrate" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimum Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Network Error" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.eo/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.eo/strings.po new file mode 100644 index 0000000000..45b79a2a90 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.eo/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.es_ar/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.es_ar/strings.po new file mode 100644 index 0000000000..eb81b8ae4f --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.es_ar/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: es_ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.es_es/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.es_es/strings.po new file mode 100644 index 0000000000..49f1421316 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Todos los subgéneros" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Las 500 mejores emisoras" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Buscar por género" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mis emisoras" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Buscar por nombre" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Buscar emisoras que reproducen una canción" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Añadir a 'Mis emisoras'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Quitar de 'Mis emisoras'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Mostrar subgéneros" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Número de emisoras en directorios" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Mostrar bitrate junto al nombre" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Elegir un servidor aleatorio (No preguntar)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrar emisoras por Bitrate" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Bitrate mínimo" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Error en la conexión" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.es_mx/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.es_mx/strings.po new file mode 100644 index 0000000000..218abf49c0 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.es_mx/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: es_MX\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Todos los subgeneros" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Las mejores 500 Estaciones" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Navegar por genero" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mis Estaciones" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Busqueda de Estaciones por nombre" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Busqueda de la Pista Actual Reproducia en la Estacion" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Agregar a 'Mis Estaciones'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Remover de 'Mis Estaciones'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Mostrar Subgeneros" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Numero de Estaciones en la lista" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Mostrar la Tasa de Bits en el Titulo" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Elejir un servidor al azar (Sin preguntar)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtro de estaciones por Tasa de Bits minima" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Tasa de Bits MInima" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Error de red" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.et_ee/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.et_ee/strings.po new file mode 100644 index 0000000000..f333a2a0f4 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.et_ee/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: et_ee\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.eu_es/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.eu_es/strings.po new file mode 100644 index 0000000000..9840484759 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.eu_es/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: eu_es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.fa_af/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.fa_af/strings.po new file mode 100644 index 0000000000..afde92f0f2 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.fa_af/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: fa_af\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.fa_ir/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.fa_ir/strings.po new file mode 100644 index 0000000000..55633ee900 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.fa_ir/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: fa_ir\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.fi_fi/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.fi_fi/strings.po new file mode 100644 index 0000000000..0684fe8155 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.fi_fi/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: fi_FI\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Selaa lajityypeittäin" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Verkkovirhe" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.fo_fo/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.fo_fo/strings.po new file mode 100644 index 0000000000..2054477883 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.fo_fo/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: fo_fo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.fr_ca/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.fr_ca/strings.po new file mode 100644 index 0000000000..fb305020cd --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.fr_ca/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: fr_CA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Tous les sous-genres" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Les 500 1res stations" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Parcourir par genre" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mes stations" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Rechercher les stations par nom" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Rechercher les stations par piste jouant actuellement" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Ajouter à « Mes stations »" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Enlever de « Mes stations »" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Afficher les sous-genres" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Numéros des stations dans la liste" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Afficher le débit binaire dans le titre" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Choisir un serveur aléatoire (ne pas demander)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrer les stations par débit binaire minimum" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Débit binaire minimum" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbits/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Erreur de réseau" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.fr_fr/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.fr_fr/strings.po new file mode 100644 index 0000000000..412f4ba49a --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.fr_fr/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Tout les sous-genres" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 stations" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Parcourir par Genre" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mes stations" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Rechercher une station par nom" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Rechercher la station jouant la piste courante" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Ajouter à 'Mes stations'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Supprimer de 'Mes stations'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Afficher les sous-genres" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Nombre de stations dans la liste" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Afficher le bitrate dans le titre" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Choisir un serveur aléatoirement (Ne pas demander)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrer les stations par échantillonage minimum" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Échantillonage minimum" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Erreur réseau" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.gl_es/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.gl_es/strings.po new file mode 100644 index 0000000000..5c7648dadc --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.gl_es/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: gl_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Tódolos subxéneros" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Mellores 500 Emisoras" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Buscar por xénero" + +msgctxt "#30003" +msgid "My Stations" +msgstr "As miñas Emisoras" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Buscar Emisora por nome" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Buscar emisora da pista en reprodución actual" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Engadir a 'As miñas Emisoras'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Eliminar de 'As miñas Emisoras'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Amosar subxéneros" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Núm. de emisoras na listaxe" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Amosar a Taxa de bits no Título" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Seleccionar un servidor ó chou (Non preguntar)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrar as estacións pola taxa de bits mínima" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Taxa de bits mínima" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Erro de rede" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.he_il/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.he_il/strings.po new file mode 100644 index 0000000000..288e81a176 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.he_il/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: he_IL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "כל תת הסגנונות" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "500 התחנות הגדולות" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "עיון לפי סגנון" + +msgctxt "#30003" +msgid "My Stations" +msgstr "התחנות שלי" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "חיפוש תחנה לפי שם" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "חיפוש תחנה המנגנת רצועה נוכחית" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "הוספה לתחנות שלי" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "הסרה מהתחנות שלי" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "הצג תת סגנונות" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "מס' תחנות ברשימה" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "הצג קצב נתונים בכותרת" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "בחר שרת אקראי (ללא התערבות משתמש)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "סנן תחנות לפי קצב נתונים מינימלי" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "קצב נתונים מינימלי" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "שגיאת רשת" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.hi_in/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.hi_in/strings.po new file mode 100644 index 0000000000..a121444731 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.hi_in/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: hi_in\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.hr_hr/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.hr_hr/strings.po new file mode 100644 index 0000000000..0e0fa6c7d8 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.hr_hr/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: hr_HR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Svi podžanrovi" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 stanica" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Pregled po žanru" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Moje stanice" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Pretraži stanice po nazivu" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Pretraži radio stanice za trenutno reproduciranu pjesmu" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Dodaj u 'Moje stanice'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Ukloni iz 'Moje stanice'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Prikaži podkategorije" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Broj stanica u popisu" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Prikaži brzinu prijenosa u naslovu" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Odaberi naizmjenice poslužitelja (ne pitaj)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtriraj stanice po minimalnoj brzini prijenosa" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimalna brzina prijenosa" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Greška mreže" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.hu_hu/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.hu_hu/strings.po new file mode 100644 index 0000000000..fce68771b4 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.hu_hu/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: hu_HU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Minden alműfaj" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 rádióállomás" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Műfaj szerinti böngészés" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Én állomásaim" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Állomás keresése név szerint" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "A jelenlegi számot játszó állomás keresése" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Hozzáadás az 'én állomásaim'-hoz" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Eltávolítás az 'én állomásaim'-ból" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Alműfajok mutatása" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Állomásszám a listában" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Bitarány mutatása a címben" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Véletlenszerű kiszolgálóválasztás (kérdezés nélkül)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Adók szűrése minimális Bitráta alapján" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimális Bitráta" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Hálózati hiba" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.hy_am/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.hy_am/strings.po new file mode 100644 index 0000000000..5b06ae6600 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.hy_am/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: hy_am\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.id_id/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.id_id/strings.po new file mode 100644 index 0000000000..515b93c81e --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.id_id/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: id_ID\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Semua subtema" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "500 Stasiun Teratas" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Jelajah menurut tema" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Stasiun Saya" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Cari Stasiun menurut nama" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Cari Stasiun yang memutar Trek Ini" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Tambah ke 'Stasiun Saya'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Hapus dari 'Stasiun Saya'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Tampilkan subtema" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Jumlah stasiun di daftar" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Tampilkan Bitrate pada Judul" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Pilih server acak (Jangan tanya)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filter stasiun berdasarkan bitrate minimum" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Bitrate minimum" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Kesalahan Jaringan" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.is_is/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.is_is/strings.po new file mode 100644 index 0000000000..10267b39c0 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.is_is/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: is_IS\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Net Villa" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.it_it/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.it_it/strings.po new file mode 100644 index 0000000000..664b3e75ad --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.it_it/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: it_IT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Tutti i sottogeneri" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Migliori 500 Stazioni" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Sfoglia per genere" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Le mie stazioni" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Cerca la Stazione per nome" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Cerca la Stazione per la traccia in esecuzione" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Aggiungi alle mie stazioni" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Rimuovi dalle mie stazioni" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Mostra Sottogeneri" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Numera le stazioni in liste" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Mostra il bitrate nel Titolo" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Scegli un server a caso (Non Chiedere)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtra le stazioni per Bitrate minimo" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Bitrate Minimo" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Errore di rete" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ja_jp/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ja_jp/strings.po new file mode 100644 index 0000000000..1772ca979c --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ja_jp/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ja_jp\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ko_kr/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ko_kr/strings.po new file mode 100644 index 0000000000..2ec176dcaa --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ko_kr/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: ko_KR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "모든 하위 장르" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 방송국" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "장르로 탐색" + +msgctxt "#30003" +msgid "My Stations" +msgstr "내 방송국" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "이름으로 방송국 검색" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "다음 트랙을 지금 방송중인 방송국 검색" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "'내 방송국'에 추가" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "'내 방송국'에서 제거" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "하위 장르 보기" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "목록에 표시할 방송국 수" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "방송국명에 비트레이트 보기" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "무작위 서버 선택 (묻지 않음)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "최저 비트레이트로 방송국 필터링" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "최저 비트레이트" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "네트워크 에러" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.lt_lt/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.lt_lt/strings.po new file mode 100644 index 0000000000..3445d33866 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.lt_lt/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: lt_LT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Visi antriniai žanrai" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Radijo stočių Top 500" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Naršyti pagal žanrą" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mano radijo stotys" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Radijo stoties paieška pagal pavadinimą" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Radijo stoties paiešką pagal šiuo metu atkuriamą takelį" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Pridėti į 'Mano radijo stotys'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Pašalinti iš 'Mano radijo stotys'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Rodyti antrinius žanrus" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Numeruoti radijo stotis sąraše" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Rodyti pralaidumą antraštėje" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Pasirinkite atsitiktinį serverį (Neklausti)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtruoti radijo stotis pagal minimalų pralaidumą" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimalus pralaidumas" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Tinklo klaida" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.lv_lv/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.lv_lv/strings.po new file mode 100644 index 0000000000..896304e449 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.lv_lv/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: lv_LV\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Tīkla kļūda" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.mk_mk/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.mk_mk/strings.po new file mode 100644 index 0000000000..f5ef9945ee --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.mk_mk/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: mk_mk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n==1 || n%10==1 ? 0 : 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ml_in/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ml_in/strings.po new file mode 100644 index 0000000000..0fe4fd6d72 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ml_in/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ml_in\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.mn_mn/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.mn_mn/strings.po new file mode 100644 index 0000000000..ba29457d36 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.mn_mn/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: mn_mn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ms_my/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ms_my/strings.po new file mode 100644 index 0000000000..e287bcd5a6 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ms_my/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ms_my\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.mt_mt/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.mt_mt/strings.po new file mode 100644 index 0000000000..8cee4b6e2d --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.mt_mt/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: mt_mt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? " +"1 : (n%100>10 && n%100<20 ) ? 2 : 3;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.my_mm/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.my_mm/strings.po new file mode 100644 index 0000000000..a451231329 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.my_mm/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: my_mm\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.nb_no/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.nb_no/strings.po new file mode 100644 index 0000000000..1a9af4e050 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.nb_no/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: nb_NO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Alle undersjangere" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Topp 500 stasjoner" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Vis etter sjanger" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mine stasjoner" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Søk etter stasjon med navn" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Søk etter stasjon som spiller sang" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Legg til i 'Mine stasjoner'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Fjern fra 'Mine stasjoner'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Vis undersjangere" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Antall stasjoner i listen" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Vis kvalitet i tittel" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Velg en tilgeldig server (Ikke spør)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrer stasjoner etter minimum kvalitet" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimum kvalitet" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Nettverksfeil" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.nl_nl/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..0ac9ddfcdb --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-05-07 15:36+0000\n" +"Last-Translator: Christian Gade \n" +"Language-Team: Dutch \n" +"Language: nl_nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.6.1\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Alle subgenres" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Blader op genre" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mijn Stations" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Zoek Station op naam" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Zoek huidige af spelende track Station" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Voeg toe aan 'Mijn Stations'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Verwijder van 'Mijn Stations'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Weergave Subgenres" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Aantal stations in lijst" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Weergave Bitrate in Titel" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Kies een willekeurige server (Niet vragen)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filter stations bij minimale Bitrate" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minimale Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Netwerk fout" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.pl_pl/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.pl_pl/strings.po new file mode 100644 index 0000000000..7ebbc74874 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.pl_pl/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Wszystkie podgatunki" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 stacji" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Przeglądaj według gatunku" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Moje stacje" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Wyszukiwanie stacji po nazwie" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Szukaj stacji po aktualnie granym utworze" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Dodaj do Moich stacji" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Usuń z Moich stacji" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Pokaż podgatunki" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Numer stacji na liście" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Pokazuj bitrate w tytule" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Wybierz przypadkowy serwer (nie pytaj)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Sortuj stacje minimalnym Bitrate" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "MInimalny Bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Błąd sieci" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.pt_br/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.pt_br/strings.po new file mode 100644 index 0000000000..16124c74ac --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.pt_br/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Todos os subgêneros" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "As 500 melhores estações" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Procurar por gênero" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Minhas estações" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Pesquisar estação por nome" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Pesquisar estação por faixa sendo reproduzida" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Adicionar a 'Minhas Estações'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Remover de 'Minhas Estações'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Mostrar subgêneros" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Numerar estações na lista" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Mostrar bitrate no título" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Escolher um servidor aleatório (Não perguntar)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrar estações por bitrate mínimo" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Bitrate mínimo" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Erro na rede" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.pt_pt/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.pt_pt/strings.po new file mode 100644 index 0000000000..9ddc248656 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.pt_pt/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: pt_PT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Todos os sub-géneros" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 Rádios" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Procurar por género" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Minhas Rádios" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Procurar Rádio por nome" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Procurar Rádio por faixa em reprodução" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Adicionar às 'Minhas Rádios'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Remover das 'Minhas Rádios'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Exibir Sub-Géneros" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Num. rádios na lista" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Exibir bitrate no Título" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Escolher um servidor ao acaso (não perguntar)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrar estações por bitrate mínimo" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Bitrate Mínimo" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Erro de Rede" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ro_ro/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ro_ro/strings.po new file mode 100644 index 0000000000..9ecedec805 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ro_ro/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ro_ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ru_ru/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ru_ru/strings.po new file mode 100644 index 0000000000..e0ef546403 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ru_ru/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Все подстили" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "ТОП 500 станций" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Просмотр по стилю" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Мои станции" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Поиск станции по названию" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Искать станции, воспроизводящие текущую дорожку" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Добавить в 'Мои станции'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Убрать из 'Моих станций'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Показать подстили" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Количество станций в списке" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Показывать битрейт в заголовке" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Выбирать случайный сервер (Не спрашивать)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Фильтр станций по минимальному битрейту" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Минимальный битрейт" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 кбит/с" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 кбит/с" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 кбит/с" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 кбит/с" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Ошибка сети" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.sk_sk/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.sk_sk/strings.po new file mode 100644 index 0000000000..fc2fc047be --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.sk_sk/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: sk_SK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Všetky pod-žánre" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Top 500 staníc" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Prehliadať podľa žánra" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Moje stanice" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Hľadať stanicu podľa mena" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Hľadať stanicu podľa práve hranej skladby" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Pridať do 'Moje stanice'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Odstrániť z 'Moje stanice'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Ukáž pod-žánre" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Počet staníc v zozname" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Ukázať dátový tok v Názve" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Zvoliť náhodný server (nepýtať sa)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Sieťová chyba" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.sl_si/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.sl_si/strings.po new file mode 100644 index 0000000000..0d1982b258 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.sl_si/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: sl_SI\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Vse podzvrsti" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "500 naj postaj" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Prebrskaj glede na zvrst" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Moje postaje" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Išči postajo po imenu" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Dodaj med »Moje postaje«" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Odstrani iz »Mojih postaj«" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Pokaži podzvrsti" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Omrežna napaka" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.sq_al/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.sq_al/strings.po new file mode 100644 index 0000000000..1fc2c48af2 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.sq_al/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: sq_al\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.sr_rs/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.sr_rs/strings.po new file mode 100644 index 0000000000..1b07fbe665 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.sr_rs/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: sr_rs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.sr_rs@latin/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.sr_rs@latin/strings.po new file mode 100644 index 0000000000..6fd32e50b1 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.sr_rs@latin/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: sr_Latn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.sv_se/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.sv_se/strings.po new file mode 100644 index 0000000000..c3e11e1dae --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.sv_se/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: sv_SE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Alla undergenrer" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "Topp 500 stationer" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Bläddra efter genre" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Mina stationer" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Sök station efter namn" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Sök station som spelar låt" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Lägg till i 'Mina stationer'" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Radera från 'Mina stationer'" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Visa undergenrer" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "Antal stationer i lista" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Visa bitrate i titel" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Välj en slumpmässig server (fråga inte)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "Filtrera stationer efter minsta bitrate" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "Minsta bitrate" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Nätverksfel" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.ta_in/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.ta_in/strings.po new file mode 100644 index 0000000000..b08fda032c --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.ta_in/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: ta_IN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "அனைத்து துணைவகைகள்" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "உச்சி 500 நிலையங்கள்" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "வகைபடி உலாவு" + +msgctxt "#30003" +msgid "My Stations" +msgstr "என்னுடைய நிலையங்கள்" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "பெயர்படி நிலையத்தனி தேடுக" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "தற்போது வாசிக்கப்படும் பாடலின் நிலையத்தனி தேடுக" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "'என்னுடைய நிலையங்கள்' பட்டியலில் சேர்க்கவும்" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "'என்னுடைய நிலையங்கள்' பட்டியலில் இருந்து நீக்கவும்" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "துணை வகைகளை காண்பிக்கவும்" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "பட்டியலில் நிலையங்கள் எண்ணை காண்பி" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "தலைப்பில் பிட்டு விகிதத்தை காண்பி" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "எதாவது சேவையகத்தை தேர்வுசெய் (கேள்வி தேவையில்லை)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "குறைந்தபட்ச பிட்டு விகிதப்படி நிலையங்களை வடிகட்டவும்" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "குறைந்தபட்ச பிட்டு விகிதம்" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "பிணைய பிழை" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.th_th/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.th_th/strings.po new file mode 100644 index 0000000000..a892179fa5 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.th_th/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: th_th\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.tr_tr/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.tr_tr/strings.po new file mode 100644 index 0000000000..ce7fd8d426 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.tr_tr/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "Tüm Alt Türler" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "En İyi 500 İstasyon" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "Tarza göre gözat" + +msgctxt "#30003" +msgid "My Stations" +msgstr "Benim İstasyonlarım" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "Isme Göre Istasyon Ara" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "Geçerli Parçayı Çalan Istasyonu Ara" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "Benim İstasyonlarım'a Ekle" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "Benim İstasyonlarım'dan Kaldır" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "Alt tarzları göster" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "İstasyonları listede numaralandır" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "Başlıkta Bit Oranını Göster" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "Rastgele Bir Sunucu Seç (Bir Daha Sorma)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "İstasyonları En Düşük Bit Oranına Göre Filtrele" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "En Düşük Bit Oranı" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "Ağ Hatası" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.uk_ua/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.uk_ua/strings.po new file mode 100644 index 0000000000..deb329e067 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.uk_ua/strings.po @@ -0,0 +1,94 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: uk_ua\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.uz_uz/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.uz_uz/strings.po new file mode 100644 index 0000000000..8fe3bbac07 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.uz_uz/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: uz_uz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.vi_vn/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.vi_vn/strings.po new file mode 100644 index 0000000000..8bdb022d47 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.vi_vn/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: vi_vn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.zh_cn/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.zh_cn/strings.po new file mode 100644 index 0000000000..f80f0062b1 --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.zh_cn/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Team-Kodi\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "所有子类" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "500强站点" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "按类别浏览" + +msgctxt "#30003" +msgid "My Stations" +msgstr "我的电台" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "按名字搜索电台" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "按当前播放曲目搜索电台" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "加入“我的电台”" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "从“我的电台”删除" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "显示子类" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "列表中电台数" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "标题显示比特率" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "选择一个随机服务器(不提问)" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "按最小比特率筛选电台" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "最小比特率" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "96 kbit/s" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "128 kbit/s" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "160 kbit/s" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "192 kbit/s" + +msgctxt "#30200" +msgid "Network Error" +msgstr "网络错误" diff --git a/plugin.audio.shoutcast/resources/language/resource.language.zh_tw/strings.po b/plugin.audio.shoutcast/resources/language/resource.language.zh_tw/strings.po new file mode 100644 index 0000000000..0724c87c2c --- /dev/null +++ b/plugin.audio.shoutcast/resources/language/resource.language.zh_tw/strings.po @@ -0,0 +1,93 @@ +# Kodi Media Center language file +# Addon Name: SHOUTcast 2 +# Addon id: plugin.audio.shoutcast +# Addon Provider: Tristan Fischer (sphere@dersphere.de) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_tw\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +msgctxt "#30000" +msgid "All subgenres" +msgstr "" + +msgctxt "#30001" +msgid "Top 500 Stations" +msgstr "" + +msgctxt "#30002" +msgid "Browse by genre" +msgstr "" + +msgctxt "#30003" +msgid "My Stations" +msgstr "" + +msgctxt "#30004" +msgid "Search Station by name" +msgstr "" + +msgctxt "#30005" +msgid "Search Current Track playing Station" +msgstr "" + +msgctxt "#30010" +msgid "Add to 'My Stations'" +msgstr "" + +msgctxt "#30011" +msgid "Remove from 'My Stations'" +msgstr "" + +msgctxt "#30100" +msgid "Show Subgenres" +msgstr "" + +msgctxt "#30101" +msgid "Num stations in list" +msgstr "" + +msgctxt "#30102" +msgid "Show Bitrate in Title" +msgstr "" + +msgctxt "#30103" +msgid "Choose a random server (Don't ask)" +msgstr "" + +msgctxt "#30104" +msgid "Filter stations by minium Bitrate" +msgstr "" + +msgctxt "#30110" +msgid "Minimum Bitrate" +msgstr "" + +msgctxt "#30111" +msgid "96 kbit/s" +msgstr "" + +msgctxt "#30112" +msgid "128 kbit/s" +msgstr "" + +msgctxt "#30113" +msgid "160 kbit/s" +msgstr "" + +msgctxt "#30114" +msgid "192 kbit/s" +msgstr "" + +msgctxt "#30200" +msgid "Network Error" +msgstr "" diff --git a/plugin.audio.shoutcast/resources/lib/__init__.py b/plugin.audio.shoutcast/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.shoutcast/resources/lib/api.py b/plugin.audio.shoutcast/resources/lib/api.py new file mode 100644 index 0000000000..a81b9f9593 --- /dev/null +++ b/plugin.audio.shoutcast/resources/lib/api.py @@ -0,0 +1,185 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2012 Tristan Fischer (sphere@dersphere.de) +# +# 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 . +# + +import json +import sys +import random +import xmltodict + +from urllib.parse import urlencode +from urllib.request import urlopen, Request, HTTPError, URLError + + +API_URL = 'http://api.shoutcast.com/' +PLAYLIST_URL = 'http://yp.shoutcast.com/sbin/tunein-station.m3u?id={station_id}' + + +class NetworkError(Exception): + pass + + +class ShoutcastApi(): + + USER_AGENT = ( + 'Mozilla/5.0 (Windows NT 6.1; WOW64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/40.0.2214.111 Safari/537.36' + ) + + def __init__(self, api_key, limit=500): + self.api_key = api_key + self.set_limit(limit) + + def set_limit(self, limit): + self.limit = int(limit) + + def get_top500_stations(self): + path = 'legacy/Top500' + data = self.__api_call(path) + return self._parse_stations(data['stationlist']) + + def get_genres(self, parent_id=None): + params = {'f': 'json'} + if parent_id: + path = 'genre/secondary' + params['parentid'] = int(parent_id) + else: + path = 'genre/primary' + data = self.__api_call(path, params) + return self._parse_genre(data['response']['data']['genrelist']) + + def get_stations(self, genre_id, page=0): + params = { + 'f': 'xml', + 'genre_id': int(genre_id), + 'limit': '%d,%d' % (self.limit * page, self.limit) + } + path = 'station/advancedsearch' + data = self.__api_call(path, params) + return self._parse_stations(data['response']['data']['stationlist']) + + def get_station(self, station_id, station_name): + # This is hacky but there is no other way to get a single station by id + stations = self.search_stations(station_name) + station = [s for s in stations if int(s['id']) == int(station_id)] + return station[0] + + def search_stations(self, search_string, page=0): + params = { + 'f': 'xml', + 'search': search_string, + 'limit': '%d,%d' % (self.limit * page, self.limit) + } + path = 'legacy/stationsearch' + data = self.__api_call(path, params) + return self._parse_stations(data['stationlist']) + + def search_current_track(self, search_string, page=0): + params = { + 'f': 'xml', + 'ct': search_string, + 'limit': '%d,%d' % (self.limit * page, self.limit) + } + path = 'station/nowplaying' + data = self.__api_call(path, params) + return self._parse_stations(data['response']['data']['stationlist']) + + @staticmethod + def _parse_stations(stations): + + def __clean(title): + s = ' - a SHOUTcast.com member station' + return title.replace(s, '') + + items = stations.get('station', []) + if not isinstance(items, list): + items = [items, ] + return [{ + 'id': int(station['@id']), + 'name': __clean(station.get('@name', '')), + 'bitrate': int(station.get('@br', 0)), + 'listeners': int(station.get('@lc', 0)), + 'current_track': station.get('@ct', ''), + 'genre': station.get('@genre', ''), + 'media_type': station.get('@mt', ''), + } for station in items] + + @staticmethod + def _parse_genre(genres): + return [{ + 'id': int(genre['id']), + 'name': genre.get('name', ''), + 'has_childs': genre.get('haschildren'), + 'has_parent': int(genre.get('parentid', 0)) != 0 + } for genre in genres.get('genre', [])] + + def resolve(self, station_id): + response = self.__urlopen(PLAYLIST_URL.format(station_id=station_id)) + stream_urls = [ + l for l in response.splitlines() + if l.strip() and not l.strip().startswith(b'#') + ] + if stream_urls: + return random.choice(stream_urls) + + def __api_call(self, path, params=None): + if not params: + params = {} + params['k'] = self.api_key + url = API_URL + path + if params: + url += '?%s' % urlencode(params) + response = self.__urlopen(url) + if params.get('f') == 'json': + data = json.loads(response) + else: + data = xmltodict.parse(response) + return data + + def __urlopen(self, url): + req = Request(url) + # req.add_header('User Agent', self.USER_AGENT) + try: + response = urlopen(req).read() + except HTTPError as error: + raise NetworkError('HTTPError: %s' % error) + except URLError as error: + raise NetworkError('URLError: %s' % error) + return response + + +def test(): + api = ShoutcastApi('sh1t7hyn3Kh0jhlV') + assert api.get_top500_stations() + genres = api.get_genres() + assert genres + for genre in genres[0:4]: + stations = api.get_stations(genre['id']) + assert stations + subgenres = api.get_genres(genre['id']) + assert subgenres + for subgenre in subgenres[0:1]: + stations = api.get_stations(subgenre['id']) + assert stations + assert api.search_stations('sky.fm') + assert api.search_current_track('Rihanna') + + +if __name__ == '__main__': + test() diff --git a/plugin.audio.shoutcast/resources/lib/plugin.py b/plugin.audio.shoutcast/resources/lib/plugin.py new file mode 100644 index 0000000000..1edcacc9ea --- /dev/null +++ b/plugin.audio.shoutcast/resources/lib/plugin.py @@ -0,0 +1,258 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2012 Tristan Fischer (sphere@dersphere.de) +# +# 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 xbmcswift2 import Plugin +from resources.lib.api import ShoutcastApi, NetworkError + +plugin = Plugin() +api = ShoutcastApi('sh1t7hyn3Kh0jhlV') +my_stations = plugin.get_storage('my_stations.json', file_format='json') + +STRINGS = { + 'all': 30000, + 'top500_stations': 30001, + 'browse_by_genre': 30002, + 'my_stations': 30003, + 'search_station': 30004, + 'search_current_track': 30005, + 'add_to_my_stations': 30010, + 'remove_from_my_stations': 30011, + 'network_error': 30200 +} + + +@plugin.route('/') +def show_root_menu(): + items = ( + {'label': _('top500_stations'), + 'fanart': plugin.fanart, + 'path': plugin.url_for('show_top500_stations')}, + {'label': _('browse_by_genre'), + 'fanart': plugin.fanart, + 'path': plugin.url_for('show_genre')}, + {'label': _('search_station'), + 'fanart': plugin.fanart, + 'path': plugin.url_for('search_station')}, + {'label': _('search_current_track'), + 'fanart': plugin.fanart, + 'path': plugin.url_for('search_current_track')}, + {'label': _('my_stations'), + 'fanart': plugin.fanart, + 'path': plugin.url_for('show_my_stations')}, + ) + return plugin.finish(items) + + +@plugin.route('/top500/') +def show_top500_stations(): + items = get_cached(api.get_top500_stations, TTL=60) + return __add_stations(items) + + +@plugin.route('/genres/', name='show_genre') +@plugin.route('/genres//', name='show_subgenre') +def show_genre(parent_genre_id=None): + show_subgenres = plugin.get_setting('show_subgenres', bool) + genres = get_cached(api.get_genres, parent_genre_id, TTL=1440) + items = [] + if show_subgenres and parent_genre_id: + items.append({ + 'label': '[[ %s ]]' % _('all'), + 'path': plugin.url_for( + endpoint='show_stations', + genre_id=str(parent_genre_id) + ) + }) + for genre in genres: + item = {'label': genre.get('name')} + if show_subgenres and genre.get('has_childs'): + item['path'] = plugin.url_for( + endpoint='show_subgenre', + parent_genre_id=str(genre['id']) + ) + else: + item['path'] = plugin.url_for( + endpoint='show_stations', + genre_id=str(genre['id']) + ) + items.append(item) + return plugin.finish(items) + + +@plugin.route('/stations//') +def show_stations(genre_id): + items = get_cached(api.get_stations, genre_id, TTL=60) + return __add_stations(items) + + +@plugin.route('/resolve/') +def resolve_play_url(station_id): + stream_url = api.resolve(station_id) + if stream_url: + plugin.set_resolved_url(stream_url) + + +@plugin.route('/search/station/') +def search_station(): + search_string = plugin.keyboard(heading=_('search_station')) + if search_string: + url = plugin.url_for( + endpoint='search_station_result', + search_string=search_string + ) + plugin.redirect(url) + + +@plugin.route('/search/station//') +def search_station_result(search_string): + stations = api.search_stations(search_string) + return __add_stations(stations) + + +@plugin.route('/search/current_track/') +def search_current_track(): + search_string = plugin.keyboard(heading=_('search_current_track')) + if search_string: + url = plugin.url_for( + endpoint='search_current_track_result', + search_string=search_string + ) + plugin.redirect(url) + + +@plugin.route('/search/current_track//') +def search_current_track_result(search_string): + stations = api.search_current_track(search_string) + return __add_stations(stations) + + +@plugin.route('/my/') +def show_my_stations(): + stations = my_stations.values() + return __add_stations(stations) + + +@plugin.route('/my/add//') +def add_to_my_stations(station_id, station_name): + station = api.get_station(station_id, station_name) + my_stations[station_id] = station + my_stations.sync() + + +@plugin.route('/my/del/') +def del_from_my_stations(station_id): + if station_id in my_stations: + del my_stations[station_id] + my_stations.sync() + + +def __add_stations(stations): + addon_id = plugin._addon.getAddonInfo('id') + icon = plugin.icon + my_stations_ids = my_stations.keys() + items = [] + show_bitrate = plugin.get_setting('show_bitrate_in_title', bool) + if plugin.get_setting('bitrate_filter_enabled', bool): + bitrates = (96, 128, 160, 192) + min_bitrate = plugin.get_setting('bitrate_filter', choices=bitrates) + else: + min_bitrate = None + for i, station in enumerate(stations): + if min_bitrate and int(station.get('bitrate', 0)) < min_bitrate: + continue + station_id = str(station['id']) + if not station_id in my_stations_ids: + context_menu = [( + _('add_to_my_stations'), + 'RunPlugin(%s)' % plugin.url_for( + endpoint='add_to_my_stations', + station_id=station_id, + station_name=station.get('name', '') + ) + )] + else: + context_menu = [( + _('remove_from_my_stations'), + 'RunPlugin(%s)' % plugin.url_for( + endpoint='del_from_my_stations', + station_id=station_id + ) + )] + if show_bitrate and station.get('bitrate'): + name = '%s [%s kbps]' % (station['name'], station['bitrate']) + else: + name = station.get('name', '') + item = { + 'label': name, + 'thumbnail': icon, + 'fanart': plugin.fanart, + 'info': { + 'count': i, + 'genre': station.get('genre') or '', + 'size': station.get('bitrate') or 0, + 'listeners': station.get('listeners') or 0, + 'artist': station.get('current_track') or '', + 'tracknumber': station['id'], + 'comment': station.get('media_type') or '' + }, + 'context_menu': context_menu, + 'is_playable': True, + 'path': plugin.url_for( + endpoint='resolve_play_url', + station_id=station['id'], + ), + 'properties': { + 'StationName': station.get('name', '') # Matrix++ only + } + } + items.append(item) + finish_kwargs = { + 'sort_methods': [ + 'LISTENERS', + 'BITRATE', + ('LABEL', '%X'), + ], + } + return plugin.finish(items, **finish_kwargs) + + +def get_cached(func, *args, **kwargs): + '''Return the result of func with the given args and kwargs + from cache or execute it if needed''' + @plugin.cached(kwargs.pop('TTL', 1440)) + def wrap(func_name, *args, **kwargs): + return func(*args, **kwargs) + return wrap(func.__name__, *args, **kwargs) + + +def _(string_id): + if string_id in STRINGS: + return plugin.get_string(STRINGS[string_id]) + else: + plugin.log.warning('String is missing: %s' % string_id) + return string_id + + +def run(): + limit = plugin.get_setting('limit', int) + api.set_limit(limit) + try: + plugin.run() + except NetworkError: + plugin.notify(msg=_('network_error')) diff --git a/plugin.audio.shoutcast/resources/settings.xml b/plugin.audio.shoutcast/resources/settings.xml new file mode 100644 index 0000000000..3ffb81c428 --- /dev/null +++ b/plugin.audio.shoutcast/resources/settings.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugin.audio.somafm/LICENSE.txt b/plugin.audio.somafm/LICENSE.txt new file mode 100644 index 0000000000..9cecc1d466 --- /dev/null +++ b/plugin.audio.somafm/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugin.audio.somafm/README.md b/plugin.audio.somafm/README.md new file mode 100644 index 0000000000..322e372e2c --- /dev/null +++ b/plugin.audio.somafm/README.md @@ -0,0 +1,44 @@ +# SomaFM XBMC Plugin + +![SomaFM icon](icon.png?raw=true) + +This description is a bit outdated. You can simply install this `addon` by browsing the official repositories from within Kodi. + +## Installation + +Installation currently requires you to know where your `addons` folder is located. Please refer to the [Kodi Wiki article on `userdata`](http://kodi.org/?title=Userdata) to find it. To find the `addons` folder, simply replace every instance of `userdata` in the article with `addons`. + +### Git + + 1. Change into your `addons` folder + 2. Clone the repository into a new folder `plugin.audio.somafm` + 3. Done + +On Unix based operating systems: + +```bash +cd ~/kodi/addons/ +git clone https://github.com/Soma-FM-Kodi-Add-On/plugin.audio.somafm.git plugin.audio.somafm +``` + +### ZIP + +Unfortunately, installing from a zip archive is not a lot easier + + 1. [Download the zip archive from GitHub](https://github.com/Soma-FM-Kodi-Add-On/plugin.audio.somafm/archive/master.zip) + 2. Extract the contents + 3. Rename the resulting folder `xbmc-somafm-master` to `plugin.audio.somafm` + 4. Move the folder to your `addons` folder + +On Unix based operating systems: + +```bash +wget --content-disposition https://github.com/Soma-FM-Kodi-Add-On/plugin.audio.somafm/archive/master.zip +unzip xbmc-somafm-master.zip +mv xbmc-somafm-master/ plugin.audio.somafm +mv plugin.audio.somafm/ ~/kodi/addons/ +``` + +![SomaFM fanart](fanart.jpg?raw=true) + +Lone DJ photo ©2000 Merin McDonell. Used with permission. diff --git a/plugin.audio.somafm/addon.xml b/plugin.audio.somafm/addon.xml new file mode 100644 index 0000000000..af75736362 --- /dev/null +++ b/plugin.audio.somafm/addon.xml @@ -0,0 +1,66 @@ + + + + + + + audio + + + GPL-3.0-or-later + all + Listener supported radio + Hörergetragenes Radio + Luisteraarondersteunde Radio + Over 30 unique channels of listener-supported, commercial-free, underground/alternative radio broadcasting from San Francisco. All music hand-picked by SomaFM's award-winning DJs and music directors. + Please consider donating at https://somafm.com to keep this service alive! + Über 30 einzigartige Sender vom hörergetragenen, werbefreien, underground / alternative Radio aus San Francisco. Die gesamte Musik ist handerlesen von SomaFMs ausgezeichneten DJs und Musikprogrammleitern. + Bitte denk über eine Spende auf https://somafm.com nach, um diesen Dienst am Leben zu halten! + Meer dan 30 unieke kanalen van luisteraarondersteunde, reclamevrije, alternatieve radiouitzendingen uit San Fransisco. Alle muziek is handgeselecteerd door SomaFM's bekroonde dj's en muzikale programmamakers. + Overweeg a.u.b. een donatie te doen op https://somafm.com om deze service in leven te houden! + https://somafm.com + https://github.com/Soma-FM-Kodi-Add-On/plugin.audio.somafm + + Version 2.0.1 - 2021-12-14 + - Updating details + - Implementing stability measures + - Feedback-based changes + + Version 2.0.0 - 2021-11-16 + - Update to Python3 + - Updating to adhere to newish automated testing tool standards + + Version 1.1.0 - 2016-02-22 + - Remove firewall mode in response to the SomaFM streaming server update + + Version 1.0.2 - 2015-11-08 + - Dutch translation and some polishing by PanderMusubi - thanks! + + Version 1.0.1 - 2015-01-01 + - Fix playback on Android client + + Version 1.0.0 - 2014-10-09 + - Use localized strings, supporting English and German for now + - Settings for caching the channel list and clearing the cache + - Include a donation reminder + + Version 0.1.0 - 2014-10-05 + - Automatically select a stream and instantly start playback when station is selected + - Cache playlist files + - Settings for quality, format and firewall streams + + Version 0.0.2 - 2013-07-21 + - Use SomaFM XML API. + + Version 0.0.1 - 2011-03-07 + - Initial version. + + + icon.png + fanart.jpg + + + diff --git a/plugin.audio.somafm/default.py b/plugin.audio.somafm/default.py new file mode 100644 index 0000000000..745d5c0367 --- /dev/null +++ b/plugin.audio.somafm/default.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +import resources.lib.addon as addon + +# Runs the add-on from here. +if __name__ == "__main__": + addon.run() diff --git a/plugin.audio.somafm/fanart.jpg b/plugin.audio.somafm/fanart.jpg new file mode 100644 index 0000000000..5e9b0a4a73 Binary files /dev/null and b/plugin.audio.somafm/fanart.jpg differ diff --git a/plugin.audio.somafm/icon.png b/plugin.audio.somafm/icon.png new file mode 100644 index 0000000000..d0e17de3a7 Binary files /dev/null and b/plugin.audio.somafm/icon.png differ diff --git a/plugin.audio.somafm/resources/language/resource.language.de_de/strings.po b/plugin.audio.somafm/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..9ff18895e1 --- /dev/null +++ b/plugin.audio.somafm/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,99 @@ +msgid "" +msgstr "" +"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 "#32000" +msgid "Stream" +msgstr "Stream" + +msgctxt "#32001" +msgid "Quality" +msgstr "Qualität" + +msgctxt "#32002" +msgid "Format" +msgstr "Format" + +msgctxt "#32004" +msgid "SomaFM" +msgstr "SomaFM" + +msgctxt "#32005" +msgid "Cache cleared." +msgstr "Cache gelöscht." + +msgctxt "#32006" +msgid "Cache" +msgstr "Cache" + +msgctxt "#32007" +msgid "Clear channel and playlist cache" +msgstr "Cache mit Senderliste und Playlists löschen" + +msgctxt "#32008" +msgid "Refresh channel list from server" +msgstr "Senderliste vom Server laden" + +msgctxt "#32009" +msgid "Please consider donating to keep SomaFM alive.\nVisit https://somafm.com" +msgstr "Bitte erhalte SomaFM mit Deiner Spende am Leben.\nBesuche https://somafm.com" + +#enums + +#quality + +msgctxt "#32913" +msgid "High, medium, low" +msgstr "Hoch, mittel, niedrig" + +msgctxt "#32912" +msgid "Medium, high, low" +msgstr "Mittel, hoch, niedrig" + +msgctxt "#32911" +msgid "Medium, low, high" +msgstr "Mittel, niedrig, hoch" + +msgctxt "#32910" +msgid "Low, medium, high" +msgstr "Niedrig, mittel, hoch" + +#format + +msgctxt "#32920" +msgid "MP3 only" +msgstr "Nur MP3" + +msgctxt "#32921" +msgid "MP3, AAC" +msgstr "MP3, AAC" + +msgctxt "#32922" +msgid "AAC, MP3" +msgstr "AAC, MP3" + +msgctxt "#32923" +msgid "AAC only" +msgstr "Nur AAC" + +#ttl + +msgctxt "#32930" +msgid "Always" +msgstr "Immer" + +msgctxt "#32931" +msgid "Once in a day" +msgstr "Einmal am Tag" + +msgctxt "#32932" +msgid "Once in a week" +msgstr "Einmal in der Woche" + +msgctxt "#32933" +msgid "Once in a month" +msgstr "Einmal im Monat" diff --git a/plugin.audio.somafm/resources/language/resource.language.en_gb/strings.po b/plugin.audio.somafm/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..2fc7e33553 --- /dev/null +++ b/plugin.audio.somafm/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,99 @@ +msgid "" +msgstr "" +"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 "#32000" +msgid "Stream" +msgstr "" + +msgctxt "#32001" +msgid "Quality" +msgstr "" + +msgctxt "#32002" +msgid "Format" +msgstr "" + +msgctxt "#32004" +msgid "SomaFM" +msgstr "" + +msgctxt "#32005" +msgid "Cache cleared." +msgstr "" + +msgctxt "#32006" +msgid "Cache" +msgstr "" + +msgctxt "#32007" +msgid "Clear channel and playlist cache" +msgstr "" + +msgctxt "#32008" +msgid "Refresh channel list from server" +msgstr "" + +msgctxt "#32009" +msgid "Please consider donating to keep SomaFM alive.\nVisit https://somafm.com" +msgstr "" + +#enums + +#quality + +msgctxt "#32913" +msgid "High, medium, low" +msgstr "" + +msgctxt "#32912" +msgid "Medium, high, low" +msgstr "" + +msgctxt "#32911" +msgid "Medium, low, high" +msgstr "" + +msgctxt "#32910" +msgid "Low, medium, high" +msgstr "" + +#format + +msgctxt "#32920" +msgid "MP3 only" +msgstr "" + +msgctxt "#32921" +msgid "MP3, AAC" +msgstr "" + +msgctxt "#32922" +msgid "AAC, MP3" +msgstr "" + +msgctxt "#32923" +msgid "AAC only" +msgstr "" + +#ttl + +msgctxt "#32930" +msgid "Always" +msgstr "" + +msgctxt "#32931" +msgid "Once in a day" +msgstr "" + +msgctxt "#32932" +msgid "Once in a week" +msgstr "" + +msgctxt "#32933" +msgid "Once in a month" +msgstr "" diff --git a/plugin.audio.somafm/resources/language/resource.language.nl_nl/strings.po b/plugin.audio.somafm/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..1c192c2963 --- /dev/null +++ b/plugin.audio.somafm/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,99 @@ +msgid "" +msgstr "" +"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 "#32000" +msgid "Stream" +msgstr "Stream" + +msgctxt "#32001" +msgid "Quality" +msgstr "Kwaliteit" + +msgctxt "#32002" +msgid "Format" +msgstr "Formaat" + +msgctxt "#32004" +msgid "SomaFM" +msgstr "SomaFM" + +msgctxt "#32005" +msgid "Cache cleared." +msgstr "Cache geleegd." + +msgctxt "#32006" +msgid "Cache" +msgstr "Cache" + +msgctxt "#32007" +msgid "Clear channel and playlist cache" +msgstr "Leeg cache van kanaal en playlist" + +msgctxt "#32008" +msgid "Refresh channel list from server" +msgstr "Ververs kanaallijst van server" + +msgctxt "#32009" +msgid "Please consider donating to keep SomaFM alive.\nVisit https://somafm.com" +msgstr "Doneer a.u.b. om SomaFM in leven te houden.\nGa naar https://somafm.com" + +#enums + +#quality + +msgctxt "#32913" +msgid "High, medium, low" +msgstr "Hoog, middel, laag" + +msgctxt "#32912" +msgid "Medium, high, low" +msgstr "Middel, hoog, laag" + +msgctxt "#32911" +msgid "Medium, low, high" +msgstr "Middel, laag, hoog" + +msgctxt "#32910" +msgid "Low, medium, high" +msgstr "Laag, middel, hoog" + +#format + +msgctxt "#32920" +msgid "MP3 only" +msgstr "Alleen MP3" + +msgctxt "#32921" +msgid "MP3, AAC" +msgstr "MP3, AAC" + +msgctxt "#32922" +msgid "AAC, MP3" +msgstr "AAC, MP#" + +msgctxt "#32923" +msgid "AAC only" +msgstr "Alleen AAC" + +#ttl + +msgctxt "#32930" +msgid "Always" +msgstr "Altijd" + +msgctxt "#32931" +msgid "Once in a day" +msgstr "Eens per dag" + +msgctxt "#32932" +msgid "Once in a week" +msgstr "Eens per week" + +msgctxt "#32933" +msgid "Once in a month" +msgstr "Eens per maand" diff --git a/plugin.audio.somafm/resources/lib/__init__.py b/plugin.audio.somafm/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.somafm/resources/lib/addon.py b/plugin.audio.somafm/resources/lib/addon.py new file mode 100644 index 0000000000..afdf7f5b74 --- /dev/null +++ b/plugin.audio.somafm/resources/lib/addon.py @@ -0,0 +1,262 @@ +import os +import shutil +import sys +import time +import urllib.parse +import urllib.request +import xml.etree.ElementTree as ET + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +import xbmcvfs +from xbmcgui import ListItem +from xbmcplugin import SORT_METHOD_GENRE, SORT_METHOD_LISTENERS, SORT_METHOD_UNSORTED + +from resources.lib.channel import Channel + +CHANNELS_FILE_NAME = "channels.xml" + +__addon__ = "SomaFM" +__addonid__ = "plugin.audio.somafm" +__version__ = "2.0.0" + +__seconds_per_day__ = 24 * 60 * 60 + + +def log(msg): + xbmc.log(str(msg), level=xbmc.LOGDEBUG) + + +log(sys.argv) + +rootURL = "https://somafm.com/" +tempdir = xbmcvfs.translatePath("special://home/userdata/addon_data/%s" % __addonid__) +xbmcvfs.mkdirs(tempdir) + +LOCAL_CHANNELS_FILE_PATH = os.path.join(tempdir, CHANNELS_FILE_NAME) + +try: + plugin_url = sys.argv[0] + handle = int(sys.argv[1]) + query = sys.argv[2] +except Exception as e: + xbmc.log(f"Initialization Failed: {e}", level=xbmc.LOGERROR) + plugin_url = "plugin://" + __addonid__ + handle = 0 + query = "" + + +def fetch_remote_channel_data(): + with urllib.request.urlopen(rootURL + CHANNELS_FILE_NAME) as response: + channel_data = response.read() + with open(LOCAL_CHANNELS_FILE_PATH, "w") as local_channels_xml: + local_channels_xml.write(channel_data.decode("utf-8")) + return channel_data + + +def fetch_local_channel_data(): + with open(LOCAL_CHANNELS_FILE_PATH) as local_channels_file: + return local_channels_file.read() + + +def fetch_cached_channel_data(): + if ( + os.path.getmtime(LOCAL_CHANNELS_FILE_PATH) + cache_ttl_in_seconds() + > time.time() + ): + return fetch_local_channel_data() + # don't delete the cached file so we can still use it as a fallback + # if something goes wrong fetching the channel data from server + + +def fetch_channel_data(*strategies): + for strategy in strategies: + try: + result = strategy() + if result is not None: + return result + except Exception as e: + xbmc.log(f"fetch_channel_data Failed: {e}", level=xbmc.LOGERROR) + + +def build_directory(): + channel_data = fetch_channel_data( + fetch_cached_channel_data, fetch_remote_channel_data, fetch_local_channel_data + ) + xml_data = ET.fromstring(channel_data) + + stations = xml_data.findall(".//channel") + for station in stations: + channel = Channel(handle, tempdir, station) + li = xbmcgui.ListItem( + channel.get_simple_element("title"), + channel.get_simple_element("description"), + plugin_url + channel.getid(), + ) + + li.setArt( + { + "icon": channel.geticon(), + "thumb": channel.getthumbnail(), + "fanart": xbmcvfs.translatePath( + "special://home/addons/%s/fanart.jpg" % __addonid__ + ), + } + ) + + li.setProperty("IsPlayable", "true") + + for element, info in [ + ("listeners", "listeners"), + ("genre", "genre"), + ("dj", "artist"), + ("description", "comment"), + ("title", "title"), + ]: + value = channel.get_simple_element(element) + li.setInfo("Music", {info: value}) + + xbmcplugin.addDirectoryItem( + handle=handle, + url=plugin_url + channel.getid(), + listitem=li, + totalItems=len(stations), + ) + xbmcplugin.addSortMethod(handle, SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(handle, SORT_METHOD_LISTENERS) + xbmcplugin.addSortMethod(handle, SORT_METHOD_GENRE) + + +def format_priority(): + setting = xbmcplugin.getSetting(handle, "priority_format") + result = [ + ["mp3"], + ["mp3", "aac"], + ["aac", "mp3"], + ["aac"], + ][int(setting)] + xbmc.log( + f"Format setting is {setting}, using priority {result}", level=xbmc.LOGDEBUG + ) + return result + + +def quality_priority(): + setting = xbmcplugin.getSetting(handle, "priority_quality") + result = [ + [ + "slowpls", + "fastpls", + "highestpls", + ], + [ + "fastpls", + "slowpls", + "highestpls", + ], + [ + "fastpls", + "highestpls", + "slowpls", + ], + [ + "highestpls", + "fastpls", + "slowpls", + ], + ][int(setting)] + xbmc.log( + f"Quality setting is {setting}, using priority {result}", level=xbmc.LOGDEBUG + ) + return result + + +def cache_ttl_in_seconds(): + setting = xbmcplugin.getSetting(handle, "cache_ttl") + result = [ + 0, + __seconds_per_day__, + 7 * __seconds_per_day__, + 30 * __seconds_per_day__, + ][int(setting)] + xbmc.log(f"Cache setting is {setting}, using ttl of {result}", level=xbmc.LOGDEBUG) + return result + + +def play(item_to_play): + channel_data = fetch_channel_data( + fetch_local_channel_data, fetch_remote_channel_data + ) + xml_data = ET.fromstring(channel_data) + try: + channel_data = xml_data.find(".//channel[@id='" + item_to_play + "']") + channel = Channel( + handle, tempdir, channel_data, quality_priority(), format_priority() + ) + except Exception as e: + xbmc.log(f"play({item_to_play}) threw an exception: {e}", level=xbmc.LOGDEBUG) + for element in xml_data.findall(".//channel"): + channel = Channel( + handle, tempdir, element, quality_priority(), format_priority() + ) + if channel.getid() == item_to_play: + break + + list_item = ListItem( + channel.get_simple_element("title"), + channel.get_simple_element("description"), + channel.get_content_url(), + ) + + list_item.setArt( + { + "icon": channel.geticon(), + "thumb": channel.getthumbnail(), + "fanart": xbmcvfs.translatePath( + "special://home/addons/%s/fanart.jpg" % __addonid__ + ), + } + ) + + xbmcplugin.setResolvedUrl(handle, True, list_item) + + +def clearcache(): + xbmc.log(f"Cache is being cleared", level=xbmc.LOGDEBUG) + try: + # delete the channels.xml + channel_file_path = os.path.join(tempdir, "channels.xml") + if os.path.isfile(channel_file_path): + os.remove(channel_file_path) + # delete all folders + for ls_item in os.listdir(tempdir): + path_to_check = tempdir + "/" + ls_item + if os.path.isdir(path_to_check): + shutil.rmtree(path_to_check) + # display dialog that cache was cleared + addon = xbmcaddon.Addon(id=__addonid__) + heading = addon.getLocalizedString(32004) + message = addon.getLocalizedString(32005) + xbmcgui.Dialog().notification(heading, message, xbmcgui.NOTIFICATION_INFO, 1000) + except Exception as e: + xbmc.log(f"Cache failed to clear: {e}", level=xbmc.LOGERROR) + + +def run(): + if handle == 0: + if query == "clearcache": + clearcache() + else: + xbmc.log(f"Unexpected query supplied: {query}", level=xbmc.LOGDEBUG) + else: + path = urllib.parse.urlparse(plugin_url).path + item_to_play = os.path.basename(path) + + if item_to_play: + play(item_to_play) + else: + build_directory() + + xbmcplugin.endOfDirectory(handle) diff --git a/plugin.audio.somafm/resources/lib/channel.py b/plugin.audio.somafm/resources/lib/channel.py new file mode 100644 index 0000000000..a7f83bb070 --- /dev/null +++ b/plugin.audio.somafm/resources/lib/channel.py @@ -0,0 +1,112 @@ +import os +import random +import shutil +import urllib.parse +import urllib.request +from xml.etree import ElementTree + +import xbmc +from xbmc import PLAYLIST_MUSIC + +__author__ = "Oderik" + + +class Channel(object): + def prepare_cache(self): + self.ensure_dir(self.cache_dir) + if not os.path.exists(self.version_file_path): + with open(self.version_file_path, "w") as version_file: + version_file.write(self.get_simple_element("updated")) + + def cleanup_cache(self): + if os.path.exists(self.version_file_path): + with open(self.version_file_path) as version_file: + cached_version = version_file.read() + if cached_version != self.get_simple_element("updated"): + version_file.close() + shutil.rmtree(self.cache_dir, True) + + def __init__( + self, + handle, + cache_dir, + source=ElementTree.Element("channel"), + quality_priority=None, + format_priority=None, + ): + self.handle = handle + self.source = source + self.cache_dir = os.path.join(cache_dir, self.getid()) + self.version_file_path = os.path.join(self.cache_dir, "updated") + self.cleanup_cache() + self.prepare_cache() + self.quality_priority = quality_priority + self.format_priority = format_priority + if not self.format_priority: + self.format_priority = ["mp3", "aac"] + if not self.quality_priority: + self.quality_priority = ["fastpls", "highestpls", "slowpls"] + + def get_simple_element(self, *tags): + for tag in tags: + element = self.source.find(".//" + tag) + if element is not None: + return element.text + + def __repr__(self): + return "{}: {} ({}, {})".format( + self.__class__.__name__, + self.getid(), + self.quality_priority, + self.format_priority, + ) + + def get_prioritized_playlists(self): + playlists = [] + for playlist_tag in self.quality_priority: + for format in self.format_priority: + for playlist_element in self.source.findall(playlist_tag): + format_attrib = playlist_element.attrib["format"] + if format in format_attrib: + playlists.append( + (playlist_tag, format_attrib, playlist_element.text) + ) + return playlists + + def getid(self): + return self.source.attrib["id"] + + def ensure_dir(self, filepath): + if not os.path.exists(filepath): + os.makedirs(filepath) + + def get_playlist_file(self, playlist_url): + url_path = urllib.parse.urlparse(playlist_url).path + filename = os.path.split(url_path)[1] + filepath = os.path.join(self.cache_dir, filename) + filepath = os.path.abspath(filepath) + if not os.path.exists(filepath): + with urllib.request.urlopen(playlist_url) as response: + self.prepare_cache() + with open(os.path.abspath(filepath), "w") as playlist_file: + playlist_file.write(response.read().decode("utf-8")) + return filepath + + def get_content_url(self): + for playlist_meta in self.get_prioritized_playlists(): + print("Trying " + str(playlist_meta)) + filepath = self.get_playlist_file(playlist_meta[2]) + play_list = xbmc.PlayList(PLAYLIST_MUSIC) + play_list.load(filepath) + streams = [] + for i in range(0, play_list.size()): + stream_url = play_list.__getitem__(i).getPath() + streams.append(stream_url) + if len(streams) > 0: + return random.choice(streams) + + def getthumbnail(self): + return self.get_simple_element("xlimage", "largeimage", "image") + + def geticon(self): + return self.get_simple_element("largeimage", "xlimage", "image") diff --git a/plugin.audio.somafm/resources/settings.xml b/plugin.audio.somafm/resources/settings.xml new file mode 100644 index 0000000000..605f5f27e4 --- /dev/null +++ b/plugin.audio.somafm/resources/settings.xml @@ -0,0 +1,63 @@ + + +
    + + + + 0 + 2 + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + 0 + Runscript(plugin.audio.somafm,0,clearcache) + + true + + + + + + +
    +
    diff --git a/plugin.audio.soundcloud/LICENSE.txt b/plugin.audio.soundcloud/LICENSE.txt new file mode 100644 index 0000000000..c85e3f5159 --- /dev/null +++ b/plugin.audio.soundcloud/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Jakob Linskeseder + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugin.audio.soundcloud/addon.py b/plugin.audio.soundcloud/addon.py new file mode 100644 index 0000000000..f024289d1f --- /dev/null +++ b/plugin.audio.soundcloud/addon.py @@ -0,0 +1,3 @@ +from resources import plugin + +plugin.run() diff --git a/plugin.audio.soundcloud/addon.xml b/plugin.audio.soundcloud/addon.xml new file mode 100644 index 0000000000..5696b9871f --- /dev/null +++ b/plugin.audio.soundcloud/addon.xml @@ -0,0 +1,36 @@ + + + + + + + + audio + + + SoundCloud – Musik- und Podcast-Streaming-Plattform + SoundCloud – Music and podcast streaming platform + SoundCloud ist eine Musik- und Podcast-Streaming-Plattform, mit der Du Millionen von Songs aus der ganzen Welt anhören kannst. + SoundCloud is a music and podcast streaming platform that lets you listen to millions of songs from around the world. + This plugin is not official, approved or endorsed by SoundCloud. + all + MIT + https://forum.kodi.tv/showthread.php?tid=206635 + https://soundcloud.com + https://github.com/jaylinski/kodi-addon-soundcloud + 4.0.2 (2022-03-13) +Fixed random CloudFront errors when sending requests +Added context-menu option for removing search-history items + +4.0.1 (2021-11-02) +Fixed error in "Discover"-folder + +4.0.0 (2020-12-12) +Added support for Kodi v19 (Matrix) + + + resources/icon.png + resources/fanart.jpg + + + diff --git a/plugin.audio.soundcloud/readme.md b/plugin.audio.soundcloud/readme.md new file mode 100644 index 0000000000..9f1441f09b --- /dev/null +++ b/plugin.audio.soundcloud/readme.md @@ -0,0 +1,80 @@ +# SoundCloud Add-on for [Kodi](https://github.com/xbmc/xbmc) + +Kodi logo + +[![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/jaylinski/kodi-addon-soundcloud.svg)](https://github.com/jaylinski/kodi-addon-soundcloud/releases) +[![Build Status](https://img.shields.io/github/workflow/status/jaylinski/kodi-addon-soundcloud/Continuous%20Integration/master.svg)](https://github.com/jaylinski/kodi-addon-soundcloud/actions) +[![Link to Kodi forum](https://img.shields.io/badge/Kodi-Forum-informational.svg)](https://forum.kodi.tv/showthread.php?tid=206635) +[![Link to Kodi wiki](https://img.shields.io/badge/Kodi-Wiki-informational.svg)](https://kodi.wiki/view/Add-on:SoundCloud) +[![Link to Kodi releases](https://img.shields.io/badge/Kodi-v19%20%22Matrix%22-green.svg)](https://kodi.wiki/view/Releases) + +This [Kodi](https://github.com/xbmc/xbmc) Add-on provides a minimal interface for SoundCloud. + +## Features + +* Search +* Discover new music +* Play tracks, albums and playlists + +## Installation + +### Kodi Repository + +Follow the instructions on [https://kodi.wiki/view/Add-on:SoundCloud](https://kodi.wiki/view/Add-on:SoundCloud). + +### Manual + +* [Download the latest release](https://github.com/jaylinski/kodi-addon-soundcloud/releases) (`plugin.audio.soundcloud.zip`) +* Copy the zip file to your Kodi system +* Open Kodi, go to Add-ons and select "Install from zip file" +* Select the file `plugin.audio.soundcloud.zip` + +## API + +Documentation of the **public** interface. + +### plugin://plugin.audio.soundcloud/play/?[track_id|playlist_id|url] + +Examples: + +* `plugin://plugin.audio.soundcloud/play/?track_id=1` +* `plugin://plugin.audio.soundcloud/play/?playlist_id=1` +* `plugin://plugin.audio.soundcloud/play/?url=https%3A%2F%2Fsoundcloud.com%2Fpslwave%2Fallwithit` + +Legacy (will be removed in v5.0): + +* `plugin://plugin.audio.soundcloud/play/?audio_id=1` Use `track_id=1` instead. + +## Development + +This add-on uses [Pipenv](https://pypi.org/project/pipenv/) to manage its dependencies. + +### Setup + +[Install Pipenv](https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv) and run `pipenv install --dev`. + +### Build + +Run `pipenv run build`. + +### Lint + +Run `pipenv run lint`. + +### Test + +Run `pipenv run test`. + +## Roadmap + +* Re-implement all features from original add-on +* Implement [enhancements](https://github.com/jaylinski/kodi-addon-soundcloud/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) + +## Attributions + +This add-on is strongly inspired by the [original add-on](https://github.com/SLiX69/plugin.audio.soundcloud) +developed by [bromix](https://kodi.tv/addon-author/bromix) and [SLiX](https://github.com/SLiX69). + +## Copyright and license + +This add-on is licensed under the MIT License - see `LICENSE.txt` for details. diff --git a/plugin.audio.soundcloud/resources/__init__.py b/plugin.audio.soundcloud/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.soundcloud/resources/fanart.jpg b/plugin.audio.soundcloud/resources/fanart.jpg new file mode 100644 index 0000000000..a16c5303f2 Binary files /dev/null and b/plugin.audio.soundcloud/resources/fanart.jpg differ diff --git a/plugin.audio.soundcloud/resources/icon.png b/plugin.audio.soundcloud/resources/icon.png new file mode 100644 index 0000000000..18ac441d03 Binary files /dev/null and b/plugin.audio.soundcloud/resources/icon.png differ diff --git a/plugin.audio.soundcloud/resources/language/resource.language.de_de/strings.po b/plugin.audio.soundcloud/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..429f82a8ec --- /dev/null +++ b/plugin.audio.soundcloud/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,148 @@ +# Kodi Media Center language file +# Addon Name: SoundCloud +# Addon id: plugin.audio.soundcloud +# Addon Provider: jaylinski +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: German (http://www.transifex.com/projects/p/xbmc-addons/language/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings - General +msgctxt "#30001" +msgid "General" +msgstr "Allgemein" + +msgctxt "#30002" +msgid "Audio" +msgstr "Audio" + +msgctxt "#30003" +msgid "Format" +msgstr "Format" + +msgctxt "#30011" +msgid "Search" +msgstr "Suche" + +msgctxt "#30012" +msgid "Items per page" +msgstr "Einträge pro Seite" + +msgctxt "#30013" +msgid "History size" +msgstr "Größe der Suchhistorie" + +msgctxt "#30060" +msgid "API v2" +msgstr "API v2" + +msgctxt "#30061" +msgid "Client-ID" +msgstr "Client-ID" + +msgctxt "#30062" +msgid "Localization" +msgstr "Sprache" + +msgctxt "#30063" +msgid "auto" +msgstr "automatisch" + +msgctxt "#30064" +msgid "disabled" +msgstr "deaktiviert" + +msgctxt "#30070" +msgid "Cache" +msgstr "Cache" + +msgctxt "#30071" +msgid "Clear cache" +msgstr "Cache leeren" + +# GUI - Root +msgctxt "#30101" +msgid "Search" +msgstr "Suche" + +msgctxt "#30102" +msgid "Charts" +msgstr "Charts" + +msgctxt "#30103" +msgid "Discover" +msgstr "Discover" + +msgctxt "#30108" +msgid "Settings" +msgstr "Einstellungen" + +msgctxt "#30109" +msgid "Sign in" +msgstr "Anmelden" + +# GUI - Search +msgctxt "#30201" +msgid "New search" +msgstr "Neue Suche" + +msgctxt "#30211" +msgid "People" +msgstr "Leute" + +msgctxt "#30212" +msgid "Albums" +msgstr "Alben" + +msgctxt "#30213" +msgid "Playlists" +msgstr "Playlists" + +msgctxt "#30214" +msgid "Spotlight" +msgstr "Spotlight" + +# GUI - Charts +msgctxt "#30301" +msgid "Top 50" +msgstr "Top 50" + +msgctxt "#30302" +msgid "New & hot" +msgstr "Neu und angesagt" + +# GUI - Settings +msgctxt "#30501" +msgid "Cache cleared" +msgstr "Cache gelöscht" + +# GUI - Context menus +msgctxt "#30601" +msgid "Remove" +msgstr "Entfernen" + +msgctxt "#30602" +msgid "Clear" +msgstr "Leeren" + +# GUI - Generic +msgctxt "#30901" +msgid "Next page" +msgstr "Nächste Seite" + +msgctxt "#30902" +msgid "Not available in your country" +msgstr "In deinem Land nicht verfügbar" + +msgctxt "#30903" +msgid "Preview" +msgstr "Vorschau" diff --git a/plugin.audio.soundcloud/resources/language/resource.language.en_gb/strings.po b/plugin.audio.soundcloud/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..65836bcfc4 --- /dev/null +++ b/plugin.audio.soundcloud/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,148 @@ +# Kodi Media Center language file +# Addon Name: SoundCloud +# Addon id: plugin.audio.soundcloud +# Addon Provider: jaylinski +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings - General +msgctxt "#30001" +msgid "General" +msgstr "" + +msgctxt "#30002" +msgid "Audio" +msgstr "" + +msgctxt "#30003" +msgid "Format" +msgstr "" + +msgctxt "#30011" +msgid "Search" +msgstr "" + +msgctxt "#30012" +msgid "Items per page" +msgstr "" + +msgctxt "#30013" +msgid "History size" +msgstr "" + +msgctxt "#30060" +msgid "API v2" +msgstr "" + +msgctxt "#30061" +msgid "Client-ID" +msgstr "" + +msgctxt "#30062" +msgid "Localization" +msgstr "" + +msgctxt "#30063" +msgid "auto" +msgstr "" + +msgctxt "#30064" +msgid "disabled" +msgstr "" + +msgctxt "#30070" +msgid "Cache" +msgstr "" + +msgctxt "#30071" +msgid "Clear cache" +msgstr "" + +# GUI - Root +msgctxt "#30101" +msgid "Search" +msgstr "" + +msgctxt "#30102" +msgid "Charts" +msgstr "" + +msgctxt "#30103" +msgid "Discover" +msgstr "" + +msgctxt "#30108" +msgid "Settings" +msgstr "" + +msgctxt "#30109" +msgid "Sign in" +msgstr "" + +# GUI - Search +msgctxt "#30201" +msgid "New search" +msgstr "" + +msgctxt "#30211" +msgid "People" +msgstr "" + +msgctxt "#30212" +msgid "Albums" +msgstr "" + +msgctxt "#30213" +msgid "Playlists" +msgstr "" + +msgctxt "#30214" +msgid "Spotlight" +msgstr "" + +# GUI - Charts +msgctxt "#30301" +msgid "Top 50" +msgstr "" + +msgctxt "#30302" +msgid "New & hot" +msgstr "" + +# GUI - Settings +msgctxt "#30501" +msgid "Cache cleared" +msgstr "" + +# GUI - Context menus +msgctxt "#30601" +msgid "Remove" +msgstr "" + +msgctxt "#30602" +msgid "Clear" +msgstr "" + +# GUI - Generic +msgctxt "#30901" +msgid "Next page" +msgstr "" + +msgctxt "#30902" +msgid "Not available in your country" +msgstr "" + +msgctxt "#30903" +msgid "Preview" +msgstr "" diff --git a/plugin.audio.soundcloud/resources/language/resource.language.nl_nl/strings.po b/plugin.audio.soundcloud/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..9c7a7970b8 --- /dev/null +++ b/plugin.audio.soundcloud/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,147 @@ +# Kodi Media Center language file +# Addon Name: SoundCloud +# Addon id: plugin.audio.soundcloud +# Addon Provider: jaylinski +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Jan Hekkerman\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings - General +msgctxt "#30001" +msgid "General" +msgstr "Algemeen" + +msgctxt "#30002" +msgid "Audio" +msgstr "Audio" + +msgctxt "#30003" +msgid "Format" +msgstr "Formaat" + +msgctxt "#30011" +msgid "Search" +msgstr "Zoeken" + +msgctxt "#30012" +msgid "Items per page" +msgstr "Items per pagina" + +msgctxt "#30013" +msgid "History size" +msgstr "Zoekgeschiedenis grootte" + +msgctxt "#30060" +msgid "API v2" +msgstr "" + +msgctxt "#30061" +msgid "Client-ID" +msgstr "" + +msgctxt "#30062" +msgid "Localization" +msgstr "" + +msgctxt "#30063" +msgid "auto" +msgstr "" + +msgctxt "#30064" +msgid "disabled" +msgstr "uitgeschakeld" + +msgctxt "#30070" +msgid "Cache" +msgstr "" + +msgctxt "#30071" +msgid "Clear cache" +msgstr "Cache wissen" + +# GUI - Root +msgctxt "#30101" +msgid "Search" +msgstr "Zoeken" + +msgctxt "#30102" +msgid "Charts" +msgstr "Hitlijsten" + +msgctxt "#30103" +msgid "Discover" +msgstr "Ontdekken" + +msgctxt "#30108" +msgid "Settings" +msgstr "Instellingen" + +msgctxt "#30109" +msgid "Sign in" +msgstr "Inloggen" + +# GUI - Search +msgctxt "#30201" +msgid "New search" +msgstr "Nieuwe zoekopdracht" + +msgctxt "#30211" +msgid "People" +msgstr "Personen" + +msgctxt "#30212" +msgid "Albums" +msgstr "Albums" + +msgctxt "#30213" +msgid "Playlists" +msgstr "Afspeellijsten" + +msgctxt "#30214" +msgid "Spotlight" +msgstr "Spotlight" + +# GUI - Charts +msgctxt "#30301" +msgid "Top 50" +msgstr "" + +msgctxt "#30302" +msgid "New & hot" +msgstr "Nieuw & Populair" + +# GUI - Settings +msgctxt "#30501" +msgid "Cache cleared" +msgstr "Cache gewist" + +# GUI - Context menus +msgctxt "#30601" +msgid "Remove" +msgstr "Verwijderen" + +msgctxt "#30602" +msgid "Clear" +msgstr "Zuiveren" + +# GUI - Generic +msgctxt "#30901" +msgid "Next page" +msgstr "Volgende pagina" + +msgctxt "#30902" +msgid "Not available in your country" +msgstr "Niet beschikbaar in jouw land" + +msgctxt "#30903" +msgid "Preview" +msgstr "" diff --git a/plugin.audio.soundcloud/resources/lib/__init__.py b/plugin.audio.soundcloud/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.soundcloud/resources/lib/kodi/__init__.py b/plugin.audio.soundcloud/resources/lib/kodi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.soundcloud/resources/lib/kodi/cache.py b/plugin.audio.soundcloud/resources/lib/kodi/cache.py new file mode 100644 index 0000000000..896e555cd1 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/kodi/cache.py @@ -0,0 +1,25 @@ +import time + + +class Cache: + def __init__(self, settings, vfs): + self.settings = settings + self.vfs = vfs + + def get(self, filename, age=60): + """ + Get a cached file. + :param filename: str + :type age: int Minutes + """ + file = self.vfs.read(filename) + + if file: + mtime = self.vfs.get_mtime(filename) + if (int(time.time()) - age * 60) > mtime: + return None + + return file + + def add(self, filename, data): + return self.vfs.write(filename, data) diff --git a/plugin.audio.soundcloud/resources/lib/kodi/items.py b/plugin.audio.soundcloud/resources/lib/kodi/items.py new file mode 100644 index 0000000000..683ae49ca1 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/kodi/items.py @@ -0,0 +1,175 @@ +from resources.lib.kodi.utils import format_bold +from resources.routes import * + +import urllib.parse +import xbmcgui + + +class Items: + def __init__(self, addon, addon_base, search_history): + self.addon = addon + self.addon_base = addon_base + self.search_history = search_history + + def root(self): + items = [] + + # Search + list_item = xbmcgui.ListItem(label=self.addon.getLocalizedString(30101)) + url = self.addon_base + PATH_SEARCH + items.append((url, list_item, True)) + + # Charts + list_item = xbmcgui.ListItem(label=self.addon.getLocalizedString(30102)) + url = self.addon_base + PATH_CHARTS + items.append((url, list_item, True)) + + # Discover + list_item = xbmcgui.ListItem(label=self.addon.getLocalizedString(30103)) + url = self.addon_base + PATH_DISCOVER + items.append((url, list_item, True)) + + # Settings + list_item = xbmcgui.ListItem(label=self.addon.getLocalizedString(30108)) + url = self.addon_base + "/?action=settings" + items.append((url, list_item, False)) + + # Sign in TODO + # list_item = xbmcgui.ListItem(label=addon.getLocalizedString(30109)) + # url = addon_base + "/action=signin" + # items.append((url, list_item, False)) + + return items + + def search(self): + items = [] + + # New search + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30201))) + url = self.addon_base + PATH_SEARCH + "?action=new" + items.append((url, list_item, True)) + + # Search history + history = self.search_history.get() + for k in sorted(list(history), reverse=True): + query = history[k].get("query") + list_item = xbmcgui.ListItem(label=query) + list_item.addContextMenuItems(self._search_context_menu(query)) + url = self.addon_base + PATH_SEARCH + "?" + urllib.parse.urlencode({ + "query": history[k].get("query") + }) + items.append((url, list_item, True)) + + return items + + def search_sub(self, query): + items = [] + + # People + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30211))) + url = self.addon_base + PATH_SEARCH + "?" + urllib.parse.urlencode({ + "action": "people", + "query": query + }) + items.append((url, list_item, True)) + + # Albums + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30212))) + url = self.addon_base + PATH_SEARCH + "?" + urllib.parse.urlencode({ + "action": "albums", + "query": query + }) + items.append((url, list_item, True)) + + # Playlists + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30213))) + url = self.addon_base + PATH_SEARCH + "?" + urllib.parse.urlencode({ + "action": "playlists", + "query": query + }) + items.append((url, list_item, True)) + + return items + + def user(self, id): + items = [] + + # Albums + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30212))) + url = self.addon_base + "/?" + urllib.parse.urlencode({ + "action": "call", + "call": "/users/{id}/albums".format(id=id) + }) + items.append((url, list_item, True)) + + # Playlists + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30213))) + url = self.addon_base + "/?" + urllib.parse.urlencode({ + "action": "call", + "call": "/users/{id}/playlists_without_albums".format(id=id) + }) + items.append((url, list_item, True)) + + # Spotlight + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30214))) + url = self.addon_base + "/?" + urllib.parse.urlencode({ + "action": "call", + "call": "/users/{id}/spotlight".format(id=id) + }) + items.append((url, list_item, True)) + + return items + + def charts(self): + items = [] + + # Top 50 + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30301))) + url = self.addon_base + PATH_CHARTS + "?" + urllib.parse.urlencode({ + "action": "top" + }) + items.append((url, list_item, True)) + + # Trending + list_item = xbmcgui.ListItem(label=format_bold(self.addon.getLocalizedString(30302))) + url = self.addon_base + PATH_CHARTS + "?" + urllib.parse.urlencode({ + "action": "trending" + }) + items.append((url, list_item, True)) + + return items + + def from_collection(self, collection): + items = [] + + for item in collection.items: + items.append(item.to_list_item(self.addon_base)) + + if collection.next_href: + next_item = xbmcgui.ListItem(label=self.addon.getLocalizedString(30901)) + url = self.addon_base + "/?" + urllib.parse.urlencode({ + "action": "call", + "call": collection.next_href + }) + items.append((url, next_item, True)) + + return items + + def _search_context_menu(self, query): + return [ + ( + self.addon.getLocalizedString(30601), + "RunPlugin({}/{}?{})".format( + self.addon_base, PATH_SEARCH, urllib.parse.urlencode({ + "action": "remove", + "query": query + }) + ) + ), + ( + self.addon.getLocalizedString(30602), + "RunPlugin({}/{}?{})".format( + self.addon_base, PATH_SEARCH, urllib.parse.urlencode({"action": "clear"}) + ) + ), + ] diff --git a/plugin.audio.soundcloud/resources/lib/kodi/search_history.py b/plugin.audio.soundcloud/resources/lib/kodi/search_history.py new file mode 100644 index 0000000000..1afa89099b --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/kodi/search_history.py @@ -0,0 +1,37 @@ +from time import time + + +class SearchHistory: + + filename = "search_history.json" + + def __init__(self, settings, vfs): + self.settings = settings + self.size = int(self.settings.get("search.history.size")) + self.vfs = vfs + self.history = self.vfs.get_json_as_obj(self.filename) + + def get(self): + return {k: self.history[k] for k in list(self.history)[:self.size]} + + def add(self, query): + for k, v in self.history.items(): + if v["query"] == query: + return + + self.history[str(int(time()))] = {"query": query} + self.history = self._reduce(self.history) + self._save() + + def remove(self, query): + self.history = {k: v for k, v in list(self.history.items()) if v["query"] != query} + self._save() + + def clear(self): + return self.vfs.delete(self.filename) + + def _save(self): + return self.vfs.save_obj_to_json(self.filename, self.history) + + def _reduce(self, search): + return {k: search[k] for k in sorted(list(search), reverse=True)[:self.size]} diff --git a/plugin.audio.soundcloud/resources/lib/kodi/settings.py b/plugin.audio.soundcloud/resources/lib/kodi/settings.py new file mode 100644 index 0000000000..59026497ed --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/kodi/settings.py @@ -0,0 +1,30 @@ +class Settings: + + AUDIO_FORMATS = { + "0": { + "mime_type": "audio/ogg; codecs=\"opus\"", + "protocol": "hls", + }, + "1": { + "mime_type": "audio/mpeg", + "protocol": "hls", + }, + "2": { + "mime_type": "audio/mpeg", + "protocol": "progressive", + } + } + + APIV2_LOCALE = { + "auto": "0", + "disabled": "1" + } + + def __init__(self, addon): + self.addon = addon + + def get(self, id): + return self.addon.getSetting(id) + + def set(self, id, value): + return self.addon.setSetting(id, value) diff --git a/plugin.audio.soundcloud/resources/lib/kodi/utils.py b/plugin.audio.soundcloud/resources/lib/kodi/utils.py new file mode 100644 index 0000000000..8ed36793ea --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/kodi/utils.py @@ -0,0 +1,2 @@ +def format_bold(text): + return "[B]" + text + "[/B]" diff --git a/plugin.audio.soundcloud/resources/lib/kodi/vfs.py b/plugin.audio.soundcloud/resources/lib/kodi/vfs.py new file mode 100644 index 0000000000..92bba3072d --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/kodi/vfs.py @@ -0,0 +1,64 @@ +import json +import os +import xbmcvfs + + +class VFS: + def __init__(self, path): + self.path = path + if not xbmcvfs.exists(self.path): + xbmcvfs.mkdir(self.path) + + def read(self, filename): + filepath = os.path.join(self.path, filename) + if xbmcvfs.exists(filepath): + with xbmcvfs.File(filepath) as file: + return file.read() + else: + return None + + def write(self, filename, string): + filepath = os.path.join(self.path, filename) + with xbmcvfs.File(filepath, "w") as file: + return filepath if file.write(string) else False + + def delete(self, filename): + filepath = os.path.join(self.path, filename) + return xbmcvfs.delete(filepath) + + def remove_dir(self, path): + dir_list, file_list = xbmcvfs.listdir(path) + + for file in file_list: + xbmcvfs.delete(os.path.join(path, file)) + + for directory in dir_list: + self.remove_dir(os.path.join(path, directory)) + + xbmcvfs.rmdir(path) + + def destroy(self): + """ + Deletes the VFS folder and all files in it. + """ + self.remove_dir(self.path) + + def get_mtime(self, filename): + """ + Returns last modification time. + :rtype: int Timestamp + """ + filepath = os.path.join(self.path, filename) + stat = xbmcvfs.Stat(filepath) + return stat.st_mtime() + + def get_json_as_obj(self, filename, default=None): + string = self.read(filename) + if string: + return json.loads(string) + else: + return default if default else {} + + def save_obj_to_json(self, filename, obj): + string = json.dumps(obj) + return self.write(filename, string) diff --git a/plugin.audio.soundcloud/resources/lib/models/__init__.py b/plugin.audio.soundcloud/resources/lib/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.soundcloud/resources/lib/models/list_item.py b/plugin.audio.soundcloud/resources/lib/models/list_item.py new file mode 100644 index 0000000000..1d173ab471 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/models/list_item.py @@ -0,0 +1,14 @@ +import xbmcgui + + +class ListItem: + id = 0 + label = "" + label2 = None + + def __init__(self, id, label): + self.id = id + self.label = label + + def to_list_item(self, addon_base): + return addon_base, xbmcgui.ListItem(label=self.label), False diff --git a/plugin.audio.soundcloud/resources/lib/models/playlist.py b/plugin.audio.soundcloud/resources/lib/models/playlist.py new file mode 100644 index 0000000000..fead0642d2 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/models/playlist.py @@ -0,0 +1,22 @@ +from resources.lib.models.list_item import ListItem +import urllib.parse +import xbmcgui + + +class Playlist(ListItem): + thumb = "" + info = {} + is_album = False + + def to_list_item(self, addon_base): + list_item = xbmcgui.ListItem(label=self.label, label2=self.label2) + list_item.setArt({"thumb": self.thumb}) + list_item.setInfo("music", { + "title": self.label + }) + url = addon_base + "/?" + urllib.parse.urlencode({ + "action": "call", + "call": "/playlists/{id}".format(id=self.id) + }) + + return url, list_item, True diff --git a/plugin.audio.soundcloud/resources/lib/models/selection.py b/plugin.audio.soundcloud/resources/lib/models/selection.py new file mode 100644 index 0000000000..a21f69c348 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/models/selection.py @@ -0,0 +1,19 @@ +from resources.lib.models.list_item import ListItem +from resources.routes import * +import urllib.parse +import xbmcgui + + +class Selection(ListItem): + info = {} + + def to_list_item(self, addon_base): + list_item = xbmcgui.ListItem(label=self.label, label2=self.label2) + list_item.setInfo("music", { + "title": self.info.get("description") + }) + url = addon_base + PATH_DISCOVER + "?" + urllib.parse.urlencode({ + "selection": self.id + }) + + return url, list_item, True diff --git a/plugin.audio.soundcloud/resources/lib/models/track.py b/plugin.audio.soundcloud/resources/lib/models/track.py new file mode 100644 index 0000000000..7791a8eca1 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/models/track.py @@ -0,0 +1,34 @@ +from resources.lib.models.list_item import ListItem +import urllib.parse +import xbmcaddon +import xbmcgui + +blocked = xbmcaddon.Addon().getLocalizedString(30902) +preview = xbmcaddon.Addon().getLocalizedString(30903) + + +class Track(ListItem): + blocked = False + preview = False + thumb = "" + media = "" + info = {} + + def to_list_item(self, addon_base): + list_item_label = "[%s] " % blocked + self.label if self.blocked else self.label + list_item_label = "[%s] " % preview + self.label if self.preview else list_item_label + list_item = xbmcgui.ListItem(label=list_item_label) + url = addon_base + "/play/?" + urllib.parse.urlencode({"media_url": self.media}) + list_item.setArt({"thumb": self.thumb}) + list_item.setInfo("music", { + "artist": self.info.get("artist"), + "duration": self.info.get("duration"), + "genre": self.info.get("genre"), + "title": self.label, + "year": self.info.get("date")[:4], + "comment": self.info.get("description") + }) + list_item.setProperty("isPlayable", "true") + list_item.setProperty("mediaUrl", self.media) + + return url, list_item, False diff --git a/plugin.audio.soundcloud/resources/lib/models/user.py b/plugin.audio.soundcloud/resources/lib/models/user.py new file mode 100644 index 0000000000..52b9207df5 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/models/user.py @@ -0,0 +1,22 @@ +from resources.lib.models.list_item import ListItem +from resources.routes import * +import urllib.parse +import xbmcgui + + +class User(ListItem): + thumb = "" + info = {} + + def to_list_item(self, addon_base): + list_item = xbmcgui.ListItem(label=self.label, label2=self.label2) + list_item.setArt({"thumb": self.thumb}) + list_item.setInfo("music", { + "title": self.info.get("description") + }) + url = addon_base + PATH_USER + "?" + urllib.parse.urlencode({ + "id": self.id, + "call": "/users/{id}/tracks".format(id=self.id) + }) + + return url, list_item, True diff --git a/plugin.audio.soundcloud/resources/lib/soundcloud/__init__.py b/plugin.audio.soundcloud/resources/lib/soundcloud/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.soundcloud/resources/lib/soundcloud/api_collection.py b/plugin.audio.soundcloud/resources/lib/soundcloud/api_collection.py new file mode 100644 index 0000000000..1fa4237f13 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/soundcloud/api_collection.py @@ -0,0 +1,5 @@ +class ApiCollection: + type = 0 + items = [] + load = [] + next_href = "" diff --git a/plugin.audio.soundcloud/resources/lib/soundcloud/api_interface.py b/plugin.audio.soundcloud/resources/lib/soundcloud/api_interface.py new file mode 100644 index 0000000000..33062dd6fc --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/soundcloud/api_interface.py @@ -0,0 +1,24 @@ +from abc import ABCMeta, abstractmethod + + +class ApiInterface(metaclass=ABCMeta): + @abstractmethod + def search(self, query, kind): pass + + @abstractmethod + def charts(self, filters): pass + + @abstractmethod + def call(self, url): pass + + @abstractmethod + def discover(self, selection): pass + + @abstractmethod + def resolve_id(self, id): pass + + @abstractmethod + def resolve_url(self, url): pass + + @abstractmethod + def resolve_media_url(self, url): pass diff --git a/plugin.audio.soundcloud/resources/lib/soundcloud/api_public.py b/plugin.audio.soundcloud/resources/lib/soundcloud/api_public.py new file mode 100644 index 0000000000..59570f3e4e --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/soundcloud/api_public.py @@ -0,0 +1,32 @@ +import requests +from resources.lib.soundcloud.api_interface import ApiInterface + + +class ApiPublic(ApiInterface): + """This class uses the official SoundCloud API.""" + + api_host = "https://api.soundcloud.com/" + + def _do_request(self, path, payload): + return requests.get(self.api_host + path, params=payload).json() + + def search(self, query, kind): + pass + + def charts(self, filters): + pass + + def call(self, url): + pass + + def discover(self, selection): + pass + + def resolve_id(self, id): + pass + + def resolve_url(self, url): + pass + + def resolve_media_url(self, url): + pass diff --git a/plugin.audio.soundcloud/resources/lib/soundcloud/api_v2.py b/plugin.audio.soundcloud/resources/lib/soundcloud/api_v2.py new file mode 100644 index 0000000000..f464610c77 --- /dev/null +++ b/plugin.audio.soundcloud/resources/lib/soundcloud/api_v2.py @@ -0,0 +1,318 @@ +import hashlib +import json +import re +import requests +import urllib.parse +import xbmc + +from resources.lib.models.playlist import Playlist +from resources.lib.models.track import Track +from resources.lib.models.selection import Selection +from resources.lib.models.user import User +from resources.lib.soundcloud.api_collection import ApiCollection +from resources.lib.soundcloud.api_interface import ApiInterface + + +class ApiV2(ApiInterface): + """This class uses the unofficial API used by the SoundCloud website.""" + + api_host = "https://api-v2.soundcloud.com" + api_client_id_cache_duration = 1440 # 24 hours + api_client_id_cache_key = "api-client-id" + api_limit = 20 + api_limit_tracks = 50 + api_lang = "en" + api_user_agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0" + api_cache = { + "discover": 120 # 2 hours + } + thumbnail_size = 500 + + def __init__(self, settings, lang, cache): + self.cache = cache + self.settings = settings + self.api_limit = int(self.settings.get("search.items.size")) + + if self.settings.get("apiv2.locale") == self.settings.APIV2_LOCALE["auto"]: + self.api_lang = lang + + @property + def api_client_id(self): + # It is possible to set a custom client ID in the settings + client_id_settings = self.settings.get("apiv2.client_id") + if client_id_settings: + xbmc.log("plugin.audio.soundcloud::ApiV2() Using custom client ID", xbmc.LOGDEBUG) + return client_id_settings + + # Check if there is a cached client ID + client_id_cached = self.cache.get( + self.api_client_id_cache_key, + self.api_client_id_cache_duration + ) + if client_id_cached: + xbmc.log("plugin.audio.soundcloud::ApiV2() Using cached client ID", xbmc.LOGDEBUG) + return client_id_cached + + # Extract client ID from website and cache it + client_id = self.fetch_client_id() + self.cache.add(self.api_client_id_cache_key, client_id) + xbmc.log("plugin.audio.soundcloud::ApiV2() Using new client ID", xbmc.LOGDEBUG) + + return client_id + + def search(self, query, kind="tracks"): + res = self._do_request("/search/" + kind, {"q": query, "limit": self.api_limit}) + return self._map_json_to_collection(res) + + def discover(self, selection_id=None): + res = self._do_request("/mixed-selections", {}, self.api_cache["discover"]) + + if selection_id and "collection" in res: + res = self._find_id_in_selection(res["collection"], selection_id) + + return self._map_json_to_collection(res) + + def charts(self, filters): + res = self._do_request("/charts", filters) + res = {"collection": [item["track"] for item in res["collection"]]} + return self._map_json_to_collection(res) + + def call(self, url): + url = urllib.parse.urlparse(url) + res = self._do_request(url.path, urllib.parse.parse_qs(url.query)) + return self._map_json_to_collection(res) + + def resolve_id(self, id): + res = self._do_request("/tracks", {"ids": id}) + return self._map_json_to_collection({"collection": res}) + + def resolve_url(self, url): + url = self._sanitize_url(url) + res = self._do_request("/resolve", {"url": url}) + return self._map_json_to_collection(res) + + def resolve_media_url(self, url): + url = urllib.parse.urlparse(url) + res = self._do_request(url.path, urllib.parse.parse_qs(url.query)) + return res.get("url") + + def _do_request(self, path, payload, cache=0): + payload["client_id"] = self.api_client_id + payload["app_locale"] = self.api_lang + headers = {"Accept-Encoding": "gzip", "User-Agent": self.api_user_agent} + path = self.api_host + path + cache_key = hashlib.sha1((path + str(payload)).encode()).hexdigest() + + xbmc.log( + "plugin.audio.soundcloud::ApiV2() Calling %s with header %s and payload %s" % + (path, str(headers), str(payload)), + xbmc.LOGDEBUG + ) + + # If caching is active, check for an existing cached file. + if cache: + cached_response = self.cache.get(cache_key, cache) + if cached_response: + xbmc.log("plugin.audio.soundcloud::ApiV2() Cache hit", xbmc.LOGDEBUG) + return json.loads(cached_response) + + # Send the request. + response = requests.get(path, headers=headers, params=payload).json() + + # If caching is active, cache the response. + if cache: + self.cache.add(cache_key, json.dumps(response)) + + return response + + def _extract_media_url(self, transcodings): + setting = self.settings.get("audio.format") + for codec in transcodings: + if self._is_preferred_codec(codec["format"], self.settings.AUDIO_FORMATS[setting]): + return codec["url"] + + # Fallback + return transcodings[0]["url"] if len(transcodings) else None + + def _find_id_in_selection(self, selection, selection_id): + for category in selection: + if category["id"] == selection_id: + if "items" in category: + return category["items"] + elif "tracks" in category: + return {"collection": category["tracks"]} + elif "items" in category: + res = self._find_id_in_selection(category["items"]["collection"], selection_id) + if res: + return res + + def _map_json_to_collection(self, json_obj): + collection = ApiCollection() + collection.items = [] # Reset list in order to resolve problems in unit tests. + collection.load = [] + collection.next_href = json_obj.get("next_href", None) + + if "kind" in json_obj and json_obj["kind"] == "track": + # If we are dealing with a single track, pack it into a dict + json_obj = {"collection": [json_obj]} + + if "collection" in json_obj: + + for item in json_obj["collection"]: + kind = item.get("kind", None) + + if kind == "track": + if "title" not in item: + # Track not fully returned by API + collection.load.append(item["id"]) + continue + + track = self._build_track(item) + collection.items.append(track) + + elif kind == "user": + user = User(id=item["id"], label=item["username"]) + user.label2 = item.get("full_name", "") + user.thumb = self._get_thumbnail(item, self.thumbnail_size) + user.info = { + "artist": item.get("description", None) + } + collection.items.append(user) + + elif kind == "playlist": + playlist = Playlist(id=item["id"], label=item.get("title")) + playlist.is_album = item.get("is_album", False) + playlist.label2 = item.get("label_name", "") + playlist.thumb = self._get_thumbnail(item, self.thumbnail_size) + playlist.info = { + "artist": item["user"]["username"] + } + collection.items.append(playlist) + + elif kind == "system-playlist": + # System playlists only appear inside selections + playlist = Selection(id=item["id"], label=item.get("title")) + playlist.thumb = self._get_thumbnail(item, self.thumbnail_size) + collection.items.append(playlist) + + elif kind == "selection": + selection = Selection(id=item["id"], label=item.get("title")) + selection.label2 = item.get("description", "") + collection.items.append(selection) + + else: + xbmc.log("plugin.audio.soundcloud::ApiV2() " + "Could not convert JSON kind to model...", + xbmc.LOGWARNING) + + elif "tracks" in json_obj: + + for item in json_obj["tracks"]: + if "title" not in item: + # Track not fully returned by API + collection.load.append(item["id"]) + continue + + track = self._build_track(item) + track.label2 = json_obj["title"] + collection.items.append(track) + + else: + raise RuntimeError("ApiV2 JSON seems to be invalid") + + # Load unresolved tracks + if collection.load: + # The API only supports a max of 50 track IDs per request: + for chunk in self._chunks(collection.load, self.api_limit_tracks): + track_ids = ",".join(str(x) for x in chunk) + loaded_tracks = self._do_request("/tracks", {"ids": track_ids}) + # Because returned tracks are not sorted, we have to manually match them + for track_id in chunk: + loaded_track = [lt for lt in loaded_tracks if lt["id"] == track_id] + if len(loaded_track): # Sometimes a track cannot be resolved + track = self._build_track(loaded_track[0]) + collection.items.append(track) + + return collection + + def _build_track(self, item): + if type(item.get("publisher_metadata")) is dict: + artist = item["publisher_metadata"].get("artist", item["user"]["username"]) + else: + artist = item["user"]["username"] + + track = Track(id=item["id"], label=item["title"]) + track.blocked = True if item.get("policy") == "BLOCK" else False + track.preview = True if item.get("policy") == "SNIP" else False + track.thumb = self._get_thumbnail(item, self.thumbnail_size) + track.media = self._extract_media_url(item["media"]["transcodings"]) + track.info = { + "artist": artist, + "genre": item.get("genre", None), + "date": item.get("display_date", None), + "description": item.get("description", None), + "duration": int(item["duration"]) / 1000 + } + + return track + + @staticmethod + def fetch_client_id(): + headers = {"Accept-Encoding": "gzip", "User-Agent": ApiV2.api_user_agent} + + # Get the HTML (includes a reference to the JS file we need) + html = requests.get("https://soundcloud.com/", headers=headers).text + + # Extract the HREF to the JS file (which contains the API key) + matches = re.findall(r"=\"(https://a-v2\.sndcdn\.com/assets/.*.js)\"", html) + + if matches: + for match in matches: + # Get the JS + response = requests.get(match, headers=headers) + response.encoding = "utf-8" # This speeds up `response.text` by 3 seconds + + # Extract the API key + key = re.search(r"exports={\"api-v2\".*client_id:\"(\w*)\"", response.text) + + if key: + return str(key.group(1)) + + raise Exception("Failed to extract client key from js") + else: + raise Exception("Failed to extract js href from html") + + @staticmethod + def _is_preferred_codec(codec, setting): + return codec["mime_type"] == setting["mime_type"] and \ + codec["protocol"] == setting["protocol"] + + @staticmethod + def _sanitize_url(url): + return url.replace("m.soundcloud.com/", "soundcloud.com/") + + @staticmethod + def _get_thumbnail(item, size): + """ + availableSizes: [ + [ 20, 't20x20'], + [ 50, 't50x50'], + [120, 't120x120'], + [200, 't200x200'], + [500, 't500x500'] + ] + """ + url = item.get( + "artwork_url", item.get("avatar_url", item.get("calculated_artwork_url", False)) + ) + + return re.sub( + r"^(.*/)(\w+)-([-a-zA-Z0-9]+)-([a-z0-9]+)\.(jpg|png|gif).*$", + r"\1\2-\3-t{x}x{y}.\5".format(x=size, y=size), + url + ) if url else None + + @staticmethod + def _chunks(lst, size): + for i in range(0, len(lst), size): + yield lst[i:i + size] diff --git a/plugin.audio.soundcloud/resources/plugin.py b/plugin.audio.soundcloud/resources/plugin.py new file mode 100644 index 0000000000..8957219d97 --- /dev/null +++ b/plugin.audio.soundcloud/resources/plugin.py @@ -0,0 +1,193 @@ +from resources.lib.soundcloud.api_v2 import ApiV2 +from resources.lib.kodi.cache import Cache +from resources.lib.kodi.items import Items +from resources.lib.kodi.search_history import SearchHistory +from resources.lib.kodi.settings import Settings +from resources.lib.kodi.vfs import VFS +from resources.routes import * +import os +import sys +import urllib.parse +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +import xbmcvfs + +addon = xbmcaddon.Addon() +addon_id = addon.getAddonInfo("id") +addon_base = "plugin://" + addon_id +addon_profile_path = xbmcvfs.translatePath(addon.getAddonInfo("profile")) +vfs = VFS(addon_profile_path) +vfs_cache = VFS(os.path.join(addon_profile_path, "cache")) +settings = Settings(addon) +cache = Cache(settings, vfs_cache) +api = ApiV2(settings, xbmc.getLanguage(xbmc.ISO_639_1), cache) +search_history = SearchHistory(settings, vfs) +listItems = Items(addon, addon_base, search_history) + + +def run(): + url = urllib.parse.urlparse(sys.argv[0]) + path = url.path + handle = int(sys.argv[1]) + args = urllib.parse.parse_qs(sys.argv[2][1:]) + xbmcplugin.setContent(handle, "songs") + + if path == PATH_ROOT: + action = args.get("action", None) + if action is None: + items = listItems.root() + xbmcplugin.addDirectoryItems(handle, items, len(items)) + xbmcplugin.endOfDirectory(handle) + elif "call" in action: + collection = listItems.from_collection(api.call(args.get("call")[0])) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) + elif "settings" in action: + addon.openSettings() + else: + xbmc.log(addon_id + ": Invalid root action", xbmc.LOGERROR) + + elif path == PATH_CHARTS: + action = args.get("action", [None])[0] + genre = args.get("genre", ["soundcloud:genres:all-music"])[0] + if action is None: + items = listItems.charts() + xbmcplugin.addDirectoryItems(handle, items, len(items)) + xbmcplugin.endOfDirectory(handle) + else: + api_result = api.charts({"kind": action, "genre": genre, "limit": 50}) + collection = listItems.from_collection(api_result) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) + + elif path == PATH_DISCOVER: + selection = args.get("selection", [None])[0] + collection = listItems.from_collection(api.discover(selection)) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) + + elif path == PATH_PLAY: + # Public params + track_id = args.get("track_id", [None])[0] + playlist_id = args.get("playlist_id", [None])[0] + url = args.get("url", [None])[0] + + # Public legacy params (@deprecated) + audio_id_legacy = args.get("audio_id", [None])[0] + track_id = audio_id_legacy if audio_id_legacy else track_id + + # Private params + media_url = args.get("media_url", [None])[0] + + if media_url: + resolved_url = api.resolve_media_url(media_url) + item = xbmcgui.ListItem(path=resolved_url) + xbmcplugin.setResolvedUrl(handle, succeeded=True, listitem=item) + elif track_id: + collection = listItems.from_collection(api.resolve_id(track_id)) + playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + resolve_list_item(handle, collection[0][1]) + playlist.add(url=collection[0][0], listitem=collection[0][1]) + elif playlist_id: + call = "/playlists/{id}".format(id=playlist_id) + collection = listItems.from_collection(api.call(call)) + playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + for item in collection: + resolve_list_item(handle, item[1]) + playlist.add(url=item[0], listitem=item[1]) + elif url: + collection = listItems.from_collection(api.resolve_url(url)) + playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + for item in collection: + resolve_list_item(handle, item[1]) + playlist.add(url=item[0], listitem=item[1]) + else: + xbmc.log(addon_id + ": Invalid play param", xbmc.LOGERROR) + + elif path == PATH_SEARCH: + action = args.get("action", None) + query = args.get("query", [""])[0] + + if action and "remove" in action: + search_history.remove(query) + xbmc.executebuiltin("Container.Refresh") + elif action and "clear" in action: + search_history.clear() + xbmc.executebuiltin("Container.Refresh") + + if query: + if action is None: + search(handle, query) + elif "people" in action: + xbmcplugin.setContent(handle, "artists") + collection = listItems.from_collection(api.search(query, "users")) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) + elif "albums" in action: + xbmcplugin.setContent(handle, "albums") + collection = listItems.from_collection(api.search(query, "albums")) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) + elif "playlists" in action: + xbmcplugin.setContent(handle, "albums") + collection = listItems.from_collection( + api.search(query, "playlists_without_albums") + ) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) + else: + xbmc.log(addon_id + ": Invalid search action", xbmc.LOGERROR) + else: + if action is None: + items = listItems.search() + xbmcplugin.addDirectoryItems(handle, items, len(items)) + xbmcplugin.endOfDirectory(handle) + elif "new" in action: + query = xbmcgui.Dialog().input(addon.getLocalizedString(30101)) + search_history.add(query) + search(handle, query) + else: + xbmc.log(addon_id + ": Invalid search action", xbmc.LOGERROR) + + # Legacy search query used by Chorus2 (@deprecated) + elif path == PATH_SEARCH_LEGACY: + query = args.get("q", [""])[0] + collection = listItems.from_collection(api.search(query)) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) + + elif path == PATH_USER: + user_id = args.get("id")[0] + default_action = args.get("call")[0] + if user_id: + items = listItems.user(user_id) + collection = listItems.from_collection(api.call(default_action)) + xbmcplugin.addDirectoryItems(handle, items, len(items)) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) + else: + xbmc.log(addon_id + ": Invalid user action", xbmc.LOGERROR) + + elif path == PATH_SETTINGS_CACHE_CLEAR: + vfs_cache.destroy() + dialog = xbmcgui.Dialog() + dialog.ok("SoundCloud", addon.getLocalizedString(30501)) + + else: + xbmc.log(addon_id + ": Path not found", xbmc.LOGERROR) + + +def resolve_list_item(handle, list_item): + resolved_url = api.resolve_media_url(list_item.getProperty("mediaUrl")) + list_item.setPath(resolved_url) + xbmcplugin.setResolvedUrl(handle, succeeded=True, listitem=list_item) + + +def search(handle, query): + search_options = listItems.search_sub(query) + collection = listItems.from_collection(api.search(query)) + xbmcplugin.addDirectoryItems(handle, search_options, len(collection)) + xbmcplugin.addDirectoryItems(handle, collection, len(collection)) + xbmcplugin.endOfDirectory(handle) diff --git a/plugin.audio.soundcloud/resources/routes.py b/plugin.audio.soundcloud/resources/routes.py new file mode 100644 index 0000000000..1a3db6d203 --- /dev/null +++ b/plugin.audio.soundcloud/resources/routes.py @@ -0,0 +1,8 @@ +PATH_ROOT = "/" +PATH_CHARTS = "/charts/" +PATH_DISCOVER = "/discover/" +PATH_PLAY = "/play/" +PATH_SEARCH = "/search/" +PATH_SEARCH_LEGACY = "/search/query/" +PATH_SETTINGS_CACHE_CLEAR = "/settings/cache/clear/" +PATH_USER = "/user/" diff --git a/plugin.audio.soundcloud/resources/settings.xml b/plugin.audio.soundcloud/resources/settings.xml new file mode 100644 index 0000000000..c9792a3270 --- /dev/null +++ b/plugin.audio.soundcloud/resources/settings.xml @@ -0,0 +1,88 @@ + + +
    + + + + 0 + 2 + + + + + + + + + + + + + 0 + 20 + + + + + + + + + + + + + + + + 0 + 10 + + + + + + + + + + + + + + + + + 3 + + + true + + + 30061 + + + + 2 + 0 + + + + + + + + + + + + 1 + RunPlugin(plugin://plugin.audio.soundcloud/settings/cache/clear/) + + true + + + + +
    +
    diff --git a/plugin.audio.sverigesradio/LICENSE.txt b/plugin.audio.sverigesradio/LICENSE.txt new file mode 100644 index 0000000000..81f7880ec0 --- /dev/null +++ b/plugin.audio.sverigesradio/LICENSE.txt @@ -0,0 +1,282 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------- \ No newline at end of file diff --git a/plugin.audio.sverigesradio/README b/plugin.audio.sverigesradio/README new file mode 100644 index 0000000000..0519ecba6e --- /dev/null +++ b/plugin.audio.sverigesradio/README @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugin.audio.sverigesradio/addon.py b/plugin.audio.sverigesradio/addon.py new file mode 100644 index 0000000000..bc5df411a8 --- /dev/null +++ b/plugin.audio.sverigesradio/addon.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from datetime import timedelta +from datetime import date +from distutils.util import strtobool +import sys +import itertools +import requests +from xbmcswift2 import Plugin + +plugin = Plugin() + +STRINGS = { + 'Sveriges_Radio': 30000, + 'unable_to_communicate': 30010, + 'unable_to_parse': 30011, + 'no_stream_found': 30012, + 'live': 30013, + 'channels': 30014, + 'categories': 30015, + 'all_programs': 30016 +} + +QUALITIES = ["lo", "normal", "hi"] +FORMATS = ["mp3", "aac"] + + +def json_date_as_datetime(jd): + sign = jd[-7] + if sign not in '-+' or len(jd) == 13: + millisecs = int(jd[6:-2]) + else: + millisecs = int(jd[6:-7]) + hh = int(jd[-7:-4]) + mm = int(jd[-4:-2]) + if sign == '-': mm = -mm + millisecs += (hh * 60 + mm) * 60000 + return datetime.datetime(1970, 1, 1) + datetime.timedelta(microseconds=millisecs * 1000) + + +def format_datetime(dt): + return dt.strftime("%Y-%m-%d %H:%M") + + +def _(string_id): + return plugin.get_string(STRINGS[string_id]) + + +def show_error(error): + dialog = xbmcgui.Dialog() + ok = dialog.ok(_('Sveriges_Radio'), error) + + +def load_url(url, params=None, headers=None): + try: + r = requests.get(url, headers=headers, params=params) + r.raise_for_status() + return r + except Exception as e: + plugin.log.error("plugin.audio.sverigesradio: unable to load url: '%s' due to '%s'" % (url, e)) + show_error(_('unable_to_communicate')) + return None + + +def load_json(url, params): + try: + headers = {'Accept': 'application/json', 'Accept-Charset': 'utf-8'} + r = load_url(url, params, headers) + if r: + return r.json() + except Exception as e: + plugin.log.error("plugin.audio.sverigesradio: unable to parse result from url: '%s' due to '%s'" % (url, e)) + show_error(_('unable_to_parse')) + return None + + +def load_channels(): + SRAPI_CHANNEL_URL = "http://api.sr.se/api/v2/channels" + quality = plugin.get_setting('quality', choices=QUALITIES) + params = {'format': 'json', 'pagination': 'false', 'audioquality': quality, 'liveaudiotemplateid': 5} + channels = load_json(SRAPI_CHANNEL_URL, params) + return channels + + +@plugin.cached() +def load_programs(channel_id='', category_id=''): + SRAPI_PROGRAM_URL = "http://api.sr.se/api/v2/programs/index" + params = {'format': 'json', 'pagination': 'false', 'filter': 'program.hasondemand', 'filterValue': 'true'} + if channel_id: + params['channelid'] = channel_id + if category_id: + params['programcategoryid'] = category_id + programs = load_json(SRAPI_PROGRAM_URL, params) + return programs + + +# @plugin.cached() +def load_program_episodes(program_id, quality, page='1'): + page_size = str(plugin.get_setting('page_size')) + SRAPI_EPISODE_URL = "http://api.sr.se/api/v2/episodes" + params = {'format': 'json', 'pagination': 'true', 'page': page, 'size': page_size, 'audioquality': quality, + 'programid': program_id} + episodes = load_json(SRAPI_EPISODE_URL, params) + return episodes + + +@plugin.cached() +def load_program_info(program_id): + SRAPI_PROGRAM_URL = "http://api.sr.se/api/v2/programs/{0}" + params = {'format': 'json', 'pagination': 'false'} + url = SRAPI_PROGRAM_URL.format(program_id) + program_info = load_json(url, params) + return program_info + + +@plugin.cached() +def load_categories(): + SRAPI_PROGRAM_CATEGORIES = "http://api.sr.se/api/v2/programcategories" + params = {'format': 'json', 'pagination': 'false'} + categories = load_json(SRAPI_PROGRAM_CATEGORIES, params) + return categories + + +def create_live_channel(channel): + name = channel['name'] + url = channel['liveaudio']['url'] + if 'image' in channel: + logo = channel['image'] + else: + logo = None + item = {'label': name, 'path': url, 'icon': logo, 'is_playable': True} + return item + + +def create_channel(channel): + name = channel['name'] + if 'image' in channel: + logo = channel['image'] + else: + logo = None + id = channel['id'] + item = {'label': name, 'path': plugin.url_for('list_channel_programs', id=id), 'icon': logo, 'is_playable': False} + return item + + +def create_program(program): + name = program['name'] + logo = program['programimage'] + id = program['id'] + item = {'label': name, 'path': plugin.url_for('list_program', id=id), 'icon': logo, 'is_playable': False} + return item + + +def create_category(category): + name = category['name'] + id = category['id'] + item = {'label': name, 'path': plugin.url_for('list_category', id=id), 'is_playable': False} + return item + + +def create_broadcast(episode, program_name, prefer_broadcasts): + name = episode['title'] + logo = episode['imageurl'] + description = episode['description'] + name = "%s - %s" % (name, description) + items = [] + if prefer_broadcasts and 'broadcast' in episode: + extract_broadcasts(items, episode['broadcast'], logo, name, program_name) + elif 'listenpodfile' in episode: + extract_pod_file(items, episode['listenpodfile'], logo, name, program_name) + elif 'downloadpodfile' in episode: + extract_pod_file(items, episode['downloadpodfile'], logo, name, program_name) + elif not prefer_broadcasts and 'broadcast' in episode: + extract_broadcasts(items, episode['broadcast'], logo, name, program_name) + return items + + +def extract_pod_file(items, pod_info, logo, name, program_name): + plugin.set_content('albums') + prefix_name = bool(strtobool(str(plugin.get_setting('prefix')))) + url = pod_info['url'] + date_fmt = xbmc.getRegion('dateshort') + " " + xbmc.getRegion('time') + # Remove seconds from timestamp + date_fmt = date_fmt[:-3] + date_str = pod_info['publishdateutc'] + date_object = datetime.fromtimestamp(float(int(date_str[6:-5]))) + date_strftime = date_object.strftime(date_fmt) + duration = pod_info['duration'] + pn = program_name + " " + date_strftime + mediatype = 'album' + album_description = date_strftime + "[CR]" + name + size = pod_info['filesizeinbytes'] + if prefix_name: + name = date_strftime + " " + name + info = {'duration': duration, 'date': date_strftime, 'title': name, 'size': size, 'album': pn, + 'artist': _('Sveriges_Radio'), 'mediatype': mediatype} + properties = {'Album_Description': album_description} + item = {'label': name, 'path': url, 'icon': logo, 'is_playable': True, 'info': info, 'properties': properties} + items.append(item) + + +def extract_broadcasts(items, broadcast, logo, name, program_name): + plugin.set_content('albums') + for file in broadcast['broadcastfiles']: + url = file['url'] + prefix_name = bool(strtobool(str(plugin.get_setting('prefix')))) + date_fmt = xbmc.getRegion('dateshort') + " " + xbmc.getRegion('time') + # Remove seconds from timestamp + date_fmt = date_fmt[:-3] + plugin.log.debug(date_fmt) + try: + date_str = file['publishdateutc'] + date_object = datetime.fromtimestamp(float(int(date_str[6:-2]) / 1000)).date() + date_object = datetime.fromtimestamp(float(int(date_str[6:-5]))) + date_strftime = date_object.strftime(date_fmt) + duration = file['duration'] + pn = program_name + " " + date_strftime + info_type = 'music' + album_description = date_strftime + "[CR]" + name + dbtype = 'album' + mediatype = 'album' + if prefix_name: + name = date_strftime + " " + name + info = {'duration': duration, 'date': date_strftime, 'title': name, 'album': pn, + 'artist': _('Sveriges_Radio'), 'comment': date_str, 'mediatype': mediatype} + properties = {'album_description': album_description} + item = {'label': name, 'info_type': info_type, 'path': url, 'icon': logo, 'is_playable': True, 'info': info, + 'properties': properties} + items.append(item) + except KeyError: + plugin.log.error("Oops! Missing values! " + file) + + +@plugin.route('/channel/') +def list_channel_programs(id): + response = load_programs(channel_id=id) + if response: + items = [create_program(program) for program in response['programs']] + plugin.add_sort_method('playlist_order') + plugin.add_sort_method('label') + return items + + +@plugin.route('/program///', name='list_program_a') +@plugin.route('/program//', name='list_program', options={'page': '1'}) +def list_program(id, page): + page = int(page) + page_size = plugin.get_setting('page_size') + QUALITIES = ["lo", "normal", "hi"] + quality = plugin.get_setting('quality', choices=QUALITIES) + response = load_program_episodes(id, quality, str(page)) + program_info = load_program_info(id) + program_name = program_info["program"]["name"] + if response: + PREFERENCE_CHOICES = [True, False] + prefer_broadcasts = plugin.get_setting('preference', choices=PREFERENCE_CHOICES) + items = [create_broadcast(episode, program_name, prefer_broadcasts) for episode in response['episodes']] + items = list(itertools.chain(*items)) + plugin.add_sort_method('playlist_order') + plugin.add_sort_method('label') + plugin.add_sort_method('date') + + if page > 1: + items.insert(0, { + 'label': '<< Prev', + 'path': plugin.url_for('list_program_a', id=id, page=str(page - 1)) + }) + if len(items) > int(page_size) - 1: + items.append({ + 'label': 'Next >>', + 'path': plugin.url_for('list_program_a', id=id, page=str(page + 1)) + }) + + if page > 1: + return plugin.finish(items, update_listing=True) + + return items + + +@plugin.route('/category/') +def list_category(id): + response = load_programs(category_id=id) + if response: + items = [create_program(program) for program in response['programs']] + plugin.add_sort_method('playlist_order') + plugin.add_sort_method('label') + return items + + +@plugin.route('/live/') +def list_live(): + response = load_channels() + if response: + items = [create_live_channel(channel) for channel in response['channels']] + plugin.add_sort_method('playlist_order') + plugin.add_sort_method('label') + return items + + +@plugin.route('/channels/') +def list_channels(): + response = load_channels() + if response: + items = [create_channel(channel) for channel in response['channels']] + plugin.add_sort_method('playlist_order') + plugin.add_sort_method('label') + return items + + +@plugin.route('/categories/') +def list_categories(): + response = load_categories() + if response: + items = [create_category(category) for category in response['programcategories']] + plugin.add_sort_method('playlist_order') + plugin.add_sort_method('label') + return items + + +@plugin.route('/allprograms/') +def list_all_programs(): + response = load_programs() + if response: + items = [create_program(program) for program in response['programs']] + plugin.add_sort_method('label') + return items + + +@plugin.route('/') +def index(): + items = [ + {'label': _('live'), 'path': plugin.url_for('list_live')}, + {'label': _('channels'), 'path': plugin.url_for('list_channels')}, + {'label': _('categories'), 'path': plugin.url_for('list_categories')}, + {'label': _('all_programs'), 'path': plugin.url_for('list_all_programs')}, + ] + return items + + +if __name__ == '__main__': + plugin.run() diff --git a/plugin.audio.sverigesradio/addon.xml b/plugin.audio.sverigesradio/addon.xml new file mode 100644 index 0000000000..6aa8354ade --- /dev/null +++ b/plugin.audio.sverigesradio/addon.xml @@ -0,0 +1,36 @@ + + + + + + + + + audio + + + all + Swedish National Radio (Sveriges Radio) + Swedish National Radio (Sveriges Radio) + Sveriges Radio + Listen to Sveriges Radio + Listen to Sveriges Radio + Lyssna på Sveriges Radio + This addon uses Sveriges Radio's official API. It may or may not work outside of Sweden, and there is mainly content in Swedish. + This addon uses Sveriges Radio's official API. It may or may not work outside of Sweden, and there is mainly content in Swedish. + sv_SE + GNU GENERAL PUBLIC LICENSE. Version 2, June 1991 + https://forum.kodi.tv/showthread.php?tid=116735 + https://github.com/dodoadoodoo/kodi-sverigesradio + https://github.com/dodoadoodoo/kodi-sverigesradio + + + icon.png + fanart.jpg + + + + diff --git a/plugin.audio.sverigesradio/changelog.txt b/plugin.audio.sverigesradio/changelog.txt new file mode 100644 index 0000000000..e2b52f7e81 --- /dev/null +++ b/plugin.audio.sverigesradio/changelog.txt @@ -0,0 +1,9 @@ +3.0.0 Updates for Matrix +2.2.0 Removed sports section, added paging, stability fixes +2.1.0 Added live stream quality selection, added sports section, removed useless format setting, +2.0.0 Rewritten using xbmcswift2 and requests libraries, added podcasts, various other fixes +1.0.4 Added required elements to addon.xml +1.0.3 Proper handling of broadcast date +1.0.2 Added changelog.txt and fanart.jpg +1.0.1 Only show programs with available ondemand content. +1.0.0 Initial rewrite using Sveriges Radio's v2 JSON API. diff --git a/plugin.audio.sverigesradio/fanart.jpg b/plugin.audio.sverigesradio/fanart.jpg new file mode 100644 index 0000000000..c8c4dd50db Binary files /dev/null and b/plugin.audio.sverigesradio/fanart.jpg differ diff --git a/plugin.audio.sverigesradio/icon.png b/plugin.audio.sverigesradio/icon.png new file mode 100644 index 0000000000..11ae8bfeb9 Binary files /dev/null and b/plugin.audio.sverigesradio/icon.png differ diff --git a/plugin.audio.sverigesradio/resources/language/resource.language.en_gb/strings.po b/plugin.audio.sverigesradio/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..8f815c6f05 --- /dev/null +++ b/plugin.audio.sverigesradio/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,106 @@ +# XBMC Media Center language file +# Addon Name: Sveriges Radio +# Addon id: plugin.audio.sverigesradio +# Addon version: 2.2.0 +# Addon Provider: Daniel +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2019-12-08 16:51+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\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 "Swedish National Radio (Sveriges Radio)" +msgstr "" + +msgctxt "Addon Description" +msgid "Listen to Sveriges Radio" +msgstr "" + +msgctxt "Addon Disclaimer" +msgid "This addon uses Sveriges Radio's official API. It may or may not work outside of Sweden, and there is mainly content in Swedish." +msgstr "" + +msgctxt "#30000" +msgid "Sveriges Radio" +msgstr "" + +msgctxt "#30010" +msgid "Unable to communicate with the Sveriges Radio servers" +msgstr "" + +msgctxt "#30011" +msgid "Unable to understand the response from the Sveriges Radio servers" +msgstr "" + +msgctxt "#30012" +msgid "No valid stream found" +msgstr "" + +msgctxt "#30013" +msgid "Live" +msgstr "" + +msgctxt "#30014" +msgid "Channels" +msgstr "" + +msgctxt "#30015" +msgid "Categories" +msgstr "" + +msgctxt "#30016" +msgid "Programs A-Ö" +msgstr "" + +msgctxt "#32000" +msgid "Settings" +msgstr "" + +msgctxt "#32001" +msgid "Quality" +msgstr "" + +msgctxt "#32002" +msgid "Prefer" +msgstr "" + +msgctxt "#32003" +msgid "Prefix" +msgstr "" + +msgctxt "#32004" +msgid "Page size (used to limit number of episodes retrieved in one go)" +msgstr "" + +msgctxt "#32111" +msgid "Low" +msgstr "" + +msgctxt "#32112" +msgid "Normal" +msgstr "" + +msgctxt "#32113" +msgid "High" +msgstr "" + +msgctxt "#32121" +msgid "Broadcasts (music)" +msgstr "" + +msgctxt "#32122" +msgid "Podfiles (sometimes extra content)" +msgstr "" + +msgctxt "#32123" +msgid "Prefix with date/time" +msgstr "" diff --git a/plugin.audio.sverigesradio/resources/language/resource.language.en_us/strings.po b/plugin.audio.sverigesradio/resources/language/resource.language.en_us/strings.po new file mode 100644 index 0000000000..8f815c6f05 --- /dev/null +++ b/plugin.audio.sverigesradio/resources/language/resource.language.en_us/strings.po @@ -0,0 +1,106 @@ +# XBMC Media Center language file +# Addon Name: Sveriges Radio +# Addon id: plugin.audio.sverigesradio +# Addon version: 2.2.0 +# Addon Provider: Daniel +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2019-12-08 16:51+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\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 "Swedish National Radio (Sveriges Radio)" +msgstr "" + +msgctxt "Addon Description" +msgid "Listen to Sveriges Radio" +msgstr "" + +msgctxt "Addon Disclaimer" +msgid "This addon uses Sveriges Radio's official API. It may or may not work outside of Sweden, and there is mainly content in Swedish." +msgstr "" + +msgctxt "#30000" +msgid "Sveriges Radio" +msgstr "" + +msgctxt "#30010" +msgid "Unable to communicate with the Sveriges Radio servers" +msgstr "" + +msgctxt "#30011" +msgid "Unable to understand the response from the Sveriges Radio servers" +msgstr "" + +msgctxt "#30012" +msgid "No valid stream found" +msgstr "" + +msgctxt "#30013" +msgid "Live" +msgstr "" + +msgctxt "#30014" +msgid "Channels" +msgstr "" + +msgctxt "#30015" +msgid "Categories" +msgstr "" + +msgctxt "#30016" +msgid "Programs A-Ö" +msgstr "" + +msgctxt "#32000" +msgid "Settings" +msgstr "" + +msgctxt "#32001" +msgid "Quality" +msgstr "" + +msgctxt "#32002" +msgid "Prefer" +msgstr "" + +msgctxt "#32003" +msgid "Prefix" +msgstr "" + +msgctxt "#32004" +msgid "Page size (used to limit number of episodes retrieved in one go)" +msgstr "" + +msgctxt "#32111" +msgid "Low" +msgstr "" + +msgctxt "#32112" +msgid "Normal" +msgstr "" + +msgctxt "#32113" +msgid "High" +msgstr "" + +msgctxt "#32121" +msgid "Broadcasts (music)" +msgstr "" + +msgctxt "#32122" +msgid "Podfiles (sometimes extra content)" +msgstr "" + +msgctxt "#32123" +msgid "Prefix with date/time" +msgstr "" diff --git a/plugin.audio.sverigesradio/resources/language/resource.language.sv_se/strings.po b/plugin.audio.sverigesradio/resources/language/resource.language.sv_se/strings.po new file mode 100644 index 0000000000..763c84df08 --- /dev/null +++ b/plugin.audio.sverigesradio/resources/language/resource.language.sv_se/strings.po @@ -0,0 +1,106 @@ +# XBMC Media Center language file +# Addon Name: Sveriges Radio +# Addon id: plugin.audio.sverigesradio +# Addon version: 2.2.0 +# Addon Provider: Daniel +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2019-12-08 16:51+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "Swedish National Radio (Sveriges Radio)" +msgstr "Sveriges Radio" + +msgctxt "Addon Description" +msgid "Listen to Sveriges Radio" +msgstr "Lyssna på Sveriges Radio" + +msgctxt "Addon Disclaimer" +msgid "This addon uses Sveriges Radio's official API. It may or may not work outside of Sweden, and there is mainly content in Swedish." +msgstr "" + +msgctxt "#30000" +msgid "Sveriges Radio" +msgstr "Sveriges Radio" + +msgctxt "#30010" +msgid "Unable to communicate with the Sveriges Radio servers" +msgstr "Kan inte kommunicera med Sveriges Radios servrar" + +msgctxt "#30011" +msgid "Unable to understand the response from the Sveriges Radio servers" +msgstr "Förstår inte svaret från Sveriges Radios servrar" + +msgctxt "#30012" +msgid "No valid stream found" +msgstr "Kan inte hitta en ström" + +msgctxt "#30013" +msgid "Live" +msgstr "Live" + +msgctxt "#30014" +msgid "Channels" +msgstr "Kanaler" + +msgctxt "#30015" +msgid "Categories" +msgstr "Kategorier" + +msgctxt "#30016" +msgid "Programs A-Ö" +msgstr "Program A-Ö" + +msgctxt "#32000" +msgid "Settings" +msgstr "Inställningar" + +msgctxt "#32001" +msgid "Quality" +msgstr "Kvalitet" + +msgctxt "#32002" +msgid "Prefer" +msgstr "Föredra" + +msgctxt "#32003" +msgid "Prefix" +msgstr "Prefix" + +msgctxt "#32004" +msgid "Page size (used to limit number of episodes retrieved in one go)" +msgstr "Sidstorlek (Används för att begränsa antalet avsnitt som hämtas)" + +msgctxt "#32111" +msgid "Low" +msgstr "Låg" + +msgctxt "#32112" +msgid "Normal" +msgstr "Normal" + +msgctxt "#32113" +msgid "High" +msgstr "Hög" + +msgctxt "#32121" +msgid "Broadcasts (music)" +msgstr "Sändningar (musik)" + +msgctxt "#32122" +msgid "Podfiles (sometimes extra content)" +msgstr "Podcasts (ibland extra innehåll)" + +msgctxt "#32123" +msgid "Prefix with date/time" +msgstr "Prefix med datum/tid" diff --git a/plugin.audio.sverigesradio/resources/settings.xml b/plugin.audio.sverigesradio/resources/settings.xml new file mode 100644 index 0000000000..e19fcd751a --- /dev/null +++ b/plugin.audio.sverigesradio/resources/settings.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plugin.audio.tripler/LICENSE.txt b/plugin.audio.tripler/LICENSE.txt new file mode 100644 index 0000000000..2ec82aaa2e --- /dev/null +++ b/plugin.audio.tripler/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2023 Simon Mollema + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugin.audio.tripler/README.md b/plugin.audio.tripler/README.md new file mode 100644 index 0000000000..0d083ab9d8 --- /dev/null +++ b/plugin.audio.tripler/README.md @@ -0,0 +1,88 @@ +# plugin.audio.tripler + +A Kodi plugin for Triple R: an independent community radio station in Melbourne, Australia. + +![Triple R Logo](resources/icon.png) + +----- + +## Frequently Asked Questions + +### What is Triple R? + +For more than 40 years Triple R has shaped and inspired the culture of Melbourne. Since its inception as an educational broadcaster in 1976, Triple R has become Australia's most influential community radio station with nearly 21,000 paid subscribers and broadcasting live to over 1,000,000 listeners per month across FM and digital (DAB+ digital radio, podcasts and online). + +Broadcasting on 102.7FM and 3RRR Digital, the Triple R grid houses over 60 diverse programs. Music shows cover every genre imaginable from pop to punk rock, from R&B and electro to jazz, hip hop, country and metal. Specialist talks programs delve into topics as varied as the environment, human rights, politics, medical issues, gardening, cultural ventures and local interests. + +### What does this plugin do? + +This plugin aims to provide as much content as possible from the [Triple R Website, rrr.org.au,](https://www.rrr.org.au) in the Kodi media player. Currently provided are the following: + +- Listen Live! +- Browse by Program +- Browse by Date +- Latest Segments +- Audio Archives +- Album Of The Week +- Soundscapes +- Events +- Subscriber Giveaways (for logged in users only!) + +----- + +## Installation + +If you don't already use [Kodi](https://kodi.tv/), download and install that first. + +### Release Version + +If you would like to use the release version with automatic updates, please install [the published release in the Kodi repository](https://kodi.tv/addons/matrix/plugin.audio.tripler/) with the following steps: + +- Open Kodi. +- Navigate to Add-ons > Install from Repository. +- Navigate to Kodi Add-on repository > Music add-ons > Triple R. +- Select "Install". + +### Latest Development Version + +If you would instead like to use the latest development version, [download the zip file](https://github.com/molzy/plugin.audio.tripler/archive/refs/heads/scraper.zip) to the same computer that is running Kodi. Afterwareds, follow these steps: + +- Open Kodi. +- From the main menu, navigate to Settings > System > Add-ons. +- Enable "Unknown Sources". +- Go back to the main menu, and navigate to Add-ons > Install from Zip File. +- If you are prompted to confirm, select "Yes" on the prompt. +- Navigate to the downloaded zip file on your filesystem. +- Select "OK". + +----- + +## Screenshots + +### Menu +![Plugin Menu](resources/screenshots/menu.jpg) + +### Browse By Program +![Browse By Program](resources/screenshots/browse-by-program.jpg) + +### Broadcast + Playlist +![Broadcast + Playlist](resources/screenshots/broadcast-playlist.jpg) + +### Album Of The Week +![Album Of The Week](resources/screenshots/album-of-the-week.jpg) + +### Browse By Date +![Browse By Date](resources/screenshots/browse-by-date.jpg) + +### Soundscape +![Soundscape](resources/screenshots/soundscape.jpg) + +----- + +## License + +This plugin was initially forked from a Triple R plugin written by [Damon Toumbourou](https://github.com/DamonToumbourou/plugin.audio.tripler). + +The plugin was then fully rewritten by Simon Mollema. + +The plugin is released under the terms of [the MIT license](LICENSE.txt). diff --git a/plugin.audio.tripler/addon.py b/plugin.audio.tripler/addon.py new file mode 100644 index 0000000000..cd5ba36cfe --- /dev/null +++ b/plugin.audio.tripler/addon.py @@ -0,0 +1,4 @@ +import resources.lib.tripler as tripler + +if __name__ == '__main__': + result = tripler.instance.parse() diff --git a/plugin.audio.tripler/addon.xml b/plugin.audio.tripler/addon.xml new file mode 100644 index 0000000000..02d8e2a1ce --- /dev/null +++ b/plugin.audio.tripler/addon.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + audio + + + all + en + Listen to Triple R: an independent community radio station in Melbourne, Australia. + For more than 40 years Triple R has shaped and inspired the culture of Melbourne. Since its inception as an educational broadcaster in 1976, Triple R has become Australia's most influential community radio station with nearly 21,000 paid subscribers and broadcasting live to over 1,000,000 listeners per month across FM and digital (DAB+ digital radio, podcasts and online). Broadcasting on 102.7FM and 3RRR Digital, the Triple R grid houses over 60 diverse programs. Music shows cover every genre imaginable from pop to punk rock, from R&B and electro to jazz, hip hop, country and metal. Specialist talks programs delve into topics as varied as the environment, human rights, politics, medical issues, gardening, cultural ventures and local interests. + MIT + xbmc@molzy.com + https://github.com/molzy/plugin.audio.tripler + Version 3.0.0 +- Rewritten with more flexible parser +- More content added from website +- Improved user interface and menus +- Browsing programs by name and date now possible +- Broadcast track playlists are available +- Support for playing music through Bandcamp, YouTube and indigiTUBE +- Support for searching for music content by title and artist +- Subscriber-only giveaways can now be entered by signing in + + + resources/icon.png + resources/fanart.png + resources/screenshots/menu.jpg + resources/screenshots/browse-by-program.jpg + resources/screenshots/browse-by-date.jpg + resources/screenshots/broadcast-playlist.jpg + resources/screenshots/album-of-the-week.jpg + resources/screenshots/soundscape.jpg + + + + diff --git a/plugin.audio.tripler/resources/fanart.png b/plugin.audio.tripler/resources/fanart.png new file mode 100644 index 0000000000..2b34c2379a Binary files /dev/null and b/plugin.audio.tripler/resources/fanart.png differ diff --git a/plugin.audio.tripler/resources/icon.png b/plugin.audio.tripler/resources/icon.png new file mode 100644 index 0000000000..ad59d5c48b Binary files /dev/null and b/plugin.audio.tripler/resources/icon.png differ diff --git a/plugin.audio.tripler/resources/language/resource.language.en_gb/strings.po b/plugin.audio.tripler/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..9e760fcf82 --- /dev/null +++ b/plugin.audio.tripler/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,348 @@ +# XBMC Media Center language file +# Addon Name: Triple R +# Addon id: plugin.audio.tripler +# Addon version: 2.0.0 +# Addon Provider: Simon Mollema +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2022-03-01 00:00+0000\n" +"PO-Revision-Date: 2022-03-01 00:00+0000\n" +"Last-Translator: Simon Mollema \n" +"Language-Team: English\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 "Listen to Triple R: an independent community radio station in Melbourne, Australia." +msgstr "" + +msgctxt "Addon Description" +msgid "For more than 40 years Triple R has shaped and inspired the culture of Melbourne. Since its inception as an educational broadcaster in 1976, Triple R has become Australia's most influential community radio station with nearly 21,000 paid subscribers and broadcasting live to over 1,000,000 listeners per month across FM and digital (DAB+ digital radio, podcasts and online). Broadcasting on 102.7FM and 3RRR Digital, the Triple R grid houses over 60 diverse programs. Music shows cover every genre imaginable from pop to punk rock, from R&B and electro to jazz, hip hop, country and metal. Specialist talks programs delve into topics as varied as the environment, human rights, politics, medical issues, gardening, cultural ventures and local interests." +msgstr "" + +msgctxt "#30999" +msgid "" +msgstr "" + +#Plugin name + +msgctxt "#30000" +msgid "Triple R" +msgstr "" + +msgctxt "#30001" +msgid "Listen Live!" +msgstr "" + +msgctxt "#30004" +msgid "> Next Page" +msgstr "" + +msgctxt "#30005" +msgid ">> Last Page" +msgstr "" + +msgctxt "#30006" +msgid "aired %s" +msgstr "" + +msgctxt "#30007" +msgid "Dated %s, Melbourne time." +msgstr "" + +msgctxt "#30008" +msgid "Play with %s" +msgstr "" + +msgctxt "#30009" +msgid "Settings" +msgstr "" + +# Settings - Account + +msgctxt "#30010" +msgid "Subscriber Account" +msgstr "" + +msgctxt "#30011" +msgid "Use Account" +msgstr "" + +msgctxt "#30012" +msgid "Email Address" +msgstr "" + +msgctxt "#30013" +msgid "Sign In" +msgstr "" + +msgctxt "#30014" +msgid "Sign Out" +msgstr "" + +msgctxt "#30015" +msgid "Enter Email Address" +msgstr "" + +msgctxt "#30016" +msgid "Enter Password" +msgstr "" + +msgctxt "#30017" +msgid "Full Name" +msgstr "" + +msgctxt "#30020" +msgid "Sign in to your Triple R account using your email address and password" +msgstr "" + +msgctxt "#30021" +msgid "Sign out of your Triple R account" +msgstr "" + +# Settings - Image Quality + +msgctxt "#30022" +msgid "Image Quality" +msgstr "" + +msgctxt "#30023" +msgid "Set the desired quality level for album art and band images" +msgstr "" + +msgctxt "#30024" +msgid "High" +msgstr "" + +msgctxt "#30025" +msgid "Medium" +msgstr "" + +msgctxt "#30026" +msgid "Low" +msgstr "" + +# Main Menu + +msgctxt "#30032" +msgid "Browse by Program" +msgstr "" + +msgctxt "#30033" +msgid "Browse by Date" +msgstr "" + +msgctxt "#30034" +msgid "Latest Programs" +msgstr "" + +msgctxt "#30035" +msgid "Latest Segments" +msgstr "" + +msgctxt "#30036" +msgid "Audio Archives" +msgstr "" + +msgctxt "#30037" +msgid "Album Of The Week" +msgstr "" + +msgctxt "#30038" +msgid "Soundscapes" +msgstr "" + +msgctxt "#30039" +msgid "Events" +msgstr "" + +msgctxt "#30040" +msgid "Subscriber Giveaways" +msgstr "" + +msgctxt "#30041" +msgid "Search" +msgstr "" + +msgctxt "#30042" +msgid "Videos" +msgstr "" + +# Broadcasts + +msgctxt "#30049" +msgid "Broadcast" +msgstr "" + +msgctxt "#30050" +msgid "Full Broadcast" +msgstr "" + +msgctxt "#30051" +msgid "Segment" +msgstr "" + +msgctxt "#30052" +msgid "Track Search" +msgstr "" + +# Date Selection + +msgctxt "#30059" +msgid "Next Day (%s)" +msgstr "" + +msgctxt "#30060" +msgid "Today (%s)" +msgstr "" + +msgctxt "#30061" +msgid "Previous Day (%s)" +msgstr "" + +msgctxt "#30062" +msgid "%s (%s)" +msgstr "" + +msgctxt "#30063" +msgid "This Month (%s)" +msgstr "" + +msgctxt "#30064" +msgid "This Year (%s)" +msgstr "" + +msgctxt "#30065" +msgid "Select Date (%s)" +msgstr "" + +# Searching + +msgctxt "#30066" +msgid "Search playlists instead" +msgstr "" + +msgctxt "#30067" +msgid "Enter Search Query" +msgstr "" + +msgctxt "#30068" +msgid "Enter Playlist Search Query" +msgstr "" + +# Subscription + +msgctxt "#30069" +msgid "Enter Giveaway" +msgstr "" + +msgctxt "#30070" +msgid "Select to enter this giveaway." +msgstr "" + +msgctxt "#30071" +msgid "Giveaway Entered" +msgstr "" + +msgctxt "#30072" +msgid "Thanks for being a Triple R subscriber!" +msgstr "" + +msgctxt "#30073" +msgid "Cannot Enter Giveaway" +msgstr "" + +msgctxt "#30074" +msgid "Already entered!" +msgstr "" + +msgctxt "#30075" +msgid "Is your subscription active?" +msgstr "" + +msgctxt "#30076" +msgid "Subscribe and sign in to enter!" +msgstr "" + +msgctxt "#30077" +msgid "%s signed in" +msgstr "" + +msgctxt "#30078" +msgid "Thanks for subscribing!" +msgstr "" + +msgctxt "#30079" +msgid "%s signed out" +msgstr "" + +msgctxt "#30081" +msgid "Subscribe To Listen" +msgstr "" + +msgctxt "#30082" +msgid "https://www.rrr.org.au/subscribe - Subscribe and sign in to enter the below giveaways!" +msgstr "" + +msgctxt "#30083" +msgid "https://www.rrr.org.au/subscribe - Subscribe and sign in to listen to all audio archives!" +msgstr "" + +msgctxt "#30084" +msgid "Subscribers Only" +msgstr "" + +msgctxt "#30085" +msgid "Sign in failure" +msgstr "" + +msgctxt "#30086" +msgid "%s could not sign in" +msgstr "" + +msgctxt "#30087" +msgid "Sign out failure" +msgstr "" + +msgctxt "#30088" +msgid "%s could not sign out" +msgstr "" + +# Context Menus + +msgctxt "#30100" +msgid "%s using the context menu." +msgstr "" + +msgctxt "#30101" +msgid "View playlist" +msgstr "" + +msgctxt "#30102" +msgid "Search for this track" +msgstr "" + +msgctxt "#30103" +msgid "Search for this artist" +msgstr "" + +msgctxt "#30104" +msgid "Search for '%s'" +msgstr "" + +msgctxt "#30105" +msgid "Search for '%s' on Triple R" +msgstr "" + +msgctxt "#30106" +msgid "Search for '%s' on Bandcamp" +msgstr "" + +msgctxt "#30107" +msgid "Search for '%s' on YouTube" +msgstr "" diff --git a/plugin.audio.tripler/resources/lib/media.py b/plugin.audio.tripler/resources/lib/media.py new file mode 100644 index 0000000000..d066e9e3d0 --- /dev/null +++ b/plugin.audio.tripler/resources/lib/media.py @@ -0,0 +1,152 @@ +import re + +class Media: + RE_BANDCAMP_ALBUM_ID = re.compile(r'https://bandcamp.com/EmbeddedPlayer/.*album=(?P[^/]+)') + RE_BANDCAMP_ALBUM_ART = re.compile(r'"art_id":(\w+)') + BANDCAMP_ALBUM_PLUGIN_BASE_URL = 'plugin://plugin.audio.kxmxpxtx.bandcamp/?mode=list_songs' + BANDCAMP_ALBUM_PLUGIN_FORMAT = '{}&album_id={}&item_type=a' + BANDCAMP_ALBUM_ART_URL = 'https://bandcamp.com/api/mobile/24/tralbum_details?band_id=1&tralbum_type=a&tralbum_id={}' + + RE_BANDCAMP_ALBUM_LINK_ID = re.compile(r'(?Phttps?://[^/\.]+\.bandcamp.com/album/[\w\-]+)') + RE_BANDCAMP_BAND_LINK_ID = re.compile(r'(?Phttps?://[^/\.]+\.bandcamp.com/)$') + + RE_BANDCAMP_TRACK_ID = re.compile(r'(?Phttps?://[^/\.]+\.bandcamp.com/(track|album)/[\w\-]+)') + BANDCAMP_TRACK_PLUGIN_BASE_URL = 'plugin://plugin.audio.kxmxpxtx.bandcamp/?mode=url' + BANDCAMP_TRACK_PLUGIN_FORMAT = '{}&url={}' + RE_BANDCAMP_TRACK_ART = re.compile(r'art_id":(?P\d+),') + RE_BANDCAMP_TRACK_BAND_ART = re.compile(r'data-band="[^"]*image_id":(?P\d+)}"') + + RE_BANDCAMP_ART_QUALITY_SEARCH = r'/img/(?P[^_]+)_(?P\d+)\.jpg' + + RE_SOUNDCLOUD_PLAYLIST_ID = re.compile(r'.+soundcloud\.com/playlists/(?P[^&]+)') + SOUNDCLOUD_PLUGIN_BASE_URL = 'plugin://plugin.audio.soundcloud/' + SOUNDCLOUD_PLUGIN_FORMAT = '{}?action=call&call=/playlists/{}' + + RE_YOUTUBE_VIDEO_ID = re.compile(r'^(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:youtube(?:-nocookie)?\.com|youtu.be)(?:\/(?:[\w\-]+\?v=|embed\/|v\/)?)(?P[\w\-]+)(?!.*list)\S*$') + YOUTUBE_PLUGIN_BASE_URL = 'plugin://plugin.video.youtube/play/' + YOUTUBE_VIDEO_PLUGIN_FORMAT = '{}?video_id={}&play=1' + YOUTUBE_VIDEO_ART_URL_FORMAT = 'https://i.ytimg.com/vi/{}/hqdefault.jpg' + + RE_YOUTUBE_PLAYLIST_ID = re.compile(r'^(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:youtube(?:-nocookie)?\.com|youtu.be)\/.+\?.*list=(?P[\w\-]+)') + YOUTUBE_PLAYLIST_PLUGIN_FORMAT = '{}?playlist_id={}&order=default&play=1' + YOUTUBE_PLAYLIST_ART_URL = 'https://youtube.com/oembed?url=https%3A//www.youtube.com/playlist%3Flist%3D{}&format=json' + + RE_INDIGITUBE_ALBUM_ID = re.compile(r'https://www.indigitube.com.au/embed/album/(?P[^"]+)') + INDIGITUBE_ALBUM_PLUGIN_BASE_URL = 'plugin://plugin.audio.indigitube/?mode=list_songs' + INDIGITUBE_ALBUM_PLUGIN_FORMAT = '{}&album_id={}' + + RE_SPOTIFY_ALBUM_ID = re.compile(r'.+spotify\.com(\/embed)?\/album\/(?P[^&?\/]+)') + RE_SPOTIFY_PLAYLIST_ID = re.compile(r'.+spotify\.com(\/embed)?\/playlist\/(?P[^&]+)') + + RE_APPLE_ALBUM_ID = re.compile(r'.+music\.apple\.com\/au\/album\/(?P.+)') + APPLE_ALBUM_URL = 'https://music.apple.com/au/album/{}' + + EXT_SEARCH_PLUGIN_FORMAT = 'plugin://plugin.audio.tripler/tracks/ext_search?q={search}' + + RE_MEDIA_URLS = { + 'bandcamp': { + 're': RE_BANDCAMP_ALBUM_ID, + 'base': BANDCAMP_ALBUM_PLUGIN_BASE_URL, + 'format': BANDCAMP_ALBUM_PLUGIN_FORMAT, + 'name': 'Bandcamp', + }, + 'bandcamp_track': { + 're': RE_BANDCAMP_TRACK_ID, + 'base': BANDCAMP_TRACK_PLUGIN_BASE_URL, + 'format': BANDCAMP_TRACK_PLUGIN_FORMAT, + 'name': 'Bandcamp', + }, + 'bandcamp_link': { + 're': RE_BANDCAMP_ALBUM_LINK_ID, + 'base': BANDCAMP_TRACK_PLUGIN_BASE_URL, + 'format': BANDCAMP_TRACK_PLUGIN_FORMAT, + 'name': 'Bandcamp', + }, + 'bandcamp_band_link': { + 're': RE_BANDCAMP_BAND_LINK_ID, + 'format': EXT_SEARCH_PLUGIN_FORMAT, + 'name': 'Bandcamp Band Search', + }, + 'soundcloud': { + 're': RE_SOUNDCLOUD_PLAYLIST_ID, + 'base': SOUNDCLOUD_PLUGIN_BASE_URL, + 'format': SOUNDCLOUD_PLUGIN_FORMAT, + 'name': 'SoundCloud', + }, + 'youtube': { + 're': RE_YOUTUBE_VIDEO_ID, + 'base': YOUTUBE_PLUGIN_BASE_URL, + 'format': YOUTUBE_VIDEO_PLUGIN_FORMAT, + 'name': 'YouTube', + }, + 'youtube_playlist': { + 're': RE_YOUTUBE_PLAYLIST_ID, + 'base': YOUTUBE_PLUGIN_BASE_URL, + 'format': YOUTUBE_PLAYLIST_PLUGIN_FORMAT, + 'name': 'YouTube', + }, + 'indigitube': { + 're': RE_INDIGITUBE_ALBUM_ID, + 'base': INDIGITUBE_ALBUM_PLUGIN_BASE_URL, + 'format': INDIGITUBE_ALBUM_PLUGIN_FORMAT, + 'name': 'indigiTUBE', + }, + 'spotify': { + 're': RE_SPOTIFY_ALBUM_ID, + 'format': EXT_SEARCH_PLUGIN_FORMAT, + 'name': 'Album Search', + }, + 'spotify_playlist': { + 're': RE_SPOTIFY_PLAYLIST_ID, + 'format': EXT_SEARCH_PLUGIN_FORMAT, + 'name': 'Playlist Search', + }, + 'apple': { + 're': RE_APPLE_ALBUM_ID, + 'format': EXT_SEARCH_PLUGIN_FORMAT, + 'name': 'Album Search', + }, + } + + def __init__(self, quality): + self.quality = quality + + def parse_media_id(self, plugin, media_id, search=''): + info = self.RE_MEDIA_URLS.get(plugin, {}) + if info: + return info.get('format', '').format(info.get('base', ''), media_id, search=search) + else: + return '' + + def parse_art(self, art): + if art and 'f4.bcbits.com' in art: + band = '/img/a' not in art + quality = self._bandcamp_band_quality() if band else self._bandcamp_album_quality() + art = re.sub(self.RE_BANDCAMP_ART_QUALITY_SEARCH, f'/img/\g_{quality}.jpg', art) + if art and '/600x600bf-60.jpg' in art: + art = art.replace('/600x600bf-60.jpg', self._apple_album_quality()) + return art + + def _bandcamp_band_quality(self): + if self.quality == 0: + return 1 # full resolution + if self.quality == 1: + return 10 # 1200px wide + if self.quality == 2: + return 25 # 700px wide + + def _bandcamp_album_quality(self): + if self.quality == 0: + return 5 # 700px wide + if self.quality == 1: + return 2 # 350px wide + if self.quality == 2: + return 9 # 210px wide + + def _apple_album_quality(self): + if self.quality == 0: + return '/600x600bf.jpg' + if self.quality == 1: + return '/600x600bf-60.jpg' + if self.quality == 2: + return '/300x300bb-60.jpg' diff --git a/plugin.audio.tripler/resources/lib/scraper.py b/plugin.audio.tripler/resources/lib/scraper.py new file mode 100644 index 0000000000..749df8a3f3 --- /dev/null +++ b/plugin.audio.tripler/resources/lib/scraper.py @@ -0,0 +1,2230 @@ +#!/usr/bin/env python +import bs4, html, time, json, re, sys +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta + +from urllib.request import Request, urlopen +from urllib.parse import parse_qs, urlencode +from urllib.error import URLError + +DATE_FORMAT = '%Y-%m-%d' + +URL_BASE = 'https://www.rrr.org.au' + +USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' + +ignore_on_air = False + + +def get(resource_path): + return urlopen_ua(Scraper.url_for(resource_path)) + +def urlopen_ua(url): + return urlopen(Request(url, headers={'User-Agent': USER_AGENT}), timeout=5) + +def get_json(url): + return urlopen_ua(url).read().decode() + +def get_json_obj(url): + return json.loads(get_json(url)) + +def strptime(s, fmt): + return datetime.fromtimestamp(time.mktime(time.strptime(s, fmt))) + + +class Resource: + def __init__(self, itemobj): + self._itemobj = itemobj + + def id(self): + return self.path.split('/')[-1] + + @property + def path(self): + return Scraper.resource_path_for(self._itemobj.find('a').attrs['href']) + + RE_CAMEL = re.compile(r'(?[^/]+?)', pattern) + + '(?:[?](?P.+))?' + + '$' + ) + + @classmethod + def resource_path_regex(cls): + return cls.regex_from(cls.RESOURCE_PATH_PATTERN) + + @classmethod + def match_resource_path(cls, path): + return cls.regex_from(cls.RESOURCE_PATH_PATTERN).match(path) + + @classmethod + def match_website_path(cls, path): + return cls.regex_from(cls.WEBSITE_PATH_PATTERN).match(path) + + @classmethod + def matching_resource_path(cls, resource_path): + if cls.match_resource_path(resource_path): + return cls(resource_path) + + + def __init__(self, resource_path): + self.resource_path = resource_path + m = self.__class__.resource_path_regex().match(self.resource_path) + if m: + self.groupdict = m.groupdict() + + def soup(self): + return bs4.BeautifulSoup(get(self.resource_path), 'html.parser') + + def url(self): + return f'{URL_BASE}{self.website_path()}' + + def website_path(self): + template = self.__class__.WEBSITE_PATH_PATTERN + + if self.groupdict.get('query_params'): + template += '?{query_params}' + + return template.format_map(self.groupdict) + + def pagination(self, pagekey='page', selfval=1, nextval=None, lastval=None): + resource_path = self.resource_path.split('?') + if len(resource_path) > 1: + resource_params = parse_qs(resource_path[-1]) + if not resource_params.get(pagekey): + resource_params[pagekey] = selfval + else: + resource_params[pagekey] = resource_params[pagekey][0] + else: + resource_params = {pagekey: selfval} + + template = resource_path[0] + '?{}' + links = {} + + links['self'] = template.format(urlencode(resource_params)) + + if nextval: + resource_params[pagekey] = nextval + else: + resource_params[pagekey] = int(resource_params[pagekey]) + 1 + links['next'] = template.format(urlencode(resource_params)) + + links_last = None + if lastval: + resource_params[pagekey] = lastval + links['last'] = template.format(urlencode(resource_params)) + + return links + + + +class ProgramsScraper(Scraper): + RESOURCE_PATH_PATTERN = '/programs' + WEBSITE_PATH_PATTERN = '/explore/programs' + + def generate(self): + return { + 'data': [ + Program(item).to_dict() + for item in self.soup().findAll('div', class_='card clearfix') + ], + 'links': { + 'self': self.__class__.RESOURCE_PATH_PATTERN + }, + } + + +class ProgramScraper(Scraper): + RESOURCE_PATH_PATTERN = '/programs/{program_id}' + WEBSITE_PATH_PATTERN = '/explore/programs/{program_id}' + + def generate(self): + soup = self.soup() + programtitle = soup.find(class_='page-banner__heading') + if programtitle: + title = programtitle.text + + thumbnail, background = None, None + programimage = soup.find(class_='card__background-image') + if programimage: + programimagesrc = re.search(r"https://[^']+", programimage.attrs.get('style')) + if programimagesrc: + thumbnail = programimagesrc[0] + + programbg = soup.find(class_='banner__image') + if programbg: + background = programbg.attrs.get('src') + + textbody = '\n'.join(( + soup.find(class_='page-banner__summary').text, + soup.find(class_='page-banner__time').text + )) + + # Aarrgh the website dragons strike again! + def map_path(path): + m = re.match('^/explore/(?P[^/]+?)/(?P[^/]+?)#episode-selector', path) + if m: + d = m.groupdict() + if d['collection'] == 'programs': + return f"/explore/{d['collection']}/{d['program']}/episodes/page" + elif d['collection'] == 'podcasts': + return f"/explore/{d['collection']}/{d['program']}/episodes" + + collections = [ + { + 'type': 'collection', + 'id': Scraper.resource_path_for(map_path(anchor.attrs['href'])), + 'attributes': { + 'title': ' - '.join((title, anchor.text)), + 'thumbnail': thumbnail, + 'background': background, + 'textbody': textbody, + }, + 'links': { + 'self': Scraper.resource_path_for(map_path(anchor.attrs['href'])), + } + } + for anchor in soup.find_all('a', class_='program-nav__anchor') + ] + highlights = soup.find('a', string=re.compile('highlights')) + if highlights: + collections.append( + { + 'type': 'collection', + 'id': Scraper.resource_path_for(highlights.attrs['href']), + 'attributes': { + 'title': ' - '.join((title, 'Segments')), + 'thumbnail': thumbnail, + 'background': background, + 'textbody': textbody, + }, + 'links': { + 'self': Scraper.resource_path_for(highlights.attrs['href']), + } + } + ) + return { + 'data': collections, + } + + +class AudioItemGenerator: + def generate(self): + return { + 'data': [ + item for item in [ + AudioItem.factory(div) + for div in self.soup().findAll(class_='card__text') + ] + ], + 'links': self.pagination() + } + +class ProgramBroadcastsScraper(Scraper): + RESOURCE_PATH_PATTERN = '/programs/{program_id}/broadcasts' + WEBSITE_PATH_PATTERN = '/explore/programs/{program_id}/episodes/page' + + def generate(self): + soup = self.soup() + programtitle = soup.find(class_='page-banner__heading') + if programtitle: + title = programtitle.text + + thumbnail, background = None, None + programimage = soup.find(class_='card__background-image') + if programimage: + programimagesrc = re.search(r"https://[^']+", programimage.attrs.get('style')) + if programimagesrc: + thumbnail = programimagesrc[0] + + programbg = soup.find(class_='banner__image') + if programbg: + background = programbg.attrs.get('src') + + textbody = '\n'.join(( + soup.find(class_='page-banner__summary').text, + soup.find(class_='page-banner__time').text + )) + + # Aarrgh the website dragons strike again! + def map_path(path): + m = re.match('^/explore/(?P[^/]+?)/(?P[^/]+?)#episode-selector', path) + if m: + d = m.groupdict() + if d['collection'] == 'programs': + return f"/explore/{d['collection']}/{d['program']}/episodes/page" + elif d['collection'] == 'podcasts': + return f"/explore/{d['collection']}/{d['program']}/episodes" + + collections = [ + { + 'type': 'collection', + 'id': Scraper.resource_path_for(map_path(anchor.attrs['href'])), + 'attributes': { + 'title': ' - '.join((title, anchor.text)), + 'thumbnail': thumbnail, + 'background': background, + 'textbody': textbody, + }, + 'links': { + 'self': Scraper.resource_path_for(map_path(anchor.attrs['href'])), + } + } + for anchor in soup.find_all('a', class_='program-nav__anchor') + ] + + # hackety - hack - hack - hack ... just blindly turn "Broadcasts" into "Segments" while nobody is looking + collections[0]['id'] = collections[0]['id'].replace('broadcasts', 'segments') + collections[0]['links']['self'] = collections[0]['id'] + collections[0]['attributes']['title'] = collections[0]['attributes']['title'].replace('Broadcasts', 'Segments') + + broadcasts = [ + item for item in [ + BroadcastCollection(div).to_dict() + for div in self.soup().findAll(class_='card') + ] + ] + + images = { + 'background': background, + } + [b['attributes'].update(images) for b in broadcasts] + + collections = [item for item in (collections[::-1] + broadcasts) if item] + + return { + 'data': collections, + 'links': self.pagination(), + } + + + +class ProgramPodcastsScraper(Scraper, AudioItemGenerator): + RESOURCE_PATH_PATTERN = '/programs/{program_id}/podcasts' + WEBSITE_PATH_PATTERN = '/explore/podcasts/{program_id}/episodes' + + +class ProgramSegmentsScraper(Scraper, AudioItemGenerator): + RESOURCE_PATH_PATTERN = '/programs/{program_id}/segments' + WEBSITE_PATH_PATTERN = '/explore/programs/{program_id}/highlights' + + +class OnDemandSegmentsScraper(Scraper, AudioItemGenerator): + RESOURCE_PATH_PATTERN = '/segments' + WEBSITE_PATH_PATTERN = '/on-demand/segments' + + +class OnDemandBroadcastsScraper(Scraper, AudioItemGenerator): + RESOURCE_PATH_PATTERN = '/broadcasts' + WEBSITE_PATH_PATTERN = '/on-demand/episodes' + + +class ArchivesScraper(Scraper, AudioItemGenerator): + RESOURCE_PATH_PATTERN = '/archives' + WEBSITE_PATH_PATTERN = '/on-demand/archives' + + +class ArchiveScraper(Scraper): + RESOURCE_PATH_PATTERN = '/archives/{item}' + WEBSITE_PATH_PATTERN = '/on-demand/archives/{item}' + + def generate(self): + item = self.soup().find(class_='adaptive-banner__audio-component') + return { + 'data': AudioItem.factory(item) + } + + +class ExternalMedia: + RE_BANDCAMP_ALBUM_ID = re.compile(r'https://bandcamp.com/EmbeddedPlayer/.*album=(?P[^/]+)') + RE_BANDCAMP_ALBUM_ART = re.compile(r'"art_id":(\w+)') + BANDCAMP_ALBUM_ART_URL = 'https://bandcamp.com/api/mobile/24/tralbum_details?band_id=1&tralbum_type=a&tralbum_id={}' + + RE_BANDCAMP_ALBUM_LINK_ID = re.compile(r'(?Phttps?://[^/\.]+\.bandcamp.com/album/[\w\-]+)') + RE_BANDCAMP_BAND_LINK_ID = re.compile(r'(?Phttps?://[^/\.]+\.bandcamp.com/)$') + + RE_BANDCAMP_TRACK_ID = re.compile(r'(?Phttps?://[^/\.]+\.bandcamp.com/track/[\w\-]+)') + RE_BANDCAMP_TRACK_ART = re.compile(r'art_id":(?P\d+),') + RE_BANDCAMP_TRACK_TITLE = re.compile(r'\

    \s+(?P[^\n]*)\s+\<\/h2\>') + RE_BANDCAMP_TRACK_ARTIST = re.compile(r'data-band="[^"]*;name":"(?P<artist>[^&]+)"') + RE_BANDCAMP_TRACK_DURATION = re.compile(r'duration":(?P<duration>[\d\.]+),') + RE_BANDCAMP_TRACK_BAND_ART = re.compile(r'data-band="[^"]*image_id":(?P<band_art_id>\d+)}"') + + RE_SOUNDCLOUD_PLAYLIST_ID = re.compile(r'.+soundcloud\.com/playlists/(?P<media_id>[^&]+)') + + RE_YOUTUBE_VIDEO_ID = re.compile(r'^(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:youtube(?:-nocookie)?\.com|youtu.be)(?:\/(?:[\w\-]+\?v=|embed\/|v\/)?)(?P<media_id>[\w\-]+)(?!.*list)\S*$') + RE_YOUTUBE_VIDEO_ART_ID = re.compile(r'^https:\/\/i\.ytimg\.com\/vi\/(?P<media_id>[\w\-]+)\/hqdefault\.jpg$') + RE_YOUTUBE_VIDEO_TITLE = re.compile(r'"videoDetails":{[^}]*,"title":"(?P<title>[^"]+)"') + RE_YOUTUBE_VIDEO_ARTIST = re.compile(r'<link itemprop="name" content="(?P<artist>[^"]+)"') + RE_YOUTUBE_VIDEO_DESC = re.compile(r'"attributedDescription":{"content":"(?P<textbody>[^{]*)","') + RE_YOUTUBE_VIDEO_DURATION = re.compile(r'itemprop="duration" content="PT(?P<hours>[\d]+H)?(?P<minutes>[\d]+M)?(?P<seconds>[\d]+S)?"') + YOUTUBE_VIDEO_DURATION_URL = 'https://www.youtube.com/watch?v={}' + YOUTUBE_VIDEO_ART_URL_FORMAT = 'https://i.ytimg.com/vi/{}/hqdefault.jpg' + + RE_YOUTUBE_PLAYLIST_ID = re.compile(r'^(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:youtube(?:-nocookie)?\.com|youtu.be)\/.+\?.*list=(?P<media_id>[\w\-]+)') + YOUTUBE_PLAYLIST_ART_URL = 'https://www.youtube.com/playlist?list={}' + RE_YOUTUBE_PLAYLIST_ART = re.compile(r'og:image" content="(?P<art_url>[^"]+)"><meta property="og:image:width" content="640"') + RE_YOUTUBE_PLAYLIST_ART_LQ = re.compile(r'og:image" content="(?P<art_url>[^?]+)[^"]+"') + RE_YOUTUBE_PLAYLIST_TITLE = re.compile(r'<meta property="og:title" content="(?P<title>[^"]+)"') + RE_YOUTUBE_PLAYLIST_ARTIST = re.compile(r'"shortBylineText":{"runs":\[{"text":"(?P<artist>[^"]+)"') + RE_YOUTUBE_PLAYLIST_DURATION = re.compile(r'"lengthText":[^}]+}},"simpleText":"(?P<duration>[^"]+)"}') + + RE_INDIGITUBE_ALBUM_ID = re.compile(r'https://www.indigitube.com.au/embed/album/(?P<media_id>[^"]+)') + INDIGITUBE_ACCESS_KEY = 'access_token=%242a%2410%24x2Zy%2FTgIAOC0UUMi3NPKc.KY49e%2FZLUJFOpBCNYAs8D72UUnlI526' + INDIGITUBE_ALBUM_URL = 'https://api.appbooks.com/content/album/{}?' + INDIGITUBE_ACCESS_KEY + INDIGITUBE_ALBUM_ART_URL = 'https://api.appbooks.com/get/{}/file/file.jpg?w=512&quality=90&' + INDIGITUBE_ACCESS_KEY + '&ext=.jpg' + + RE_SPOTIFY_ALBUM_ID = re.compile(r'.+spotify\.com(\/embed)?\/album\/(?P<media_id>[^&?\/]+)') + RE_SPOTIFY_PLAYLIST_ID = re.compile(r'.+spotify\.com(\/embed)?\/playlist\/(?P<media_id>[^&]+)') + RE_SPOTIFY_ALBUM_ART = re.compile(r'\-\-image\-src:url\((\&\#x27\;|\')(?P<art_url>[^\&\']+)(\&\#x27\;|\')') + RE_SPOTIFY_DURATION = re.compile(r'<\/h4><div class="[^"]+">(?P<duration>[^<]+)</div></li>') + + RE_APPLE_ALBUM_ID = re.compile(r'.+music\.apple\.com\/au\/album\/(?P<media_id>.+)') + APPLE_ALBUM_URL = 'https://music.apple.com/au/album/{}' + RE_APPLE_ALBUM_ART = re.compile(r'meta name="twitter:image" content="(?P<art_url>[^"]+)">') + RE_APPLE_DURATION = re.compile(r'meta property="music:song:duration" content="PT(?P<hours>[\d]+H)?(?P<minutes>[\d]+M)?(?P<seconds>[\d]+S)?">') + + RE_MEDIA_URLS = { + 'bandcamp': { + 're': RE_BANDCAMP_ALBUM_ID, + }, + 'bandcamp_link': { + 're': RE_BANDCAMP_ALBUM_LINK_ID, + }, + 'bandcamp_band_link': { + 're': RE_BANDCAMP_BAND_LINK_ID, + }, + 'bandcamp_track': { + 're': RE_BANDCAMP_TRACK_ID, + }, + 'soundcloud': { + 're': RE_SOUNDCLOUD_PLAYLIST_ID, + }, + 'youtube': { + 're': RE_YOUTUBE_VIDEO_ID, + }, + 'youtube_art': { + 're': RE_YOUTUBE_VIDEO_ART_ID, + }, + 'youtube_playlist': { + 're': RE_YOUTUBE_PLAYLIST_ID, + }, + 'indigitube': { + 're': RE_INDIGITUBE_ALBUM_ID, + }, + 'spotify': { + 're': RE_SPOTIFY_ALBUM_ID, + }, + 'spotify_playlist': { + 're': RE_SPOTIFY_PLAYLIST_ID, + }, + 'apple': { + 're': RE_APPLE_ALBUM_ID, + }, + } + + fetch_yt_video = False + + def media_items(self, iframes, fetch_album_art=False, fetch_yt_video=False): + matches = [] + self.fetch_yt_video = fetch_yt_video + + for iframe in iframes: + if not iframe.get('src'): + continue + media_id = None + for plugin, info in self.RE_MEDIA_URLS.items(): + plugin_match = re.match(info.get('re'), iframe.get('src')) + if plugin_match: + media_id = plugin_match.groupdict().get('media_id') + if media_id: + break + + matches.append({ + 'media_id': media_id, + 'src': iframe.get('src'), + 'attrs': iframe.get('attrs') if iframe.get('attrs') else {}, + 'plugin': plugin if plugin_match else None, + }) + + if fetch_album_art: + executor = ThreadPoolExecutor(max_workers=3) + art_exec = [executor.submit(self.get_album_art, match=match) for match in matches] + matches = [match.result() for match in art_exec] + + return matches + + def get_album_art(self, match={}): + result = match + media_id, plugin = match['media_id'], match['plugin'] + album_art = {} + if plugin == 'bandcamp': + album_art = self.bandcamp_album_art(media_id) + elif plugin == 'bandcamp_link': + album_art = self.bandcamp_track_art(media_id) + elif plugin == 'bandcamp_band_link': + album_art = self.bandcamp_band_art(media_id) + elif plugin == 'bandcamp_track': + album_art = self.bandcamp_track_art(media_id) + elif plugin == 'indigitube': + album_art = self.indigitube_album_art(media_id) + elif plugin == 'spotify' or plugin == 'spotify_playlist': + album_art = self.spotify_album_art(match['src']) + elif plugin == 'apple': + album_art = self.apple_album_art(media_id) + elif plugin == 'youtube_playlist': + album_art = self.youtube_playlist_art(media_id) + elif plugin == 'youtube' or plugin == 'youtube_art': + result['plugin'] = 'youtube' + if self.fetch_yt_video: + album_art = self.youtube_video_duration(media_id) + album_art['art'] = self.YOUTUBE_VIDEO_ART_URL_FORMAT.format(media_id) + + result['thumbnail'] = album_art.get('art') + result['background'] = album_art.get('band') + result['duration'] = album_art.get('duration') + if 'attrs' not in result.keys(): + result['attrs'] = {} + if 'title' not in result['attrs'].keys() and album_art.get('title'): + result['attrs']['title'] = album_art.get('title') + if 'artist' not in result['attrs'].keys() and album_art.get('artist'): + result['attrs']['artist'] = album_art.get('artist') + if 'textbody' not in result['attrs'].keys() and album_art.get('textbody'): + result['attrs']['textbody'] = album_art.get('textbody') + return result + + def get_sum_duration(self, duration_matches): + durations = [int(x.split(':')[0]) * 60 + int(x.split(':')[1]) for x in duration_matches] + return sum(durations) + + def get_pt_duration(self, duration): + result = 0 + if duration['hours']: + result += int(duration['hours'][:-1]) * 3600 + if duration['minutes']: + result += int(duration['minutes'][:-1]) * 60 + if duration['seconds']: + result += int(duration['seconds'][:-1]) + return result + + def bandcamp_album_art(self, album_id): + api_url = self.BANDCAMP_ALBUM_ART_URL.format(album_id) + try: + json_obj = get_json_obj(api_url) + except URLError as e: + return {} + + art_id = json_obj.get('art_id') + band_id = json_obj.get('band', {}).get('image_id') + + duration = 0.0 + for track in json_obj.get('tracks', []): + duration += float(track.get('duration', '0')) + + result = {} + if art_id: + result['art'] = f'https://f4.bcbits.com/img/a{art_id}_5.jpg' + if band_id: + result['band'] = f'https://f4.bcbits.com/img/{band_id}_20.jpg' + if duration: + result['duration'] = int(duration) + return result + + def bandcamp_track_art(self, track_url): + try: + track_page = get_json(track_url) + except URLError as e: + return {} + + art_match = re.search(self.RE_BANDCAMP_TRACK_ART, track_page) + band_match = re.search(self.RE_BANDCAMP_TRACK_BAND_ART, track_page) + title_match = re.search(self.RE_BANDCAMP_TRACK_TITLE, track_page) + artist_match = re.search(self.RE_BANDCAMP_TRACK_ARTIST, track_page) + duration_matches = re.finditer(self.RE_BANDCAMP_TRACK_DURATION, track_page) + result = {} + if art_match: + art_id = art_match.groupdict().get('art_id') + result['art'] = f'https://f4.bcbits.com/img/a{art_id}_5.jpg' + if band_match: + band_id = band_match.groupdict().get('band_art_id') + result['band'] = f'https://f4.bcbits.com/img/{band_id}_20.jpg' + if title_match: + result['title'] = title_match.groupdict().get('title', '').strip() + if artist_match: + result['artist'] = artist_match.groupdict().get('artist') + + duration = 0.0 + for match in duration_matches: + duration += float(match.groupdict().get('duration', '0')) + result['duration'] = int(duration) + + return result + + def bandcamp_band_art(self, track_url): + try: + track_page = get_json(track_url) + except URLError as e: + return {} + + band_match = re.search(self.RE_BANDCAMP_TRACK_BAND_ART, track_page) + artist_match = re.search(self.RE_BANDCAMP_TRACK_ARTIST, track_page) + result = {} + if band_match: + band_id = band_match.groupdict().get('band_art_id') + result['band'] = f'https://f4.bcbits.com/img/{band_id}_20.jpg' + result['art'] = result['band'] + if artist_match: + result['artist'] = artist_match.groupdict().get('artist') + result['title'] = result['artist'] + + return result + + def indigitube_album_art(self, album_id): + api_url = self.INDIGITUBE_ALBUM_URL.format(album_id) + result = {} + + try: + json_obj = get_json_obj(api_url) + except URLError as e: + return result + + data = json_obj.get('data', {}) + + art_id = data.get('coverImage', {}).get('_id') + if art_id: + result['art'] = self.INDIGITUBE_ALBUM_ART_URL.format(art_id) + description = json_obj.get('data', {}).get('description', '') + if description: + result['textbody'] = re.compile(r'<[^>]+>').sub('', description) + result['title'] = json_obj.get('title') + result['artist'] = json_obj.get('realms', [{}])[0].get('title') + + return result + + def youtube_video_duration(self, video_id): + video_url = self.YOUTUBE_VIDEO_DURATION_URL.format(video_id) + try: + video_page = get_json(video_url) + except URLError as e: + return {} + + duration_match = re.search(self.RE_YOUTUBE_VIDEO_DURATION, video_page) + title_match = re.search(self.RE_YOUTUBE_VIDEO_TITLE, video_page) + artist_match = re.search(self.RE_YOUTUBE_VIDEO_ARTIST, video_page) + desc_match = re.search(self.RE_YOUTUBE_VIDEO_DESC, video_page) + result = {'duration': 0} + if duration_match: + gd = duration_match.groupdict() + result['duration'] = self.get_pt_duration(gd) + if title_match: + result['title'] = title_match.groupdict().get('title', '').strip() + if artist_match: + result['artist'] = artist_match.groupdict().get('artist', '').strip() + if desc_match: + result['textbody'] = html.unescape(desc_match.groupdict().get('textbody', '').strip()) + + return result + + def youtube_playlist_art(self, playlist_id): + api_url = self.YOUTUBE_PLAYLIST_ART_URL.format(playlist_id) + try: + playlist_page = get_json(api_url) + except URLError as e: + return {} + + art_match = re.search(self.RE_YOUTUBE_PLAYLIST_ART, playlist_page) + duration_matches = re.findall(self.RE_YOUTUBE_PLAYLIST_DURATION, playlist_page) + title_match = re.search(self.RE_YOUTUBE_PLAYLIST_TITLE, playlist_page) + artist_match = re.search(self.RE_YOUTUBE_PLAYLIST_ARTIST, playlist_page) + + result = {} + if art_match: + result['art'] = art_match.groupdict().get('art_url').replace('&', '&') + '&ext=.jpg' + else: + art_match = re.search(self.RE_YOUTUBE_PLAYLIST_ART_LQ, playlist_page) + result['art'] = art_match.groupdict().get('art_url') + if duration_matches: + result['duration'] = self.get_sum_duration(duration_matches) + if title_match: + result['title'] = html.unescape(title_match.groupdict().get('title', '').strip()) + if artist_match: + result['artist'] = html.unescape(artist_match.groupdict().get('artist', '').strip()) + return result + + def spotify_album_art(self, src): + api_url = src + try: + spotify_page = get_json(api_url) + except URLError as e: + return {} + + art_match = re.search(self.RE_SPOTIFY_ALBUM_ART, spotify_page) + duration_matches = re.findall(self.RE_SPOTIFY_DURATION, spotify_page) + + result = {} + if art_match: + result['art'] = art_match.groupdict().get('art_url') + if duration_matches: + result['duration'] = self.get_sum_duration(duration_matches) + return result + + def apple_album_art(self, album_id): + api_url = self.APPLE_ALBUM_URL.format(album_id) + try: + album_page = get_json(api_url) + except URLError as e: + return {} + + art_match = re.search(self.RE_APPLE_ALBUM_ART, album_page) + duration_match = re.finditer(self.RE_APPLE_DURATION, album_page) + result = {'duration': 0} + + if art_match: + result['art'] = art_match.groupdict().get('art_url') + for duration in duration_match: + gd = duration.groupdict() + result['duration'] += self.get_pt_duration(duration) + + return result + +class FeaturedAlbumScraper(Scraper, ExternalMedia): + RESOURCE_PATH_PATTERN = '/featured_albums/{album_id}' + WEBSITE_PATH_PATTERN = '/explore/album-of-the-week/{album_id}' + + @property + def path(self): + return self.resource_path + + def generate(self): + pagesoup = self.soup() + + iframes = [ + { + 'src': iframe.attrs.get('src'), + 'attrs': None + } + for iframe in pagesoup.findAll('iframe') + if iframe.attrs.get('src') + ] + album_urls = self.media_items(iframes, fetch_album_art=True, fetch_yt_video=True) + + album_copy = '\n'.join([p.text for p in pagesoup.find(class_='feature-album__copy').findAll("p", recursive=False)]) + album_image = pagesoup.find(class_='audio-summary__album-artwork') + album_info = pagesoup.find(class_='album-banner__copy') + album_title = album_info.find(class_='album-banner__heading', recursive=False).text + album_artist = album_info.find(class_='album-banner__artist', recursive=False).text + + album_type = 'featured_album' + album_id = self.resource_path.split('/')[-1] + background = None + duration = None + + for album in [album for album in album_urls if album.get('plugin')]: + album_type = album.get('plugin') + album_id = album.get('media_id') + background = album.get('background') + duration = album.get('duration') + + data = [ + { + 'type': album_type, + 'id': album_id, + 'attributes': { + 'title': album_title, + 'artist': album_artist, + 'textbody': album_copy, + 'duration': duration, + }, + 'links': { + 'self': self.path, + } + } + ] + + if album_image: + data[0]['attributes']['thumbnail'] = album_image.attrs.get('src') + + if background: + data[0]['attributes']['background'] = background + + return { + 'data': data, + } + + + +class FeaturedAlbumsScraper(Scraper): + RESOURCE_PATH_PATTERN = '/featured_albums' + WEBSITE_PATH_PATTERN = '/explore/album-of-the-week' + + def generate(self): + return { + 'data': [ + FeaturedAlbum(item).to_dict() + for item in self.soup().findAll('div', class_='card clearfix') + ], + 'links': self.pagination() + } + + +class NewsItemsScraper(Scraper): + RESOURCE_PATH_PATTERN = '/news_items' + WEBSITE_PATH_PATTERN = '/explore/news-articles' + + def generate(self): + return { + 'data': [ + News(item).to_dict() + for item in self.soup().findAll(class_='list-view__item') + ], + 'links': self.pagination(), + } + + +class NewsItemScraper(Scraper): + RESOURCE_PATH_PATTERN = '/news_items/{item}' + WEBSITE_PATH_PATTERN = '/explore/news-articles/{item}' + + +class ProgramBroadcastScraper(Scraper): + RESOURCE_PATH_PATTERN = '/programs/{program_id}/broadcasts/{item}' + WEBSITE_PATH_PATTERN = '/explore/programs/{program_id}/episodes/{item}' + + def generate(self): + soup = self.soup() + programbg = soup.find(class_='banner__image') + programbg = programbg.attrs.get('src') if programbg else None + + broadcast = ProgramBroadcast( + soup.find(class_='audio-summary') + ).to_dict() + broadcast['attributes']['textbody'] = soup.find(class_='page-banner__summary').text + + segments = [ + ProgramBroadcastSegment(item).to_dict() + for item in soup.findAll(class_='episode-detail__highlights-item') + ] + + tracks = [ + ProgramBroadcastTrack(item).to_dict() + for item in soup.findAll(class_='audio-summary__track clearfix') + ] + + items = [] + for item in ([broadcast] + segments + tracks): + if not item: + continue + if programbg and not item.get('attributes', {}).get('background'): + item['attributes']['background'] = programbg + items.append(item) + + return { + 'data': items + } + + +class ProgramPodcastScraper(Scraper): + RESOURCE_PATH_PATTERN = '/programs/{program_id}/podcasts/{item}' + WEBSITE_PATH_PATTERN = '/explore/podcasts/{program_id}/episodes/{item}' + + def generate(self): + return {'data': []} + + +class ProgramSegmentScraper(Scraper): + RESOURCE_PATH_PATTERN = '/segments/{item}' + WEBSITE_PATH_PATTERN = '/on-demand/segments/{item}' + + def generate(self): + return {'data': []} + + +class ScheduleScraper(Scraper): + RESOURCE_PATH_PATTERN = '/schedule' + WEBSITE_PATH_PATTERN = '/explore/schedule' + + def generate(self): + soup = self.soup() + date = soup.find(class_='calendar__hidden-input').attrs.get('value') + prevdate, nextdate = [x.find('a').attrs.get('href').split('=')[-1] for x in soup.findAll(class_='page-nav__item')] + return { + 'data': [ + ScheduleItem(item).to_dict() + for item in self.soup().findAll(class_='list-view__item') + ], + 'links': self.pagination(pagekey='date', selfval=date, nextval=prevdate), + } + + +class SearchScraper(Scraper): + RESOURCE_PATH_PATTERN = '/search' + WEBSITE_PATH_PATTERN = '/search' + + def generate(self): + return { + 'data': [ + SearchItem(item).to_dict() + for item in self.soup().findAll(class_='search-result') + ], + 'links': self.pagination(), + } + + +class SoundscapesScraper(Scraper): + RESOURCE_PATH_PATTERN = '/soundscapes' + WEBSITE_PATH_PATTERN = '/explore/soundscape' + + def generate(self): + return { + 'data': [ + Soundscape(item).to_dict() + for item in self.soup().findAll(class_='list-view__item') + ], + 'links': self.pagination() + } + + +class SoundscapeScraper(Scraper, ExternalMedia): + RESOURCE_PATH_PATTERN = '/soundscapes/{item}' + WEBSITE_PATH_PATTERN = '/explore/soundscape/{item}' + + def generate(self): + pagesoup = self.soup() + + iframes = [] + section = pagesoup.find('section', class_='copy') + for heading in section.findAll(['h1', 'h2', 'h3', 'h4', 'p'], recursive=False): + iframe = heading.find_next_sibling() + while iframe != None and iframe.find('iframe') == None: + iframe = iframe.find_next_sibling() + if iframe == None or len(heading.text) < 2: + continue + + aotw = len(heading.text.split('**')) > 1 + + attrs = { + 'id': ' '.join(heading.text.split('**')[0].split(' - ')), + 'title': heading.text.split('**')[0].split(' - ')[-1].split(' – ')[-1], + 'artist': heading.text.split(' - ')[0].split(' – ')[0], + 'featured_album': heading.text.split('**')[1] if aotw else '', + } + media = { + 'src': iframe.find('iframe').attrs.get('src'), + 'attrs': attrs, + } + if aotw: + iframes.insert(0, media) + else: + iframes.append(media) + + media_items = self.media_items(iframes, fetch_album_art=True, fetch_yt_video=True) + soundscape_date = pagesoup.find(class_='news-item__title').text.split(' - ')[-1] + + data = [] + for media in media_items: + dataitem = {} + attributes = { + 'subtitle': soundscape_date, + 'artist': media.get('attrs').get('artist'), + 'thumbnail': media.get('thumbnail'), + } + + if media.get('background'): + attributes['background'] = media.get('background') + + if media.get('duration'): + attributes['duration'] = media.get('duration') + + if media.get('plugin'): + # dataitem['id'] = media.get('attrs').get('id', '').replace(' ', '-').lower() + dataitem['id'] = media.get('media_id') + dataitem['type'] = media.get('plugin') + attributes['title'] = media.get('attrs').get('title') + # attributes['url'] = media.get('url') + else: + dataitem['id'] = '' + attributes['title'] = media.get('attrs').get('title') + + if media.get('attrs').get('textbody'): + attributes['textbody'] = media.get('attrs').get('textbody', '').strip() + else: + attributes['textbody'] = '{}\n{}\n'.format( + media.get('attrs').get('title'), + media.get('attrs').get('featured_album') + ).strip() + + dataitem['attributes'] = attributes + + data.append(dataitem) + + return { + 'data': data, + } + + +class Program(Resource): + @property + def path(self): + return f"{Scraper.resource_path_for(self._itemobj.find('a').attrs['href'])}/broadcasts?page=1" + + def id(self): + return self.path.split("/")[2] + + @property + def title(self): + return self._itemobj.find('h1', class_='card__title' ).find('a').text + + @property + def textbody(self): + return self._itemobj.find('p').text + + def attributes(self): + return { + 'title': self.title, + 'thumbnail': self.thumbnail, + 'textbody': self.textbody, + } + + +class Topic(Resource): + @property + def title(self): + return self._itemobj.find('a').text + + def attributes(self): + return { + 'title': self.title + } + + +class TopicsScraper(Scraper): + RESOURCE_PATH_PATTERN = '/topics' + WEBSITE_PATH_PATTERN = '/' + + def generate(self): + return { + 'data': [ + Topic(item).to_dict() + for item in self.soup().findAll(class_='topic-list__item') + ], + 'links': { + 'self': self.__class__.RESOURCE_PATH_PATTERN + }, + } + + +class TopicScraper(Scraper): + RESOURCE_PATH_PATTERN = '/topics/{topic}' + WEBSITE_PATH_PATTERN = '/topics/{topic}' + + def generate(self): + return { + 'data': [ + SearchItem(item).to_dict() + for item in self.soup().findAll(class_='search-result') + ], + 'links': self.pagination(), + } + + +class TracksSearchScraper(Scraper): + RESOURCE_PATH_PATTERN = '/tracks/search' + WEBSITE_PATH_PATTERN = '/tracks/search' + + def generate(self): + return { + 'data': [ + BroadcastTrack(item).to_dict() + for item in self.soup().findAll(class_='search-result') + ], + } + + +class TrackScraper(Scraper): + RESOURCE_PATH_PATTERN = '/tracks/{track_id}' + WEBSITE_PATH_PATTERN = '/tracks/{track_id}' + + def generate(self): + return {'data': []} + + +class Track(Resource): + def __init__(self, path, artist, title): + self._path = path + self.artist = artist + self.title = title + + @property + def path(self): + return self._path + + def id(self): + return self.path.split('/')[-1] + + def attributes(self): + return { + 'title': self.title, + 'artist': self.artist, + } + + +class EventsScraper(Scraper): + RESOURCE_PATH_PATTERN = '/events' + WEBSITE_PATH_PATTERN = '/events' + + def generate(self): + return { + 'data': [ + Event(item).to_dict() + for item in self.soup().findAll('div', class_='card') + ], + 'links': self.pagination() + } + + +class EventScraper(Scraper, ExternalMedia): + RESOURCE_PATH_PATTERN = '/events/{item}' + WEBSITE_PATH_PATTERN = '/events/{item}' + + @property + def path(self): + return self.resource_path + + def generate(self): + item = self.soup().find(class_='event') + venue = item.find(class_='event__venue-address-details') + eventdetails = item.find(class_='event__details-copy').get_text(' ').strip() + copy = item.find(class_='copy') + textbody = copy.get_text('\n') + + flag_label = item.find(class_='flag-label') + if flag_label: + event_type = flag_label.text.replace(' ', '-').lower() + else: + # event_type = None + event_type = 'event' + + result = { + 'data': [ + { + 'type': event_type, + 'id': Resource.id(self), + 'attributes': { + 'title': item.find(class_='event__title').text, + 'venue': venue.get_text(' ') if venue else '', + 'textbody': '\n'.join((eventdetails, textbody)), + }, + 'links': { + 'self': self.resource_path, + } + } + ], + } + + for link in copy.find_all(['a', 'iframe']): + link_href = { + 'src': link.attrs.get('href', link.attrs.get('src')), + } + media = self.media_items([link_href], fetch_album_art=True, fetch_yt_video=True)[0] + if media.get('plugin'): + dataitem = {} + if media.get('plugin'): + dataitem['id'] = media.get('media_id') + dataitem['type'] = media.get('plugin') + else: + dataitem['id'] = '' + + dataitem['attributes'] = { + 'thumbnail': media.get('thumbnail'), + 'background': media.get('background'), + 'duration': media.get('duration'), + 'title': media.get('attrs').get('title'), + 'textbody': media.get('attrs').get('textbody', media.get('attrs').get('title')), + 'artist': media.get('attrs').get('artist'), + } + + result['data'].append(dataitem) + + return result + + +class GiveawaysScraper(Scraper): + RESOURCE_PATH_PATTERN = '/giveaways' + WEBSITE_PATH_PATTERN = '/subscriber-giveaways' + + def generate(self): + return { + 'data': [ + Giveaway(item).to_dict() + for item in self.soup().findAll(class_='list-view__item') + ], + } + + +class GiveawayScraper(Scraper): + RESOURCE_PATH_PATTERN = '/giveaways/{giveaway}' + WEBSITE_PATH_PATTERN = '/subscriber-giveaways/{giveaway}' + + @property + def path(self): + return self.resource_path + + def generate(self): + item = self.soup().find(class_='subscriber_giveaway') + banner = self.soup().find(class_='compact-banner') + closes = banner.find(class_='compact-banner__date').text + textbody = item.find(class_='subscriber-giveaway__copy').get_text(' ') + + return { + 'data': [ + { + 'type': 'giveaway', + 'id': Resource.id(self), + 'attributes': { + 'title': banner.find(class_='compact-banner__heading').text, + 'textbody': f'{closes}\n\n{textbody}', + 'thumbnail': item.find(class_='summary-inset__artwork').attrs.get('src'), + }, + 'links': { + 'self': '/'.join((self.resource_path, 'entries')), + } + } + ], + } + + +class VideoScraper(Scraper): + RESOURCE_PATH_PATTERN = '/videos/{item}' + WEBSITE_PATH_PATTERN = '/explore/videos/{item}' + + def generate(self): + return {'data': []} + + +class VideosScraper(Scraper, ExternalMedia): + RESOURCE_PATH_PATTERN = '/videos' + WEBSITE_PATH_PATTERN = '/explore/videos' + + def generate(self): + pagesoup = self.soup() + + images = [] + for card in pagesoup.findAll(class_='card'): + img = card.find('img', class_='scalable-image__image') + carddate = card.find('span', class_='card__meta') + cardurl = card.find('a', class_='card__anchor') + #time.strptime(carddate.text, '%d %B %Y') + + attrs = { + 'id': cardurl.attrs.get('href', '/').split('/')[-1], + 'title': img.attrs.get('alt'), + 'date': carddate.text, + } + media = { + 'src': img.attrs.get('data-src'), + 'attrs': attrs, + } + images.append(media) + + media_items = self.media_items(images, fetch_album_art=True) + + data = [] + for media in media_items: + dataitem = {} + attributes = { + 'subtitle': media.get('attrs').get('date'), + 'artist': media.get('attrs').get('artist'), + 'thumbnail': media.get('thumbnail'), + } + + if media.get('background'): + attributes['background'] = media.get('background') + + if media.get('duration'): + attributes['duration'] = media.get('duration') + + if media.get('plugin'): + dataitem['id'] = media.get('media_id') + dataitem['type'] = media.get('plugin') + attributes['title'] = media.get('attrs').get('title') + else: + dataitem['id'] = '' + attributes['title'] = media.get('attrs').get('title') + + attributes['textbody'] = media.get('attrs').get('title').strip() + + dataitem['attributes'] = attributes + + data.append(dataitem) + + return { + 'data': data, + 'links': self.pagination(), + } + + + +### Scrapers ############################################## + +class FeaturedAlbum(Resource): + @property + def title(self): + return self._itemobj.find('h1', class_='card__title' ).find('a').text + + @property + def subtitle(self): + return self._itemobj.find(class_='card__meta').text + + @property + def textbody(self): + return self._itemobj.find('p').text + + def attributes(self): + return { + 'title': self.title, + 'subtitle': self.subtitle, + 'thumbnail': self.thumbnail, + 'textbody': self.textbody, + } + + +class Giveaway(Resource): + @property + def title(self): + return self._itemobj.find('span').text + + @property + def textbody(self): + return self._itemobj.find('p').text + + def attributes(self): + return { + 'title': self.title, + 'textbody': self.textbody, + 'thumbnail': self.thumbnail, + } + + +class News(Resource): + @property + def title(self): + return self._itemobj.find(class_='list-view__title').text + + @property + def type(self): + return 'news_item' + + @property + def textbody(self): + return self._itemobj.find(class_='list-view__summary').text + + def attributes(self): + return { + 'title': self.title, + 'textbody': self.textbody, + } + + +class Soundscape(Resource): + @property + def title(self): + return self._itemobj.find('span').text.replace(':', '').replace('Triple R ', '') + + @property + def subtitle(self): + return self._itemobj.find('span').text.split(' - ')[-1] + + @property + def textbody(self): + return self._itemobj.find('p').text + + def attributes(self): + return { + 'title': self.title, + 'subtitle': self.subtitle, + 'textbody': self.textbody, + 'thumbnail': self.thumbnail, + } + + +class Event(Resource): + @property + def _itemtitle(self): + return self._itemobj.find(class_='card__title').find('a').text + + @property + def title(self): + if self.label: + return ' - '.join((self._itemtitle, self._itemdate, self.label)) + else: + return ' - '.join((self._itemtitle, self._itemdate)) + + @property + def label(self): + label = self._itemobj.find(class_='card__label') + return label.text if label else '' + + @property + def _itemtype(self): + return self._itemobj.find(class_='card__meta').find('div').text + + @property + def type(self): + return self._itemtype.replace(' ', '-').lower() + + @property + def img(self): + return self._itemobj.find('a', class_='card__anchor').find('img') + + @property + def _itemdate(self): + meta = self._itemobj.find('span', class_='card__meta') + metadiv = meta.findAll('div') + if len(metadiv) > 0: + return metadiv[0].text + else: + return meta.text if meta else '' + + @property + def venue(self): + meta = self._itemobj.find('span', class_='card__meta') + metadiv = meta.findAll('div') + if len(metadiv) > 1: + return metadiv[1].text + + @property + def textbody(self): + venue = self.venue + return '\n'.join((self._itemtitle, 'Date: ' + self._itemdate, ('Venue:\n' + venue) if venue else '', '', self._itemtype)) + + def attributes(self): + return { + 'title': self.title, + 'thumbnail': self.thumbnail, + 'venue': self.venue, + 'textbody': self.textbody, + } + + +class ScheduleItem: + def __init__(self, itemobj): + self._itemobj = itemobj + self._audio_item = AudioItem.factory(itemobj) + + @property + def path(self): + path = Scraper.resource_path_for(self._itemobj.find('a').attrs['href']) + segments = path.split('?')[0].split('/') + if 'programs' in segments and 'broadcasts' not in segments: + path += '/broadcasts?page=1' + + return path + + @property + def start(self): + return self._itemobj.attrs.get('data-timeslot-start') + + @property + def end(self): + return self._itemobj.attrs.get('data-timeslot-end') + + @property + def _on_air_status(self): + if self.start and self.end and '+' in self.start: + start = self.start.split('+') + end = self.end.split('+') + td = timedelta(hours=int(start[1][:2])) + try: + start = strptime(start[0], '%Y-%m-%dT%H:%M:%S') - td + end = strptime(end[0], '%Y-%m-%dT%H:%M:%S') - td + return start, end + except (ValueError, TypeError) as e: + pass + return None, None + + @property + def textbody(self): + return self._itemobj.find('p').text + + @property + def duration(self): + if self.audio_item: + return self.audio_item.get('attributes').get('duration') + + @property + def content(self): + content = json.loads(self._itemobj.find(class_='hide-from-all').attrs['data-content']) + content['title'] = content.pop('name') + + if self.audio_item: + content['type'] = 'broadcast_index' + content['title'] = self.audio_item.get('attributes').get('title') + else: + if '/broadcasts?page=1' not in self.path: + content['type'] = 'broadcast_index' + elif content['type'] == 'programs': + content['type'] = 'program' + else: + content['type'] = 'scheduled' + + start, end = self._on_air_status + if (not ignore_on_air) and start and end: + localtime = datetime.utcnow() + if start < localtime and end > localtime: + flag_label = self._itemobj.find(class_='flag-label__on-air').next_sibling + if flag_label: + content['on_air'] = flag_label.string + img = self._itemobj.find(class_='list-view__image') + if img: + content['thumbnail'] = img.attrs.get('data-src') + + return content + + @property + def audio_item(self): + return self._audio_item or {} + + def to_dict(self): + attrs = { + **self.content, + 'start': self.start, + 'end': self.end, + 'textbody': self.textbody, + 'duration': self.duration, + } + itemid = attrs.pop('id') + itemtype = attrs.pop('type') + + return { + 'type': itemtype, + 'id': itemid, + 'attributes': attrs, + 'links': { + 'self': self.path + } + } + + +class ItemType: + def from_label(val): + default = "_".join(val.lower().split()) + return { + 'album_of_the_week': 'featured_album', + 'audio_archive': 'archive', + 'broadcast_episode': 'broadcast', + 'news': 'news_item', + 'podcast_episode': 'podcast', + }.get(default, default) + + +class SearchItem(Resource): + @property + def type(self): + return ItemType.from_label(self._itemobj.find(class_='flag-label').text) + + @property + def title(self): + return self._itemobj.find(class_='search-result__title').text + + @property + def textbody(self): + body = self._itemobj.find(class_='search-result__body') + if body: + return "\n\n".join([item.text for item in body.children]) + + def attributes(self): + return { + **Resource.attributes(self), + 'textbody': self.textbody, + } + + +class BroadcastTrack(Resource): + def id(self): + return f'{SearchItem.id(self)}.{self.track.id()}' + + @property + def title(self): + return f'{self.track.artist} - {self.track.title} (Broadcast on {self.broadcast_date} by {self.program_title})' + + RE = re.compile(r'Played (?P<played_date>[^/]+) by (?P<played_by>.+)View all plays$') + @property + def played(self): + return self.RE.match(self._itemobj.find(class_='search-result__meta-info').text) + + @property + def broadcast_date(self): + return time.strftime(DATE_FORMAT, time.strptime(self.played['played_date'], '%A %d %b %Y')) + + @property + def program_title(self): + return self.played['played_by'] + + @property + def track(self): + return Track( + Scraper.resource_path_for(self._itemobj.find(class_='search-result__meta-links').find('a').attrs['href']), + self._itemobj.find(class_='search-result__track-artist').text, + self._itemobj.find(class_='search-result__track-title').text, + ) + + def attributes(self): + return { + 'broadcast_date': self.broadcast_date, + 'program_title': self.program_title, + } + + def relationships(self): + return { + 'broadcast': { + 'links': { + # TODO - FIXME: + # Nb. this shouldn't be `self.path` as this class is a BroadcastTrack not a Broadcast + # which _also_ means that BroadcastTrack shouldn't have a `links.self` + 'related': self.path + }, + 'data': { + 'type': 'broadcast', + 'id': Resource.id(self), + }, + }, + 'track': { + 'links': { + 'related': self.track.path, + }, + 'data': { + 'type': self.track.type, + 'id': self.track.id(), + }, + }, + } + + def included(self): + return [ + self.track.to_dict(), + ] + + +class PlayableResource(Resource): + @property + def _playable(self): + view_playable_div = self._itemobj.find(lambda tag:tag.name == 'div' and 'data-view-playable' in tag.attrs) + if view_playable_div: + return json.loads(view_playable_div.attrs['data-view-playable'])['items'][0] + else: + return {} + + @property + def _data(self): + return self._playable.get('data', {}) + + @property + def _audio_data(self): + return self._data.get('audio_file', {}) + + @property + def _on_air_toggle(self): + dataview = self._itemobj.attrs.get('data-view-on-air-toggle') + if dataview: + return json.loads(dataview) + + @property + def _on_air_status(self): + toggle = self._on_air_toggle + if toggle: + start = toggle.get('startTime').split('+') + end = toggle.get('endTime').split('+') + td = timedelta(hours=int(start[1][:2])) + try: + start = strptime(start[0], '%Y-%m-%dT%H:%M:%S') - td + end = strptime(end[0], '%Y-%m-%dT%H:%M:%S') - td + return start, end + except (ValueError, TypeError) as e: + pass + return None, None + + @property + def type(self): + t = self._playable.get('type') + if t == 'clip': + return 'segment' + if t == 'broadcast_episode': + return 'broadcast' + else: + return t + + def id(self): + if self._playable: + return str(self._playable.get('source_id')) + + @property + def path(self): + return + + @property + def title(self): + if self._data: + return self._data.get('title') + else: + start, end = self._on_air_status + localtime = datetime.utcnow() + title = None + + if start and end and self._on_air_toggle: + if start > localtime: + title = self._itemobj.find(class_=self._on_air_toggle.get('upcomingEl')[1:]) + if start < localtime and end > localtime: + title = self._itemobj.find(class_=self._on_air_toggle.get('onAirEl')[1:]) + if end < localtime: + title = self._itemobj.find(class_=self._on_air_toggle.get('offAirEl')[1:]) + elif self._on_air_toggle: + title = self._itemobj.find(class_=self._on_air_toggle.get('offAirEl')[1:]) + + return title.find('span').text if title else None + + @property + def subtitle(self): + return self._data.get('subtitle') + + @property + def textbody(self): + return None + + @property + def _itemtime(self): + if self.subtitle: + try: + return time.strptime(self.subtitle, '%d %B %Y') + except ValueError: + return + + @property + def date(self): + if self._itemtime: + return time.strftime(DATE_FORMAT, self._itemtime) + + @property + def year(self): + if self._itemtime: + return self._itemtime[0] + + @property + def aired(self): + return self.date + + @property + def duration(self): + if self._audio_data: + return round(self._audio_data.get('duration', 0)) + elif self._data: + return round(self._data.get('duration', 0)) + + @property + def url(self): + if self._data and self._data.get('timestamp'): + return f"https://ondemand.rrr.org.au/getclip?bw=h&l={self.duration}&m=r&p=1&s={self._data.get('timestamp')}" + elif self._audio_data and self._audio_data.get('path'): + return self._audio_data.get('path') + else: + start, end = self._on_air_status + localtime = datetime.utcnow() + + if start and end: + if start < localtime and end > localtime: + return 'https://ondemand.rrr.org.au/stream/ws-hq.m3u' + + @property + def thumbnail(self): + if self._data: + return self._data.get('image', {}).get('path') + else: + img = self._itemobj.find(class_='audio-summary__image') + if img: + return img.attrs.get('data-src') + + def attributes(self): + return { + 'title': self.title, + 'subtitle': self.subtitle, + 'textbody': self.textbody, + 'date': self.date, + 'year': self.year, + 'aired': self.aired, + 'duration': self.duration, + 'url': self.url, + 'thumbnail': self.thumbnail, + } + + +class ProgramBroadcast(PlayableResource): + ''' + <div data-view-playable=' + { + "component":"episode_player", + "formattedDuration":"02:00:00", + "shareURL":"https://www.rrr.org.au/explore/programs/the-international-pop-underground/episodes/22347-the-international-pop-underground-19-october-2022", + "sharedMomentBaseURL":"https://www.rrr.org.au/shared/broadcast-episode/22347", + "items":[ + { + "type":"broadcast_episode", + "source_id":22347, + "player_item_id":269091, + "data":{ + "title":"The International Pop Underground – 19 October 2022", + "subtitle":"19 October 2022", + "timestamp":"20221019200000", + "duration":7200, + "platform_id":1, + "image":{ + "title":"International Pop Underground program image" + "path":"https://cdn-images-w3.rrr.org.au/81wyES6vU8Hyr8MdSUu_kY6cBGA=/300x300/https://s3.ap-southeast-2.amazonaws.com/assets-w3.rrr.org.au/assets/041/aa8/63b/041aa863b5c3655493e6771ea91c13bb55e94d24/International%20Pop%20Underground.jpg" + } + } + } + ] + }" + ''' + + + +class ProgramBroadcastSegment(PlayableResource): + ''' + <div data-view-playable=' + { + "component": "player_buttons", + "size": "normal", + "items": [ + { + "type": "clip", + "source_id": 3021, + "player_item_id": 270803, + "data": { + "title": "International Pop Underground: Guatemalan Cellist/Songwriter Mabe Fratti Seeks Transcendence", + "subtitle": "19 October 2022", + "platform_id": 1, + "timestamp": "20221019211747", + "duration": 1097, + "image": { + "title": "Mabe Fratti", + "path": "https://cdn-images-w3.rrr.org.au/1v6kamv_8_4xheocBJCa6FKZY_8=/300x300/https://s3.ap-southeast-2.amazonaws.com/assets-w3.rrr.org.au/assets/3a7/61f/143/3a761f1436b97a186be0cf578962436d9c5404a8/Mabe-Fratti.jpg" + } + } + } + ] + } + '><div class="d-flex"> + ''' + + + +class ProgramBroadcastTrack(Resource, ExternalMedia): + _media = {} + + def id(self): + if self.media: + return self.media + else: + return re.sub(r'[\[\]\{\}\(\)\.\/\\,\:\;]', '', f'{self.artist}-{self.title}'.lower().replace(' ', '-')) + + @property + def type(self): + if self.media: + return self._media.get('plugin') + else: + return super().type + + @property + def artist(self): + return self._itemobj.find(class_='audio-summary__track-artist').text.strip() + + @property + def broadcast_artist(self): + params = { 'q': self.artist } + return '/tracks/search?' + urlencode(params) + + @property + def broadcast_track(self): + params = { 'q': f'{self.title} - {self.artist}' } + return '/tracks/search?' + urlencode(params) + + @property + def title(self): + return self._itemobj.find(class_='audio-summary__track-title').text.strip() + + def _get_media(self): + if not self._media: + href = self._itemobj.find(class_='audio-summary__track-title').attrs.get('href') + if href: + self._media = self.media_items([{'src': href}], fetch_album_art=True)[0] + return self._media if self._media else {} + + @property + def media(self): + return self._get_media().get('media_id') + + @property + def thumbnail(self): + return self._get_media().get('thumbnail') + + @property + def background(self): + return self._get_media().get('background') + + @property + def duration(self): + return self._get_media().get('duration') + + def attributes(self): + attr = { + 'artist': self.artist, + 'title': self.title, + } + if self.thumbnail: + attr['thumbnail'] = self.thumbnail + if self.background: + attr['background'] = self.background + if self.duration: + attr['duration'] = self.duration + return attr + + def links(self): + return { + 'broadcast_artist': self.broadcast_artist, + 'broadcast_track': self.broadcast_track, + } + + +class BroadcastCollection(Resource): + @property + def type(self): + return 'broadcast_index' + + def id(self): + return self.path + + @property + def _playable(self): + view_playable_div = self._itemobj.find(lambda tag:tag.name == 'div' and 'data-view-playable' in tag.attrs) + if view_playable_div: + return json.loads(view_playable_div.attrs['data-view-playable'])['items'][0] + else: + return {} + + @property + def _data(self): + return self._playable.get('data', {}) + + @property + def duration(self): + if self._data: + return round(self._data.get('duration')) + + @property + def title(self): + return self._itemobj.find(class_='card__title').text + + @property + def thumbnail(self): + programimage = self._itemobj.find(class_='card__background-image') + if programimage: + programimagesrc = re.search(r"https://[^']+", programimage.attrs.get('style')) + if programimagesrc: + return programimagesrc[0] + + programimage = self._itemobj.find(class_='scalable-image__image') + if programimage: + return programimage.attrs.get('data-src') + + @property + def textbody(self): + cardbody = self._itemobj.find(class_='card__meta') + if cardbody: + return cardbody.text + + def attributes(self): + return { + 'title': self.title, + 'textbody': self.textbody, + 'thumbnail': self.thumbnail, + 'duration': self.duration, + } + + + +class AudioItem: + + @classmethod + def factory(cls, item): + cardbody = item.find(class_='card__body') + if cardbody: + textbody = cardbody.text + else: + cardbody = item.find(class_='card__meta') + if cardbody: + textbody = cardbody.text + else: + textbody = '' + + view_playable_div = item.find(lambda tag:tag.name == 'div' and 'data-view-playable' in tag.attrs) + if view_playable_div: + view_playable = view_playable_div.attrs['data-view-playable'] + itemobj = json.loads(view_playable)['items'][0] + + if 'data-view-account-toggle' in view_playable_div.parent.parent.attrs: + itemobj['subscription_required'] = True + else: + itemobj['subscription_required'] = False + + if itemobj['type'] == 'clip': + obj = Segment(item, itemobj, textbody) + elif itemobj['type'] == 'broadcast_episode': + obj = Broadcast(item, itemobj, textbody) + elif itemobj['type'] == 'audio_archive_item': + obj = Archive(item, itemobj, textbody) + elif itemobj['type'] == 'podcast_episode': + obj = Podcast(item, itemobj, textbody) + else: + obj = AudioItem(item, itemobj, textbody) + return obj.to_dict() + else: + # Should we _also_ have a NonPlayable AudioItem ? + return None + + + def __init__(self, item, itemobj, textbody): + self._item = item + self._itemobj = itemobj + self._itemdata = itemobj['data'] + self.textbody = textbody + + @property + def resource_path(self): + card_anchor = self._item.find(class_='card__anchor') + if card_anchor: + return Scraper.resource_path_for(card_anchor.attrs['href']) + + @property + def type(self): + return self.__class__.__name__.lower() + + @property + def subscription_required(self): + return self._itemobj.get('subscription_required') + + @property + def id(self): + return str(self._itemobj['source_id']) + + @property + def title(self): + return self._itemdata['title'] + + @property + def subtitle(self): + return self._itemdata['subtitle'] + + @property + def _itemtime(self): + return time.strptime(self._itemdata['subtitle'], '%d %B %Y') + + @property + def date(self): + return time.strftime(DATE_FORMAT, self._itemtime) + + @property + def year(self): + return self._itemtime[0] + + @property + def aired(self): + return self.date + + @property + def duration(self): + duration = self._itemobj.get('data', {}).get('duration', {}) + if not duration: + audio_file = self._itemdata.get('audio_file') + if audio_file: + duration = audio_file['duration'] + else: + duration = 0 + return round(duration) + + @property + def thumbnail(self): + return self._itemdata['image']['path'] if 'image' in self._itemdata.keys() else '' + + @property + def url(self): + audio_file = self._itemdata.get('audio_file') + if audio_file: + return audio_file['path'] + else: + ts = self._itemdata['timestamp'] + l = self.duration + return 'https://ondemand.rrr.org.au/getclip?bw=h&l={}&m=r&p=1&s={}'.format(l, ts) + + def to_dict(self): + item = { + 'type': self.type, + 'id': self.id, + 'attributes': { + 'title': self.title, + 'subtitle': self.subtitle, + 'textbody': self.textbody, + 'date': self.date, + 'year': self.year, + 'aired': self.aired, + 'duration': self.duration, + 'url': self.url, + 'thumbnail': self.thumbnail, + }, + 'links': { + 'self': self.resource_path, + } + } + if self.subscription_required: + item['links']['subscribe'] = '/subscribe' + return item + + +class Archive(AudioItem): + '' + +class Broadcast(AudioItem): + '' + +class Segment(AudioItem): + '' + +class Podcast(AudioItem): + '' + + +if __name__ == "__main__": + print(json.dumps(Scraper.call(sys.argv[1]))) diff --git a/plugin.audio.tripler/resources/lib/tripler.py b/plugin.audio.tripler/resources/lib/tripler.py new file mode 100644 index 0000000000..123ebfc084 --- /dev/null +++ b/plugin.audio.tripler/resources/lib/tripler.py @@ -0,0 +1,630 @@ +from bs4 import BeautifulSoup +from datetime import datetime, timedelta +import time, sys, os, json, re +import pytz +from xbmcaddon import Addon +import xbmcgui +import xbmcplugin +import xbmc + +from resources.lib.scraper import Scraper +from resources.lib.website import TripleRWebsite +from resources.lib.media import Media + +from urllib.parse import parse_qs, urlencode, unquote_plus, quote_plus + +class TripleR(): + def __init__(self): + self.matrix = '19.' in xbmc.getInfoLabel('System.BuildVersion') + self.handle = int(sys.argv[1]) + self.id = 'plugin.audio.tripler' + self.url = 'plugin://' + self.id + self.tz = pytz.timezone('Australia/Melbourne') + self.addon = Addon() + self.dialog = xbmcgui.Dialog() + self._respath = os.path.join(self.addon.getAddonInfo('path'), 'resources') + self.icon = os.path.join(self._respath, 'icon.png') + self.fanart = os.path.join(self._respath, 'fanart.png') + self.website = TripleRWebsite(os.path.join(self._respath, 'cookies.lwp')) + self._signed_in = -1 + self.supported_plugins = Media.RE_MEDIA_URLS.keys() + quality = self.addon.getSetting('image_quality') + self.quality = int(quality) if quality else 1 + self.media = Media(self.quality) + + self.nextpage = self.get_string(30004) + self.lastpage = self.get_string(30005) + + def get_string(self, string_id): + return self.addon.getLocalizedString(string_id) + + def _notify(self, title, message): + xbmc.log(f'TripleR plugin notification: {title} - {message}', xbmc.LOGDEBUG) + self.dialog.notification(title, message, icon=self.icon) + + def parse(self): + args = parse_qs(sys.argv[2][1:]) + segments = sys.argv[0].split('/')[3:] + xbmc.log("TripleR plugin called: " + str(sys.argv), xbmc.LOGDEBUG) + + if 'schedule' in segments and args.get('picker'): + date = self.select_date(args.get('picker')[0]) + if date: + args['date'] = date + + k_title = args.get('k_title', [None])[0] + if k_title: + xbmcplugin.setPluginCategory(self.handle, k_title) + del args['k_title'] + + if args.get('picker'): + del args['picker'] + + if 'search' in segments and not args.get('q'): + search = self.search(tracks=('tracks' in segments)) + if search: + args['q'] = search + else: + return + + if 'ext_search' in segments: + self.ext_search(args) + return + + path = '/' + '/'.join(segments) + if args: + path += '?' + urlencode(args, doseq=True) + + if len(segments[0]) < 1: + return self.main_menu() + elif 'subscribe' in segments: + self._notify(self.get_string(30084), self.get_string(30083)) + elif 'settings' in segments: + self.login() + Addon().openSettings() + elif 'sign-in' in segments: + if self.sign_in(): + xbmc.executebuiltin("Container.Refresh") + elif 'sign-out' in segments: + self.sign_out() + xbmc.executebuiltin("Container.Refresh") + elif 'entries' in segments: + if self.addon.getSettingBool('authenticated'): + self.subscriber_giveaway(path=path) + else: + self._notify(self.get_string(30073), self.get_string(30076)) + elif 'play' in args: + self.play_stream(handle=self.handle, args=args, segments=segments) + return None + else: + scraped = Scraper.call(path) + parsed = self.parse_programs(**scraped, args=args, segments=segments, k_title=k_title) + if parsed: + return parsed + + def main_menu(self): + items = [ + self.livestream_item(), + {'label': self.get_string(30032), 'path': self.url + '/programs', 'icon': 'DefaultPartyMode.png'}, + {'label': self.get_string(30033), 'path': self.url + '/schedule', 'icon': 'DefaultYear.png'}, + # {'label': self.get_string(30034), 'path': self.url + '/broadcasts', 'icon': 'DefaultPlaylist.png'}, + {'label': self.get_string(30035), 'path': self.url + '/segments', 'icon': 'DefaultPlaylist.png'}, + {'label': self.get_string(30036), 'path': self.url + '/archives', 'icon': 'DefaultPlaylist.png'}, + {'label': self.get_string(30037), 'path': self.url + '/featured_albums', 'icon': 'DefaultMusicAlbums.png'}, + {'label': self.get_string(30038), 'path': self.url + '/soundscapes', 'icon': 'DefaultSets.png'}, + {'label': self.get_string(30042), 'path': self.url + '/videos', 'icon': 'DefaultMusicVideos.png'}, + {'label': self.get_string(30039), 'path': self.url + '/events', 'icon': 'DefaultPVRGuide.png'}, + {'label': self.get_string(30040), 'path': self.url + '/giveaways', 'icon': 'DefaultAddonsRecentlyUpdated.png'}, + {'label': self.get_string(30041), 'path': self.url + '/search', 'icon': 'DefaultMusicSearch.png'}, + ] + if self.login(): + emailaddress = self.addon.getSetting('emailaddress') + fullname = self.addon.getSetting('fullname') + name = fullname if fullname else emailaddress + items.append( + { + 'label': f'{self.get_string(30014)} ({name})', + 'path': self.url + '/sign-out', + 'icon': 'DefaultUser.png', + } + ) + else: + items.append( + { + 'label': self.get_string(30013), + 'path': self.url + '/sign-in', + 'icon': 'DefaultUser.png', + } + ) + + listitems = [] + + for item in items: + path = self._k_title(item['path'], item['label']) + li = xbmcgui.ListItem(item['label'], '', path, True) + li.setArt( + { + 'icon': item['icon'], + 'fanart': self.fanart, + } + ) + if 'properties' in item: + li.setProperties(item['properties']) + listitems.append((path, li, item.get('properties') == None)) + + xbmcplugin.addDirectoryItems(self.handle, listitems, len(listitems)) + xbmcplugin.addSortMethod(self.handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.endOfDirectory(self.handle) + + def livestream_item(self): + item = { + 'label': self.get_string(30001), + 'path': 'https://ondemand.rrr.org.au/stream/ws-hq.m3u', + 'icon': self.icon, + 'properties': { + 'StationName': self.get_string(30000), + 'IsPlayable': 'true' + }, + } + return item + + def _sub_item(self, text): + path = self.url + '/settings' + li = xbmcgui.ListItem(text, '', path, True) + li.setArt({'thumbnail': os.path.join(self._respath, 'qr-subscribe.png')}) + return (path, li, True) + + def _k_title(self, url, title): + if title: + return url + ('?' if '?' not in url else '&') + 'k_title=' + quote_plus(title) + else: + return url + + def select_date(self, self_date): + self_date_str = '/'.join([i for i in self_date.split('-')[::-1]]) + dialog_title = self.get_string(30065) % (self.get_string(30033)) + picked_date_str = self.dialog.input(dialog_title, defaultt=str(self_date_str), type=xbmcgui.INPUT_DATE) + + if picked_date_str: + date_str = '-'.join([i.zfill(2) for i in picked_date_str.replace(' ', '').split('/')[::-1]]) + current = datetime(*(time.strptime(date_str, '%Y-%m-%d')[0:6]), tzinfo=self.tz) + daydelta = datetime.now(self.tz) - current - timedelta(hours=6) + if daydelta.days != 0: + return date_str + + return None + + def context_item(self, label, path, plugin=None): + plugin = plugin if plugin else self.id + return (self.get_string(label), f'Container.Update(plugin://{plugin}{path})') + + def play_stream(self, handle, args, segments): + li = xbmcgui.ListItem( + label = args.get('title', [''])[0], + path = unquote_plus(args.get('play', [''])[0]), + offscreen=True, + ) + li.setArt( + { + 'thumb': unquote_plus(args.get('thumbnail', [''])[0]).replace(' ', '%20'), + 'fanart': unquote_plus(args.get('fanart', [''])[0]).replace(' ', '%20'), + } + ) + if not self.matrix: + vi = li.getVideoInfoTag() + vi.setTitle(args.get('title', [''])[0]) + vi.setMediaType('song') + else: + li.setInfo('video', + { + 'title': args.get('title', [''])[0], + 'mediatype': 'song', + } + ) + xbmcplugin.setResolvedUrl(self.handle, True, li) + + def parse_programs(self, data, args, segments, links=None, k_title=None): + items = [] + + for menuitem in data: + if menuitem is None: + continue + m_id, m_type = menuitem.get('id', ''), menuitem.get('type', '') + m_links = menuitem.get('links', {}) + m_self = m_links.get('self', '/') + m_sub = m_links.get('subscribe') + m_playlist = m_links.get('playlist') + attributes = menuitem.get('attributes', {}) + if attributes is None: + continue + + textbody = attributes.get('textbody', '') + thumbnail = attributes.get('thumbnail', '') + fanart = attributes.get('background', self.fanart) + pathurl = None + + if attributes.get('subtitle') and not ('soundscapes' in segments and len(segments) > 1): + textbody = '\n'.join((self.get_string(30007) % (attributes.get('subtitle')), textbody)) + + if attributes.get('venue'): + textbody = '\n'.join((attributes['venue'], textbody)) + + if m_type in self.supported_plugins: + title = attributes.get('title', '') + artist = attributes.get('artist') + if artist: + title = f'{artist} - {title}' + pathurl = self.media.parse_media_id(m_type, m_id, quote_plus(title.split('(')[0].strip())) + + name = Media.RE_MEDIA_URLS[m_type].get('name') + title = f'{title} ({name})' + textbody = self.get_string(30008) % (name) + '\n' + textbody + + if 'bandcamp' in m_type or 'apple' in m_type: + thumbnail = self.media.parse_art(thumbnail) + if fanart != self.fanart: + fanart = self.media.parse_art(fanart) + + if not thumbnail: + thumbnail = 'DefaultMusicSongs.png' + + if m_type in ['bandcamp_track', 'youtube']: + is_playable = True + else: + is_playable = False + else: + title = attributes.get('title', '') + artist = attributes.get('artist') + pathurl = attributes.get('url') + if artist: + title = f'{artist} - {title}' + if m_type == 'broadcast' and pathurl: + title = f'{title} ({self.get_string(30050)})' + if m_type == 'broadcast_index' and 'schedule' in segments: + title = f'{title} ({self.get_string(30049)})' + if m_type == 'segment': + title = f'{title} ({self.get_string(30051)})' + on_air = attributes.get('on_air') + if on_air: + title = f'{title} ({on_air})' + is_playable = True + + if m_type == 'program_broadcast_track': + title = f'{title} ({self.get_string(30052)})' + thumbnail = 'DefaultMusicSongs.png' + ext_search = m_links.get('broadcast_track').replace('search', 'ext_search') + pathurl = self._k_title(self.url + ext_search, attributes.get('title')) + is_playable = False + + icon = thumbnail + + if m_sub: + if not self.login() or not self.subscribed(): + icon = 'OverlayLocked.png' + title = f'{self.get_string(30081)} - {title}' + textbody = f'{self.get_string(30081)}\n{textbody}' + pathurl = self.url + m_sub + is_playable = False + else: + title = f'{self.get_string(30084)} - {title}' + + if m_type == 'giveaway' and 'entries' in m_self.split('/'): + title += ' ({})'.format(self.get_string(30069)) + textbody = '\n'.join((self.get_string(30070), textbody)) + + if attributes.get('start') and attributes.get('end'): + datestart = datetime.fromisoformat(attributes['start']) + dateend = datetime.fromisoformat(attributes['end']) + start = datetime.strftime(datestart, '%H:%M') + end = datetime.strftime(dateend, '%H:%M') + textbody = f'{start} - {end}\n{textbody}' + title = ' - '.join((start, end, title)) + + if attributes.get('aired'): + aired = self.get_string(30006) % (attributes['aired']) + else: + aired = attributes.get('date', '') + + if pathurl: + is_playable = not pathurl.startswith('plugin://') + if is_playable: + encodedurl = quote_plus(pathurl) + pathurl = '{}/{}?play={}&title={}&thumbnail={}&fanart={}'.format( + self.url, + '/'.join(segments), + quote_plus(encodedurl), + quote_plus(title), + quote_plus(thumbnail), + quote_plus(fanart), + ) + mediatype = 'song' + info_type = 'video' + else: + pathurl = self._k_title(self.url + m_self, attributes.get('title')) + is_playable = False + mediatype = '' + info_type = 'video' + + date, year = attributes.get('date', ''), attributes.get('year', '') + if date: + date = time.strftime('%d.%m.%Y', time.strptime(date, '%Y-%m-%d')) + year = date[0] + else: + # prevents log entries regarding empty date string + date = time.strftime('%d.%m.%Y', time.localtime()) + + + li = xbmcgui.ListItem(title, aired, pathurl, True) + li.setArt( + { + 'icon': icon, + 'thumb': thumbnail, + 'fanart': fanart, + } + ) + li.setProperties({ + 'StationName': self.get_string(30000), + 'IsPlayable': 'true' if is_playable else 'false', + }) + + context_menu = [] + + if m_playlist: + textbody += f'\n\n{self.get_string(30100)}' % (self.get_string(30101)) + context_menu.append(self.context_item(30101, m_playlist)) + + if 'broadcast_track' in m_links: + if m_type != 'program_broadcast_track': + textbody += f'\n{self.get_string(30100)}' % (self.get_string(30102)) + ext_search = m_links.get('broadcast_track').replace('search', 'ext_search') + context_menu.append(self.context_item(30102, ext_search)) + + if context_menu: + li.addContextMenuItems(context_menu) + + if not self.matrix: + vi = li.getVideoInfoTag() + # vi.setDbId((abs(hash(m_id)) % 2147083647) + 400000) + vi.setTitle(title) + vi.setPlot(textbody) + vi.setDateAdded(date) + if year.isdecimal(): + vi.setYear(int(year)) + vi.setFirstAired(aired) + vi.setPremiered(aired) + if attributes.get('duration', 0) > 0: + vi.setDuration(attributes.get('duration')) + if mediatype: + vi.setMediaType(mediatype) + else: # Matrix v19.0 + vi = { + 'title': title, + 'plot': textbody, + 'date': date, + 'year': year, + 'premiered': aired, + 'aired': aired, + } + + if attributes.get('duration', 0) > 0: + vi['duration'] = attributes.get('duration') + if mediatype: + vi['mediatype'] = mediatype + + li.setInfo('video', vi) + + items.append((pathurl, li, not is_playable)) + + + if 'schedule' in segments: + self_date = links.get('self', '?date=').split('?date=')[-1] + next_date = links.get('next', '?date=').split('?date=')[-1] + + if links.get('next'): + path = self.url + self._k_title(links['next'], k_title) + li = xbmcgui.ListItem(self.get_string(30061) % (next_date), '', path, True) + items.insert(0, (path, li, True)) + + path = self.url + self._k_title(f'/schedule?picker={self_date}', k_title) + li = xbmcgui.ListItem(self.get_string(30065) % (self_date), '', path, True) + li.setArt({'icon': 'DefaultPVRGuide.png'}) + items.insert(0, (path, li, True)) + + elif 'giveaways' in segments: + if not self.login() or not self.subscribed(): + items.insert(0, self._sub_item(self.get_string(30082))) + + elif links and links.get('next'): + if len(items) > 0: + if links.get('next'): + path = self.url + self._k_title(links['next'], k_title) + li = xbmcgui.ListItem(self.nextpage, '', path, True) + items.append((path, li, True)) + if links.get('last'): + path = self.url + self._k_title(links['last'], k_title) + li = xbmcgui.ListItem(self.lastpage, '', path, True) + items.append((path, li, True)) + + + if 'archives' in segments: + if not self.login() or not self.subscribed(): + items.insert(0, self._sub_item(self.get_string(30083))) + + elif 'search' in segments and 'tracks' not in segments: + link = links.get('self').split('?page=')[0] + path = self.url + '/tracks' + link + li = xbmcgui.ListItem(self.get_string(30066), '', path, True) + li.setArt({'icon': 'DefaultMusicSearch.png'}) + items.insert(0, (path, li, True)) + + xbmcplugin.addSortMethod(self.handle, xbmcplugin.SORT_METHOD_UNSORTED, labelMask='%L', label2Mask='%D') + if len(segments) > 3 and 'broadcasts' in segments[2]: + # broadcast playlist + xbmcplugin.setContent(self.handle, 'episodes') + elif 'segments' in segments or 'archives' in segments: + # any segment or archive listing + xbmcplugin.setContent(self.handle, 'episodes') + elif len(segments) == 3 and 'broadcasts' in segments: + # index of broadcasts + xbmcplugin.setContent(self.handle, 'songs') + elif len(segments) == 2 and 'soundscapes' in segments: + # soundscape + xbmcplugin.setContent(self.handle, 'songs') + elif len(segments) == 2 and 'featured_albums' in segments: + # featured albums + xbmcplugin.setContent(self.handle, 'songs') + else: + xbmcplugin.setContent(self.handle, '') + + xbmcplugin.addDirectoryItems(self.handle, items, len(items)) + xbmcplugin.endOfDirectory(self.handle) + + def search(self, tracks=False): + prompt = self.get_string(30068 if tracks else 30067) + return self.dialog.input(prompt, type=xbmcgui.INPUT_ALPHANUM) + + def ext_search(self, args): + q = args.get('q', ['']) + title = q[0] + opts = q + if ' - ' in q[0]: + qsplit = q[0].split(' - ') + opts.append(qsplit[0]) + opts.append(qsplit[1]) + + yt_addon = 'special://home/addons/plugin.video.youtube/' + yt_icon = yt_addon + ('icon.png' if self.matrix else 'resources/media/icon.png') + + options = [] + for opt in opts: + query = urlencode({'q': [opt]}, doseq=True) + options.append({ + 'label': self.get_string(30105) % opt, + 'path': self.url + '/tracks/search?' + query, + 'icon': self.icon, + }) + for opt in opts: + query_sub = urlencode({'query': [opt]}, doseq=True) + options.append({ + 'label': self.get_string(30106) % opt, + 'path': 'plugin://plugin.audio.kxmxpxtx.bandcamp/?mode=search&action=search&' + query_sub, + 'icon': 'special://home/addons/plugin.audio.kxmxpxtx.bandcamp/icon.png', + }) + for opt in opts: + query = urlencode({'q': [opt]}, doseq=True) + options.append({ + 'label': self.get_string(30107) % opt, + 'path': 'plugin://plugin.video.youtube/kodion/search/query/?' + query, + 'icon': yt_icon, + }) + + listitems = [] + for item in options: + li = xbmcgui.ListItem(item['label'], '', item['path'], True) + li.setArt( + { + 'thumb': item.get('icon', 'DefaultMusicSearch.png'), + 'icon': 'DefaultMusicSearch.png', + 'fanart': self.fanart, + } + ) + listitems.append((item['path'], li, True)) + + xbmcplugin.setPluginCategory(self.handle, self.get_string(30104) % title) + xbmcplugin.addDirectoryItems(self.handle, listitems, len(listitems)) + xbmcplugin.addSortMethod(self.handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.endOfDirectory(self.handle) + + def sign_in(self): + emailaddress = self.dialog.input(self.get_string(30015), type=xbmcgui.INPUT_ALPHANUM) + if emailaddress == '': + return False + password = self.dialog.input(self.get_string(30016), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT) + if password == '': + return False + return self.login(prompt=True, emailaddress=emailaddress, password=password) + + def login(self, prompt=False, emailaddress=None, password=None): + if self._signed_in != -1: + return self._signed_in + if self.addon.getSettingBool('authenticated') and self.website.logged_in(): + return True + + emailSetting = self.addon.getSetting('emailaddress') + if emailaddress is None: + emailaddress = emailSetting + + logged_in = self.website.login(emailaddress, password) + + if logged_in: + if prompt: + self._notify(self.get_string(30077) % (emailaddress), self.get_string(30078)) + if not self.addon.getSettingBool('authenticated'): + self.addon.setSetting('subscribed-check', '0') + self.addon.setSettingBool('authenticated', True) + self.subscribed() + + if emailSetting == '': + self.addon.setSetting('emailaddress', emailaddress) + for cookie in logged_in: + if cookie.name == 'account': + fullname = json.loads(unquote_plus(cookie.value)).get('name') + if fullname: + self.addon.setSetting('fullname', fullname) + self._signed_in = logged_in + else: + if prompt: + self._notify(self.get_string(30085), self.get_string(30086) % (emailaddress)) + self.addon.setSettingBool('authenticated', False) + self.addon.setSetting('emailaddress', '') + self.addon.setSetting('fullname', '') + + return logged_in + + def sign_out(self, emailaddress=None): + if emailaddress is None: + emailaddress = self.addon.getSetting('emailaddress') + if self.website.logout(): + self.addon.setSettingBool('authenticated', False) + self.addon.setSetting('subscribed-check', '0') + self.addon.setSettingInt('subscribed', 0) + self._signed_in = -1 + if emailaddress: + self._notify(self.get_string(30079) % (emailaddress), self.get_string(30078)) + self.addon.setSetting('emailaddress', '') + self.addon.setSetting('fullname', '') + return True + else: + if emailaddress: + self._notify(self.get_string(30087), self.get_string(30088) % (emailaddress)) + return False + + def subscribed(self): + if not self.addon.getSettingBool('authenticated'): + return False + check = int(self.addon.getSetting('subscribed-check')) + now = int(time.time()) + if now - check < (15*60): + setting = self.addon.getSettingInt('subscribed') + subscribed = (setting == 1) + else: + subscribed = self.website.subscribed() + self.addon.setSettingInt('subscribed', 1 if subscribed else 0) + self.addon.setSetting('subscribed-check', str(now)) + return subscribed + + def subscriber_giveaway(self, path): + if self.login(): + source = self.website.enter(path) + + if 'Thank you! You have been entered' in source: + self._notify(self.get_string(30071), self.get_string(30072)) + elif 'already entered this giveaway' in source: + self._notify(self.get_string(30073), self.get_string(30074)) + else: + self._notify(self.get_string(30073), self.get_string(30075)) + + else: + self._notify(self.get_string(30073), self.get_string(30076)) + +instance = TripleR() diff --git a/plugin.audio.tripler/resources/lib/website.py b/plugin.audio.tripler/resources/lib/website.py new file mode 100644 index 0000000000..874ac8a082 --- /dev/null +++ b/plugin.audio.tripler/resources/lib/website.py @@ -0,0 +1,131 @@ +from resources.lib.scraper import USER_AGENT + +from urllib.request import Request, build_opener, HTTPCookieProcessor +from urllib.parse import urlencode +from urllib.error import HTTPError + +import http.cookiejar +import os + +class TripleRWebsite(): + def __init__(self, cookiepath): + self._cookiepath = cookiepath + self.cj = http.cookiejar.LWPCookieJar() + + def _loadcj(self): + if os.path.isfile(self._cookiepath): + self.cj.load(self._cookiepath) + return True + else: + return False + + def _delcj(self): + self.cj = http.cookiejar.LWPCookieJar() + try: + os.remove(self._cookiepath) + except: + pass + + def request(self, url, data=None): + if data: + req = Request(url, data.encode()) + else: + req = Request(url) + req.add_header('User-Agent', USER_AGENT) + + opener = build_opener(HTTPCookieProcessor(self.cj)) + + try: + response = opener.open(req) + except HTTPError as e: + return e + + source = response.read().decode() + response.close() + + return source + + def login(self, emailaddress, password): + if password is None and self._loadcj(): + account_url = 'https://www.rrr.org.au/account' + source = self.request(account_url) + if self._check_login(source, emailaddress): + return self.cj + else: + return False + + if emailaddress and password: + login_url = 'https://www.rrr.org.au/sign-in' + login_data = urlencode( + { + 'subscriber_account[email]': emailaddress, + 'subscriber_account[password]': password, + '_csrf': ['', 'javascript-disabled'], + } + ) + + source = self.request(login_url, data=login_data) + + if isinstance(source, HTTPError): + return False + + if source and self._check_login(source, emailaddress): + self.cj.save(self._cookiepath) + return self.cj + else: + return False + + def _check_login(self, source, emailaddress): + if emailaddress.lower() in source.lower(): + return True + else: + return False + + def logout(self): + logout_url = 'https://www.rrr.org.au/sign-out' + logout_data = urlencode( + { + '_csrf': ['', 'javascript-disabled'], + } + ) + source = self.request(logout_url, data=logout_data) + if isinstance(source, HTTPError): + if source.code == 500: + return True + else: + return False + if source: + self._delcj() + return True + else: + return False + + def logged_in(self): + return self._loadcj() + + def subscribed(self): + check_url = 'https://www.rrr.org.au/account/check-active.json' + source = self.request(check_url) + if isinstance(source, HTTPError): + if source.code == 500: + return True + else: + return False + return self._check_subscription(source) + + def _check_subscription(self, source): + if '"active":' in source and 'true' in source: + return True + else: + return False + + def enter(self, resource_path): + entry_url = ''.join(('https://www.rrr.org.au/subscriber-', resource_path[1:])) + entry_data = urlencode( + { + 'entry[null]': '', + '_csrf': ['', 'javascript-disabled'], + } + ) + + return self.request(entry_url, entry_data) diff --git a/plugin.audio.tripler/resources/qr-subscribe.png b/plugin.audio.tripler/resources/qr-subscribe.png new file mode 100644 index 0000000000..5be65e8353 Binary files /dev/null and b/plugin.audio.tripler/resources/qr-subscribe.png differ diff --git a/plugin.audio.tripler/resources/screenshots/album-of-the-week.jpg b/plugin.audio.tripler/resources/screenshots/album-of-the-week.jpg new file mode 100644 index 0000000000..735a71a7dd Binary files /dev/null and b/plugin.audio.tripler/resources/screenshots/album-of-the-week.jpg differ diff --git a/plugin.audio.tripler/resources/screenshots/broadcast-playlist.jpg b/plugin.audio.tripler/resources/screenshots/broadcast-playlist.jpg new file mode 100644 index 0000000000..0f2cbf4e69 Binary files /dev/null and b/plugin.audio.tripler/resources/screenshots/broadcast-playlist.jpg differ diff --git a/plugin.audio.tripler/resources/screenshots/browse-by-date.jpg b/plugin.audio.tripler/resources/screenshots/browse-by-date.jpg new file mode 100644 index 0000000000..169766e2ec Binary files /dev/null and b/plugin.audio.tripler/resources/screenshots/browse-by-date.jpg differ diff --git a/plugin.audio.tripler/resources/screenshots/browse-by-program.jpg b/plugin.audio.tripler/resources/screenshots/browse-by-program.jpg new file mode 100644 index 0000000000..7cb817ddae Binary files /dev/null and b/plugin.audio.tripler/resources/screenshots/browse-by-program.jpg differ diff --git a/plugin.audio.tripler/resources/screenshots/menu.jpg b/plugin.audio.tripler/resources/screenshots/menu.jpg new file mode 100644 index 0000000000..160792df8f Binary files /dev/null and b/plugin.audio.tripler/resources/screenshots/menu.jpg differ diff --git a/plugin.audio.tripler/resources/screenshots/soundscape.jpg b/plugin.audio.tripler/resources/screenshots/soundscape.jpg new file mode 100644 index 0000000000..24172ebe9d Binary files /dev/null and b/plugin.audio.tripler/resources/screenshots/soundscape.jpg differ diff --git a/plugin.audio.tripler/resources/settings.xml b/plugin.audio.tripler/resources/settings.xml new file mode 100644 index 0000000000..6e63302c33 --- /dev/null +++ b/plugin.audio.tripler/resources/settings.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" ?> +<settings version="1"> + <section id="plugin.audio.tripler"> + <category id="subscriber account" label="30010" help=""> + <group id="1"> + <setting id="authenticated" type="boolean" label="30999" help=""> + <level>4</level> + <default>false</default> + </setting> + <setting id="fullname" type="string" label="30017" help=""> + <level>0</level> + <default/> + <enable>false</enable> + <constraints> + <allowempty>true</allowempty> + </constraints> + <dependencies> + <dependency type="visible"> + <condition operator="!is" setting="authenticated">false</condition> + </dependency> + </dependencies> + <control type="edit" format="string"> + <heading>30017</heading> + </control> + </setting> + <setting id="emailaddress" type="string" label="30012" help=""> + <level>0</level> + <default/> + <enable>false</enable> + <constraints> + <allowempty>true</allowempty> + </constraints> + <dependencies> + <dependency type="visible"> + <condition operator="!is" setting="authenticated">false</condition> + </dependency> + </dependencies> + <control type="edit" format="string"> + <heading>30012</heading> + </control> + </setting> + <setting id="subscribed" type="integer" label="30075" help="30075"> + <level>0</level> + <default>0</default> + <enable>false</enable> + <constraints> + <options> + <option label="No">0</option> + <option label="Yes">1</option> + </options> + </constraints> + <dependencies> + <dependency type="visible"> + <condition operator="!is" setting="authenticated">false</condition> + </dependency> + </dependencies> + <control type="list" format="string"> + <heading>30075</heading> + </control> + </setting> + <setting id="subscribed-check" type="string" label="30999" help=""> + <level>4</level> + <default/> + <constraints> + <allowempty>true</allowempty> + </constraints> + <control type="edit" format="string"> + <heading>30999</heading> + </control> + </setting> + <setting id="sign-in" type="action" label="30013" help="30020"> + <level>0</level> + <dependencies> + <dependency type="visible"> + <condition operator="!is" setting="authenticated">true</condition> + </dependency> + </dependencies> + <control type="button" format="action"> + <data>RunPlugin("plugin://plugin.audio.tripler/sign-in")</data> + </control> + </setting> + <setting id="sign-out" type="action" label="30014" help="30021"> + <level>0</level> + <dependencies> + <dependency type="visible"> + <condition operator="!is" setting="authenticated">false</condition> + </dependency> + </dependencies> + <control type="button" format="action"> + <data>RunPlugin("plugin://plugin.audio.tripler/sign-out")</data> + </control> + </setting> + </group> + <group id="2"> + <setting id="image_quality" type="integer" label="30022" help="30023"> + <level>0</level> + <default>1</default> + <constraints> + <options> + <option label="30024">0</option> + <option label="30025">1</option> + <option label="30026">2</option> + </options> + </constraints> + <control type="list" format="string"> + <heading>30022</heading> + </control> + </setting> + </group> + </category> + </section> +</settings> diff --git a/plugin.audio.vrt.radio/LICENSE b/plugin.audio.vrt.radio/LICENSE new file mode 100644 index 0000000000..f288702d2f --- /dev/null +++ b/plugin.audio.vrt.radio/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/plugin.audio.vrt.radio/README.md b/plugin.audio.vrt.radio/README.md new file mode 100644 index 0000000000..a13e2c3955 --- /dev/null +++ b/plugin.audio.vrt.radio/README.md @@ -0,0 +1,43 @@ +[![GitHub release](https://img.shields.io/github/release/add-ons/plugin.audio.vrt.radio.svg)](https://github.com/add-ons/plugin.audio.vrt.radio/releases) +[!![CI](https://github.com/add-ons/plugin.audio.vrt.radio/workflows/CI/badge.svg)](https://github.com/add-ons/plugin.audio.vrt.radio/actions?query=workflow:CI) +[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-yellow.svg)](https://opensource.org/licenses/GPLv3) +[![Contributors](https://img.shields.io/github/contributors/add-ons/plugin.audio.vrt.radio.svg)](https://github.com/add-ons/plugin.audio.vrt.radio/graphs/contributors) + +# VRT Radio Kodi addon +**plugin.audio.vrt.radio** is a [Kodi](https://kodi.tv/) add-on for listening to all VRT radio streams. + +## Installing +In Kodi, simply search for `VRT Radio`, and install the add-on. + +Alternatively, you can download a ZIP archive from the [GitHub releases](https://github.com/add-ons/plugin.audio.vrt.radio/releases) +and install it directly in Kodi using the **Install via Zip** option. + +## Reporting issues +You can report issues at [our GitHub project](https://github.com/add-ons/plugin.audio.vrt.radio). + +## Releases + +### v0.1.4 (2020-12-19) +- Add Dutch translation +- Add additional channels +- Improve channel logo's and fanart +- Release for Leia and Matrix + +### v0.1.3 (2020-06-17) +- Small fix to IPTV Manager support + +### v0.1.2 (2020-06-02) +- Switch from MP3 to AAC by default +- Added channel Radio 2 Bene Bene +- Added channel StuBru Hooray +- Added channel StuBru Bruut +- Added support for IPTV Manager + +### v0.1.1 (2020-04-12) +- Added channel Radio 1 Classics +- Added channel StuBru #ikluisterbelgisch +- Added channel MNM Back to the 90's & 00's +- Removed channel Sporza + +### v0.1.0 (2019-05-01) +- Initial working release diff --git a/plugin.audio.vrt.radio/addon.xml b/plugin.audio.vrt.radio/addon.xml new file mode 100644 index 0000000000..d0a6e97023 --- /dev/null +++ b/plugin.audio.vrt.radio/addon.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<addon id="plugin.audio.vrt.radio" name="VRT Radio" version="0.1.4+matrix.1" provider-name="dagwieers"> + <requires> + <import addon="script.module.routing" version="0.2.0"/> + <import addon="xbmc.python" version="3.0.0"/> + </requires> + + <extension point="xbmc.python.pluginsource" library="resources/lib/addon_entry.py"> + <provides>audio</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <summary lang="en_GB">Listen to VRT Radio streams</summary> + <description lang="en_GB">With this addon you can browse and liste the VRT Radio streams.</description> + <summary lang="nl_NL">Luister naar VRT Radio streams</summary> + <description lang="nl_NL">Met deze addon kan je alle VRT Radio streams beluisteren.</description> + <language>en nl</language> + <platform>all</platform> + <license>GPL-3.0-only</license> + <website>https://github.com/add-ons/plugin.audio.vrt.radio/wiki</website> + <source>https://github.com/add-ons/plugin.audio.vrt.radio</source> + <forum>https://github.com/add-ons/plugin.audio.vrt.radio/issues</forum> + <news> +v0.1.4 (2020-12-19) +- Add Dutch translation +- Add additional channels +- Improve channel logo's and fanart +- Release for Leia and Matrix + +v0.1.3 (2020-06-17) +- Small fix to IPTV Manager support + +v0.1.2 (2020-06-02) +- Switch from MP3 to AAC by default +- Added channel Radio 2 Bene Bene +- Added channel StuBru Hooray +- Added channel StuBru Bruut +- Added support for IPTV Manager + +v0.1.1 (2020-04-12) +- Added channel Radio 1 Classics +- Added channel StuBru #ikluisterbelgisch +- Added channel MNM Back to the 90's & 00's +- Removed channel Sporza + +v0.1.0 (2019-05-01) +- First release + </news> + <assets> + <icon>resources/media/icon.png</icon> + <fanart>resources/media/fanart.jpg</fanart> + </assets> + <reuselanguageinvoker>true</reuselanguageinvoker> + </extension> +</addon> diff --git a/plugin.audio.vrt.radio/channels.m3u8 b/plugin.audio.vrt.radio/channels.m3u8 new file mode 100644 index 0000000000..7d23fce016 --- /dev/null +++ b/plugin.audio.vrt.radio/channels.m3u8 @@ -0,0 +1,87 @@ +#EXTM3U +#EXTINF:-1 tvg-id="een" tvg-name="Eén" group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logo/een/een_LOGO_zwart.png",Eén +plugin://plugin.video.vrt.nu/play/id/vualto_een_geo?ext=.pvr + +#EXTINF:-1 tvg-id="canvas" tvg-name="Canvas", group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logo/canvas/CANVAS_logo_lichtblauw.jpg",Canvas +plugin://plugin.video.vrt.nu/play/id/vualto_canvas_geo?ext=.pvr + +#EXTINF:-1 tvg-id="ketnet" tvg-name="Ketnet group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logo/ketnet/ketnet_LOGO_rood_geel.png",Ketnet +plugin://plugin.video.vrt.nu/play/id/vualto_ketnet_geo?ext=.pvr + +#EXTINF:-1 tvg-id="ketnetjr" tvg-name="Ketnet Junior" group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logo/ketnet/ketnet_LOGO_rood_geel.png",Ketnet Junior +plugin://plugin.video.vrt.nu/play/id/ketnet_jr?ext=.pvr + +#EXTINF:-1 tvg-id="sporza" tvg-name="Sporza" group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logo/sporza/sporza_logo_zwart.png",Sporza +plugin://plugin.video.vrt.nu/play/id/vualto_sporza_geo?ext=.pvr + +#EXTINF:-1 tvg-id="vrtnws" tvg-name="VRT NWS" group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logos/vrtnws.png",VRT NWS +plugin://plugin.video.vrt.nu/play/id/vualto_nieuws?ext=.pvr + +#EXTINF:-1 tvg-id="radio1tv" tvg-name="Radio 1" group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logos/radio1.png",Radio 1 +plugin://plugin.video.vrt.nu/play/id/vualto_radio1?ext=.pvr + +#EXTINF:-1 tvg-id="radio2tv" tvg-name="Radio 2" group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logo/radio2/RADIO2_RED_RGB.png",Radio 2 +plugin://plugin.video.vrt.nu/play/id/vualto_radio2?ext=.pvr + +#EXTINF:-1 tvg-id="klaratv" tvg-name="Klara" group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logos/klara.png",Klara +plugin://plugin.video.vrt.nu/play/id/vualto_klara?ext=.pvr + +#EXTINF:-1 tvg-id="stubrutv" tvg-name="Studio Brussel" group-title="VRT NU" tvg-logo="https://images.vrt.be/orig/2019/03/12/1e383cf5-44a7-11e9-abcc-02b7b76bf47f.png",Studio Brussel +plugin://plugin.video.vrt.nu/play/id/vualto_stubru?ext=.pvr + +#EXTINF:-1 tvg-id="mnm" tvg-name="MNM" group-title="VRT NU" tvg-logo="https://images.vrt.be/height100/logo/mnm/logo_witte_achtergrond.png",MNM +plugin://plugin.video.vrt.nu/play/id/vualto_mnm?ext=.pvr + +#EXTINF:-1 tvg-id="vrt-events1" tvg-name="VRT Events stream 1" group-title="VRT NU" tvg-logo="https://images.vrt.be/orig/logo/vrt.png",VRT Events stream 1 +plugin://plugin.video.vrt.nu/play/id/vualto_events1_geo?ext=.pvr + +#EXTINF:-1 tvg-id="vrt-events2" tvg-name="VRT Events stream 2" group-title="VRT NU" tvg-logo="https://images.vrt.be/orig/logo/vrt.png",VRT Events stream 2 +plugin://plugin.video.vrt.nu/play/id/vualto_events2_geo?ext=.pvr + +#EXTINF:-1 tvg-id="vrt-events3" tvg-name="VRT Events stream 3" group-title="VRT NU" tvg-logo="https://images.vrt.be/orig/logo/vrt.png",VRT Events stream 3 +plugin://plugin.video.vrt.nu/play/id/vualto_events3_geo?ext=.pvr + +#EXTINF:-1 radio="true" tvg-id="radio1" tvg-name="Radio 1" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/logos/radio1.png",Radio 1 +http://icecast.vrtcdn.be/radio1-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="radio1-classics" tvg-name="Radio 1 Classics" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/2020/04/08/b1c35b45-7961-11ea-aae0-02b7b76bf47f.png",Radio 1 Classics +http://icecast.vrtcdn.be/radio1_classics_high.mp3 + +#EXTINF:-1 radio="true" tvg-id="radio2" tvg-name="Radio 2" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/logo/radio2/RADIO2_RED_RGB.png",Radio 2 +http://icecast.vrtcdn.be/ra2ovl-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="klara" tvg-name="Klara" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/logos/klara.png",Klara +http://icecast.vrtcdn.be/klara-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="klara-continuo" tvg-name="Klara Continuo" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/logos/klara_continuo.png",Klara Continuo +http://icecast.vrtcdn.be/klaracontinuo-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="stubru" tvg-name="Studio Brussel" group-title="VRT Radio" tvg-logo="https://images.vrt.be/orig/2019/03/12/1e383cf5-44a7-11e9-abcc-02b7b76bf47f.png",Studio Brussel +http://icecast.vrtcdn.be/stubru-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="stubru-tijdloze" tvg-name="StuBru De Tijdloze" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/2019/02/21/923b0fe2-35ce-11e9-abcc-02b7b76bf47f.png",StuBru De Tijdloze +http://icecast.vrtcdn.be/stubru_tijdloze-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="stubru-tgs" tvg-name="StuBru #ikluisterbelgisch" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/2020/04/02/53dad354-74af-11ea-aae0-02b7b76bf47f.png",StuBru #ikluisterbelgisch +http://icecast.vrtcdn.be/stubru_tgs-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="mnm" tvg-name="MNM" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/logo/mnm/logo_witte_achtergrond.png",MNM +http://icecast.vrtcdn.be/mnm-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="mnm-backtothe90sand00s" tvg-name="MNM Back to the 90's & 00's" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/2019/02/21/76e54cd8-35f1-11e9-abcc-02b7b76bf47f.png",MNM Back to the 90's & 00's +http://icecast.vrtcdn.be/mnm_90s00s-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="mnm-hits" tvg-name="MNM Hits" group-title="VRT Radio" tvg-logo="https://images.vrt.be/orig/logo/mnm/mnm_hits_logo_2018.png",MNM Hits +http://icecast.vrtcdn.be/mnm_hits-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="mnm-urbanice" tvg-name="MNM UrbaNice" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/logo/mnm_urbanice_logo.png",MNM UrbaNice +http://icecast.vrtcdn.be/mnm_urb-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="ketnet-hits" tvg-name="Ketnet Hits" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/logo/ketnet/ketnet_hits_rgb.png",Ketnet Hits +http://icecast.vrtcdn.be/ketnetradio-high.mp3 + +#EXTINF:-1 radio="true" tvg-id="vrtnws-radio" tvg-name="VRT NWS" group-title="VRT Radio" tvg-logo="https://images.vrt.be/height100/logos/vrtnws.png",VRT NWS +https://progressive-audio.lwc.vrtcdn.be/content/fixed/11_11niws-snip_hi.mp3 + +#EXTINF:-1 radio="true" tvg-id="vrt-events-radio" tvg-name="VRT Events" group-title="VRT Radio" tvg-logo="https://images.vrt.be/orig/logo/vrt.png",VRT Events +http://icecast.vrtcdn.be/vrtevent-high.mp3 diff --git a/plugin.audio.vrt.radio/resources/language/resource.language.en_gb/strings.po b/plugin.audio.vrt.radio/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..dc1f2431f2 --- /dev/null +++ b/plugin.audio.vrt.radio/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,40 @@ +# Kodi Media Center language file +# Addon Name: VRT Radio +# Addon id: plugin.audio.vrt.radio +# Addon Provider: dagwieers +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"POT-Creation-Date: 2019-05-01 15:08+0001\n" +"PO-Revision-Date: 2019-05-01 15:17+0001\n" +"Last-Translator: Dag" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30860" +msgid "Integration" +msgstr "" + +msgctxt "#30861" +msgid "Integration with other Kodi add-ons" +msgstr "" + +msgctxt "#30875" +msgid "Install IPTV Manager add-on…" +msgstr "" + +msgctxt "#30877" +msgid "Enable IPTV Manager integration" +msgstr "" + +msgctxt "#30879" +msgid "IPTV Manager settings…" +msgstr "" + +msgctxt "#30966" +msgid "Using a SOCKS proxy requires the PySocks library (script.module.pysocks) installed." +msgstr "" diff --git a/plugin.audio.vrt.radio/resources/language/resource.language.nl_nl/strings.po b/plugin.audio.vrt.radio/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..be22632ff8 --- /dev/null +++ b/plugin.audio.vrt.radio/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,31 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30860" +msgid "Integration" +msgstr "Integratie" + +msgctxt "#30861" +msgid "Integration with other Kodi add-ons" +msgstr "Integratie met andere Kodi add-ons" + +msgctxt "#30875" +msgid "Install IPTV Manager add-on…" +msgstr "Installeer de IPTV Manager add-on…" + +msgctxt "#30877" +msgid "Enable IPTV Manager integration" +msgstr "Activeer IPTV Manager integratie" + +msgctxt "#30879" +msgid "IPTV Manager settings…" +msgstr "IPTV Manager instellingen…" + +msgctxt "#30966" +msgid "Using a SOCKS proxy requires the PySocks library (script.module.pysocks) installed." +msgstr "Het gebruik van SOCKS proxies vereist dat de PySocks library (script.module.pysocks) geïnstalleerd is." diff --git a/plugin.audio.vrt.radio/resources/lib/__init__.py b/plugin.audio.vrt.radio/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.audio.vrt.radio/resources/lib/addon.py b/plugin.audio.vrt.radio/resources/lib/addon.py new file mode 100644 index 0000000000..5efa736bd0 --- /dev/null +++ b/plugin.audio.vrt.radio/resources/lib/addon.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" This is the actual VRT Radio audio plugin entry point """ + +from __future__ import absolute_import, division, unicode_literals + +import routing +from xbmcgui import ListItem +from xbmcplugin import addDirectoryItems, addSortMethod, endOfDirectory, setResolvedUrl, SORT_METHOD_LABEL, SORT_METHOD_UNSORTED + +from data import CHANNELS + +plugin = routing.Plugin() # pylint: disable=invalid-name + + +@plugin.route('/') +def main_menu(): + """The VRT Radio plugin main menu""" + radio_items = [] + for channel in CHANNELS: + + if channel.get('aac_128'): + url = channel.get('aac_128') + else: + url = channel.get('mp3_128') + + item = ListItem( + label=channel.get('label'), + label2=channel.get('tagline'), + path=url, + ) + item.setArt(dict(icon=channel.get('logo'), fanart=channel.get('backdrop'))) + item.setInfo(type='video', infoLabels=dict( + mediatype='music', + plot='[B]%(label)s[/B]\n[I]%(tagline)s[/I]\n\n[COLOR yellow]%(website)s[/COLOR]' % channel, + )) + item.setProperty(key='IsPlayable', value='true') + radio_items.append((url, item, False)) + + ok = addDirectoryItems(plugin.handle, radio_items, len(radio_items)) + addSortMethod(plugin.handle, sortMethod=SORT_METHOD_UNSORTED) + addSortMethod(plugin.handle, sortMethod=SORT_METHOD_LABEL) + endOfDirectory(plugin.handle, ok) + + +@plugin.route('/play/<name>') +def play(name): + channel = next((channel for channel in CHANNELS if channel['name'] == name), None) + stream = channel.get('mp3_128') + setResolvedUrl(plugin.handle, True, listitem=ListItem(path=stream)) + + +@plugin.route('/iptv/channels') +def iptv_channels(): + """Return JSON-M3U formatted data for all live channels""" + from iptvmanager import IPTVManager + port = int(plugin.args.get('port')[0]) + IPTVManager(port).send_channels() + + +@plugin.route('/iptv/epg') +def iptv_epg(): + """Return JSON-M3U formatted data for all live channels""" + from iptvmanager import IPTVManager + port = int(plugin.args.get('port')[0]) + IPTVManager(port).send_epg() + + +def run(argv): + ''' Addon entry point from wrapper ''' + plugin.run(argv) diff --git a/plugin.audio.vrt.radio/resources/lib/addon_entry.py b/plugin.audio.vrt.radio/resources/lib/addon_entry.py new file mode 100644 index 0000000000..2520a91df6 --- /dev/null +++ b/plugin.audio.vrt.radio/resources/lib/addon_entry.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +"""This is the actual VRT Radio audio plugin entry point""" + +from __future__ import absolute_import, division, unicode_literals +import sys +from addon import run +run(sys.argv) diff --git a/plugin.audio.vrt.radio/resources/lib/data.py b/plugin.audio.vrt.radio/resources/lib/data.py new file mode 100644 index 0000000000..4a8b027f9e --- /dev/null +++ b/plugin.audio.vrt.radio/resources/lib/data.py @@ -0,0 +1,407 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, unicode_literals + +# https://services.vrt.be/htmlview/?href=%2Fchannel%2Fs&rel=http%3A%2F%2Fservices.vrt.be%2Fchannel%2Frel%2Fchannels +CHANNELS = [ + dict( + id='11', + name='radio1', + label='Radio 1', + tagline='Altijd Benieuwd', + website='https://radio1.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/47303075-8243-434b-8199-2e62cf4dd97a/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/47303075-8243-434b-8199-2e62cf4dd97a/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/radio1-high.mp3', + mp3_64='http://icecast.vrtcdn.be/radio1-mid.mp3', + aac_128='http://icecast.vrtcdn.be/radio1.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio1.png', + backdrop='https://images.vrt.be/orig/2018/11/26/786646bd-f14b-11e8-abcc-02b7b76bf47f.jpg', + epg_id='radio1.be', + preset=901, + ), + dict( + id='110', + name='radio1-classics', + label='Radio 1 Classics', + tagline='Een eindeloze stroom aan onsterfelijke klassiekers', + website='https://radio1.be/', + hls_128='https://live-radio-vrt.akamaized.net/groupa/live/0a479081-ca2c-40fb-bc16-a10c8d3708c0/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-vrt.akamaized.net/groupa/live/0a479081-ca2c-40fb-bc16-a10c8d3708c0/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/radio1_classics_high.mp3', + mp3_64='http://icecast.vrtcdn.be/radio1_classics_mid.mp3', + aac_128='http://icecast.vrtcdn.be/radio1_classics.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio1classics.png', + backdrop='https://images.vrt.be/orig/2018/11/26/786646bd-f14b-11e8-abcc-02b7b76bf47f.jpg', + epg_id='radio1classics.be', + preset=920, + ), + dict( + id='111', + name='radio1-lagelandenlijst', + label='Radio 1 Lage Landenlijst', + tagline='Sterren komen, sterren gaan. Een onsterfelijke stroom Nederlandstalige klassiekers.', + website='https://radio1.be/', + hls_128='https://live-radio-vrt.akamaized.net/groupc/live/4023dbf7-2934-459c-8ded-f91619f58df5/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-vrt.akamaized.net/groupc/live/4023dbf7-2934-459c-8ded-f91619f58df5/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/radio1_lagelanden_high.mp3', + mp3_64='http://icecast.vrtcdn.be/radio1_lagelanden_mid.mp3', + aac_128='http://icecast.vrtcdn.be/radio1_lagelanden.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio1lagelandenlijst.png', + backdrop='https://images.vrt.be/orig/2018/11/26/786646bd-f14b-11e8-abcc-02b7b76bf47f.jpg', + epg_id='radio1lagelandenlijst.be', + preset=921, + ), + dict( + id='22', + name='radio2-vlaams-brabant', + label='Radio 2 (Vlaams-Brabant)', + tagline='De grootste familie', + website='https://radio2.be/', + hls_128='', + mpeg_dash_128='', + mp3_128='http://icecast.vrtcdn.be/ra2vlb-high.mp3', + mp3_64='http://icecast.vrtcdn.be/ra2vlb-mid.mp3', + aac_128='http://icecast.vrtcdn.be/ra2vlb.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio2vlbr.png', + backdrop='https://images.vrt.be/orig/2014/10/29/3a06a4e4-5f6f-11e4-aec2-00163edf75b7.jpg', + epg_id='radio2vlb.be', + preset=902, + ), + dict( + id='120', + name='radio2-benebene', + label='Radio 2 Bene Bene', + tagline='De hélé dag artiesten van bij ons', + website='https://radio2.be/', + hls_128='https://live-radio-vrt.akamaized.net/groupc/live/8f25e0f4-5cc0-4f76-863c-32ae9ca3a5bc/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-vrt.akamaized.net/groupc/live/8f25e0f4-5cc0-4f76-863c-32ae9ca3a5bc/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/radio2_benebene_high.mp3', + mp3_64='http://icecast.vrtcdn.be/radio2_benebene_mid.mp3', + aac_128='http://icecast.vrtcdn.be/radio2_benebene.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio2benebene.png', + backdrop='https://images.vrt.be/orig/2014/10/29/3a06a4e4-5f6f-11e4-aec2-00163edf75b7.jpg', + epg_id='radio2benebene.be', + preset=923, + ), + dict( + id='121', + name='radio2-unwind', + label='Radio 2 Unwind', + tagline='Ontspannen genieten met Radio2', + website='https://radio2.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/622f7b87-f49d-4639-93ae-ca73d991c705/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/622f7b87-f49d-4639-93ae-ca73d991c705/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/radio2_unwind_high.mp3', + mp3_64='http://icecast.vrtcdn.be/radio2_unwind-mid.mp3', + aac_128='http://icecast.vrtcdn.be/radio2_unwind.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio2unwind.png', + backdrop='https://images.vrt.be/orig/2014/10/29/3a06a4e4-5f6f-11e4-aec2-00163edf75b7.jpg', + epg_id='radio2unwind.be', + preset=924, + ), + dict( + id='31', + name='klara', + label='Klara', + tagline='Blijf verwonderd', + website='https://klara.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/a9f36fda-cb3c-4b4e-9405-a5bba55654c0/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/a9f36fda-cb3c-4b4e-9405-a5bba55654c0/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/klara-high.mp3', + mp3_64='http://icecast.vrtcdn.be/klara-mid.mp3', + aac_128='http://icecast.vrtcdn.be/klara.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/klara.png', + backdrop='https://images.vrt.be/orig/2014/11/19/2cef6c86-6fcb-11e4-aec2-00163edf75b7.jpg', + epg_id='klara.be', + preset=903, + ), + dict( + id='32', + name='klara-continuo', + label='Klara Continuo', + tagline='Non-stop klassieke muziek', + website='https://klara.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/0d06dbbe-92d4-4cfe-a0b3-ccc6b7a32ec4/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/0d06dbbe-92d4-4cfe-a0b3-ccc6b7a32ec4/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/klaracontinuo-high.mp3', + mp3_64='http://icecast.vrtcdn.be/klaracontinuo-mid.mp3', + aac_128='http://icecast.vrtcdn.be/klaracontinuo.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/klaracontinuo.png', + backdrop='https://images.vrt.be/orig/2014/08/14/cfbe90a7-23b6-11e4-8e74-00163edf75b7.jpg', + epg_id='klaracontinuo.be', + preset=929, + ), + dict( + id='41', + name='stubru', + label='Studio Brussel', + tagline='Life is Music', + website='https://stubru.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/f404f0f3-3917-40fd-80b6-a152761072fe/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/f404f0f3-3917-40fd-80b6-a152761072fe/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/stubru-high.mp3', + mp3_64='http://icecast.vrtcdn.be/stubru-mid.mp3', + aac_128='http://icecast.vrtcdn.be/stubru.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/stubru.png', + backdrop='https://images.vrt.be/orig/2014/08/14/8e1aa87c-23b2-11e4-8e74-00163edf75b7.jpg', + epg_id='stubru.be', + preset=904, + ), + dict( + id='44', + name='stubru-tijdloze', + label='StuBru De Tijdloze', + tagline='Altijd en overal de beste Tijdloze muziek', + website='https://stubru.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/582109ca-1e71-4330-93fc-e9affee94d7d/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/582109ca-1e71-4330-93fc-e9affee94d7d/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/stubru_tijdloze-high.mp3', + mp3_64='http://icecast.vrtcdn.be/stubru_tijdloze-mid.mp3', + aac_128='http://icecast.vrtcdn.be/stubru_tijdloze.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/detijdloze.png', + backdrop='https://images.vrt.be/orig/2014/08/14/8e1aa87c-23b2-11e4-8e74-00163edf75b7.jpg', + epg_id='stubrutijdloze.be', + preset=930, + ), + dict( + id='45', + name='stubru-tgs', + label='StuBru #ikluisterbelgisch', + tagline='Non-Stop. Nieuw. Belgisch.', + website='https://stubru.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/23384e71-2b6a-43f1-8ad6-02c4ebb8bdf7/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/23384e71-2b6a-43f1-8ad6-02c4ebb8bdf7/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/stubru_tgs-high.mp3', + mp3_64='http://icecast.vrtcdn.be/stubru_tgs-mid.mp3', + aac_128='http://icecast.vrtcdn.be/stubru_tgs.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/ikluisterbelgisch.png', + backdrop='https://images.vrt.be/orig/2014/08/14/8e1aa87c-23b2-11e4-8e74-00163edf75b7.jpg', + epg_id='stubruikluisterbelgisch.be', + preset=931, + ), + dict( + id='141', + name='stubru-bruut', + label='StuBru Bruut', + tagline='Alleen maar stevige gitaren', + website='https://stubru.be/', + hls_128='https://live-radio-vrt.akamaized.net/groupc/live/556a688c-d0b6-4969-9a01-747e4e4944bb/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-vrt.akamaized.net/groupc/live/556a688c-d0b6-4969-9a01-747e4e4944bb/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/stubru_bruut-high.mp3', + mp3_64='http://icecast.vrtcdn.be/stubru_bruut-mid.mp3', + aac_128='http://icecast.vrtcdn.be/stubru_bruut.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/bruut.png', + backdrop='https://images.vrt.be/orig/2014/08/14/8e1aa87c-23b2-11e4-8e74-00163edf75b7.jpg', + epg_id='stubrubruut.be', + preset=932, + ), + dict( + id='140', + name='stubru-hooray', + label='StuBru Hooray', + tagline='Non-stop hiphop', + website='https://stubru.be/', + hls_128='https://live-radio-vrt.akamaized.net/groupc/live/5024acc8-180a-4c50-b05f-0f32f2aba0ea/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-vrt.akamaized.net/groupc/live/5024acc8-180a-4c50-b05f-0f32f2aba0ea/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/stubru_hiphophooray-high.mp3', + mp3_64='http://icecast.vrtcdn.be/stubru_hiphophooray-mid.mp3', + aac_128='http://icecast.vrtcdn.be/stubru_hiphophooray.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/hooray.png', + backdrop='https://images.vrt.be/orig/2014/08/14/8e1aa87c-23b2-11e4-8e74-00163edf75b7.jpg', + epg_id='stubruhooray.be', + preset=933, + ), + dict( + id='142', + name='stubru-untz', + label='StuBru Untz', + tagline='The party never stops', + website='https://stubru.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/90cb8bb1-1ed0-40d3-bbad-47690a2a5fc3/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/90cb8bb1-1ed0-40d3-bbad-47690a2a5fc3/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/stubru_untz-high.mp3', + mp3_64='http://icecast.vrtcdn.be/stubru_untz-mid.mp3', + aac_128='http://icecast.vrtcdn.be/stubru_untz.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/bruut.png', + backdrop='https://images.vrt.be/orig/2014/08/14/8e1aa87c-23b2-11e4-8e74-00163edf75b7.jpg', + epg_id='stubruuntz.be', + preset=934, + ), + dict( + id='55', + name='mnm', + label='MNM', + tagline='Music and More', + website='https://mnm.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/68dc3b80-040e-4a75-a394-72f3bb7aff9a/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/68dc3b80-040e-4a75-a394-72f3bb7aff9a/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/mnm-high.mp3', + mp3_64='http://icecast.vrtcdn.be/mnm-mid.mp3', + aac_128='http://icecast.vrtcdn.be/mnm.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/mnm.png', + backdrop='https://images.vrt.be/orig/2014/08/13/b662a3a3-22da-11e4-8e74-00163edf75b7.png', + epg_id='mnm.be', + preset=905, + ), + dict( + id='56', + name='mnm-hits', + label='MNM Hits', + tagline='Music and More - The Hits', + website='https://mnm.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/35dd91de-0352-4865-8632-17e5af8dc6ba/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/35dd91de-0352-4865-8632-17e5af8dc6ba/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/mnm_hits-high.mp3', + mp3_64='http://icecast.vrtcdn.be/mnm_hits-mid.mp3', + aac_128='http://icecast.vrtcdn.be/mnm_hits.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/mnmhits.png', + backdrop='https://images.vrt.be/orig/2014/08/13/c4a7e1e7-22e1-11e4-8e74-00163edf75b7.png', + epg_id='mnmhits.be', + preset=940, + ), + dict( + id='57', + name='mnm-juice', + label='MNM Juice', + tagline='De online stream met de beste juicy vibes van MNM', + website='https://mnm.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/da0b681c-73db-4c9e-af32-7921591d3fbd/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/da0b681c-73db-4c9e-af32-7921591d3fbd/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/mnm_urb-high.mp3', + mp3_64='http://icecast.vrtcdn.be/mnm_urb-mid.mp3', + aac_128='http://icecast.vrtcdn.be/mnm_urb.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/mnmjuice.png', + backdrop='https://images.vrt.be/orig/2014/08/13/c4a7e1e7-22e1-11e4-8e74-00163edf75b7.png', + epg_id='mnmjuice.be', + preset=941, + ), + dict( + id='58', + name='mnm-90s00s', + label="MNM Back to the 90's & nillies", + tagline='Music and More', + website='https://mnm.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/a2050115-96cb-4151-afda-cbd177407e6e/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/a2050115-96cb-4151-afda-cbd177407e6e/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/mnm_90s00s-high.mp3', + mp3_64='http://icecast.vrtcdn.be/mnm_90s00s-mid.mp3', + aac_128='http://icecast.vrtcdn.be/mnm_90s00s.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/mnm90s00s.png', + backdrop='https://images.vrt.be/orig/2014/08/13/c4a7e1e7-22e1-11e4-8e74-00163edf75b7.png', + epg_id='mnmbacktothe90snillies.be', + preset=942, + ), + dict( + id='13', + name='vrtnws', + label='VRT Nieuws', + tagline='Ieder moment het meest recente nieuws', + website='https://www.vrtnieuws.be/', + hls_128='https://ondemand-radio-cf-vrt.akamaized.net/audioonly/content/fixed/11_11niws-snip_hi.mp4/.m3u8', + mpeg_dash_128='https://ondemand-radio-cf-vrt.akamaized.net/audioonly/content/fixed/11_11niws-snip_hi.mp4/.mpd', + mp3_128='https://progressive-audio.lwc.vrtcdn.be/content/fixed/11_11niws-snip_hi.mp3', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/vrtnws.png', + backdrop='https://images.vrt.be/orig/2014/08/14/0ad165f8-23b7-11e4-8e74-00163edf75b7.jpg', + epg_id='vrtnws.be', + preset=950, + ), + dict( + id='03', + name='ketnet-hits', + label='Ketnet Hits', + tagline='De hipste, de coolste én de plezantste hits op een rijtje!', + website='https://ketnet.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/014a9eea-af85-4da6-aab2-c472ca8d0149/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/014a9eea-af85-4da6-aab2-c472ca8d0149/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/ketnetradio-high.mp3', + mp3_64='http://icecast.vrtcdn.be/ketnetradio-mid.mp3', + aac_128='http://icecast.vrtcdn.be/ketnetradio.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/ketnethits.png', + backdrop='https://images.vrt.be/orig/2015/06/18/ea4ace5e-1586-11e5-8223-00163edf48dd.jpg', + epg_id='ketnethits.be', + preset=944, + ), + dict( + id='71', + name='vrt-event', + label='VRT Event', + tagline='', + website='https://vrt.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/779d53fc-9472-4fe8-b62a-1d38c5878c60/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupa/live/779d53fc-9472-4fe8-b62a-1d38c5878c60/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/vrtevent-high.mp3', + mp3_64='http://icecast.vrtcdn.be/vrtevent-mid.mp3', + aac_128='http://icecast.vrtcdn.be/vrtevent.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/vrtnws.png', + backdrop='https://images.vrt.be/orig/2014/08/14/0ad165f8-23b7-11e4-8e74-00163edf75b7.jpg', + epg_id='vrtevent.be', + preset=951, + ), + dict( + id='21', + name='radio2-antwerpen', + label='Radio 2 (Antwerpen)', + tagline='De grootste familie', + website='https://radio2.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/033d312d-31f7-400a-b81a-61195f0b79c5/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/033d312d-31f7-400a-b81a-61195f0b79c5/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/ra2ant-high.mp3', + mp3_64='http://icecast.vrtcdn.be/ra2ant-mid.mp3', + aac_128='http://icecast.vrtcdn.be/ra2ant.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio2ant.png', + backdrop='https://images.vrt.be/orig/2014/10/29/3a06a4e4-5f6f-11e4-aec2-00163edf75b7.jpg', + epg_id='radio2ant.be', + preset=925, + ), + dict( + id='23', + name='radio2-limburg', + label='Radio 2 (Limburg)', + tagline='De grootste familie', + website='https://radio2.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/d9c49923-b49f-4ab3-8532-4e9bd850b4e2/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/d9c49923-b49f-4ab3-8532-4e9bd850b4e2/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/ra2lim-high.mp3', + mp3_64='http://icecast.vrtcdn.be/ra2lim-mid.mp3', + aac_128='http://icecast.vrtcdn.be/ra2lim.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio2lim.png', + backdrop='https://images.vrt.be/orig/2014/10/29/3a06a4e4-5f6f-11e4-aec2-00163edf75b7.jpg', + epg_id='radio2lim.be', + preset=926, + ), + dict( + id='24', + name='radio2-oost-vlaanderen', + label='Radio 2 (Oost-Vlaanderen)', + tagline='De grootste familie', + website='https://radio2.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/93a8a402-9008-4a97-b473-bc107be7524d/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupb/live/93a8a402-9008-4a97-b473-bc107be7524d/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/ra2ovl-high.mp3', + mp3_64='http://icecast.vrtcdn.be/ra2ovl-mid.mp3', + aac_128='http://icecast.vrtcdn.be/ra2ovl.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio2ovl.png', + backdrop='https://images.vrt.be/orig/2014/10/29/3a06a4e4-5f6f-11e4-aec2-00163edf75b7.jpg', + epg_id='radio2ovl.be', + preset=927, + ), + dict( + id='25', + name='radio2-west-vlaanderen', + label='Radio 2 (West-Vlaanderen)', + tagline='De grootste familie', + website='https://radio2.be/', + hls_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/604e4a0e-22e8-4f99-ad5e-4f62d27dfec4/live.isml/.m3u8', + mpeg_dash_128='https://live-radio-cf-vrt.akamaized.net/groupc/live/604e4a0e-22e8-4f99-ad5e-4f62d27dfec4/live.isml/.mpd', + mp3_128='http://icecast.vrtcdn.be/ra2wvl-high.mp3', + mp3_64='http://icecast.vrtcdn.be/ra2wvl-mid.mp3', + aac_128='http://icecast.vrtcdn.be/ra2wvl.aac', + logo='https://radioplayer.vrt.be/iframe/img/channelLogos/radio2wvl.png', + backdrop='https://images.vrt.be/orig/2014/10/29/3a06a4e4-5f6f-11e4-aec2-00163edf75b7.jpg', + epg_id='radio2wvl.be', + preset=928, + ), +] diff --git a/plugin.audio.vrt.radio/resources/lib/iptvmanager.py b/plugin.audio.vrt.radio/resources/lib/iptvmanager.py new file mode 100644 index 0000000000..919fdd9b57 --- /dev/null +++ b/plugin.audio.vrt.radio/resources/lib/iptvmanager.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2020, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +"""Implementation of IPTVManager class""" + +from __future__ import absolute_import, division, unicode_literals +from data import CHANNELS + + +class IPTVManager: + """Interface to IPTV Manager""" + + def __init__(self, port): + """Initialize IPTV Manager object""" + self.port = port + + def via_socket(func): # pylint: disable=no-self-argument + """Send the output of the wrapped function to socket""" + + def send(self): + """Decorator to send over a socket""" + import json + import socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('127.0.0.1', self.port)) + try: + sock.sendall(json.dumps(func()).encode()) # pylint: disable=not-callable + finally: + sock.close() + + return send + + @via_socket + def send_channels(): # pylint: disable=no-method-argument + """Return JSON-M3U formatted information to IPTV Manager""" + streams = [] + for channel in CHANNELS: + if not channel.get('mp3_128'): + continue + streams.append(dict( + id=channel.get('epg_id'), + name=channel.get('label'), + logo=channel.get('logo'), + stream=channel.get('mp3_128'), + preset=channel.get('preset'), + radio=True, + )) + return dict(version=1, streams=streams) + + @via_socket + def send_epg(): # pylint: disable=no-method-argument + """Return JSONTV formatted information to IPTV Manager""" + from schedule import Schedule + epg_data = Schedule().get_epg_data() + return dict(version=1, epg=epg_data) diff --git a/plugin.audio.vrt.radio/resources/lib/kodiutils.py b/plugin.audio.vrt.radio/resources/lib/kodiutils.py new file mode 100644 index 0000000000..24fc34a48f --- /dev/null +++ b/plugin.audio.vrt.radio/resources/lib/kodiutils.py @@ -0,0 +1,1101 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +"""All functionality that requires Kodi imports""" + +from __future__ import absolute_import, division, unicode_literals +from contextlib import contextmanager +from sys import version_info + +import xbmc +import xbmcaddon +import xbmcplugin +from utils import from_unicode, to_unicode + +ADDON = xbmcaddon.Addon() + +SORT_METHODS = dict( + # date=xbmcplugin.SORT_METHOD_DATE, + dateadded=xbmcplugin.SORT_METHOD_DATEADDED, + duration=xbmcplugin.SORT_METHOD_DURATION, + episode=xbmcplugin.SORT_METHOD_EPISODE, + # genre=xbmcplugin.SORT_METHOD_GENRE, + # label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE, + label=xbmcplugin.SORT_METHOD_LABEL, + title=xbmcplugin.SORT_METHOD_TITLE, + # none=xbmcplugin.SORT_METHOD_UNSORTED, + # FIXME: We would like to be able to sort by unprefixed title (ignore date/episode prefix) + # title=xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE, + unsorted=xbmcplugin.SORT_METHOD_UNSORTED, +) + +WEEKDAY_LONG = { + '0': xbmc.getLocalizedString(17), + '1': xbmc.getLocalizedString(11), + '2': xbmc.getLocalizedString(12), + '3': xbmc.getLocalizedString(13), + '4': xbmc.getLocalizedString(14), + '5': xbmc.getLocalizedString(15), + '6': xbmc.getLocalizedString(16), +} + +MONTH_LONG = { + '01': xbmc.getLocalizedString(21), + '02': xbmc.getLocalizedString(22), + '03': xbmc.getLocalizedString(23), + '04': xbmc.getLocalizedString(24), + '05': xbmc.getLocalizedString(25), + '06': xbmc.getLocalizedString(26), + '07': xbmc.getLocalizedString(27), + '08': xbmc.getLocalizedString(28), + '09': xbmc.getLocalizedString(29), + '10': xbmc.getLocalizedString(30), + '11': xbmc.getLocalizedString(31), + '12': xbmc.getLocalizedString(32), +} + +WEEKDAY_SHORT = { + '0': xbmc.getLocalizedString(47), + '1': xbmc.getLocalizedString(41), + '2': xbmc.getLocalizedString(42), + '3': xbmc.getLocalizedString(43), + '4': xbmc.getLocalizedString(44), + '5': xbmc.getLocalizedString(45), + '6': xbmc.getLocalizedString(46), +} + +MONTH_SHORT = { + '01': xbmc.getLocalizedString(51), + '02': xbmc.getLocalizedString(52), + '03': xbmc.getLocalizedString(53), + '04': xbmc.getLocalizedString(54), + '05': xbmc.getLocalizedString(55), + '06': xbmc.getLocalizedString(56), + '07': xbmc.getLocalizedString(57), + '08': xbmc.getLocalizedString(58), + '09': xbmc.getLocalizedString(59), + '10': xbmc.getLocalizedString(60), + '11': xbmc.getLocalizedString(61), + '12': xbmc.getLocalizedString(62), +} + + +class SafeDict(dict): + """A safe dictionary implementation that does not break down on missing keys""" + def __missing__(self, key): + """Replace missing keys with the original placeholder""" + return '{' + key + '}' + + +def addon_icon(): + """Cache and return add-on icon""" + return get_addon_info('icon') + + +def addon_id(): + """Cache and return add-on ID""" + return get_addon_info('id') + + +def addon_fanart(): + """Cache and return add-on fanart""" + return get_addon_info('fanart') + + +def addon_name(): + """Cache and return add-on name""" + return get_addon_info('name') + + +def addon_path(): + """Cache and return add-on path""" + return get_addon_info('path') + + +def addon_profile(): + """Cache and return add-on profile""" + return to_unicode(xbmc.translatePath(ADDON.getAddonInfo('profile'))) + + +def url_for(name, *args, **kwargs): + """Wrapper for routing.url_for() to lookup by name""" + import addon + return addon.plugin.url_for(getattr(addon, name), *args, **kwargs) + + +def show_listing(list_items, category=None, sort='unsorted', ascending=True, content=None, cache=None, selected=None): + """Show a virtual directory in Kodi""" + from xbmcgui import ListItem + from addon import plugin + + set_property('container.url', 'plugin://' + addon_id() + plugin.path) + xbmcplugin.setPluginFanart(handle=plugin.handle, image=from_unicode(addon_fanart())) + + usemenucaching = get_setting_bool('usemenucaching', default=True) + if cache is None: + cache = usemenucaching + elif usemenucaching is False: + cache = False + + if content: + # content is one of: files, songs, artists, albums, movies, tvshows, episodes, musicvideos + xbmcplugin.setContent(plugin.handle, content=content) + + # Jump through hoops to get a stable breadcrumbs implementation + category_label = '' + if category: + if not content: + category_label = 'VRT NU / ' + if plugin.path.startswith(('/favorites/', '/resumepoints/')): + category_label += localize(30428) + ' / ' # My + if isinstance(category, int): + category_label += localize(category) + else: + category_label += category + elif not content: + category_label = 'VRT NU' + xbmcplugin.setPluginCategory(handle=plugin.handle, category=category_label) + + # FIXME: Since there is no way to influence descending order, we force it here + if not ascending: + sort = 'unsorted' + + # NOTE: When showing tvshow listings and 'showoneoff' was set, force 'unsorted' + if get_setting_bool('showoneoff', default=True) and sort == 'label' and content == 'tvshows': + sort = 'unsorted' + + # Add all sort methods to GUI (start with preferred) + xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[sort]) + for key in sorted(SORT_METHODS): + if key != sort: + xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[key]) + + # FIXME: This does not appear to be working, we have to order it ourselves +# xbmcplugin.setProperty(handle=plugin.handle, key='sort.ascending', value='true' if ascending else 'false') +# if ascending: +# xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS[sort])) +# else: +# # NOTE: When descending, use unsorted +# xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS['unsorted'])) + + listing = [] + showfanart = get_setting_bool('showfanart', default=True) + for title_item in list_items: + # Three options: + # - item is a virtual directory/folder (not playable, path) + # - item is a playable file (playable, path) + # - item is non-actionable item (not playable, no path) + is_folder = bool(not title_item.is_playable and title_item.path) + is_playable = bool(title_item.is_playable and title_item.path) + + list_item = ListItem(label=title_item.label) + + prop_dict = dict( + IsInternetStream='true' if is_playable else 'false', + IsPlayable='true' if is_playable else 'false', + IsFolder='false' if is_folder else 'true', + ) + if title_item.prop_dict: + title_item.prop_dict.update(prop_dict) + else: + title_item.prop_dict = prop_dict + # NOTE: The setProperties method is new in Kodi18 + try: + list_item.setProperties(title_item.prop_dict) + except AttributeError: + for key, value in list(title_item.prop_dict.items()): + list_item.setProperty(key=key, value=str(value)) + + # FIXME: The setIsFolder method is new in Kodi18, so we cannot use it just yet + # list_item.setIsFolder(is_folder) + + if showfanart: + # Add add-on fanart when fanart is missing + if not title_item.art_dict: + title_item.art_dict = dict(fanart=addon_fanart()) + elif not title_item.art_dict.get('fanart'): + title_item.art_dict.update(fanart=addon_fanart()) + list_item.setArt(title_item.art_dict) + + if title_item.info_dict: + # type is one of: video, music, pictures, game + list_item.setInfo(type='video', infoLabels=title_item.info_dict) + + if title_item.stream_dict: + # type is one of: video, audio, subtitle + list_item.addStreamInfo('video', title_item.stream_dict) + + if title_item.context_menu: + list_item.addContextMenuItems(title_item.context_menu) + + url = None + if title_item.path: + url = title_item.path + + listing.append((url, list_item, is_folder)) + + # Jump to specific item + if selected is not None: + pass +# from xbmcgui import getCurrentWindowId, Window +# wnd = Window(getCurrentWindowId()) +# wnd.getControl(wnd.getFocusId()).selectItem(selected) + + succeeded = xbmcplugin.addDirectoryItems(plugin.handle, listing, len(listing)) + xbmcplugin.endOfDirectory(plugin.handle, succeeded, updateListing=False, cacheToDisc=cache) + + +def play(stream, video=None): + """Create a virtual directory listing to play its only item""" + try: # Python 3 + from urllib.parse import unquote + except ImportError: # Python 2 + from urllib2 import unquote + + from xbmcgui import ListItem + from addon import plugin + + play_item = ListItem(path=stream.stream_url) + if video and hasattr(video, 'info_dict'): + play_item.setProperty('subtitle', video.label) + play_item.setArt(video.art_dict) + play_item.setInfo( + type='video', + infoLabels=video.info_dict + ) + play_item.setProperty('inputstream.adaptive.max_bandwidth', str(get_max_bandwidth() * 1000)) + play_item.setProperty('network.bandwidth', str(get_max_bandwidth() * 1000)) + if stream.stream_url is not None and stream.use_inputstream_adaptive: + if kodi_version_major() < 19: + play_item.setProperty('inputstreamaddon', 'inputstream.adaptive') + else: + play_item.setProperty('inputstream', 'inputstream.adaptive') + play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd') + play_item.setContentLookup(False) + play_item.setMimeType('application/dash+xml') + if stream.license_key is not None: + import inputstreamhelper + is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha') + if is_helper.check_inputstream(): + play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') + play_item.setProperty('inputstream.adaptive.license_key', stream.license_key) + + subtitles_visible = get_setting_bool('showsubtitles', default=True) + # Separate subtitle url for hls-streams + if subtitles_visible and stream.subtitle_url is not None: + log(2, 'Subtitle URL: {url}', url=unquote(stream.subtitle_url)) + play_item.setSubtitles([stream.subtitle_url]) + + log(1, 'Play: {url}', url=unquote(stream.stream_url)) + xbmcplugin.setResolvedUrl(plugin.handle, bool(stream.stream_url), listitem=play_item) + + while not xbmc.Player().isPlaying() and not xbmc.Monitor().abortRequested(): + xbmc.sleep(100) + xbmc.Player().showSubtitles(subtitles_visible) + + +def get_search_string(search_string=None): + """Ask the user for a search string""" + keyboard = xbmc.Keyboard(search_string, localize(30134)) + keyboard.doModal() + if keyboard.isConfirmed(): + search_string = to_unicode(keyboard.getText()) + return search_string + + +def ok_dialog(heading='', message=''): + """Show Kodi's OK dialog""" + from xbmcgui import Dialog + if not heading: + heading = addon_name() + if kodi_version_major() < 19: + return Dialog().ok(heading=heading, line1=message) + return Dialog().ok(heading=heading, message=message) + + +def notification(heading='', message='', icon='info', time=4000): + """Show a Kodi notification""" + from xbmcgui import Dialog + if not heading: + heading = addon_name() + if not icon: + icon = addon_icon() + Dialog().notification(heading=heading, message=message, icon=icon, time=time) + + +def multiselect(heading='', options=None, autoclose=0, preselect=None, use_details=False): + """Show a Kodi multi-select dialog""" + from xbmcgui import Dialog + if not heading: + heading = addon_name() + return Dialog().multiselect(heading=heading, options=options, autoclose=autoclose, preselect=preselect, useDetails=use_details) + + +def set_locale(): + """Load the proper locale for date strings, only once""" + if hasattr(set_locale, 'cached'): + return getattr(set_locale, 'cached') + from locale import Error, LC_ALL, setlocale + locale_lang = get_global_setting('locale.language').split('.')[-1] + locale_lang = locale_lang[:-2] + locale_lang[-2:].upper() + # NOTE: setlocale() only works if the platform supports the Kodi configured locale + try: + setlocale(LC_ALL, locale_lang) + except (Error, ValueError) as exc: + if locale_lang != 'en_GB': + log(3, "Your system does not support locale '{locale}': {error}", locale=locale_lang, error=exc) + set_locale.cached = False + return False + set_locale.cached = True + return True + + +def localize(string_id, **kwargs): + """Return the translated string from the .po language files, optionally translating variables""" + if kwargs: + from string import Formatter + return Formatter().vformat(ADDON.getLocalizedString(string_id), (), SafeDict(**kwargs)) + return ADDON.getLocalizedString(string_id) + + +def localize_time(time): + """Return localized time""" + time_format = xbmc.getRegion('time').replace(':%S', '') # Strip off seconds + return time.strftime(time_format).lstrip('0') # Remove leading zero on all platforms + + +def localize_date(date, strftime): + """Return a localized date, even if the system does not support your locale""" + has_locale = set_locale() + # When locale is supported, return original format + if has_locale: + return date.strftime(strftime) + # When locale is unsupported, translate weekday and month + if '%A' in strftime: + strftime = strftime.replace('%A', WEEKDAY_LONG[date.strftime('%w')]) + elif '%a' in strftime: + strftime = strftime.replace('%a', WEEKDAY_SHORT[date.strftime('%w')]) + if '%B' in strftime: + strftime = strftime.replace('%B', MONTH_LONG[date.strftime('%m')]) + elif '%b' in strftime: + strftime = strftime.replace('%b', MONTH_SHORT[date.strftime('%m')]) + return date.strftime(strftime) + + +def localize_datelong(date): + """Return a localized long date string""" + return localize_date(date, xbmc.getRegion('datelong')) + + +def localize_from_data(name, data): + """Return a localized name string from a Dutch data object""" + # Return if Kodi language is Dutch + if get_global_setting('locale.language') == 'resource.language.nl_nl': + return name + return next((localize(item.get('msgctxt')) for item in data if item.get('name') == name), name) + + +def get_setting(key, default=None): + """Get an add-on setting as string""" + try: + value = to_unicode(ADDON.getSetting(key)) + except RuntimeError: # Occurs when the add-on is disabled + return default + if value == '' and default is not None: + return default + return value + + +def get_setting_bool(key, default=None): + """Get an add-on setting as boolean""" + try: + return ADDON.getSettingBool(key) + except (AttributeError, TypeError): # On Krypton or older, or when not a boolean + value = get_setting(key, default) + if value not in ('false', 'true'): + return default + return bool(value == 'true') + except RuntimeError: # Occurs when the add-on is disabled + return default + + +def get_setting_int(key, default=None): + """Get an add-on setting as integer""" + try: + return ADDON.getSettingInt(key) + except (AttributeError, TypeError): # On Krypton or older, or when not an integer + value = get_setting(key, default) + try: + return int(value) + except ValueError: + return default + except RuntimeError: # Occurs when the add-on is disabled + return default + + +def get_setting_float(key, default=None): + """Get an add-on setting""" + try: + return ADDON.getSettingNumber(key) + except (AttributeError, TypeError): # On Krypton or older, or when not a float + value = get_setting(key, default) + try: + return float(value) + except ValueError: + return default + except RuntimeError: # Occurs when the add-on is disabled + return default + + +def set_setting(key, value): + """Set an add-on setting""" + return ADDON.setSetting(key, from_unicode(str(value))) + + +def set_setting_bool(key, value): + """Set an add-on setting as boolean""" + try: + return ADDON.setSettingBool(key, value) + except (AttributeError, TypeError): # On Krypton or older, or when not a boolean + if value in ['false', 'true']: + return set_setting(key, value) + if value: + return set_setting(key, 'true') + return set_setting(key, 'false') + + +def set_setting_int(key, value): + """Set an add-on setting as integer""" + try: + return ADDON.setSettingInt(key, value) + except (AttributeError, TypeError): # On Krypton or older, or when not an integer + return set_setting(key, value) + + +def set_setting_float(key, value): + """Set an add-on setting""" + try: + return ADDON.setSettingNumber(key, value) + except (AttributeError, TypeError): # On Krypton or older, or when not a float + return set_setting(key, value) + + +def open_settings(): + """Open the add-in settings window, shows Credentials""" + ADDON.openSettings() + + +def get_global_setting(key): + """Get a Kodi setting""" + result = jsonrpc(method='Settings.GetSettingValue', params=dict(setting=key)) + return result.get('result', {}).get('value') + + +def get_advanced_setting(key, default=None): + """Get a setting from advancedsettings.xml""" + as_path = xbmc.translatePath('special://masterprofile/advancedsettings.xml') + if not exists(as_path): + return default + from xml.etree.ElementTree import parse, ParseError + try: + as_root = parse(as_path).getroot() + except ParseError: + return default + value = as_root.find(key) + if value is not None: + if value.text is None: + return default + return value.text + return default + + +def get_advanced_setting_int(key, default=0): + """Get a setting from advancedsettings.xml as an integer""" + if not isinstance(default, int): + default = 0 + setting = get_advanced_setting(key, default) + if not isinstance(setting, int): + setting = int(setting.strip()) if setting.strip().isdigit() else default + return setting + + +def get_property(key, default=None, window_id=10000): + """Get a Window property""" + from xbmcgui import Window + value = to_unicode(Window(window_id).getProperty(key)) + if value == '' and default is not None: + return default + return value + + +def set_property(key, value, window_id=10000): + """Set a Window property""" + from xbmcgui import Window + return Window(window_id).setProperty(key, from_unicode(value)) + + +def clear_property(key, window_id=10000): + """Clear a Window property""" + from xbmcgui import Window + return Window(window_id).clearProperty(key) + + +def notify(sender, message, data): + """Send a notification to Kodi using JSON RPC""" + result = jsonrpc(method='JSONRPC.NotifyAll', params=dict( + sender=sender, + message=message, + data=data, + )) + if result.get('result') != 'OK': + log_error('Failed to send notification: {error}', error=result.get('error').get('message')) + return False + log(2, 'Succesfully sent notification') + return True + + +def get_playerid(): + """Get current playerid""" + result = dict() + while not result.get('result'): + result = jsonrpc(method='Player.GetActivePlayers') + return result.get('result', [{}])[0].get('playerid') + + +def get_max_bandwidth(): + """Get the max bandwidth based on Kodi and add-on settings""" + vrtnu_max_bandwidth = get_setting_int('max_bandwidth', default=0) + global_max_bandwidth = int(get_global_setting('network.bandwidth')) + if vrtnu_max_bandwidth != 0 and global_max_bandwidth != 0: + return min(vrtnu_max_bandwidth, global_max_bandwidth) + if vrtnu_max_bandwidth != 0: + return vrtnu_max_bandwidth + if global_max_bandwidth != 0: + return global_max_bandwidth + return 0 + + +def has_socks(): + """Test if socks is installed, and use a static variable to remember""" + if hasattr(has_socks, 'cached'): + return getattr(has_socks, 'cached') + try: + import socks # noqa: F401; pylint: disable=unused-variable,unused-import + except ImportError: + has_socks.cached = False + return None # Detect if this is the first run + has_socks.cached = True + return True + + +def get_proxies(): + """Return a usable proxies dictionary from Kodi proxy settings""" + usehttpproxy = get_global_setting('network.usehttpproxy') + if usehttpproxy is not True: + return None + + try: + httpproxytype = int(get_global_setting('network.httpproxytype')) + except ValueError: + httpproxytype = 0 + + socks_supported = has_socks() + if httpproxytype != 0 and not socks_supported: + # Only open the dialog the first time (to avoid multiple popups) + if socks_supported is None: + ok_dialog('', localize(30966)) # Requires PySocks + return None + + proxy_types = ['http', 'socks4', 'socks4a', 'socks5', 'socks5h'] + + proxy = dict( + scheme=proxy_types[httpproxytype] if 0 <= httpproxytype < 5 else 'http', + server=get_global_setting('network.httpproxyserver'), + port=get_global_setting('network.httpproxyport'), + username=get_global_setting('network.httpproxyusername'), + password=get_global_setting('network.httpproxypassword'), + ) + + if proxy.get('username') and proxy.get('password') and proxy.get('server') and proxy.get('port'): + proxy_address = '{scheme}://{username}:{password}@{server}:{port}'.format(**proxy) + elif proxy.get('username') and proxy.get('server') and proxy.get('port'): + proxy_address = '{scheme}://{username}@{server}:{port}'.format(**proxy) + elif proxy.get('server') and proxy.get('port'): + proxy_address = '{scheme}://{server}:{port}'.format(**proxy) + elif proxy.get('server'): + proxy_address = '{scheme}://{server}'.format(**proxy) + else: + return None + + return dict(http=proxy_address, https=proxy_address) + + +def get_cond_visibility(condition): + """Test a condition in XBMC""" + return xbmc.getCondVisibility(condition) + + +def has_inputstream_adaptive(): + """Whether InputStream Adaptive is installed and enabled in add-on settings""" + return get_setting_bool('useinputstreamadaptive', default=True) and has_addon('inputstream.adaptive') + + +def has_addon(name): + """Checks if add-on is installed and enabled""" + if kodi_version_major() < 19: + return xbmc.getCondVisibility('System.HasAddon(%s)' % name) == 1 + return xbmc.getCondVisibility('System.AddonIsEnabled(%s)' % name) == 1 + + +def has_credentials(): + """Whether the add-on has credentials filled in""" + return bool(get_setting('username') and get_setting('password')) + + +def kodi_version(): + """Returns full Kodi version as string""" + return xbmc.getInfoLabel('System.BuildVersion').split(' ')[0] + + +def kodi_version_major(): + """Returns major Kodi version as integer""" + return int(kodi_version().split('.')[0]) + + +def can_play_drm(): + """Whether this Kodi can do DRM using InputStream Adaptive""" + return get_setting_bool('usedrm', default=True) and get_setting_bool('useinputstreamadaptive', default=True) and supports_drm() + + +def supports_drm(): + """Whether this Kodi version supports DRM decryption using InputStream Adaptive""" + return kodi_version_major() > 17 + + +def get_tokens_path(): + """Cache and return the userdata tokens path""" + if not hasattr(get_tokens_path, 'cached'): + get_tokens_path.cached = addon_profile() + 'tokens/' + return getattr(get_tokens_path, 'cached') + + +COLOUR_THEMES = dict( + dark=dict(highlighted='yellow', availability='blue', geoblocked='red', greyedout='gray'), + light=dict(highlighted='brown', availability='darkblue', geoblocked='darkred', greyedout='darkgray'), + custom=dict( + highlighted=get_setting('colour_highlighted'), + availability=get_setting('colour_availability'), + geoblocked=get_setting('colour_geoblocked'), + greyedout=get_setting('colour_greyedout') + ) +) + + +def themecolour(kind): + """Get current theme color by kind (highlighted, availability, geoblocked, greyedout)""" + theme = get_setting('colour_theme', 'dark') + color = COLOUR_THEMES.get(theme).get(kind, COLOUR_THEMES.get('dark').get(kind)) + return color + + +def colour(text): + """Convert stub color bbcode into colors from the settings""" + theme = get_setting('colour_theme', 'dark') + text = text.format(**COLOUR_THEMES.get(theme)) + return text + + +def get_cache_path(): + """Cache and return the userdata cache path""" + if not hasattr(get_cache_path, 'cached'): + get_cache_path.cached = addon_profile() + 'cache/' + return getattr(get_cache_path, 'cached') + + +def get_addon_info(key): + """Return addon information""" + return to_unicode(ADDON.getAddonInfo(key)) + + +def listdir(path): + """Return all files in a directory (using xbmcvfs)""" + from xbmcvfs import listdir as vfslistdir + return vfslistdir(path) + + +def mkdir(path): + """Create a directory (using xbmcvfs)""" + from xbmcvfs import mkdir as vfsmkdir + log(3, "Create directory '{path}'.", path=path) + return vfsmkdir(path) + + +def mkdirs(path): + """Create directory including parents (using xbmcvfs)""" + from xbmcvfs import mkdirs as vfsmkdirs + log(3, "Recursively create directory '{path}'.", path=path) + return vfsmkdirs(path) + + +def exists(path): + """Whether the path exists (using xbmcvfs)""" + from xbmcvfs import exists as vfsexists + return vfsexists(path) + + +@contextmanager +def open_file(path, flags='r'): + """Open a file (using xbmcvfs)""" + from xbmcvfs import File + fdesc = File(path, flags) + yield fdesc + fdesc.close() + + +def stat_file(path): + """Return information about a file (using xbmcvfs)""" + from xbmcvfs import Stat + return Stat(path) + + +def delete(path): + """Remove a file (using xbmcvfs)""" + from xbmcvfs import delete as vfsdelete + log(3, "Delete file '{path}'.", path=path) + return vfsdelete(path) + + +def delete_cached_thumbnail(url): + """Remove a cached thumbnail from Kodi in an attempt to get a realtime live screenshot""" + # Get texture + result = jsonrpc(method='Textures.GetTextures', params=dict( + filter=dict( + field='url', + operator='is', + value=url, + ), + )) + if result.get('result', {}).get('textures') is None: + log_error('URL {url} not found in texture cache', url=url) + return False + + texture_id = next((texture.get('textureid') for texture in result.get('result').get('textures')), None) + if not texture_id: + log_error('URL {url} not found in texture cache', url=url) + return False + log(2, 'found texture_id {id} for url {url} in texture cache', id=texture_id, url=url) + + # Remove texture + result = jsonrpc(method='Textures.RemoveTexture', params=dict(textureid=texture_id)) + if result.get('result') != 'OK': + log_error('failed to remove {url} from texture cache: {error}', url=url, error=result.get('error', {}).get('message')) + return False + + log(2, 'succesfully removed {url} from texture cache', url=url) + return True + + +def input_down(): + """Move the cursor down""" + jsonrpc(method='Input.Down') + + +def current_container_url(): + """Get current container plugin:// url""" + url = xbmc.getInfoLabel('Container.FolderPath') + if url == '': + return None + return url + + +def container_refresh(url=None): + """Refresh the current container or (re)load a container by URL""" + if url: + log(3, 'Execute: Container.Refresh({url})', url=url) + xbmc.executebuiltin('Container.Refresh({url})'.format(url=url)) + else: + log(3, 'Execute: Container.Refresh') + xbmc.executebuiltin('Container.Refresh') + + +def container_update(url): + """Update the current container while respecting the path history.""" + if url: + log(3, 'Execute: Container.Update({url})', url=url) + xbmc.executebuiltin('Container.Update({url})'.format(url=url)) + else: + # URL is a mandatory argument for Container.Update, use Container.Refresh instead + container_refresh() + + +def container_reload(url=None): + """Only update container if the play action was initiated from it""" + if url is None: + url = get_property('container.url') + if current_container_url() != url: + return + container_update(url) + + +def end_of_directory(): + """Close a virtual directory, required to avoid a waiting Kodi""" + from addon import plugin + xbmcplugin.endOfDirectory(handle=plugin.handle, succeeded=False, updateListing=False, cacheToDisc=False) + + +def wait_for_resumepoints(): + """Wait for resumepoints to be updated""" + update = get_property('vrtnu_resumepoints') + if update == 'busy': + import time + timeout = time.time() + 5 # 5 seconds timeout + log(3, 'Resumepoint update is busy, wait') + while update != 'ready': + if time.time() > timeout: # Exit loop in case something goes wrong + break + xbmc.sleep(50) + update = get_property('vrtnu_resumepoints') + set_property('vrtnu_resumepoints', None) + log(3, 'Resumepoint update is ready, continue') + return True + return False + + +def log(level=1, message='', **kwargs): + """Log info messages to Kodi""" + debug_logging = get_global_setting('debug.showloginfo') # Returns a boolean + max_log_level = get_setting_int('max_log_level', default=0) + if not debug_logging and not (level <= max_log_level and max_log_level != 0): + return + if kwargs: + from string import Formatter + message = Formatter().vformat(message, (), SafeDict(**kwargs)) + message = '[{addon}] {message}'.format(addon=addon_id(), message=message) + xbmc.log(from_unicode(message), level % 3 if debug_logging else 2) + + +def log_access(argv): + """Log addon access""" + log(1, 'Access: {url}{qs}', url=argv[0], qs=argv[2] if len(argv) > 2 else '') + + +def log_error(message, **kwargs): + """Log error messages to Kodi""" + if kwargs: + from string import Formatter + message = Formatter().vformat(message, (), SafeDict(**kwargs)) + message = '[{addon}] {message}'.format(addon=addon_id(), message=message) + xbmc.log(from_unicode(message), 4) + + +def jsonrpc(*args, **kwargs): + """Perform JSONRPC calls""" + from json import dumps, loads + + # We do not accept both args and kwargs + if args and kwargs: + log_error('Wrong use of jsonrpc()') + return None + + # Process a list of actions + if args: + for (idx, cmd) in enumerate(args): + if cmd.get('id') is None: + cmd.update(id=idx) + if cmd.get('jsonrpc') is None: + cmd.update(jsonrpc='2.0') + return loads(xbmc.executeJSONRPC(dumps(args))) + + # Process a single action + if kwargs.get('id') is None: + kwargs.update(id=0) + if kwargs.get('jsonrpc') is None: + kwargs.update(jsonrpc='2.0') + return loads(xbmc.executeJSONRPC(dumps(kwargs))) + + +def human_delta(seconds): + """Return a human-readable representation of the TTL""" + from math import floor + days = int(floor(seconds / (24 * 60 * 60))) + seconds = seconds % (24 * 60 * 60) + hours = int(floor(seconds / (60 * 60))) + seconds = seconds % (60 * 60) + if days: + return '%d day%s and %d hour%s' % (days, 's' if days != 1 else '', hours, 's' if hours != 1 else '') + minutes = int(floor(seconds / 60)) + seconds = seconds % 60 + if hours: + return '%d hour%s and %d minute%s' % (hours, 's' if hours != 1 else '', minutes, 's' if minutes != 1 else '') + if minutes: + return '%d minute%s and %d second%s' % (minutes, 's' if minutes != 1 else '', seconds, 's' if seconds != 1 else '') + return '%d second%s' % (seconds, 's' if seconds != 1 else '') + + +def get_cache(path, ttl=None): # pylint: disable=redefined-outer-name + """Get the content from cache, if it is still fresh""" + if not get_setting_bool('usehttpcaching', default=True): + return None + + fullpath = get_cache_path() + path + if not exists(fullpath): + return None + + from time import localtime, mktime + mtime = stat_file(fullpath).st_mtime() + now = mktime(localtime()) + if ttl is not None and now >= mtime + ttl: + return None + +# if ttl is None: +# log(3, "Cache '{path}' is forced from cache.", path=path) +# else: +# log(3, "Cache '{path}' is fresh, expires in {time}.", path=path, time=human_delta(mtime + ttl - now)) + with open_file(fullpath, 'r') as fdesc: + return get_json_data(fdesc) + + +def update_cache(path, data): + """Update the cache, if necessary""" + if not get_setting_bool('usehttpcaching', default=True): + return + + fullpath = get_cache_path() + path + if not exists(fullpath): + # Create cache directory if missing + if not exists(get_cache_path()): + mkdirs(get_cache_path()) + write_cache(path, data) + return + + with open_file(fullpath, 'r') as fdesc: + cache = fdesc.read() + + # Avoid writes if possible (i.e. SD cards) + if cache == data: + update_timestamp(path) + return + + write_cache(path, data) + + +def write_cache(path, data): + """Write data to cache""" + log(3, "Write cache '{path}'.", path=path) + fullpath = get_cache_path() + path + with open_file(fullpath, 'w') as fdesc: + # dump(data, fdesc, encoding='utf-8') + fdesc.write(data) + + +def update_timestamp(path): + """Update a file's timestamp""" + from os import utime + fullpath = get_cache_path() + path + log(3, "Cache '{path}' has not changed, updating mtime only.", path=path) + utime(fullpath, None) + + +def ttl(kind='direct'): + """Return the HTTP cache ttl in seconds based on kind of relation""" + if kind == 'direct': + return get_setting_int('httpcachettldirect', default=5) * 60 + if kind == 'indirect': + return get_setting_int('httpcachettlindirect', default=60) * 60 + return 5 * 60 + + +def get_json_data(response, fail=None): + """Return json object from HTTP response""" + from json import load, loads + try: + if (3, 0, 0) <= version_info < (3, 6, 0): # the JSON object must be str, not 'bytes' + return loads(to_unicode(response.read())) + return load(response) + except TypeError as exc: # 'NoneType' object is not callable + log_error('JSON TypeError: {exc}', exc=exc) + return fail + except ValueError as exc: # No JSON object could be decoded + log_error('JSON ValueError: {exc}', exc=exc) + return fail + + +def get_url_json(url, cache=None, headers=None, data=None, fail=None): + """Return HTTP data""" + try: # Python 3 + from urllib.error import HTTPError + from urllib.parse import unquote + from urllib.request import urlopen, Request + except ImportError: # Python 2 + from urllib2 import HTTPError, unquote, urlopen, Request + + if headers is None: + headers = dict() + req = Request(url, headers=headers) + + if data is not None: + log(2, 'URL post: {url}', url=unquote(url)) + log(2, 'URL post data: {data}', data=data) + req.data = data + else: + log(2, 'URL get: {url}', url=unquote(url)) + try: + json_data = get_json_data(urlopen(req), fail=fail) + except HTTPError as exc: + if hasattr(req, 'selector'): # Python 3.4+ + url_length = len(req.selector) + else: # Python 2.7 + url_length = len(req.get_selector()) + if exc.code == 400 and 7600 <= url_length <= 8192: + ok_dialog(heading='HTTP Error 400', message=localize(30967)) + log_error('HTTP Error 400: Probably exceeded maximum url length: ' + 'VRT Search API url has a length of {length} characters.', length=url_length) + return fail + if exc.code == 413 and url_length > 8192: + ok_dialog(heading='HTTP Error 413', message=localize(30967)) + log_error('HTTP Error 413: Exceeded maximum url length: ' + 'VRT Search API url has a length of {length} characters.', length=url_length) + return fail + if exc.code == 431: + ok_dialog(heading='HTTP Error 431', message=localize(30967)) + log_error('HTTP Error 431: Request header fields too large: ' + 'VRT Search API url has a length of {length} characters.', length=url_length) + return fail + json_data = get_json_data(exc, fail=fail) + if json_data is None: + raise + else: + if cache: + from json import dumps + update_cache(cache, dumps(json_data)) + return json_data + + +def get_cached_url_json(url, cache, headers=None, ttl=None, fail=None): # pylint: disable=redefined-outer-name + """Return data from cache, if any, else make an HTTP request""" + # Get api data from cache if it is fresh + json_data = get_cache(cache, ttl=ttl) + if json_data is not None: + return json_data + return get_url_json(url, cache=cache, headers=headers, fail=fail) + + +def refresh_caches(cache_file=None): + """Invalidate the needed caches and refresh container""" + files = ['favorites.json', 'oneoff.json', 'resume_points.json'] + if cache_file and cache_file not in files: + files.append(cache_file) + invalidate_caches(*files) + container_refresh() + notification(message=localize(30981)) + + +def invalidate_caches(*caches): + """Invalidate multiple cache files""" + import fnmatch + _, files = listdir(get_cache_path()) + # Invalidate caches related to menu list refreshes + removes = set() + for expr in caches: + removes.update(fnmatch.filter(files, expr)) + for filename in removes: + delete(get_cache_path() + filename) diff --git a/plugin.audio.vrt.radio/resources/lib/schedule.py b/plugin.audio.vrt.radio/resources/lib/schedule.py new file mode 100644 index 0000000000..99a5b02c6e --- /dev/null +++ b/plugin.audio.vrt.radio/resources/lib/schedule.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2020, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +"""Implements VRT Radio schedules""" + +from __future__ import absolute_import, division, unicode_literals + +try: # Python 3 + from urllib.request import build_opener, install_opener, ProxyHandler +except ImportError: # Python 2 + from urllib2 import build_opener, install_opener, ProxyHandler + +from data import CHANNELS +from kodiutils import get_proxies, get_url_json, log +from utils import find_entry, html_to_kodi + + +class Schedule: + """This implements VRT Radio schedules""" + + WEEK_SCHEDULE = 'http://services.vrt.be/epg/schedules/thisweek?channel_type=radio&type=week' + + def __init__(self): + """Initializes TV-guide object""" + install_opener(build_opener(ProxyHandler(get_proxies()))) + + def get_epg_data(self): + """Return EPG data""" + epg_data = dict() + epg_url = self.WEEK_SCHEDULE + schedule = get_url_json(url=epg_url, headers=dict(accept='application/vnd.epg.vrt.be.schedule_3.1+json'), fail={}) + for event in schedule.get('events', []): + channel_id = event.get('channel', dict(code=None)).get('code') + if channel_id is None: + log(2, 'No channel code found in EPG event: {event}', event=event) + continue + channel = find_entry(CHANNELS, 'id', channel_id) + if channel is None: + log(2, 'No channel found using code: {code}', code=channel_id) + continue + epg_id = channel.get('epg_id') + if epg_id not in epg_data: + epg_data[epg_id] = [] + if event.get('images'): + image = event.get('images')[0].get('url') + else: + image = None + epg_data[epg_id].append(dict( + start=event.get('startTime'), + stop=event.get('endTime'), + image=image, + title=event.get('title'), + description=html_to_kodi(event.get('description', '')), + )) + return epg_data diff --git a/plugin.audio.vrt.radio/resources/lib/utils.py b/plugin.audio.vrt.radio/resources/lib/utils.py new file mode 100644 index 0000000000..81047137fb --- /dev/null +++ b/plugin.audio.vrt.radio/resources/lib/utils.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +"""Implements static functions used elsewhere in the add-on""" + +from __future__ import absolute_import, division, unicode_literals +import re + +try: # Python 3 + from html import unescape +except ImportError: # Python 2 + from HTMLParser import HTMLParser + + def unescape(string): + """Expose HTMLParser's unescape""" + return HTMLParser().unescape(string) + +HTML_MAPPING = [ + (re.compile(r'<(/?)i(|\s[^>]+)>', re.I), '[\\1I]'), + (re.compile(r'<(/?)b(|\s[^>]+)>', re.I), '[\\1B]'), + (re.compile(r'<em(|\s[^>]+)>', re.I), '[B][COLOR={highlighted}]'), + (re.compile(r'</em>', re.I), '[/COLOR][/B]'), + (re.compile(r'<li>', re.I), '- '), + (re.compile(r'</?(li|ul)(|\s[^>]+)>', re.I), '\n'), + (re.compile(r'</?(div|p|span)(|\s[^>]+)>', re.I), ''), + (re.compile('<br>\n{0,1}', re.I), ' '), # This appears to be specific formatting for VRT NU, but unwanted by us + (re.compile('( \n){2,}', re.I), '\n'), # Remove repeating non-blocking spaced newlines +] + + +def to_unicode(text, encoding='utf-8', errors='strict'): + """Force text to unicode""" + if isinstance(text, bytes): + return text.decode(encoding, errors=errors) + return text + + +def from_unicode(text, encoding='utf-8', errors='strict'): + """Force unicode to text""" + import sys + if sys.version_info.major == 2 and isinstance(text, unicode): # noqa: F821; pylint: disable=undefined-variable + return text.encode(encoding, errors) + return text + + +def capitalize(string): + """Ensure the first character is uppercase""" + string = string.strip() + return string[0].upper() + string[1:] + + +def strip_newlines(text): + """Strip newlines and trailing whitespaces""" + return text.replace('\n', '').strip() + + +def html_to_kodi(text): + """Convert VRT HTML content into Kodit formatted text""" + for key, val in HTML_MAPPING: + text = key.sub(val, text) + return unescape(text).strip() + + +def add_https_proto(url): + """Add HTTPS protocol to URL that lacks it""" + if url.startswith('//'): + return 'https:' + url + if url.startswith('/'): + return 'https://www.vrt.be' + url + return url + + +def find_entry(dlist, key, value, default=None): + """Find (the first) dictionary in a list where key matches value""" + return next((entry for entry in dlist if entry.get(key) == value), default) + + +def youtube_to_plugin_url(url): + """Convert a YouTube URL to a Kodi plugin URL""" + url = url.replace('https://www.youtube.com/', 'plugin://plugin.video.youtube/') + if not url.endswith('/'): + url += '/' + return url diff --git a/plugin.audio.vrt.radio/resources/media/fanart.jpg b/plugin.audio.vrt.radio/resources/media/fanart.jpg new file mode 100644 index 0000000000..02739084ec Binary files /dev/null and b/plugin.audio.vrt.radio/resources/media/fanart.jpg differ diff --git a/plugin.audio.vrt.radio/resources/media/icon.png b/plugin.audio.vrt.radio/resources/media/icon.png new file mode 100644 index 0000000000..cf6ac8f9c9 Binary files /dev/null and b/plugin.audio.vrt.radio/resources/media/icon.png differ diff --git a/plugin.audio.vrt.radio/resources/settings.xml b/plugin.audio.vrt.radio/resources/settings.xml new file mode 100644 index 0000000000..4965de8ae6 --- /dev/null +++ b/plugin.audio.vrt.radio/resources/settings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<settings> + <category label="30860"> <!--Integration --> + <setting label="30861" type="lsep"/> <!-- Integration with other add-ons --> + <!-- IPTV Manager --> + <setting label="30875" help="30876" type="action" action="InstallAddon(service.iptv.manager)" option="close" visible="!System.HasAddon(service.iptv.manager)"/> <!-- Install IPTV Manager add-on --> + <setting label="30877" help="30878" type="bool" id="iptv.enabled" default="true" visible="String.StartsWith(System.BuildVersion,18) + System.HasAddon(service.iptv.manager) | System.AddonIsEnabled(service.iptv.manager)" /> + <setting label="30879" help="30880" type="action" action="Addon.OpenSettings(service.iptv.manager)" enable="eq(-1,true)" option="close" visible="String.StartsWith(System.BuildVersion,18) + System.HasAddon(service.iptv.manager) | System.AddonIsEnabled(service.iptv.manager)" subsetting="true"/> <!-- IPTV Manager settings --> + <setting type="text" id="iptv.channels_uri" default="plugin://plugin.audio.vrt.radio/iptv/channels" visible="false"/> + <setting type="text" id="iptv.epg_uri" default="plugin://plugin.audio.vrt.radio/iptv/epg" visible="false"/> + </category> + +</settings> diff --git a/plugin.audio.wdr3konzert/LICENSE.txt b/plugin.audio.wdr3konzert/LICENSE.txt new file mode 100644 index 0000000000..23cb790338 --- /dev/null +++ b/plugin.audio.wdr3konzert/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.audio.wdr3konzert/addon.xml b/plugin.audio.wdr3konzert/addon.xml new file mode 100644 index 0000000000..90f18f88eb --- /dev/null +++ b/plugin.audio.wdr3konzert/addon.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<addon id="plugin.audio.wdr3konzert" name="WDR3 Konzertplayer" version="2.0.1" provider-name="sarbes"> + <requires> + <import addon="xbmc.python" version="3.0.0"/> + <import addon="script.module.libwdr" version="2.0.0"/> + </requires> + <extension point="xbmc.python.pluginsource" library="default.py"> + <provides>audio</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <platform>all</platform> + <language>de</language> + <license>GPL-2.0-only</license> + <source>https://github.com/sarbes/plugin.audio.wdr3konzert</source> + <forum>https://forum.kodi.tv/showthread.php?tid=353899</forum> + <website>https://www1.wdr.de/radio/wdr3/konzertplayer/index.html</website> + <summary lang="en_GB">Play concert content from the German tv broadcaster "WDR3".</summary> + <description lang="en_GB">This add-on lists the concert content of the WD3.</description> + <summary lang="de_DE">Die Konzerte des WDR3.</summary> + <description lang="de_DE">Dieses Add-on bietet Zugriff auf Konzerte des WDR3.</description> + <disclaimer lang="de_DE">Dieses Add-on wird von keiner Sendeanstalt unterstützt und ist daher inoffiziell.</disclaimer> + <assets> + <icon>resources/icon.png</icon> + <fanart>resources/fanart.jpg</fanart> + </assets> + </extension> +</addon> diff --git a/plugin.audio.wdr3konzert/default.py b/plugin.audio.wdr3konzert/default.py new file mode 100644 index 0000000000..cc598d4a36 --- /dev/null +++ b/plugin.audio.wdr3konzert/default.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +import libwdr + + +class wdrkonzert(libwdr.libwdr): + def libWdrListMain(self): + l = [] + l.append({'metadata':{'name':self.translation(30000)}, 'params':{'mode':'libWdrListId', 'id':'konzertplayer-uebersicht-100'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30001)}, 'params':{'mode':'libWdrListId', 'id':'konzertplayer-uebersicht-klassik-100'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(30002)}, 'params':{'mode':'libWdrListId', 'id':'konzertplayer-uebersicht-jazz-100'}, 'type':'dir'}) + #l.append({'metadata':{'name':self.translation(30000)}, 'params':{'mode':'listItems','uri':'/kalender/'}, 'type':'dir'})#'New' + #l.append({'metadata':{'name':self.translation(30001)}, 'params':{'mode':'listItems','uri':'/klassische-musik/'}, 'type':'dir'})#'Klassische Musik' + #l.append({'metadata':{'name':self.translation(30002)}, 'params':{'mode':'listItems','uri':'/jazz-and-more/'}, 'type':'dir'})#'Jazz and More' + return {'items':l,'name':'root'} + + + +wdr = wdrkonzert() +wdr.action() \ No newline at end of file diff --git a/plugin.audio.wdr3konzert/resources/fanart.jpg b/plugin.audio.wdr3konzert/resources/fanart.jpg new file mode 100644 index 0000000000..a57757fc13 Binary files /dev/null and b/plugin.audio.wdr3konzert/resources/fanart.jpg differ diff --git a/plugin.audio.wdr3konzert/resources/icon.png b/plugin.audio.wdr3konzert/resources/icon.png new file mode 100644 index 0000000000..b13d6b420b Binary files /dev/null and b/plugin.audio.wdr3konzert/resources/icon.png differ diff --git a/plugin.audio.wdr3konzert/resources/language/resource.language.de_de/strings.po b/plugin.audio.wdr3konzert/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..fa5ad52592 --- /dev/null +++ b/plugin.audio.wdr3konzert/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,25 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.2.4\n" +"Last-Translator: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: de_DE\n" + +msgctxt "#30000" +msgid "New" +msgstr "Neu" + +msgctxt "#30001" +msgid "Classical Music" +msgstr "Klassische Musik" + +msgctxt "#30002" +msgid "Jazz and More" +msgstr "Klassische Musik" diff --git a/plugin.audio.wdr3konzert/resources/language/resource.language.en_gb/strings.po b/plugin.audio.wdr3konzert/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..c39d2193c6 --- /dev/null +++ b/plugin.audio.wdr3konzert/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,25 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.2.4\n" +"Last-Translator: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: en_GB\n" + +msgctxt "#30000" +msgid "New" +msgstr "New" + +msgctxt "#30001" +msgid "Classical Music" +msgstr "Classical Music" + +msgctxt "#30002" +msgid "Jazz and More" +msgstr "Jazz and More" diff --git a/plugin.audio.wdraudiothek/LICENSE.txt b/plugin.audio.wdraudiothek/LICENSE.txt new file mode 100644 index 0000000000..23cb790338 --- /dev/null +++ b/plugin.audio.wdraudiothek/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.audio.wdraudiothek/addon.xml b/plugin.audio.wdraudiothek/addon.xml new file mode 100644 index 0000000000..1de4aaa7b8 --- /dev/null +++ b/plugin.audio.wdraudiothek/addon.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<addon id="plugin.audio.wdraudiothek" name="WDR Audiothek" version="2.0.0" provider-name="sarbes"> + <requires> + <import addon="xbmc.python" version="3.0.0"/> + <import addon="script.module.libwdr" version="2.0.0"/> + </requires> + <extension point="xbmc.python.pluginsource" library="default.py"> + <provides>audio</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <platform>all</platform> + <language>de</language> + <license>GPL-2.0-only</license> + <source>https://github.com/sarbes/plugin.video.wdraudiothek</source> + <forum>https://forum.kodi.tv/showthread.php?tid=353899</forum> + <website>https://www1.wdr.de/mediathek/audio/index.html</website> + <summary lang="en_GB">Play audio content from the German tv broadcaster "WDR".</summary> + <description lang="en_GB">This add-on lists the audio content of the WDR Mediathek.</description> + <summary lang="de_DE">Die Audiothek des WDR.</summary> + <description lang="de_DE">Dieses Add-on bietet Zugriff auf die Audiothek des WDR. Hier gibt es Nachrichten und andere Beiträge.</description> + <disclaimer lang="de_DE">Dieses Add-on wird von keiner Sendeanstalt unterstützt und ist daher inoffiziell.</disclaimer> + <assets> + <icon>resources/icon.png</icon> + <fanart>resources/fanart.jpg</fanart> + </assets> + </extension> +</addon> diff --git a/plugin.audio.wdraudiothek/default.py b/plugin.audio.wdraudiothek/default.py new file mode 100644 index 0000000000..1179a80084 --- /dev/null +++ b/plugin.audio.wdraudiothek/default.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +import libwdr + +class wdraudio(libwdr.libwdr): + + def libWdrListMain(self): + l = [] + l.append({'metadata':{'name':self.translation(32030)}, 'params':{'mode':'libWdrListId', 'id':'audio-uebersicht-100'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(32132)}, 'params':{'mode':'libMediathekListLetters','ignore':'#,x', 'subParams':'{"mode":"libWdrListLetter"}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(32133)}, 'params':{'mode':'libMediathekListDate', 'subParams':'{"mode":"libWdrListDateVideos"}'}, 'type':'dir'}) + return {'items':l,'name':'root'} + + def libWdrListLetter(self): + import libwdrrssandroidparser + if self.params["letter"] == 'c': + return libwdrrssandroidparser.parseShows(f'sendungen-{self.params["letter"]}-102') + else: + return libwdrrssandroidparser.parseShows(f'sendungenabisz-{self.params["letter"]}-100') + + def libWdrListDateVideos(self): + self.params['id'] = f'sendung-verpasst-audios-100~_tag-{self.params["ddmmyyyy"]}' + return self.libWdrListId() + + +wdr = wdraudio() +wdr.action() \ No newline at end of file diff --git a/plugin.audio.wdraudiothek/resources/fanart.jpg b/plugin.audio.wdraudiothek/resources/fanart.jpg new file mode 100644 index 0000000000..a57757fc13 Binary files /dev/null and b/plugin.audio.wdraudiothek/resources/fanart.jpg differ diff --git a/plugin.audio.wdraudiothek/resources/icon.png b/plugin.audio.wdraudiothek/resources/icon.png new file mode 100644 index 0000000000..b13d6b420b Binary files /dev/null and b/plugin.audio.wdraudiothek/resources/icon.png differ diff --git a/plugin.audio.wdrrockpalast/LICENSE.txt b/plugin.audio.wdrrockpalast/LICENSE.txt new file mode 100644 index 0000000000..23cb790338 --- /dev/null +++ b/plugin.audio.wdrrockpalast/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.audio.wdrrockpalast/addon.xml b/plugin.audio.wdrrockpalast/addon.xml new file mode 100644 index 0000000000..f6ad782172 --- /dev/null +++ b/plugin.audio.wdrrockpalast/addon.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<addon id="plugin.audio.wdrrockpalast" name="WDR Rockpalast" version="2.0.0" provider-name="sarbes"> + <requires> + <import addon="xbmc.python" version="3.0.0"/> + <import addon="script.module.libwdr" version="2.0.0"/> + </requires> + <extension point="xbmc.python.pluginsource" library="default.py"> + <provides>audio</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <platform>all</platform> + <language>de</language> + <license>GPL-2.0-only</license> + <source>https://github.com/sarbes/plugin.video.wdrrockpalast</source> + <forum>https://forum.kodi.tv/showthread.php?tid=353899</forum> + <website>https://www1.wdr.de/fernsehen/rockpalast/startseite/index.html</website> + <summary lang="en_GB">This add-on lists all videos of the WDR Rockpalast.</summary> + <description lang="en_GB">This is a music add-on for the WDR Rockpalast. The content may be geolocked.</description> + <summary lang="de_DE">Dies ist ein Music Add-on für den WDR Rockpalast.</summary> + <description lang="de_DE">Dieses Add-on listet alle Videos vom WDR Rockpalast. Die Inhalte sind unter Umständen nur aus Deutschland erreichbar.</description> + <disclaimer lang="de_DE">Dieses Add-on wird von keiner Sendeanstalt unterstützt und ist daher inoffiziell.</disclaimer> + <assets> + <icon>resources/icon.png</icon> + <fanart>resources/fanart.jpg</fanart> + </assets> + </extension> +</addon> diff --git a/plugin.audio.wdrrockpalast/default.py b/plugin.audio.wdrrockpalast/default.py new file mode 100644 index 0000000000..1ad0c98678 --- /dev/null +++ b/plugin.audio.wdrrockpalast/default.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +import libwdr + +class rockpalast(libwdr.libwdr): + def __init__(self): + libwdr.libwdr.__init__(self) + + def libWdrListMain(self): + d = {'items':[]} + d['items'].append({'metadata':{'name':self.translation(32030)}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-100~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2019'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast264~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2018'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-156~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2017'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-136~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2016'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-106~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2015'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-108~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2014'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-110~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2013'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-112~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2012'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-114~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2011'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-116~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2010'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-118~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2009'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-120~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2008'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-122~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2007'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-124~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2006'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-126~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2005'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-128~_format-mp111_type-rss.feed'}, 'type':'dir'}) + d['items'].append({'metadata':{'name':'2004'}, 'params':{'mode':'libWdrListFeed', 'url':'http://www1.wdr.de/mediathek/video/sendungen/rockpalast/rockpalast-132~_format-mp111_type-rss.feed'}, 'type':'dir'}) + return d + +r = rockpalast() +r.action() \ No newline at end of file diff --git a/plugin.audio.wdrrockpalast/resources/fanart.jpg b/plugin.audio.wdrrockpalast/resources/fanart.jpg new file mode 100644 index 0000000000..d316c3e3ca Binary files /dev/null and b/plugin.audio.wdrrockpalast/resources/fanart.jpg differ diff --git a/plugin.audio.wdrrockpalast/resources/icon.png b/plugin.audio.wdrrockpalast/resources/icon.png new file mode 100644 index 0000000000..1d6761d110 Binary files /dev/null and b/plugin.audio.wdrrockpalast/resources/icon.png differ diff --git a/plugin.googledrive/LICENSE.txt b/plugin.googledrive/LICENSE.txt new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/plugin.googledrive/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/plugin.googledrive/README.md b/plugin.googledrive/README.md new file mode 100644 index 0000000000..345a9d1792 --- /dev/null +++ b/plugin.googledrive/README.md @@ -0,0 +1,24 @@ +# Google Drive KODI Addon + +Play all your media from Google Drive including Videos, Music and Pictures (including Google Photos). +* Unlimited accounts +* Team Drives support +* Google Photos support +* Playback your music and videos. Listing of videos with thumbnails. +* ~~Use Google Drive as a source.~~ (Not currently working due to changes in the Google Drive API) +* Subtitles can be assigned automatically if a .str file exists with the same name as the video. +* Export your videos to your library (.strm files). You can export your music too, but kodi won't support it yet. It's a Kodi issue for now. +* Show your photos individually or run a slideshow of them. Listing of pictures with thumbnails. +* Auto-Refreshed slideshow. +* Use of OAuth 2 login. You don't have to write your user/password within the add-on. Use the login process in your browser. +* Extremely fast. Using the Google Drive API + +This program is not affiliated with or sponsored by Google. + + +### Installation + +* From the **Kodi Add-on repository** +* From **[my repository](https://github.com/cguZZman/repository.plugins)** if for any reason the latest version is still not in the Kodi Add-on repository. +* Manual, by downloading the source code and creating your zip. +If your installation is manual, you **must install first** the latest version of the **[common module](https://github.com/cguZZman/script.module.clouddrive.common)**. \ No newline at end of file diff --git a/plugin.googledrive/addon.xml b/plugin.googledrive/addon.xml new file mode 100644 index 0000000000..89192738e6 --- /dev/null +++ b/plugin.googledrive/addon.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<addon id="plugin.googledrive" name="Google Drive" version="1.5.0" provider-name="Carlos Guzman (cguZZman)"> + <requires> + <import addon="xbmc.python" version="3.0.0" /> + <import addon="script.module.clouddrive.common" version="1.4.0"/> + </requires> + <extension point="xbmc.python.pluginsource" library="entrypoint.py"> + <provides>image audio video</provides> + </extension> + <extension point="xbmc.service" library="service.py" /> + <extension point="xbmc.addon.metadata"> + <platform>all</platform> + <summary lang="en_GB">Google Drive for KODI</summary> + <description lang="en_GB"> +Play all your media from Google Drive including Videos, Music and Pictures (including Google Photos). + - Unlimited number of accounts. + - Team Drives support + - Google Photos support + - Search over your drive. + - Auto-Refreshed slideshow. + - Export your videos to your library (.strm files) + - Use Google Drive as a source. **(Not currently working due to changes in the Google Drive API) + - This program is not affiliated with or sponsored by Google. + </description> + <summary lang="he_IL">כונן Google Drive של מיקרוסופט עבור קודי</summary> + <description lang="he_IL"> +הפעל את כל המדיה שלך מ- Google Drive כולל וידאו, מוסיקה ותמונות. +  - מספר בלתי מוגבל של חשבונות. +  - חיפוש ומציאת הכונן שלך. +  - ריענון מצגת אוטומטית. +  - ייצוא קטעי הווידאו לספרייה שלך (קבצי .strm) +  - תוכנית זו אינה קשורה או ממומנת על ידי Google. + </description> + <license>GPL-3.0-or-later</license> + <source>https://github.com/cguZZman/plugin.googledrive</source> + <forum>https://github.com/cguZZman/plugin.googledrive/issues</forum> + <website>https://addons.kodi.tv/show/plugin.googledrive</website> + <assets> + <icon>icon.png</icon> + <fanart>fanart.jpg</fanart> + </assets> + <news> +v1.5.0 released Jan 21, 2023: +- Kodi 20 fix + </news> + <disclaimer lang="en_GB"> +This cloud drive addon uses a third-party authentication mechanism commonly known as OAuth 2.0. +If you want to know more about OAuth 2.0 you can visit the following pages: +- https://oauth.net/2/ +- https://developers.google.com/identity/protocols/OAuth2 +- https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth + +Kodi and myself take no responsibility or liability. + +The authentication server URL is specified in Settings / Advanced / Sign-in Server. The Sign-in Server implements the OAuth 2.0 protocol. +The complete source code of the Sign-in Server can be download here: https://github.com/cguZZman/drive-login +You can clone the project and host it in your own server. + </disclaimer> + </extension> +</addon> diff --git a/plugin.googledrive/entrypoint.py b/plugin.googledrive/entrypoint.py new file mode 100644 index 0000000000..17c5c2b1cd --- /dev/null +++ b/plugin.googledrive/entrypoint.py @@ -0,0 +1,21 @@ +#------------------------------------------------------------------------------- +# Copyright (C) 2017 Carlos Guzman (cguZZman) carlosguzmang@protonmail.com +# +# This file is part of Google Drive for Kodi +# +# Google Drive for Kodi 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. +# +# Cloud Drive Common Module for Kodi 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 <http://www.gnu.org/licenses/>. +#------------------------------------------------------------------------------- + +from resources.lib.addon import GoogleDriveAddon +GoogleDriveAddon().route() diff --git a/plugin.googledrive/fanart.jpg b/plugin.googledrive/fanart.jpg new file mode 100644 index 0000000000..7130be98b1 Binary files /dev/null and b/plugin.googledrive/fanart.jpg differ diff --git a/plugin.googledrive/icon.png b/plugin.googledrive/icon.png new file mode 100644 index 0000000000..e29a50263d Binary files /dev/null and b/plugin.googledrive/icon.png differ diff --git a/plugin.googledrive/resources/__init__.py b/plugin.googledrive/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.googledrive/resources/language/resource.language.en_gb/strings.po b/plugin.googledrive/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..624de9cb63 --- /dev/null +++ b/plugin.googledrive/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,165 @@ +# Kodi Media Center language file +# Addon Name: Google Drive +# Addon id: plugin.googledrive +# Addon Provider: Carlos Guzman (cguZZman) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Player Service" +msgstr "" + +msgctxt "#32001" +msgid "Export Service" +msgstr "" + +msgctxt "#32002" +msgid "Source Service" +msgstr "" + +msgctxt "#32003" +msgid "Before full export, remove previous local files and folders" +msgstr "" + +msgctxt "#32004" +msgid "Automatically set subtitles from cloud drive" +msgstr "" + +msgctxt "#32005" +msgid "Auto-Refreshed slideshow" +msgstr "" + +msgctxt "#32006" +msgid "Refresh interval in minutes" +msgstr "" + +msgctxt "#32007" +msgid "Google Photos" +msgstr "" + +msgctxt "#32008" +msgid "Always ask for the stream format before play" +msgstr "" + +msgctxt "#32009" +msgid "Loading stream formats..." +msgstr "" + +msgctxt "#32010" +msgid "Recursive auto-refreshed slideshow" +msgstr "" + +msgctxt "#32011" +msgid "Resume playing when resume point exists in library" +msgstr "" + +msgctxt "#32012" +msgid "Open Cloud Drive Common Settings..." +msgstr "" + +msgctxt "#32013" +msgid "My Drive" +msgstr "" + +msgctxt "#32014" +msgid "Starred" +msgstr "" + +msgctxt "#32015" +msgid "Original format" +msgstr "" + +msgctxt "#32016" +msgid "Select the stream format" +msgstr "" + +msgctxt "#32017" +msgid "Ask to resume if resume point exists in library" +msgstr "" + +msgctxt "#32018" +msgid "Save resume points and watched status in library" +msgstr "" + +msgctxt "#32019" +msgid "Do not include filename extension in a .strm" +msgstr "" + +msgctxt "#32020" +msgid "Hide exporting progress dialog" +msgstr "" + +msgctxt "#32030" +msgid "Collaboration" +msgstr "" + +msgctxt "#32031" +msgid "Report errors automatically to help resolve them quickly" +msgstr "" + +msgctxt "#32032" +msgid "Advanced" +msgstr "" + +msgctxt "#32033" +msgid "Sign-in Server" +msgstr "" + +msgctxt "#32034" +msgid "Cache expiration time (in minutes)" +msgstr "" + +msgctxt "#32035" +msgid "Clear cache now" +msgstr "" + +msgctxt "#32067" +msgid "Services" +msgstr "" + +msgctxt "#32068" +msgid "Allow using Google Drive as a source" +msgstr "" + +msgctxt "#32069" +msgid " Source server port - http://localhost:<port>/source" +msgstr "" + +msgctxt "#32070" +msgid "Default stream quality" +msgstr "" + +msgctxt "#32071" +msgid "Check for Google ban" +msgstr "" + +msgctxt "#32072" +msgid "Banned: %s" +msgstr "" + +msgctxt "#32073" +msgid "Response code: %s" +msgstr "" + +msgctxt "#32074" +msgid "Download metadata files (.nfo, .strm)" +msgstr "" + +msgctxt "#32075" +msgid "Skip unmodified files" +msgstr "" + +msgctxt "#32076" +msgid "Play choosing stream format" +msgstr "" \ No newline at end of file diff --git a/plugin.googledrive/resources/language/resource.language.es_es/strings.po b/plugin.googledrive/resources/language/resource.language.es_es/strings.po new file mode 100644 index 0000000000..133d50640f --- /dev/null +++ b/plugin.googledrive/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,85 @@ +# Kodi Media Center language file +# Addon Name: Google Drive +# Addon id: plugin.googledrive +# Addon Provider: Carlos Guzman (cguZZman) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Account" +msgstr "Cuenta" + +msgctxt "#32001" +msgid "Video Library Export Folder" +msgstr "Directorio de exportación de la biblioteca de vídeos" + +msgctxt "#32002" +msgid "Music Library Export Folder" +msgstr "Directorio de exportación de la biblioteca de música" + +msgctxt "#32003" +msgid "Before export to library, remove previous files and folders" +msgstr "Antes de exportar a biblioteca, eliminar ficheros y directorios anteriores" + +msgctxt "#32004" +msgid "When playing videos, set the subtitle file located next to the video (.srt)" +msgstr "Durante la reproducción de vídeo, establecer el fichero de subítulos ubicado junto al vídeo (.srt)" + +msgctxt "#32005" +msgid "Auto-Refreshed slideshow" +msgstr "Actualizar automaticamente las diapositivas" + +msgctxt "#32006" +msgid "Refresh interval in minutes" +msgstr "Intervalo de actualización en minutos" + +msgctxt "#32007" +msgid "Google Photos" +msgstr "Google Photos" + +msgctxt "#32008" +msgid "Always ask for the stream format before play" +msgstr "Preguntar siempre por el formato de la reproducción" + +msgctxt "#32009" +msgid "Loading stream formats..." +msgstr "Cargando formatos de reproducción" + +msgctxt "#32010" +msgid "Recursive auto-refreshed slideshow" +msgstr "Auto-Actualizar recursivamente las diapositivas" + +msgctxt "#32011" +msgid "Common Settings" +msgstr "Ajustes" + +msgctxt "#32012" +msgid "Open Cloud Drive Common Settings..." +msgstr "Configuración de Drive..." + +msgctxt "#32013" +msgid "My Drive" +msgstr "Mi Disco" + +msgctxt "#32014" +msgid "Starred" +msgstr "Seleccionados" + +msgctxt "#32015" +msgid "Original format" +msgstr "Formato original" + +msgctxt "#32016" +msgid "Select the stream format" +msgstr "Seleccionar formato de la reproducción" diff --git a/plugin.googledrive/resources/language/resource.language.he_il/strings.po b/plugin.googledrive/resources/language/resource.language.he_il/strings.po new file mode 100644 index 0000000000..83986dc499 --- /dev/null +++ b/plugin.googledrive/resources/language/resource.language.he_il/strings.po @@ -0,0 +1,77 @@ +# Kodi Media Center language file +# Addon Name: Google Drive +# Addon id: plugin.googledrive +# Addon Provider: Carlos Guzman (cguZZman) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2017-10-19 11:41+0300\n" +"Last-Translator: A. Dambledore\n" +"Language-Team: Eng2Heb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: he_IL\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Account" +msgstr "חשבון" + +msgctxt "#32001" +msgid "Video Library Export Folder" +msgstr "תיקיית ייצוא ספריית וידאו" + +msgctxt "#32002" +msgid "Music Library Export Folder" +msgstr "תיקיית ייצוא ספריית מוזיקה" + +msgctxt "#32003" +msgid "Before export to library, remove previous files and folders" +msgstr "לפני ייצוא לספריה, הסר את הקבצים והתיקיות הקודמים" + +msgctxt "#32004" +msgid "When playing videos, set the subtitle file located next to the video (.srt)" +msgstr "בעת ניגון וידאו, הגדר את קובץ הכתוביות הממוקם ליד הווידאו (.srt)" + +msgctxt "#32005" +msgid "Auto-Refreshed slideshow" +msgstr "מצגת עם רענון אוטומטי" + +msgctxt "#32006" +msgid "Refresh interval in minutes" +msgstr "מרווח זמן לרענון בדקות" + +msgctxt "#32007" +msgid "Google Photos" +msgstr "אלבומי Google" + +msgctxt "#32008" +msgid "-unused-Special: Camera Roll" +msgstr "-לא בשימוש-מיוחד: סרט צילום" + +msgctxt "#32009" +msgid "-unused-Special: Music" +msgstr "-לא בשימוש-מיוחד: מוזיקה" + +msgctxt "#32010" +msgid "Recursive auto-refreshed slideshow" +msgstr "רקורסיבית אוטומטי לרענן שקופיות" + +msgctxt "#32011" +msgid "Common Settings" +msgstr "הגדרות נפוצות" + +msgctxt "#32012" +msgid "Open Cloud Drive Common Settings..." +msgstr "פתח הגדרות נפוצות של כונן ענן ..." + +msgctxt "#32013" +msgid "My Drive" +msgstr "הכונן שלי" + +msgctxt "#32014" +msgid "Starred" +msgstr "כיכב" diff --git a/plugin.googledrive/resources/language/resource.language.it_it/strings.po b/plugin.googledrive/resources/language/resource.language.it_it/strings.po new file mode 100644 index 0000000000..f889d53d22 --- /dev/null +++ b/plugin.googledrive/resources/language/resource.language.it_it/strings.po @@ -0,0 +1,77 @@ +# Kodi Media Center language file +# Addon Name: Google Drive +# Addon id: plugin.googledrive +# Addon Provider: Carlos Guzman (cguZZman) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Account" +msgstr "Account" + +msgctxt "#32001" +msgid "Video Library Export Folder" +msgstr "Cartella di export della Libreria Video" + +msgctxt "#32002" +msgid "Music Library Export Folder" +msgstr "Cartella di export della Libreria Musica" + +msgctxt "#32003" +msgid "Before export to library, remove previous files and folders" +msgstr "Prima di esportare la libreria, cancella i file e cartelle precedenti" + +msgctxt "#32004" +msgid "When playing videos, set the subtitle file located next to the video (.srt)" +msgstr "Quando riproduci video, imposta il file dei sottotitoli del video situato accanto al video (.srt)" + +msgctxt "#32005" +msgid "Auto-Refreshed slideshow" +msgstr "Auto aggiorna slideshow" + +msgctxt "#32006" +msgid "Refresh interval in minutes" +msgstr "Intervallo in minuti per l'aggiornamento" + +msgctxt "#32007" +msgid "Google Photos" +msgstr "Google Photos" + +msgctxt "#32008" +msgid "-unused-Special: Camera Roll" +msgstr "-unused-Special: Rullino fotografico" + +msgctxt "#32009" +msgid "-unused-Special: Music" +msgstr "-unused-Special: Musica" + +msgctxt "#32010" +msgid "Recursive auto-refreshed slideshow" +msgstr "Auto aggiornamento ricursivo slideshow" + +msgctxt "#32011" +msgid "Common Settings" +msgstr "Impostazioni comuni" + +msgctxt "#32012" +msgid "Open Cloud Drive Common Settings..." +msgstr "Impostazioni comuni di Open Cloud..." + +msgctxt "#32013" +msgid "My Drive" +msgstr "Mio Drive" + +msgctxt "#32014" +msgid "Starred" +msgstr "Recitato" diff --git a/plugin.googledrive/resources/language/resource.language.pt_br/strings.po b/plugin.googledrive/resources/language/resource.language.pt_br/strings.po new file mode 100644 index 0000000000..abcea64f30 --- /dev/null +++ b/plugin.googledrive/resources/language/resource.language.pt_br/strings.po @@ -0,0 +1,77 @@ +# Kodi Media Center language file +# Addon Name: Google Drive +# Addon id: plugin.googledrive +# Addon Provider: Carlos Guzman (cguZZman) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Account" +msgstr "Conta" + +msgctxt "#32001" +msgid "Video Library Export Folder" +msgstr "Diretório de exportação da biblioteca de videos" + +msgctxt "#32002" +msgid "Music Library Export Folder" +msgstr "Diretório de exportação da biblioteca de música" + +msgctxt "#32003" +msgid "Before export to library, remove previous files and folders" +msgstr "Antes de exportar a biblioteca, remover arquivos e diretórios anteriores" + +msgctxt "#32004" +msgid "When playing videos, set the subtitle file located next to the video (.srt)" +msgstr "Quando tocar os videos, usar as legendas que estão no mesmo diretorio do video (.srt)" + +msgctxt "#32005" +msgid "Auto-Refreshed slideshow" +msgstr "Slideshow atualizado automaticamente" + +msgctxt "#32006" +msgid "Refresh interval in minutes" +msgstr "Tempo de atualização em minutos" + +msgctxt "#32007" +msgid "Google Photos" +msgstr "Google Fotos" + +msgctxt "#32008" +msgid "-unused-Special: Camera Roll" +msgstr "-unused-Special: Rolo de Camera" + +msgctxt "#32009" +msgid "-unused-Special: Music" +msgstr "-unused-Special: Musica" + +msgctxt "#32010" +msgid "Recursive auto-refreshed slideshow" +msgstr "Slideshow autalizado automaticamente de forma recursiva" + +msgctxt "#32011" +msgid "Common Settings" +msgstr "Configurações comuns" + +msgctxt "#32012" +msgid "Open Cloud Drive Common Settings..." +msgstr "Abrir configurações comuns doCloud Drive..." + +msgctxt "#32013" +msgid "My Drive" +msgstr "Meu Drive" + +msgctxt "#32014" +msgid "Starred" +msgstr "Com estrela" diff --git a/plugin.googledrive/resources/lib/__init__.py b/plugin.googledrive/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.googledrive/resources/lib/addon.py b/plugin.googledrive/resources/lib/addon.py new file mode 100644 index 0000000000..02812ff58d --- /dev/null +++ b/plugin.googledrive/resources/lib/addon.py @@ -0,0 +1,199 @@ +#------------------------------------------------------------------------------- +# Copyright (C) 2017 Carlos Guzman (cguZZman) carlosguzmang@protonmail.com +# +# This file is part of Google Drive for Kodi +# +# Google Drive for Kodi 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. +# +# Cloud Drive Common Module for Kodi 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 <http://www.gnu.org/licenses/>. +#------------------------------------------------------------------------------- + +import urllib + +from clouddrive.common.remote.request import Request +from clouddrive.common.ui.addon import CloudDriveAddon +from clouddrive.common.ui.utils import KodiUtils +from clouddrive.common.utils import Utils +from resources.lib.provider.googledrive import GoogleDrive +from clouddrive.common.ui.logger import Logger + + +class GoogleDriveAddon(CloudDriveAddon): + _provider = GoogleDrive() + _change_token = None + choose_stream_format = False + + def __init__(self): + self._child_count_supported = False + super(GoogleDriveAddon, self).__init__() + + def get_provider(self): + return self._provider + + def get_my_files_menu_name(self): + return self._addon.getLocalizedString(32013) + + def get_custom_drive_folders(self, driveid): + drive_folders = [] + self._provider.configure(self._account_manager, driveid) + if not self._provider._is_shared_drive: + drive_folders.append({'name' : self._common_addon.getLocalizedString(32058), 'path' : 'sharedWithMe'}) + if self._content_type == 'image': + drive_folders.append({'name' : self._addon.getLocalizedString(32007), 'path' : 'photos'}) + drive_folders.append({'name' : self._addon.getLocalizedString(32014), 'path' : 'starred'}) + return drive_folders + + def new_change_token_slideshow(self, change_token, driveid, item_driveid=None, item_id=None, path=None): + self._provider.configure(self._account_manager, driveid) + if not change_token: + response = self._provider.get('/changes/startPageToken', parameters = self._provider._parameters) + self._change_token = Utils.get_safe_value(response, 'startPageToken') + change_token = 1 + else: + page_token = self._change_token + while page_token: + self._provider._parameters['pageToken'] = page_token + self._provider._parameters['fields'] = 'nextPageToken,newStartPageToken,changes(file(id,name,parents))' + response = self._provider.get('/changes', parameters = self._provider._parameters) + if self.cancel_operation(): + return + self._change_token = Utils.get_safe_value(response, 'newStartPageToken', self._change_token) + changes = Utils.get_safe_value(response, 'changes', []) + for change in changes: + f = Utils.get_safe_value(change, 'file', {}) + parents = Utils.get_safe_value(f, 'parents', []) + parents.append(f['id']) + if item_id in parents: + return change_token + 1 + page_token = Utils.get_safe_value(response, 'nextPageToken') + return change_token + + def _get_url_original(self, driveid, item_driveid=None, item_id=None): + self._provider.configure(self._account_manager, driveid) + item = self._provider.get_item(item_driveid=item_driveid, item_id=item_id, include_download_info = True) + url = item['download_info']['url'] + url += "|Authorization=%s" % urllib.parse.quote("Bearer %s" % self._provider.get_access_tokens()['access_token']) + return url + + def _get_item_play_url(self, file_name, driveid, item_driveid=None, item_id=None, is_subtitle=False): + url = None + if self._content_type == 'video' and not is_subtitle: + if KodiUtils.get_addon_setting('ask_stream_format') == 'false' and not self.choose_stream_format: + if KodiUtils.get_addon_setting('default_stream_quality') == 'Original': + url = self._get_url_original(driveid, item_driveid, item_id) + else: + url = self._select_stream_format(driveid, item_driveid, item_id, True) + else: + url = self._select_stream_format(driveid, item_driveid, item_id, False) + if not url: + url = self._get_url_original(driveid, item_driveid, item_id) + return url + + def _select_stream_format(self, driveid, item_driveid=None, item_id=None, auto=False): + url = None + if not auto: + self._progress_dialog.update(0, self._addon.getLocalizedString(32009)) + self._provider.configure(self._account_manager, driveid) + self._provider.get_item(item_driveid, item_id) + request = Request('https://drive.google.com/get_video_info', urllib.parse.urlencode({'docid' : item_id}), {'authorization': 'Bearer %s' % self._provider.get_access_tokens()['access_token']}) + response_text = request.request() + response_params = dict(urllib.parse.parse_qsl(response_text)) + if not auto: + self._progress_dialog.close() + if Utils.get_safe_value(response_params, 'status', '') == 'ok': + fmt_list = Utils.get_safe_value(response_params, 'fmt_list', '').split(',') + stream_formats = [] + for fmt in fmt_list: + data = fmt.split('/') + stream_formats.append(data[1]) + stream_formats.append(self._addon.getLocalizedString(32015)) + Logger.debug('Stream formats: %s' % Utils.str(stream_formats)) + select = -1 + if auto: + select = self._auto_select_stream(stream_formats) + else: + select = self._dialog.select(self._addon.getLocalizedString(32016), stream_formats, 8000, 0) + Logger.debug('Selected: %s' % Utils.str(select)) + if select == -1: + self._cancel_operation = True + elif select != len(stream_formats) - 1: + data = fmt_list[select].split('/') + fmt_stream_map = Utils.get_safe_value(response_params, 'fmt_stream_map', '').split(',') + + for fmt in fmt_stream_map: + stream_data = fmt.split('|') + if stream_data[0] == data[0]: + url = stream_data[1] + break + if url: + cookie_header = '' + for cookie in request.response_cookies: + if cookie_header: cookie_header += ';' + cookie_header += cookie.name + '=' + cookie.value; + url += '|cookie=' + urllib.parse.quote(cookie_header) + return url + + def _auto_select_stream(self, streams): + select = -1 + allowedQualitied = ['Original format','1920x1080','1280x720','854x480','640x360'] + max_qual = KodiUtils.get_addon_setting('default_stream_quality') + if max_qual == '1080p': + allowedQualitied = ['1920x1080','1280x720','854x480','640x360','Original format'] + elif max_qual == '720p': + allowedQualitied = ['1280x720','854x480','640x360','Original format'] + elif max_qual == '480p': + allowedQualitied = ['854x480','640x360','Original format'] + elif max_qual == '360p': + allowedQualitied = ['640x360','Original format'] + for q in allowedQualitied: + if q in streams: + select = streams.index(q) + break + return select + + def play_stream_format(self, driveid, item_driveid=None, item_id=None): + self.choose_stream_format = True + self.play(driveid, item_driveid, item_id) + + def get_context_options(self, list_item, params, is_folder): + context_options = [] + if Utils.get_safe_value(params, 'action', '') == 'play': + p = params.copy() + p['action'] = 'check_google_ban' + context_options.append((self._addon.getLocalizedString(32071), 'RunPlugin('+self._addon_url + '?' + urllib.parse.urlencode(p)+')')) + p['action'] = 'play_stream_format' + cmd = 'PlayMedia(%s?%s)' % (self._addon_url, urllib.parse.urlencode(p),) + context_options.append((self._addon.getLocalizedString(32076), cmd)) + return context_options + + def check_google_ban(self, driveid, item_driveid=None, item_id=None): + self._provider.configure(self._account_manager, driveid) + self._progress_dialog.update(0, '') + item = self._provider.get_item(item_driveid=item_driveid, item_id=item_id, include_download_info = True) + url = item['download_info']['url'] + request_params = { + 'read_content' : False, + 'on_complete': lambda request: self.display_google_ban_result(request), + } + self._provider.prepare_request('get', url, request_params = request_params).request() + + def display_google_ban_result(self, request): + self._progress_dialog.close() + color = 'lime' + ban = self._common_addon.getLocalizedString(32013) + if request.response_code == 403 or request.response_code == 429: + color = 'red' + ban = self._common_addon.getLocalizedString(32033) + msg = self._addon.getLocalizedString(32072) % '[B][COLOR %s]%s[/COLOR][/B]' % (color, ban,) + msg += '\n' + self._addon.getLocalizedString(32073) % '[B]%s[/B]' % Utils.str(request.response_code) + msg += '\n' + request.response_text + self._dialog.ok(self._addon_name, msg) diff --git a/plugin.googledrive/resources/lib/provider/__init__.py b/plugin.googledrive/resources/lib/provider/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.googledrive/resources/lib/provider/googledrive.py b/plugin.googledrive/resources/lib/provider/googledrive.py new file mode 100644 index 0000000000..7201673adb --- /dev/null +++ b/plugin.googledrive/resources/lib/provider/googledrive.py @@ -0,0 +1,399 @@ +#------------------------------------------------------------------------------- +# Copyright (C) 2017 Carlos Guzman (cguZZman) carlosguzmang@protonmail.com +# +# This file is part of Google Drive for Kodi +# +# Google Drive for Kodi 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. +# +# Cloud Drive Common Module for Kodi 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 <http://www.gnu.org/licenses/>. +#------------------------------------------------------------------------------- + +import urllib +import datetime +import copy + +from clouddrive.common.remote.provider import Provider +from clouddrive.common.utils import Utils +from clouddrive.common.ui.logger import Logger +from clouddrive.common.ui.utils import KodiUtils +from clouddrive.common.exception import RequestException, ExceptionUtils +from clouddrive.common.cache.cache import Cache +try: + from urllib.error import HTTPError +except ImportError: + from urllib2 import HTTPError + +class GoogleDrive(Provider): + _default_parameters = {'spaces': 'drive', 'supportsAllDrives': 'true', 'prettyPrint': 'false'} + _is_shared_drive = False + _shared_drive_parameters = {'includeItemsFromAllDrives': 'true', 'corpora': 'drive', 'driveId': ''} + _user = None + + def __init__(self, source_mode = False): + super(GoogleDrive, self).__init__('googledrive', source_mode) + self.download_requires_auth = True + self._items_cache = Cache(KodiUtils.get_addon_info('id'), 'items', datetime.timedelta(minutes=KodiUtils.get_cache_expiration_time())) + + def configure(self, account_manager, driveid): + super(GoogleDrive, self).configure(account_manager, driveid) + drive = account_manager.get_by_driveid('drive', driveid) + self._is_shared_drive = drive and 'type' in drive and (drive['type'] == 'drive#drive' or drive['type'] == 'drive#teamDrive') + + def _get_api_url(self): + return 'https://www.googleapis.com/drive/v3' + + def _get_request_headers(self): + return None + + def get_account(self, request_params=None, access_tokens=None): + me = self.get('/about', parameters={'fields':'user'}, request_params=request_params, access_tokens=access_tokens) + if not me or not 'user' in me: + raise Exception('NoAccountInfo') + self._user = me['user'] + return { 'id' : self._user['permissionId'], 'name' : self._user['displayName']} + + def get_drives(self, request_params=None, access_tokens=None): + drives = [{ + 'id' : self._user['permissionId'], + 'name' : '', + 'type' : '' + }] + try: + all_shareddrives_fetch = False + page_token = None + parameters = {'pageSize': 100} + while not all_shareddrives_fetch: + if page_token: + parameters['pageToken'] = page_token + response = self.get('/drives', parameters=parameters, request_params=request_params, access_tokens=access_tokens) + if response and 'drives' in response: + for drive in response['drives']: + drives.append({ + 'id' : drive['id'], + 'name' : Utils.get_safe_value(drive, 'name', drive['id']), + 'type' : drive['kind'] + }) + if response and 'nextPageToken' in response: + page_token = response['nextPageToken'] + else: + all_shareddrives_fetch = True + except RequestException as ex: + httpex = ExceptionUtils.extract_exception(ex, HTTPError) + if not httpex or httpex.code != 403: + raise ex + return drives + + def get_drive_type_name(self, drive_type): + if drive_type == 'drive#drive' or drive_type == 'drive#teamDrive': + return 'Shared Drive' + return drive_type + + def prepare_parameters(self): + parameters = copy.deepcopy(self._default_parameters) + if self._is_shared_drive: + parameters.update(self._shared_drive_parameters) + parameters['driveId'] = self._driveid + return parameters + + def _get_field_parameters(self): + file_fileds = 'id,name,modifiedTime,size,mimeType' + if not self.source_mode: + file_fileds = file_fileds + ',description,hasThumbnail,thumbnailLink,owners(permissionId),parents,trashed,imageMediaMetadata(width),videoMediaMetadata,shortcutDetails' + return file_fileds + + def get_folder_items(self, item_driveid=None, item_id=None, path=None, on_items_page_completed=None, include_download_info=False, on_before_add_item=None): + item_driveid = Utils.default(item_driveid, self._driveid) + is_album = item_id and item_id[:6] == 'album-' + if is_album: + item_id = item_id[6:] + parameters = self.prepare_parameters() + if item_id: + parameters['q'] = '\'%s\' in parents' % item_id + elif path == 'sharedWithMe' or path == 'starred': + parameters['q'] = path + elif path != 'photos': + if path == '/': + parent = self._driveid if self._is_shared_drive else 'root' + parameters['q'] = '\'%s\' in parents' % parent + elif not is_album: + item = self.get_item_by_path(path, include_download_info) + parameters['q'] = '\'%s\' in parents' % item['id'] + + parameters['fields'] = 'files(%s),kind,nextPageToken' % self._get_field_parameters() + if 'q' in parameters: + parameters['q'] += ' and not trashed' + + self.configure(self._account_manager, self._driveid) + provider_method = self.get + url = '/files' + parameters['pageSize'] = 1000 + items = [] + if path == 'photos': + self._photos_provider = GooglePhotos() + self._photos_provider.configure(self._account_manager, self._driveid) + parameters = {} + provider_method = self._photos_provider.get + url = '/albums' + items.append(self._extract_item({'id': 'photos', 'title': 'Photos', 'kind': 'album'})) + elif is_album: + self._photos_provider = GooglePhotos() + self._photos_provider.configure(self._account_manager, self._driveid) + if item_id == 'photos': + parameters = {} + else: + parameters = {'albumId': item_id} + provider_method = self._photos_provider.post + url = '/mediaItems:search' + + files = provider_method(url, parameters = parameters) + if self.cancel_operation(): + return + items.extend(self.process_files(files, parameters, on_items_page_completed, include_download_info, on_before_add_item=on_before_add_item)) + return items + + def search(self, query, item_driveid=None, item_id=None, on_items_page_completed=None): + item_driveid = Utils.default(item_driveid, self._driveid) + parameters = self.prepare_parameters() + parameters['fields'] = 'files(%s),kind,nextPageToken' % self._get_field_parameters() + query = 'fullText contains \'%s\'' % Utils.str(query) + if item_id: + query += ' and \'%s\' in parents' % item_id + parameters['q'] = query + ' and not trashed' + parameters['pageSize'] = 1000 + files = self.get('/files', parameters = parameters) + if self.cancel_operation(): + return + return self.process_files(files, parameters, on_items_page_completed) + + def process_files(self, files, parameters, on_items_page_completed=None, include_download_info=False, extra_info=None, on_before_add_item=None): + items = [] + if files: + kind = Utils.get_safe_value(files, 'kind', '') + collection = [] + if kind == 'drive#fileList': + collection = files['files'] + elif kind == 'drive#changeList': + collection = files['changes'] + elif 'albums' in files: + kind = 'album' + collection = files['albums'] + elif 'mediaItems' in files: + kind = 'media_item' + collection = files['mediaItems'] + if collection: + for f in collection: + f['kind'] = Utils.get_safe_value(f, 'kind', kind) + item = self._extract_item(f, include_download_info) + if item: + if on_before_add_item: + on_before_add_item(item) + items.append(item) + if on_items_page_completed: + on_items_page_completed(items) + if type(extra_info) is dict: + if 'newStartPageToken' in files: + extra_info['change_token'] = files['newStartPageToken'] + if 'nextPageToken' in files: + parameters['pageToken'] = files['nextPageToken'] + url = '/files' + provider_method = self.get + if kind == 'drive#changeList': + url = '/changes' + elif kind == 'album': + url = '/albums' + provider_method = self._photos_provider.get + elif kind == 'media_item': + url = '/mediaItems:search' + provider_method = self._photos_provider.post + next_files = provider_method(url, parameters = parameters) + if self.cancel_operation(): + return + items.extend(self.process_files(next_files, parameters, on_items_page_completed, include_download_info, extra_info, on_before_add_item)) + return items + + def _extract_item(self, f, include_download_info=False): + kind = Utils.get_safe_value(f, 'kind', '') + if kind == 'drive#change': + change_type = Utils.get_safe_value(f, 'changeType', '') + if change_type == 'file': + if 'file' in f: + f = f['file'] + else: + f['id'] = Utils.get_safe_value(f, 'fileId') + f['trashed'] = Utils.get_safe_value(f, 'removed') + f['modifiedTime'] = Utils.get_safe_value(f, 'time') + else: + return {} + size = int('%s' % Utils.get_safe_value(f, 'size', 0)) + is_album = kind == 'album' + is_media_items = kind == 'media_item' + item_id = f['id'] + if is_album: + mimetype = 'application/vnd.google-apps.folder' + name = Utils.get_safe_value(f, 'title', item_id) + else: + mimetype = Utils.get_safe_value(f, 'mimeType', '') + name = Utils.get_safe_value(f, 'name', '') + if mimetype == 'application/vnd.google-apps.shortcut': + shortcut = Utils.get_safe_value(f, 'shortcutDetails') + item_id = Utils.get_safe_value(shortcut, 'targetId', item_id) + mimetype = Utils.get_safe_value(shortcut, 'targetMimeType', mimetype) + if is_media_items: + name = Utils.get_safe_value(f, 'filename', item_id) + item = { + 'id': item_id, + 'name': name, + 'name_extension' : Utils.get_extension(name), + 'parent': Utils.get_safe_value(f, 'parents', ['root'])[0], + 'drive_id' : Utils.get_safe_value(Utils.get_safe_value(f, 'owners', [{}])[0], 'permissionId'), + 'mimetype' : mimetype, + 'last_modified_date' : Utils.get_safe_value(f,'modifiedTime'), + 'size': size, + 'description': Utils.get_safe_value(f, 'description', ''), + 'deleted' : Utils.get_safe_value(f, 'trashed', False) + } + if item['mimetype'] == 'application/vnd.google-apps.folder': + item['folder'] = { + 'child_count' : 0 + } + if is_media_items: + item['url'] = f['baseUrl'] + '=d' + item['thumbnail'] = f['baseUrl'] + '=w100-h100' + if 'mediaMetadata' in f: + + metadata = f['mediaMetadata'] + if 'video' in metadata: + item['url'] += 'v' + item['video'] = { + 'width' : Utils.get_safe_value(metadata, 'width'), + 'height' : Utils.get_safe_value(metadata, 'height') + } + item['last_modified_date'] = Utils.get_safe_value(metadata, 'creationTime') + if 'videoMediaMetadata' in f: + video = f['videoMediaMetadata'] + item['video'] = { + 'width' : Utils.get_safe_value(video, 'width'), + 'height' : Utils.get_safe_value(video, 'height'), + 'duration' : int('%s' % Utils.get_safe_value(video, 'durationMillis', 0)) / 1000 + } + if 'imageMediaMetadata' in f or 'mediaMetadata' in f: + item['image'] = { + 'size' : size + } + if 'hasThumbnail' in f and f['hasThumbnail']: + item['thumbnail'] = Utils.get_safe_value(f, 'thumbnailLink') + if is_album: + item['thumbnail'] = Utils.get_safe_value(f, 'coverPhotoBaseUrl') + item['id'] = 'album-' + item['id'] + if include_download_info: + if is_media_items: + item['download_info'] = { + 'url' : item['url'] + } + else: + parameters = { + 'alt': 'media', + } + url = self._get_api_url() + '/files/%s' % item['id'] + if 'size' not in f and item['mimetype'] == 'application/vnd.google-apps.document': + url += '/export' + parameters['mimeType'] = Utils.default(Utils.get_mimetype_by_extension(item['name_extension']), Utils.get_mimetype_by_extension('pdf')) + url += '?%s' % urllib.parse.urlencode(parameters) + item['download_info'] = { + 'url' : url + } + return item + + def get_item_by_path(self, path, include_download_info=False): + parameters = self.prepare_parameters() + if path[-1:] == '/': + path = path[:-1] + Logger.debug(path + ' <- Target') + key = '%s%s' % (self._driveid, path,) + Logger.debug('Testing item from cache: %s' % key) + item = self._items_cache.get(key) + if not item: + parameters['fields'] = 'files(%s)' % self._get_field_parameters() + index = path.rfind('/') + filename = urllib.parse.unquote(path[index+1:]) + parent = path[0:index] + if not parent: + parent = 'root' + else: + parent = self.get_item_by_path(parent, include_download_info)['id'] + item = None + parameters['q'] = '\'%s\' in parents and name = \'%s\'' % (Utils.str(parent), Utils.str(filename).replace("'","\\'")) + parameters['pageSize'] = 1000 + files = self.get('/files', parameters = parameters) + if (len(files['files']) > 0): + for f in files['files']: + item = self._extract_item(f, include_download_info) + break + else: + Logger.debug('Found in cache.') + if not item: + raise RequestException('Not found by path', HTTPError(path, 404, 'Not found', None, None), 'Request URL: %s' % path, None) + + else: + self._items_cache.set(key, item) + return item + + def get_subtitles(self, parent, name, item_driveid=None, include_download_info=False): + parameters = self.prepare_parameters() + item_driveid = Utils.default(item_driveid, self._driveid) + subtitles = [] + parameters['fields'] = 'files(' + self._get_field_parameters() + ')' + parameters['q'] = 'name contains \'%s\'' % Utils.str(Utils.remove_extension(name)).replace("'","\\'") + parameters['pageSize'] = 1000 + files = self.get('/files', parameters = parameters) + for f in files['files']: + subtitle = self._extract_item(f, include_download_info) + if subtitle['name_extension'].lower() in ('srt','idx','sub','sbv','ass','ssa','smi'): + subtitles.append(subtitle) + return subtitles + + def get_item(self, item_driveid=None, item_id=None, path=None, find_subtitles=False, include_download_info=False): + parameters = self.prepare_parameters() + item_driveid = Utils.default(item_driveid, self._driveid) + parameters['fields'] = self._get_field_parameters() + if not item_id and path == '/': + item_id = 'root' + if item_id: + f = self.get('/files/%s' % item_id, parameters = parameters) + item = self._extract_item(f, include_download_info) + else: + item = self.get_item_by_path(path, include_download_info) + + if find_subtitles: + subtitles = self.get_subtitles(item['parent'], item['name'], item_driveid, include_download_info) + if subtitles: + item['subtitles'] = subtitles + return item + + def changes(self): + change_token = self.get_change_token() + if not change_token: + change_token = Utils.get_safe_value(self.get('/changes/startPageToken', parameters = self.prepare_parameters()), 'startPageToken') + extra_info = {} + parameters = self.prepare_parameters() + parameters['pageToken'] = change_token + parameters['pageSize'] = 1000 + parameters['fields'] = 'kind,nextPageToken,newStartPageToken,changes(kind,fileId,removed,changeType,time,file(%s))' % self._get_field_parameters() + f = self.get('/changes', parameters = parameters) + changes = self.process_files(f, parameters, include_download_info=True, extra_info=extra_info) + self.persist_change_token(Utils.get_safe_value(extra_info, 'change_token')) + return changes + +class GooglePhotos(GoogleDrive): + def _get_api_url(self): + return 'https://photoslibrary.googleapis.com/v1' + diff --git a/plugin.googledrive/resources/settings.xml b/plugin.googledrive/resources/settings.xml new file mode 100644 index 0000000000..236634de0c --- /dev/null +++ b/plugin.googledrive/resources/settings.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<settings > + <category label="32067"> + <setting label="32000" type="lsep"/> + <setting label="32004" type="bool" id="set_subtitle" default="true" value="true"/> + <setting label="32011" type="bool" id="resume_playing" default="true" visible="!String.IsEmpty(window(home).property(iskrypton))" /> + <setting label="32018" type="bool" id="save_resume_watched" default="true" visible="!String.IsEmpty(window(home).property(iskrypton))" /> + <setting label="32017" type="bool" id="ask_resume" default="true" visible="String.IsEmpty(window(home).property(iskrypton))" /> + <setting label="32008" type="bool" id="ask_stream_format" default="false" value="false"/> + <setting label="32070" type="labelenum" id="default_stream_quality" subsetting="true" values="Original|1080p|720p|480p|360p" default="Original" value="Original" enable="eq(-1,false)"/> + <setting label="32001" type="lsep"/> + <setting label="32003" type="bool" id="clean_folder" default="true" value="true"/> + <setting label="32019" type="bool" id="no_extension_strm" default="false" value="false"/> + <setting label="32020" type="bool" id="hide_export_progress" default="false" value="false"/> + <setting label="32002" type="lsep"/> + <setting label="32068" type="bool" id="allow_directory_listing" default="true"/> + <setting label="32069" type="number" id="port_directory_listing" default="8587"/> + <setting label="32005" type="lsep"/> + <setting label="32006" type="number" id="slideshow_refresh_interval" default="5" value="5"/> + <setting label="32010" type="bool" id="slideshow_recursive" default="false" value="false"/> + </category> + <category label="32032"> + <setting label="32033" type="text" id="sign-in-server" default="https://drive-login.herokuapp.com"/> + <setting label="32034" type="number" id="cache-expiration-time" default="5"/> + <setting label="32035" type="action" option="close" action="RunPlugin(plugin://plugin.googledrive/?action=_clear_cache)"/> + <setting label="32012" type="action" option="close" action="RunPlugin(plugin://plugin.googledrive/?action=_open_common_settings)"/> + </category> + <category label="32030"> + <setting label="32031" type="bool" id="report_error" default="false"/> + </category> +</settings> \ No newline at end of file diff --git a/plugin.googledrive/service.py b/plugin.googledrive/service.py new file mode 100644 index 0000000000..ee7d45a571 --- /dev/null +++ b/plugin.googledrive/service.py @@ -0,0 +1,30 @@ +#------------------------------------------------------------------------------- +# Copyright (C) 2017 Carlos Guzman (cguZZman) carlosguzmang@protonmail.com +# +# This file is part of Google Drive for Kodi +# +# Google Drive for Kodi 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. +# +# Cloud Drive Common Module for Kodi 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 <http://www.gnu.org/licenses/>. +#------------------------------------------------------------------------------- + +from clouddrive.common.service.download import DownloadService +from clouddrive.common.service.source import SourceService +from clouddrive.common.service.utils import ServiceUtil +from resources.lib.provider.googledrive import GoogleDrive +from clouddrive.common.service.export import ExportService +from clouddrive.common.service.player import PlayerService + + +if __name__ == '__main__': + ServiceUtil.run([DownloadService(GoogleDrive), SourceService(GoogleDrive), + ExportService(GoogleDrive), PlayerService(GoogleDrive)]) \ No newline at end of file diff --git a/plugin.image.bancasapo/LICENSE b/plugin.image.bancasapo/LICENSE new file mode 100644 index 0000000000..b616f5b958 --- /dev/null +++ b/plugin.image.bancasapo/LICENSE @@ -0,0 +1,692 @@ +Copyright (c) 2017 enen92 <enen92@kodi.tv> (http://24.sapo.pt/jornais) + +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 <http://www.gnu.org/licenses/> + + + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + 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 <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. + diff --git a/plugin.image.bancasapo/README.md b/plugin.image.bancasapo/README.md new file mode 100644 index 0000000000..3d9ad0e657 --- /dev/null +++ b/plugin.image.bancasapo/README.md @@ -0,0 +1,16 @@ +# plugin.image.bancasapo + +![Kodi Addon-Check (Krypton)](https://github.com/enen92/plugin.image.bancasapo/workflows/Kodi%20Addon-Check%20(Krypton)/badge.svg) +![Kodi Addon-Check (Matrix)](https://github.com/enen92/plugin.image.bancasapo/workflows/Kodi%20Addon-Check%20(Matrix)/badge.svg) + +Daily portuguese newspapper covers in Kodi + +* Screenshots + +![Screenshot screensaver](https://github.com/enen92/plugin.image.bancasapo/blob/master/resources/images/screenshot-01.jpg?raw=true) + +![Screenshot screensaver](https://github.com/enen92/plugin.image.bancasapo/blob/master/resources/images/screenshot-02.jpg?raw=true) + +![Screenshot screensaver](https://github.com/enen92/plugin.image.bancasapo/blob/master/resources/images/screenshot-03.jpg?raw=true) + +![Screenshot screensaver](https://github.com/enen92/plugin.image.bancasapo/blob/master/resources/images/screenshot-04.jpg?raw=true) diff --git a/plugin.image.bancasapo/addon.xml b/plugin.image.bancasapo/addon.xml new file mode 100644 index 0000000000..39faf2a91d --- /dev/null +++ b/plugin.image.bancasapo/addon.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<addon id="plugin.image.bancasapo" name="Banca Sapo" version="3.0.2+matrix.1" provider-name="enen92"> + <requires> + <import addon="xbmc.python" version="3.0.0"/> + <import addon="script.module.routing" version="0.2.0"/> + <import addon="script.module.requests" version="2.22.0"/> + </requires> + <extension point="xbmc.python.pluginsource" library="main.py"> + <provides>image</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <summary lang="en_GB">Daily portuguese newspapper covers in Kodi</summary> + <summary lang="pt_PT">Capas diárias dos jornais portugueses no Kodi</summary> + <description lang="en_GB">A Kodi image plugin for Banca Sapo</description> + <description lang="pt_PT">Um plugin para o Kodi para aceder à Banca Sapo</description> + <language>pt</language> + <platform>all</platform> + <license>GPL-3.0</license> + <website>https://24.sapo.pt/jornais</website> + <email>enen92@kodi.tv</email> + <source>https://github.com/enen92/plugin.image.bancasapo</source> + <news> + 3.0.2: + - Fix regex + </news> + <disclaimer lang="en_GB">Addon not endorsed by SAPO</disclaimer> + <disclaimer lang="pt_PT">Addon não desenvolvido pelo SAPO</disclaimer> + <assets> + <icon>resources/images/icon.png</icon> + <fanart>resources/images/fanart.jpg</fanart> + <screenshot>resources/images/screenshot-01.jpg</screenshot> + <screenshot>resources/images/screenshot-02.jpg</screenshot> + <screenshot>resources/images/screenshot-03.jpg</screenshot> + <screenshot>resources/images/screenshot-04.jpg</screenshot> + </assets> + </extension> +</addon> diff --git a/plugin.image.bancasapo/changelog.txt b/plugin.image.bancasapo/changelog.txt new file mode 100644 index 0000000000..353f05d43b --- /dev/null +++ b/plugin.image.bancasapo/changelog.txt @@ -0,0 +1,10 @@ +3.0.2: +- Fix regex + +3.0.1: +- Automatic submissions to krypton and matrix +- Fix languages +- Addon-check fixes + +v2.0.0 +- Old XBMC addon fixed to work for krypton \ No newline at end of file diff --git a/plugin.image.bancasapo/main.py b/plugin.image.bancasapo/main.py new file mode 100644 index 0000000000..e95eb4af2b --- /dev/null +++ b/plugin.image.bancasapo/main.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from resources.lib import kodilogging +from resources.lib import plugin + +import logging +import xbmcaddon + +# Keep this file to a minimum, as Kodi +# doesn't keep a compiled copy of this +ADDON = xbmcaddon.Addon() +kodilogging.config() + +plugin.run() diff --git a/plugin.image.bancasapo/resources/__init__.py b/plugin.image.bancasapo/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.image.bancasapo/resources/images/fanart.jpg b/plugin.image.bancasapo/resources/images/fanart.jpg new file mode 100644 index 0000000000..8cb5119aaf Binary files /dev/null and b/plugin.image.bancasapo/resources/images/fanart.jpg differ diff --git a/plugin.image.bancasapo/resources/images/icon.png b/plugin.image.bancasapo/resources/images/icon.png new file mode 100644 index 0000000000..dfc417d208 Binary files /dev/null and b/plugin.image.bancasapo/resources/images/icon.png differ diff --git a/plugin.image.bancasapo/resources/images/newspapericon.png b/plugin.image.bancasapo/resources/images/newspapericon.png new file mode 100644 index 0000000000..ee0dacb3b3 Binary files /dev/null and b/plugin.image.bancasapo/resources/images/newspapericon.png differ diff --git a/plugin.image.bancasapo/resources/images/screenshot-01.jpg b/plugin.image.bancasapo/resources/images/screenshot-01.jpg new file mode 100644 index 0000000000..4938a1f352 Binary files /dev/null and b/plugin.image.bancasapo/resources/images/screenshot-01.jpg differ diff --git a/plugin.image.bancasapo/resources/images/screenshot-02.jpg b/plugin.image.bancasapo/resources/images/screenshot-02.jpg new file mode 100644 index 0000000000..2560ad7d65 Binary files /dev/null and b/plugin.image.bancasapo/resources/images/screenshot-02.jpg differ diff --git a/plugin.image.bancasapo/resources/images/screenshot-03.jpg b/plugin.image.bancasapo/resources/images/screenshot-03.jpg new file mode 100644 index 0000000000..a10231ab15 Binary files /dev/null and b/plugin.image.bancasapo/resources/images/screenshot-03.jpg differ diff --git a/plugin.image.bancasapo/resources/images/screenshot-04.jpg b/plugin.image.bancasapo/resources/images/screenshot-04.jpg new file mode 100644 index 0000000000..4e9541af81 Binary files /dev/null and b/plugin.image.bancasapo/resources/images/screenshot-04.jpg differ diff --git a/plugin.image.bancasapo/resources/language/resource.language.en_gb/strings.po b/plugin.image.bancasapo/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..47d88c3b66 --- /dev/null +++ b/plugin.image.bancasapo/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,31 @@ +# Kodi Media Center language file +# Addon Name: Banca Sapo +# Addon id: plugin.image.bancasapo +# Addon Provider: enen92 +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: This is a comment + +msgctxt "#32000" +msgid "Banca Sapo" +msgstr "" + +msgctxt "#32001" +msgid "Unable to access website. Please try again later or contact the addon author." +msgstr "" + +msgctxt "#32002" +msgid "Debug mode" +msgstr "" \ No newline at end of file diff --git a/plugin.image.bancasapo/resources/language/resource.language.pt_pt/strings.po b/plugin.image.bancasapo/resources/language/resource.language.pt_pt/strings.po new file mode 100644 index 0000000000..a8c7e21f4e --- /dev/null +++ b/plugin.image.bancasapo/resources/language/resource.language.pt_pt/strings.po @@ -0,0 +1,31 @@ +# Kodi Media Center language file +# Addon Name: Banca Sapo +# Addon id: plugin.image.bancasapo +# Addon Provider: enen92 +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Portuguese (http://www.transifex.com/projects/p/xbmc-addons/language/pt/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: This is a comment + +msgctxt "#32000" +msgid "Banca Sapo" +msgstr "Banca Sapo" + +msgctxt "#32001" +msgid "Unable to access website. Please try again later or contact the addon author." +msgstr "Não foi possível aceder ao website. Por favor tente novamente mais tarde ou contacte o autor do addon" + +msgctxt "#32002" +msgid "Modo de depuração" +msgstr "" \ No newline at end of file diff --git a/plugin.image.bancasapo/resources/lib/__init__.py b/plugin.image.bancasapo/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.image.bancasapo/resources/lib/kodilogging.py b/plugin.image.bancasapo/resources/lib/kodilogging.py new file mode 100644 index 0000000000..06f00d3bf2 --- /dev/null +++ b/plugin.image.bancasapo/resources/lib/kodilogging.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from resources.lib.kodiutils import get_setting_as_bool + +import logging +import xbmc +import xbmcaddon + + +class KodiLogHandler(logging.StreamHandler): + + def __init__(self): + logging.StreamHandler.__init__(self) + addon_id = xbmcaddon.Addon().getAddonInfo('id') + prefix = "[%s] " % addon_id + formatter = logging.Formatter(prefix + '%(name)s: %(message)s') + self.setFormatter(formatter) + + def emit(self, record): + levels = { + logging.CRITICAL: xbmc.LOGFATAL, + logging.ERROR: xbmc.LOGERROR, + logging.WARNING: xbmc.LOGWARNING, + logging.INFO: xbmc.LOGINFO, + logging.DEBUG: xbmc.LOGDEBUG, + logging.NOTSET: xbmc.LOGNONE, + } + if get_setting_as_bool('debug'): + try: + xbmc.log(self.format(record), levels[record.levelno]) + except UnicodeEncodeError: + xbmc.log(self.format(record).encode( + 'utf-8', 'ignore'), levels[record.levelno]) + + def flush(self): + pass + + +def config(): + logger = logging.getLogger() + logger.addHandler(KodiLogHandler()) + logger.setLevel(logging.DEBUG) diff --git a/plugin.image.bancasapo/resources/lib/kodiutils.py b/plugin.image.bancasapo/resources/lib/kodiutils.py new file mode 100644 index 0000000000..9b1ccb4891 --- /dev/null +++ b/plugin.image.bancasapo/resources/lib/kodiutils.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +import xbmcaddon +from sys import version_info + +PY3 = version_info > (3, 0) +ADDON = xbmcaddon.Addon() + + +def translate(string_id): + if PY3: + return ADDON.getLocalizedString(string_id) + return ADDON.getLocalizedString(string_id).encode('utf-8') + + +def get_setting(setting): + if PY3: + return ADDON.getSetting(setting).strip() + return ADDON.getSetting(setting).strip().decode('utf-8') + + +def get_setting_as_bool(setting): + return get_setting(setting).lower() == "true" \ No newline at end of file diff --git a/plugin.image.bancasapo/resources/lib/plugin.py b/plugin.image.bancasapo/resources/lib/plugin.py new file mode 100644 index 0000000000..a5baaf833e --- /dev/null +++ b/plugin.image.bancasapo/resources/lib/plugin.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +import requests +import re +import os +import sys +import routing +import logging +import xbmcaddon +from resources.lib import kodilogging +from resources.lib.kodiutils import translate +from xbmcgui import Dialog,ListItem +from xbmcplugin import addDirectoryItem, endOfDirectory + + +ADDON = xbmcaddon.Addon() +logger = logging.getLogger(ADDON.getAddonInfo('id')) +kodilogging.config() +plugin = routing.Plugin() + +GLOBAL_FANART = os.path.join( + ADDON.getAddonInfo('path'), + 'resources', + 'images', + 'fanart.jpg' +) +GLOBAL_NEWSPAPPER_ICON = os.path.join( + ADDON.getAddonInfo('path'), + 'resources', + 'images', + 'newspapericon.png' +) +BANCA_SAPO_URL = "http://24.sapo.pt/jornais" + + +@plugin.route('/') +def categories(): + try: + req = requests.get(BANCA_SAPO_URL).text + except: + raise_notification() + + categories_regex = re.findall(r'<a href="/jornais/(.+?)" class="\[ \]">(.+?)</a>', req) + for uri, category in categories_regex: + liz = ListItem(cleanup(category)) + liz.setArt({ + "thumb":GLOBAL_NEWSPAPPER_ICON, + "fanart": GLOBAL_FANART + }) + addDirectoryItem(plugin.handle, + plugin.url_for(show_category, uri), + liz, + True + ) + endOfDirectory(plugin.handle) + + +@plugin.route('/category/<category_id>') +def show_category(category_id): + try: + req = requests.get('{}/{}'.format(BANCA_SAPO_URL, category_id)).text + except: + raise_notification() + + match = re.findall(r'data-data-extrameta="newspaper-id.+?data-original-src="(.+?)".+?data-share-url=.+?title="(.+?)".+?source data-srcset="(.+?)" srcset', req, re.DOTALL) + for cover, newspapper, thumb in match: + if thumb.startswith('//'): thumb = '{}{}'.format('http:', thumb) + newspapper = cleanup(newspapper) + liz = ListItem(newspapper) + liz.setArt({ + "thumb":thumb, + "fanart": GLOBAL_FANART + }) + addDirectoryItem(plugin.handle, cover, liz) + endOfDirectory(plugin.handle) + + +def cleanup(field): + return field.replace("<span>", "").replace("</span>", "") + + +def raise_notification(): + Dialog().ok(translate(32000), translate(32001)) + sys.exit(0) + + +def run(): + plugin.run() diff --git a/plugin.image.bancasapo/resources/settings.xml b/plugin.image.bancasapo/resources/settings.xml new file mode 100644 index 0000000000..96f7359af4 --- /dev/null +++ b/plugin.image.bancasapo/resources/settings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<settings> + <setting id="debug" type="bool" label="32001" default="false"/> +</settings> + diff --git a/plugin.image.dumpert/LICENSE.txt b/plugin.image.dumpert/LICENSE.txt new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/plugin.image.dumpert/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/plugin.image.dumpert/__init__.py b/plugin.image.dumpert/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.image.dumpert/addon.py b/plugin.image.dumpert/addon.py new file mode 100644 index 0000000000..4b98fe476d --- /dev/null +++ b/plugin.image.dumpert/addon.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import str +import sys +import urllib.parse +import xbmc + +from resources.lib.dumpert_const import ADDON, DATE, VERSION, SETTINGS + +# Parse parameters... +if len(sys.argv[2]) == 0: + # + # Main menu + # + xbmc.log("[ADDON] %s, Python Version %s" % (ADDON, str(sys.version)), xbmc.LOGDEBUG) + xbmc.log("[ADDON] %s v%s (%s) is starting, ARGV = %s" % (ADDON, VERSION, DATE, repr(sys.argv)), + xbmc.LOGDEBUG) + + if SETTINGS.getSetting('onlyshownewimagescategory') == 'true': + import resources.lib.dumpert_json as plugin + else: + import resources.lib.dumpert_main as plugin +else: + action = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['action'][0] + # + # Search + # + if action == 'search': + import resources.lib.dumpert_search as plugin + # + # Timemachine + # + elif action == 'timemachine': + import resources.lib.dumpert_timemachine as plugin + # + # JSON + # + if action == 'json': + import resources.lib.dumpert_json as plugin + +plugin.Main() diff --git a/plugin.image.dumpert/addon.xml b/plugin.image.dumpert/addon.xml new file mode 100644 index 0000000000..f0740d1246 --- /dev/null +++ b/plugin.image.dumpert/addon.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<addon + id="plugin.image.dumpert" + name="Dumpert" + version="1.0.4+matrix.1" + provider-name="Skipmode A1"> + <requires> + <import addon="xbmc.python" version="3.0.0"/> + <import addon="script.module.beautifulsoup4" version="4.3.2"/> + <import addon="script.module.requests" version="2.4.3"/> + <import addon="script.module.future" version="0.0.1"/> + <import addon="script.module.html5lib" version="0.999.0"/> + </requires> + <extension point="xbmc.python.pluginsource" library="addon.py"> + <provides>image</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <summary lang="en_GB">Watch funny pictures from Dumpert.nl (dutch)</summary> + <description lang="en_GB">Watch funny pictures from Dumpert.nl (dutch)</description> + <disclaimer lang="en_GB">For bugs, requests or general questions visit the Dumpert.nl thread on the Kodi forum.</disclaimer> + <summary lang="nl_NL">Bekijk grappige plaatjes van Dumpert.nl (dutch)</summary> + <description lang="nl_NL">Bekijk grappige plaatjes van Dumpert.nl (dutch)</description> + <disclaimer lang="nl_NL">Bugs of andere feedback op deze plugin kan geplaatst worden in de Dumpert.nl thread op het Kodi forum.</disclaimer> + <language>nl</language> + <platform>all</platform> + <license>GNU General Public License v3.0 only</license> + <forum>https://forum.kodi.tv/showthread.php?tid=315439</forum> + <website>https://dumpert.nl</website> + <source>https://github.com/skipmodea1/plugin.image.dumpert</source> + <news>v1.0.4 (2019-09-21) + - MAJOR changes in the addon due to redesigned website + - moved to Krypton + - updated logos + </news> + <assets> + <icon>resources/icon.png</icon> + <fanart>resources/fanart.jpg</fanart> + </assets> + </extension> +</addon> \ No newline at end of file diff --git a/plugin.image.dumpert/resources/__init__.py b/plugin.image.dumpert/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.image.dumpert/resources/fanart-blur.jpg b/plugin.image.dumpert/resources/fanart-blur.jpg new file mode 100644 index 0000000000..c6fa0a3bce Binary files /dev/null and b/plugin.image.dumpert/resources/fanart-blur.jpg differ diff --git a/plugin.image.dumpert/resources/fanart.jpg b/plugin.image.dumpert/resources/fanart.jpg new file mode 100644 index 0000000000..fa2c3350e3 Binary files /dev/null and b/plugin.image.dumpert/resources/fanart.jpg differ diff --git a/plugin.image.dumpert/resources/icon.png b/plugin.image.dumpert/resources/icon.png new file mode 100644 index 0000000000..fbe7a92763 Binary files /dev/null and b/plugin.image.dumpert/resources/icon.png differ diff --git a/plugin.image.dumpert/resources/language/resource.language.en_gb/strings.po b/plugin.image.dumpert/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..c4e10fa16a --- /dev/null +++ b/plugin.image.dumpert/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,164 @@ +# XBMC Media Center language file +# Addon Name: Dumpert +# Addon id: plugin.image.dumpert +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-06-04 07:34+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE\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 "Watch funny images from Dumpert.nl (dutch)" +msgstr "" + +msgctxt "Addon Description" +msgid "Watch funny images from Dumpert.nl (dutch)" +msgstr "" + +msgctxt "Addon Disclaimer" +msgid "For bugs, requests or general questions visit the Dumpert.nl thread on the XBMC forum." +msgstr "" + +msgctxt "#30000" +msgid "Top" +msgstr "" + +msgctxt "#30001" +msgid "New" +msgstr "" + +msgctxt "#30004" +msgid "Search" +msgstr "" + +msgctxt "#30005" +msgid "Top Time machine (daily/weekly/montly Toppers)" +msgstr "" + +msgctxt "#30007" +msgid "Dumpert TV" +msgstr "" + +msgctxt "#30008" +msgid "Daily Top" +msgstr "" + +msgctxt "#30009" +msgid "Weekly Top" +msgstr "" + +msgctxt "#30010" +msgid "Monthly Top" +msgstr "" + +msgctxt "#30100" +msgid "Version" +msgstr "" + +msgctxt "#30101" +msgid "Author" +msgstr "" + +msgctxt "#30102" +msgid "Website" +msgstr "" + +msgctxt "#30200" +msgid "Video player" +msgstr "" + +msgctxt "#30201" +msgid "Auto" +msgstr "" + +msgctxt "#30202" +msgid "DVDPlayer" +msgstr "" + +msgctxt "#30203" +msgid "MPlayer" +msgstr "" + +msgctxt "#30300" +msgid "Maximum Image quality" +msgstr "" + +msgctxt "#30301" +msgid "Low" +msgstr "" + +msgctxt "#30302" +msgid "Medium" +msgstr "" + +msgctxt "#30303" +msgid "High" +msgstr "" + +msgctxt "#30501" +msgid "%s to %s" +msgstr "" + +msgctxt "#30502" +msgid "Page %u" +msgstr "" + +msgctxt "#30503" +msgid "Next page" +msgstr "" + +msgctxt "#30504" +msgid "Getting image location..." +msgstr "" + +msgctxt "#30505" +msgid "No image found." +msgstr "" + +msgctxt "#30506" +msgid "Error showing image file." +msgstr "" + +msgctxt "#30507" +msgid "Error getting page: %s" +msgstr "" + +msgctxt "#30508" +msgid "What are you looking for?" +msgstr "" + +msgctxt "#30509" +msgid "Pick a date" +msgstr "" + +msgctxt "#30510" +msgid "Day top for day %s" +msgstr "" + +msgctxt "#30511" +msgid "Week top for week %s %s" +msgstr "" + +msgctxt "#30512" +msgid "Month top for month %s" +msgstr "" + +msgctxt "#30600" +msgid "Show NSFW content" +msgstr "" + +msgctxt "#30610" +msgid "Only show New image category" +msgstr "" + +msgctxt "#30620" +msgid "Refresh page" +msgstr "" \ No newline at end of file diff --git a/plugin.image.dumpert/resources/language/resource.language.nl_nl/strings.po b/plugin.image.dumpert/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..9a9208e165 --- /dev/null +++ b/plugin.image.dumpert/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,168 @@ +# XBMC Media Center language file +# Addon Name: Dumpert +# Addon id: plugin.image.dumpert +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-06-04 07:34+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "Watch funny images from Dumpert.nl (dutch)" +msgstr "Bekijk grappige plaatjes van Dumpert.nl (dutch)" + +msgctxt "Addon Description" +msgid "Watch funny photos from Dumpert.nl (dutch)" +msgstr "Bekijk grappige plaatjes van Dumpert.nl (dutch)" + +msgctxt "Addon Disclaimer" +msgid "For bugs, requests or general questions visit the Dumpert.nl thread on the Kodi forum." +msgstr "Bugs of andere feedback op deze plugin kan geplaatst worden in de Dumpert.nl thread op het Kodi forum." + +msgctxt "#30000" +msgid "Top" +msgstr "Toppers" + +msgctxt "#30001" +msgid "New" +msgstr "Images" + +msgctxt "#30004" +msgid "Search" +msgstr "Zoeken" + +msgctxt "#30005" +msgid "Top Time machine (daily/weekly/montly Toppers)" +msgstr "Toppers Tijdmachine: Toppers per dag/week/maand" + +msgctxt "#30007" +msgid "Dumpert TV" +msgstr "Dumpert TV" + +msgctxt "#30008" +msgid "Daily Top" +msgstr "Dagelijkse Toppers" + +msgctxt "#30009" +msgid "Weekly Top" +msgstr "Wekelijkse Toppers" + +msgctxt "#30010" +msgid "Monthly Top" +msgstr "Maandelijkse Toppers" + +msgctxt "#30100" +msgid "Version" +msgstr "Versie" + +msgctxt "#30101" +msgid "Author" +msgstr "Auteur" + +msgctxt "#30102" +msgid "Website" +msgstr "Website" + +msgctxt "#30200" +msgid "Video player" +msgstr "Video player" + +msgctxt "#30201" +msgid "Auto" +msgstr "Auto" + +msgctxt "#30202" +msgid "DVDPlayer" +msgstr "DVDPlayer" + +msgctxt "#30203" +msgid "MPlayer" +msgstr "MPlayer" + +msgctxt "#30300" +msgid "Image quality" +msgstr "Plaatjes kwaliteit" + +msgctxt "#30301" +msgid "Low" +msgstr "Laag" + +msgctxt "#30302" +msgid "Medium" +msgstr "Midden" + +msgctxt "#30303" +msgid "High" +msgstr "Hoog" + +msgctxt "#30400" +msgid "Debug" +msgstr "Debug" + +msgctxt "#30501" +msgid "%s to %s" +msgstr "%s tot %s" + +msgctxt "#30502" +msgid "Page %u" +msgstr "Pagina %u" + +msgctxt "#30503" +msgid "Next page" +msgstr "Volgende pagina" + +msgctxt "#30504" +msgid "Getting image location..." +msgstr "Plaatje wordt opgehaald..." + +msgctxt "#30505" +msgid "No image found." +msgstr "Plaatje niet gevonden." + +msgctxt "#30506" +msgid "Error showing image file." +msgstr "Fout bij het laten zien van het plaatje." + +msgctxt "#30507" +msgid "Error getting page: %s" +msgstr "Fout bij ophalen van pagina %s" + +msgctxt "#30508" +msgid "What are you looking for?" +msgstr "Wat zoek je?" + +msgctxt "#30509" +msgid "Pick a date" +msgstr "Kies een datum" + +msgctxt "#30510" +msgid "Day top for day %s" +msgstr "Dagtop voor dag %s" + +msgctxt "#30511" +msgid "Week top for week %s %s" +msgstr "Weektop voor week %s %s" + +msgctxt "#30512" +msgid "Month top for month %s" +msgstr "Maandtop voor maand %s" + +msgctxt "#30600" +msgid "Show NSFW content" +msgstr "Toon NSFW filmpjes" + +msgctxt "#30610" +msgid "Only show New image category" +msgstr "Toon alleen Nieuw plaatje categorie" + +msgctxt "#30620" +msgid "Refresh page" +msgstr "Ververs pagina" \ No newline at end of file diff --git a/plugin.image.dumpert/resources/lib/__init__.py b/plugin.image.dumpert/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.image.dumpert/resources/lib/dumpert_const.py b/plugin.image.dumpert/resources/lib/dumpert_const.py new file mode 100644 index 0000000000..255f8aa6f4 --- /dev/null +++ b/plugin.image.dumpert/resources/lib/dumpert_const.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import sys +import os +import xbmc +import xbmcaddon +from bs4 import BeautifulSoup + +# +# Constants +# +ADDON = "plugin.image.dumpert" +SETTINGS = xbmcaddon.Addon(id=ADDON) +LANGUAGE = SETTINGS.getLocalizedString +IMAGES_PATH = os.path.join(xbmcaddon.Addon(id=ADDON).getAddonInfo('path'), 'resources') +LATEST_URL = "https://api-live.dumpert.nl/mobile_api/json/foto/latest/0/" +TOPPERS_URL = "https://api-live.dumpert.nl/mobile_api/json/foto/toppers/0/" +SEARCH_URL = "https://api-live.dumpert.nl/mobile_api/json/search/" +DAY_TOPPERS_URL = "https://api-live.dumpert.nl/mobile_api/json/foto/top5/dag/" +WEEK_TOPPERS_URL = "https://api-live.dumpert.nl/mobile_api/json/foto/top5/week/" +MONTH_TOPPERS_URL = "https://api-live.dumpert.nl/mobile_api/json/foto/top5/maand/" +SFW_HEADERS = {'X-Dumpert-NSFW': '0'} +NSFW_HEADERS = {'X-Dumpert-NSFW': '1'} +DAY = "day" +WEEK = "week" +MONTH = "month" +DATE = "2019-09-21" +VERSION = "1.0.4" + + +if sys.version_info[0] > 2: + unicode = str + + +def convertToUnicodeString(s, encoding='utf-8'): + """Safe decode byte strings to Unicode""" + if isinstance(s, bytes): # This works in Python 2.7 and 3+ + s = s.decode(encoding) + return s + + +def convertToByteString(s, encoding='utf-8'): + """Safe encode Unicode strings to bytes""" + if isinstance(s, unicode): + s = s.encode(encoding) + return s + + +def log(name_object, object): + try: + # Let's try and remove any non-ascii stuff first + object = object.encode('ascii', 'ignore') + except: + pass + + try: + xbmc.log("[ADDON] %s v%s (%s) debug mode, %s = %s" % ( + ADDON, VERSION, DATE, name_object, convertToUnicodeString(object)), xbmc.LOGDEBUG) + except: + xbmc.log("[ADDON] %s v%s (%s) debug mode, %s = %s" % ( + ADDON, VERSION, DATE, name_object, + "Unable to log the object due to an error while converting it to an unicode string"), xbmc.LOGDEBUG) + + +def getSoup(html,default_parser="html5lib"): + soup = BeautifulSoup(html, default_parser) + return soup \ No newline at end of file diff --git a/plugin.image.dumpert/resources/lib/dumpert_json.py b/plugin.image.dumpert/resources/lib/dumpert_json.py new file mode 100644 index 0000000000..5f00a91479 --- /dev/null +++ b/plugin.image.dumpert/resources/lib/dumpert_json.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import object +from datetime import datetime, timedelta +import os +import requests +import sys +import urllib.request, urllib.parse, urllib.error +import xbmcgui +import xbmcplugin +import json + +from resources.lib.dumpert_const import LANGUAGE, IMAGES_PATH, SETTINGS, convertToUnicodeString, log, SFW_HEADERS, NSFW_HEADERS, \ + DAY, WEEK, MONTH, DAY_TOPPERS_URL, WEEK_TOPPERS_URL, MONTH_TOPPERS_URL, LATEST_URL + +# +# Main class +# +class Main(object): + # + # Init + # + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + # log("ARGV", repr(sys.argv)) + + # Parse parameters + try: + self.plugin_category = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['plugin_category'][0] + self.next_page_possible = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['next_page_possible'][0] + except KeyError: + self.plugin_category = LANGUAGE(30001) + self.next_page_possible = "True" + try: + self.period = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['period'][0] + except KeyError: + self.period = "" + try: + self.days_deducted_from_today = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['days_deducted_from_today'][0] + except KeyError: + self.days_deducted_from_today = "0" + try: + self.foto_list_page_url = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['url'][0] + except KeyError: + # If the only-show-new-images-category switch is turned on, this will be empty the first time + if self.period == "": + self.foto_list_page_url = LATEST_URL + # If period is filled in we will construct the url + else: + self.foto_list_page_url = "" + + log("self.foto_list_page_url", self.foto_list_page_url) + + self.next_url = "" + + # Constuct the next url based on days_deducted_from_today + if self.period == DAY or self.period == WEEK or self.period == MONTH: + # For some strange reason converting a string to a datetime object does NOT work here :( + # Thus we have to do this silly stuff to be able to determine the next_url + current_url_datetime_object = datetime.now() + next_url_datetime_object = datetime.now() + + # log("self.days_deducted_from_today", self.days_deducted_from_today) + + self.days_deducted_from_today = int(self.days_deducted_from_today) + if self.period == DAY: + # If we don't have a current foto list page url, lets construct it + if self.foto_list_page_url == "": + current_url_datetime_object = current_url_datetime_object - timedelta(days=self.days_deducted_from_today) + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/dag/2019-09-19/ + self.foto_list_page_url = DAY_TOPPERS_URL + current_url_datetime_object.strftime('%Y-%m-%d') + + # log("Generated self.foto_list_page_url day", self.foto_list_page_url) + + self.days_deducted_from_today = self.days_deducted_from_today + 1 + # Let's deduct all the cumulated days + next_url_datetime_object = next_url_datetime_object - timedelta(days=self.days_deducted_from_today) + self.days_deducted_from_today = str(self.days_deducted_from_today) + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/dag/2019-09-18/ + # This should be an url of the day before the date of the current foto url + self.next_url = DAY_TOPPERS_URL + next_url_datetime_object.strftime('%Y-%m-%d') + + elif self.period == WEEK: + # If we don't have a current foto list page url, lets construct it + if self.foto_list_page_url == "": + current_url_datetime_object = current_url_datetime_object - timedelta(days=self.days_deducted_from_today) + # For some reason date.strftime('%Y%W') will now contain the weeknumber that is 1 below the weeknumber should be for the site + # Let's add a week to fix that + current_url_datetime_object = current_url_datetime_object + timedelta(days=7) + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/week/201938/ + self.foto_list_page_url = WEEK_TOPPERS_URL + current_url_datetime_object.strftime('%Y%W') + + # log("Generated self.foto_list_page_url week", self.foto_list_page_url) + + # For some reason date.strftime('%Y%W') will now contain the weeknumber that is 1 below the weeknumber should be for the site + # Let's add a week to fix that + next_url_datetime_object = next_url_datetime_object + timedelta(days=7) + # Let's deduct all the cumulated days + self.days_deducted_from_today = self.days_deducted_from_today + 7 + next_url_datetime_object = next_url_datetime_object - timedelta(days=self.days_deducted_from_today) + + # Let's skip week "00" + if next_url_datetime_object.strftime('%W') == "00": + + # log("skipping week 00", "skipping week 00") + + self.days_deducted_from_today = self.days_deducted_from_today + 7 + next_url_datetime_object = next_url_datetime_object - timedelta(days=7) + + self.days_deducted_from_today = str(self.days_deducted_from_today) + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/week/201937/ + self.next_url = WEEK_TOPPERS_URL + next_url_datetime_object.strftime('%Y%W') + + elif self.period == MONTH: + # If we don't have a current foto list page url, lets construct it + if self.foto_list_page_url == "": + current_url_datetime_object = current_url_datetime_object - timedelta(days=self.days_deducted_from_today) + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/maand/201909/ + self.foto_list_page_url = MONTH_TOPPERS_URL + current_url_datetime_object.strftime('%Y%m') + + # log("Generated self.foto_list_page_url month", self.foto_list_page_url) + + current_url_datetime_object = current_url_datetime_object - timedelta(days=self.days_deducted_from_today) + + self.days_deducted_from_today = self.days_deducted_from_today + 27 + # Let's deduct all the cumulated days + next_url_datetime_object = next_url_datetime_object - timedelta(days=self.days_deducted_from_today) + + # If the year/month didn't change, up the days deducted some more + if current_url_datetime_object.strftime('%Y%m') == next_url_datetime_object.strftime('%Y%m'): + + # log("forcing next month", "forcing next month") + + self.days_deducted_from_today = self.days_deducted_from_today + 5 + # Let's deduct 5 more days + next_url_datetime_object = next_url_datetime_object - timedelta(days=5) + + self.days_deducted_from_today = str(self.days_deducted_from_today) + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/maand/201908/ + self.next_url = MONTH_TOPPERS_URL + next_url_datetime_object.strftime('%Y%m') + + log("self.next_url", self.next_url) + + # "https://api-live.dumpert.nl/mobile_api/json/foto/latest/0/" + else: + # Determine current page number and base_url + # find last slash + pos_of_last_slash = self.foto_list_page_url.rfind('/') + # remove last slash + self.foto_list_page_url = self.foto_list_page_url[0: pos_of_last_slash] + pos_of_last_slash = self.foto_list_page_url.rfind('/') + self.base_url = self.foto_list_page_url[0: pos_of_last_slash + 1] + self.current_page = self.foto_list_page_url[pos_of_last_slash + 1:] + self.current_page = int(self.current_page) + # add last slash + self.foto_list_page_url = str(self.foto_list_page_url) + "/" + + log("self.foto_list_page_url", self.foto_list_page_url) + + # + # Get the fotos... + # + self.getFotos() + + # + # Get fotos... + # + def getFotos(self): + # + # Init + # + listing = [] + + if SETTINGS.getSetting('nsfw') == 'true': + response = requests.get(self.foto_list_page_url, headers=NSFW_HEADERS) + else: + response = requests.get(self.foto_list_page_url, headers=SFW_HEADERS) + + # response.status + json_source = response.text + json_source = convertToUnicodeString(json_source) + data = json.loads(json_source) + if not data['success']: + xbmcplugin.endOfDirectory(self.plugin_handle) + return + + # max_foto_quality = SETTINGS.getSetting('foto') + + total_items = len(data['items']) + + # log("total_items", total_items) + + for item in data['items']: + title = item['title'] + title = convertToUnicodeString(title) + description = item['description'] + description = convertToUnicodeString(description) + thumbnail_url = item['stills']['still-large'] + for i in item['media']: + duration = i.get('duration',False) + + # {"gentime":1569050758,"items":[{"date":"2019-09-21T07:22:27+02:00","description":"Politie deelt vijftien prenten uit aan trouwturken","id":"7759341_b7ad3577","media":[{"description":"","duration":0,"mediatype":"FOTO","variants":[{"uri":"https://media.dumpert.nl/foto/b7ad3577_20190920_230634.jpg.jpg","version":"foto"}]}],"nopreroll":false,"nsfw":false,"stats":{"kudos_today":1592,"kudos_total":1592,"views_today":32673,"views_total":32673},"still":"https://media.dumpert.nl/stills/7759341_b7ad3577.jpg","stills":{"still":"https://media.dumpert.nl/stills/7759341_b7ad3577.jpg","still-large":"https://media.dumpert.nl/stills/large/7759341_b7ad3577.jpg","still-medium":"https://media.dumpert.nl/stills/medium/7759341_b7ad3577.jpg","thumb":"https://media.dumpert.nl/sq_thumbs/7759341_b7ad3577.jpg","thumb-medium":"https://media.dumpert.nl/sq_thumbs/medium/7759341_b7ad3577.jpg"},"tags":"auto politie bekeuring rotterdam trouwen stoet asociaal","thumbnail":"https://media.dumpert.nl/sq_thumbs/7759341_b7ad3577.jpg","title":"Trouwstoet mag dokken","upload_id":""},{"date":"2019-09-21T07:21:31+02:00","description":"Gooi jij hem ff vol?","id":"7759365_e4593f40","media":[{"description":"","duration":0,"med + + file = "" + + # Only process foto items + try: + foto_type = item['media'][0]['mediatype'] + if foto_type == 'FOTO': + process_item = True + else: + process_item = False + except IndexError: + process_item = False + + if process_item: + file = self.find_image(file, item) + + # log("title", title) + + log("json file", file) + + list_item = xbmcgui.ListItem(label=title) + list_item.setInfo( + type='pictures', + infoLabels={ + "title": title, + "picturepath": file, + "exif:path": file + }) + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + list_item.setProperty('IsPlayable', 'true') + + is_folder = False + # Add refresh option to context menu + list_item.addContextMenuItems([(LANGUAGE(30620), 'Container.Refresh')]) + # Add the item + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), + url=file, + listitem=list_item, + isFolder=is_folder, + totalItems=total_items) + + # Next page entry + if self.next_page_possible == 'True': + thumbnail_url = os.path.join(IMAGES_PATH, 'next-page.png') + list_item = xbmcgui.ListItem(LANGUAGE(30503)) + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + list_item.setProperty('IsPlayable', 'false') + # If the next url is still empty, we have to make one + # "https://api-live.dumpert.nl/mobile_api/json/foto/latest/1/" + if self.next_url == "": + next_page = self.current_page + 1 + self.next_url = str(self.base_url) + str(next_page) + '/' + parameters = {"action": "json", + "plugin_category": self.plugin_category, + "url": self.next_url, + "period": self.period, + "next_page_possible": self.next_page_possible, + "days_deducted_from_today": self.days_deducted_from_today} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + is_folder = True + # Add refresh option to context menu + list_item.addContextMenuItems([(LANGUAGE(30620), 'Container.Refresh')]) + # Add our item to the listing as a 3-element tuple. + listing.append((url, list_item, is_folder)) + + # Add our listing to Kodi. + # Large lists and/or slower systems benefit from adding all items at once via addDirectoryItems + # instead of adding one by one via addDirectoryItem. + xbmcplugin.addDirectoryItems(self.plugin_handle, listing, len(listing)) + # Disable sorting + xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self.plugin_handle) + + + def find_image(self, file, item): + if file == "": + try: + file = item['media'][0]['variants'][0]['uri'] + except IndexError: + pass + if file == "": + try: + file = item['media'][0]['variants'][1]['uri'] + except IndexError: + pass + if file == "": + try: + file = item['media'][0]['variants'][2]['uri'] + except IndexError: + pass + return file \ No newline at end of file diff --git a/plugin.image.dumpert/resources/lib/dumpert_main.py b/plugin.image.dumpert/resources/lib/dumpert_main.py new file mode 100644 index 0000000000..6171249c94 --- /dev/null +++ b/plugin.image.dumpert/resources/lib/dumpert_main.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library + +standard_library.install_aliases() +from builtins import object +import os +import sys +import urllib.request, urllib.parse, urllib.error +import xbmcgui +import xbmcplugin + +from resources.lib.dumpert_const import LANGUAGE, IMAGES_PATH, DAY, WEEK, MONTH, LATEST_URL, TOPPERS_URL, SEARCH_URL + +# +# Main class +# +class Main(object): + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + # + # Nieuw + # + title = LANGUAGE(30001) + parameters = {"action": "json", + "plugin_category": title, + "url": LATEST_URL, + "next_page_possible": "True"} + self.add_dir(parameters, title) + + # + # Toppers + # + title = LANGUAGE(30000) + parameters = {"action": "json", + "plugin_category": title, + "url": TOPPERS_URL, + "next_page_possible": "True"} + self.add_dir(parameters, title) + + + # + # Dag Toppers + # + title = LANGUAGE(30008) + parameters = {"action": "json", + "plugin_category": title, + "period": DAY, + "days_deducted_from_today": "0", + "next_page_possible": "True"} + self.add_dir(parameters, title) + + # + # Week Toppers + # + title = LANGUAGE(30009) + parameters = {"action": "json", + "plugin_category": title, + "period": WEEK, + "days_deducted_from_today": "0", + "next_page_possible": "True"} + self.add_dir(parameters, title) + + # + # Maand Toppers + # + title = LANGUAGE(30010) + parameters = {"action": "json", + "plugin_category": title, + "period": MONTH, + "days_deducted_from_today": "0", + "next_page_possible": "True"} + self.add_dir(parameters, title) + + # + # Timemachine: Toppers for a given date + # + title = LANGUAGE(30005) + parameters = {"action": "timemachine", + "plugin_category": title, + "next_page_possible": "True"} + self.add_dir(parameters, title) + + # + # Search + # + title = LANGUAGE(30004) + parameters = {"action": "search", + "plugin_category": title, + "url": SEARCH_URL, + "next_page_possible": "True"} + self.add_dir(parameters, title) + + # Disable sorting + xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self.plugin_handle) + + def add_dir(self, parameters, title): + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(title) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) diff --git a/plugin.image.dumpert/resources/lib/dumpert_search.py b/plugin.image.dumpert/resources/lib/dumpert_search.py new file mode 100644 index 0000000000..ef33603bb6 --- /dev/null +++ b/plugin.image.dumpert/resources/lib/dumpert_search.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import object +import sys +import xbmc + +from resources.lib.dumpert_const import LANGUAGE, log, convertToUnicodeString + + +# +# Main class +# +class Main(object): + # + # Init + # + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + log("ARGV", repr(sys.argv)) + + # Get search term from user + keyboard = xbmc.Keyboard('', LANGUAGE(30508)) + keyboard.doModal() + + if keyboard.isConfirmed(): + search_term = keyboard.getText() + # If the user has entered nothing, we stop + if search_term == "": + sys.exit(0) + else: + # If the user cancels the input box, we stop + sys.exit(0) + + sys.argv[2] = convertToUnicodeString(sys.argv[2]) + + # Converting URL argument to proper query string like 'https://api-live.dumpert.nl/mobile_api/json/search/fiets/0/' + sys.argv[2] = sys.argv[2] + search_term + "/0/" + + log("sys.argv[2]", sys.argv[2]) + + import resources.lib.dumpert_json as plugin + + plugin.Main() diff --git a/plugin.image.dumpert/resources/lib/dumpert_timemachine.py b/plugin.image.dumpert/resources/lib/dumpert_timemachine.py new file mode 100644 index 0000000000..366677d4d4 --- /dev/null +++ b/plugin.image.dumpert/resources/lib/dumpert_timemachine.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import object +import os +import sys +import urllib.request, urllib.parse, urllib.error +import xbmcgui +import xbmcplugin +from datetime import datetime, timedelta +import time + +from resources.lib.dumpert_const import LANGUAGE, IMAGES_PATH, log, DAY, WEEK, MONTH, DAY_TOPPERS_URL, WEEK_TOPPERS_URL, \ + MONTH_TOPPERS_URL + + +# +# Main class +# +class Main(object): + # + # Init + # + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + log("ARGV", repr(sys.argv)) + + self.plugin_category = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['plugin_category'][0] + self.next_page_possible = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['next_page_possible'][0] + + # Ask the user for a date + date = xbmcgui.Dialog().numeric(1, LANGUAGE(30509)) + if date is None: + date = datetime.now() + else: + date = date.replace(' ', '') + try: + try: + date = datetime.strptime(date, '%d/%m/%Y') + except TypeError: + date = datetime(*(time.strptime(date, '%d/%m/%Y')[0:6])) + except ValueError: + date = datetime.now() + + # If the date is in the future or too old, set it to the current date + if date > datetime.now() or date < datetime(2006, 1, 1): + date = datetime.now() + + date_now = datetime.now() + + # days + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/dag/2019-09-18/ + daily_toppers_url = DAY_TOPPERS_URL + date.strftime('%Y-%m-%d') + + delta = date_now - date + days_deducted_from_today = delta.days + + log("days_deducted_from_today for days", str(days_deducted_from_today)) + + title = LANGUAGE(30510) % (date.strftime('%d %b %Y')) + parameters = {"action": "json", + "plugin_category": self.plugin_category, + "url": daily_toppers_url, + "period": DAY, + "next_page_possible": self.next_page_possible, + "days_deducted_from_today": days_deducted_from_today} + self.add_folder(parameters, title) + + # weeks. + # Here we do something a bit odd. + # For some reason date.strftime('%Y%W') will now contain the weeknumber of last (!) week and not this week + # Let's add a week to fix that for the url and the title + date_plus_a_week = date + timedelta(days=7) + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/week/201938/ + weekly_toppers_url = WEEK_TOPPERS_URL + date_plus_a_week.strftime('%Y%W') + title = LANGUAGE(30511) % (date_plus_a_week.strftime('%W'), date_plus_a_week.strftime('%Y')) + + delta = date_now - date + days_deducted_from_today = delta.days + + log("days_deducted_from_today for months", str(days_deducted_from_today)) + + parameters = {"action": "json", + "plugin_category": self.plugin_category, + "url": weekly_toppers_url, + "period": WEEK, + "next_page_possible": self.next_page_possible, + "days_deducted_from_today": days_deducted_from_today} + self.add_folder(parameters, title) + + # months + # https://api-live.dumpert.nl/mobile_api/json/foto/top5/maand/201909/ + monthly_toppers_url = MONTH_TOPPERS_URL + date.strftime('%Y%m') + + delta = date_now - date + days_deducted_from_today = delta.days + + log("days_deducted_from_today for weeks", str(days_deducted_from_today)) + + title = LANGUAGE(30512) % (date.strftime('%b %Y')) + parameters = {"action": "json", + "plugin_category": self.plugin_category, + "url": monthly_toppers_url, + "period": MONTH, + "next_page_possible": self.next_page_possible, + "days_deducted_from_today": days_deducted_from_today} + self.add_folder(parameters, title) + + # Disable sorting + xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self.plugin_handle) + + def add_folder(self, parameters, title): + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(title) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) \ No newline at end of file diff --git a/plugin.image.dumpert/resources/next-page.png b/plugin.image.dumpert/resources/next-page.png new file mode 100644 index 0000000000..b12e695539 Binary files /dev/null and b/plugin.image.dumpert/resources/next-page.png differ diff --git a/plugin.image.dumpert/resources/settings.xml b/plugin.image.dumpert/resources/settings.xml new file mode 100644 index 0000000000..d04db621b7 --- /dev/null +++ b/plugin.image.dumpert/resources/settings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<settings> + <setting id="nsfw" label="30600" type="bool" default="false"/> + <setting id="onlyshownewimagescategory" label="30610" type="bool" default="false"/> +</settings> diff --git a/plugin.library.node.editor/LICENSE.txt b/plugin.library.node.editor/LICENSE.txt new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/plugin.library.node.editor/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.library.node.editor/addon.xml b/plugin.library.node.editor/addon.xml new file mode 100644 index 0000000000..3eb5802251 --- /dev/null +++ b/plugin.library.node.editor/addon.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<addon id="plugin.library.node.editor" name="Library Node Editor" version="2.0.5" provider-name="Unfledged, Team-Kodi"> + <requires> + <import addon="xbmc.python" version="3.0.0"/> + <import addon="script.module.unidecode" version="1.1.1"/> + </requires> + <extension point="xbmc.python.pluginsource" library="plugin.py"> + <provides>executable</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <platform>all</platform> + <license>GPL-2.0-only</license> + <forum>https://forum.kodi.tv/showthread.php?tid=224512</forum> + <source>https://github.com/XBMC-Addons/plugin.library.node.editor</source> + <assets> + <icon>icon.png</icon> + </assets> + <news> + 2.0.5 (31/12/2021) + - Sync translations + </news> + <summary lang="da_DK">Administrer brugerdefinerede biblioteksnoder.</summary> + <summary lang="de_DE">Benutzerdefinierte Bibliotheksknoten verwalten.</summary> + <summary lang="en_GB">Manage custom library nodes.</summary> + <summary lang="en_NZ">Manage custom library nodes.</summary> + <summary lang="en_US">Manage custom library nodes.</summary> + <summary lang="fr_CA">Gérer des nœuds de médiathèque personnalisés.</summary> + <summary lang="fr_FR">Gére des nœuds de médiathèque personnalisés.</summary> + <summary lang="it_IT">Gestisci nodi libreria personalizzati</summary> + <summary lang="ko_KR">사용자 정의 라이브러리 노드를 관리합니다.</summary> + <summary lang="lt_LT">Tvarkykite savus bibliotekos mazgus.</summary> + <summary lang="nl_NL">Beheer aangepaste bibliotheek nodes.</summary> + <summary lang="pl_PL">Zarządzanie niestandardowymi węzłami biblioteki.</summary> + <summary lang="pt_BR">Gerenciar nós customizados da biblioteca.</summary> + <summary lang="ro_RO">Administrați noduri mediatecă personalizate</summary> + <summary lang="ru_RU">Управление узлами пользовательских медиатек.</summary> + <summary lang="sv_SE">Hantera anpassade biblioteksnoder.</summary> + <summary lang="vi_VN">Quản lý các nút thư viện tùy chỉnh.</summary> + <summary lang="zh_CN">管理自定义资料库节点。</summary> + <description lang="da_DK">Opret og rediger brugerdefinerede biblioteksnoder.</description> + <description lang="de_DE">Benutzerdefinierte Bibliotheksknoten erstellen und bearbeiten.</description> + <description lang="en_GB">Create and edit custom library nodes.</description> + <description lang="en_NZ">Create and edit custom library nodes.</description> + <description lang="en_US">Create and edit custom library nodes.</description> + <description lang="fr_CA">Créer et modifier des nœuds de médiathèque personnalisés.</description> + <description lang="fr_FR">Crée et modifie des nœuds de médiathèque personnalisés.</description> + <description lang="it_IT">Crea e modifica nodi libreria personalizzati.</description> + <description lang="ko_KR">사용자 라이브러리 노드를 생성하고 편집합니다.</description> + <description lang="lt_LT">Sukurkite ir keiskite savus bibliotekos mazgus.</description> + <description lang="nl_NL">Creëer en wijzig aangepaste bibliotheek nodes.</description> + <description lang="pl_PL">Tworzenie i edytowanie niestandardowych węzłów biblioteki.</description> + <description lang="pt_BR">Crie e edite nós customizados na biblioteca.</description> + <description lang="ru_RU">Создание и редактирование пользовательских узлов медиатеки.</description> + <description lang="sv_SE">Skapa och redigera anpassade biblioteksnoder.</description> + <description lang="zh_CN">创建和编辑自定义资料库节点。</description> + </extension> +</addon> diff --git a/plugin.library.node.editor/icon.png b/plugin.library.node.editor/icon.png new file mode 100644 index 0000000000..4a62edfbc1 Binary files /dev/null and b/plugin.library.node.editor/icon.png differ diff --git a/plugin.library.node.editor/plugin.py b/plugin.library.node.editor/plugin.py new file mode 100644 index 0000000000..acff29b64a --- /dev/null +++ b/plugin.library.node.editor/plugin.py @@ -0,0 +1,6 @@ +import sys +from resources.lib import addon + + +if __name__ == "__main__": + addon.run(sys.argv) diff --git a/plugin.library.node.editor/resources/__init__.py b/plugin.library.node.editor/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.library.node.editor/resources/language/resource.language.af_ZA/strings.po b/plugin.library.node.editor/resources/language/resource.language.af_ZA/strings.po new file mode 100644 index 0000000000..149b81a346 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.af_ZA/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-05-07 12:40+0000\n" +"Last-Translator: Christian Gade <gade@kodi.tv>\n" +"Language-Team: Afrikaans (South Africa) <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/af_za/>\n" +"Language: af_ZA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.6.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Video Biblioteek" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Musiek Biblioteek" + +msgctxt "#30100" +msgid "Delete" +msgstr "Wis uit" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Redigeer etiket" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Soek vir ikoon" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "Orde" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Stop" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Groep" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Stop" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Orden" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.am_ET/strings.po b/plugin.library.node.editor/resources/language/resource.language.am_ET/strings.po new file mode 100644 index 0000000000..369fc3f363 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.am_ET/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Amharic (http://www.transifex.com/projects/p/xbmc-addons/language/am/)\n" +"Language: am\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "ማጥፊያ" + +msgctxt "#30101" +msgid "Edit label" +msgstr "ምልክት ማረሚያ" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "ማቆሚያ" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "ቡድን" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "ማቆሚያ" + +msgctxt "#30313" +msgid "Icon" +msgstr "ምልክት" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.ar_SA/strings.po b/plugin.library.node.editor/resources/language/resource.language.ar_SA/strings.po new file mode 100644 index 0000000000..81ded229b2 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ar_SA/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Arabic (http://www.transifex.com/projects/p/xbmc-addons/language/ar/)\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "حذف" + +msgctxt "#30101" +msgid "Edit label" +msgstr "تحرير العلامة" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "مسار" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "مجموعة" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "مسار" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "ترتيب" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.az_AZ/strings.po b/plugin.library.node.editor/resources/language/resource.language.az_AZ/strings.po new file mode 100644 index 0000000000..c0bd8f6a5a --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.az_AZ/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Azerbaijani (http://www.transifex.com/projects/p/xbmc-addons/language/az/)\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Sil" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Məkan" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Məkan" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.be_BY/strings.po b/plugin.library.node.editor/resources/language/resource.language.be_BY/strings.po new file mode 100644 index 0000000000..8524afdfd2 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.be_BY/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-05-07 12:40+0000\n" +"Last-Translator: Christian Gade <gade@kodi.tv>\n" +"Language-Team: Belarusian <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/be_by/>\n" +"Language: be_BY\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Weblate 4.6.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Відэабібліятэка" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Музычная бібліятэка" + +msgctxt "#30100" +msgid "Delete" +msgstr "Выдаліць" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Рэдагаваць адмеціну" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Пошук значка" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "Чарга" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Шлях" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Група" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Шлях" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Упарадкавана па" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.bg_BG/strings.po b/plugin.library.node.editor/resources/language/resource.language.bg_BG/strings.po new file mode 100644 index 0000000000..0a6b0893f7 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.bg_BG/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Bulgarian (http://www.transifex.com/projects/p/xbmc-addons/language/bg/)\n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +msgctxt "#30000" +msgid "Add content..." +msgstr "Добави съдържание..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Добави път..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Добави подредба..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Добави ограничение..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Добави групиране..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Добави правило..." + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Видео библиотека" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Музикална библиотека" + +msgctxt "#30100" +msgid "Delete" +msgstr "Изтрий" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Редактирай етикета" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Редактирай иконата" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Преглед за икона" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Редактирай видимостта" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Добави към главното меню" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Преглед за сойност" + +msgctxt "#30200" +msgid "Content" +msgstr "Съдържание" + +msgctxt "#30201" +msgid "Order" +msgstr "Ред" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Групиране" + +msgctxt "#30203" +msgid "Limit" +msgstr "Ограничение" + +msgctxt "#30204" +msgid "Path" +msgstr "Път" + +msgctxt "#30205" +msgid "Rule" +msgstr "Правило" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Задайте име" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Задайте условие за видимост" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Начало със стандартните" + +msgctxt "#30305" +msgid "Field" +msgstr "Поле" + +msgctxt "#30306" +msgid "Operator" +msgstr "Оператор" + +msgctxt "#30307" +msgid "Value" +msgstr "Стойност" + +msgctxt "#30308" +msgid "Content type" +msgstr "Тип съдържание" + +msgctxt "#30309" +msgid "Content" +msgstr "Съдържание" + +msgctxt "#30310" +msgid "Group" +msgstr "Група" + +msgctxt "#30311" +msgid "Limit" +msgstr "Ограничение" + +msgctxt "#30312" +msgid "Path" +msgstr "Път" + +msgctxt "#30313" +msgid "Icon" +msgstr "Икона" + +msgctxt "#30314" +msgid "Order by" +msgstr "Подреди по" + +msgctxt "#30315" +msgid "Direction" +msgstr "Ред" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Наистина ли желаете да го изтриете?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Наистина ли желаете да изтриете правилото?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Редът изисква параметър \"Съдържание\"" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Редактирай реда" diff --git a/plugin.library.node.editor/resources/language/resource.language.bs_BA/strings.po b/plugin.library.node.editor/resources/language/resource.language.bs_BA/strings.po new file mode 100644 index 0000000000..bf876de17f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.bs_BA/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Bosnian (http://www.transifex.com/projects/p/xbmc-addons/language/bs/)\n" +"Language: bs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Izbriši" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Uredi natpis" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Putanja" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupa" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Putanja" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Složi po" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.ca_ES/strings.po b/plugin.library.node.editor/resources/language/resource.language.ca_ES/strings.po new file mode 100644 index 0000000000..6e647d31ee --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ca_ES/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Catalan (http://www.transifex.com/projects/p/xbmc-addons/language/ca/)\n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Biblioteca de vídeo" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Biblioteca de música" + +msgctxt "#30100" +msgid "Delete" +msgstr "Elimina" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Edita l'etiqueta" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Cerca una icona" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Camí" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "Valor" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grup" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Camí" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icona" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordena per" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.cs_CZ/strings.po b/plugin.library.node.editor/resources/language/resource.language.cs_CZ/strings.po new file mode 100644 index 0000000000..744580421f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.cs_CZ/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Czech (http://www.transifex.com/projects/p/xbmc-addons/language/cs/)\n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Hudební knihovna" + +msgctxt "#30100" +msgid "Delete" +msgstr "Smazat" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Upravit popisek" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Vyhledej ikonku" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Cesta" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Pole" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operátor" + +msgctxt "#30307" +msgid "Value" +msgstr "Hodnota" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Skupina" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Cesta" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikona" + +msgctxt "#30314" +msgid "Order by" +msgstr "Řadit" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.cy_GB/strings.po b/plugin.library.node.editor/resources/language/resource.language.cy_GB/strings.po new file mode 100644 index 0000000000..5173ab4a6d --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.cy_GB/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Welsh (http://www.transifex.com/projects/p/xbmc-addons/language/cy/)\n" +"Language: cy\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Dileu" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Golygu labeli" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Pori am eiconau" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Llwybr" + +msgctxt "#30205" +msgid "Rule" +msgstr "Rheol" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grŵp" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Llwybr" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Trefnu yn ôl" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.da_DK/strings.po b/plugin.library.node.editor/resources/language/resource.language.da_DK/strings.po new file mode 100644 index 0000000000..b28491534f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.da_DK/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-11-09 11:30+0000\n" +"Last-Translator: Christian Gade <gade@kodi.tv>\n" +"Language-Team: Danish <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/da_dk/>\n" +"Language: da_DK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Administrer brugerdefinerede biblioteksnoder." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Opret og rediger brugerdefinerede biblioteksnoder." + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "Tilføj indhold..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Tilføj sti..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Tilføj rækkefølge..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Tilføj grænse..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Tilføj gruppering..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Tilføj regel..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Ny node..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Ny overordnet node..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Nulstil biblioteksnoder til standard..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "Tilføj komponent..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Rediger manuelt..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videobibliotek" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Musikbibliotek" + +msgctxt "#30100" +msgid "Delete" +msgstr "Slet" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Rediger navn" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Rediger ikon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Se efter ikon" + +msgctxt "#30104" +msgid "Move node" +msgstr "Flyt node" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Rediger synlighed" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Tilføj til hovedmenu" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Søg efter værdi" + +msgctxt "#30200" +msgid "Content" +msgstr "Indhold" + +msgctxt "#30201" +msgid "Order" +msgstr "Rækkefølge" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Gruppering" + +msgctxt "#30203" +msgid "Limit" +msgstr "Grænse" + +msgctxt "#30204" +msgid "Path" +msgstr "Sti" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regel" + +msgctxt "#30206" +msgid "Match" +msgstr "Match" + +msgctxt "#30300" +msgid "Set name" +msgstr "Indstil navn" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Indstil synlighed" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Indstil indeks for rækkefølge" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Navn på ny overordnet node" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Start med standarder" + +msgctxt "#30305" +msgid "Field" +msgstr "Felt" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operatør" + +msgctxt "#30307" +msgid "Value" +msgstr "Værdi" + +msgctxt "#30308" +msgid "Content type" +msgstr "Indholdstype" + +msgctxt "#30309" +msgid "Content" +msgstr "Indhold" + +msgctxt "#30310" +msgid "Group" +msgstr "Gruppe" + +msgctxt "#30311" +msgid "Limit" +msgstr "Grænse" + +msgctxt "#30312" +msgid "Path" +msgstr "Sti" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Sorter efter" + +msgctxt "#30315" +msgid "Direction" +msgstr "Retning" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Navn på ny node" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "Henter værdier..." + +msgctxt "#30318" +msgid "Property" +msgstr "Egenskab" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Kan ikke kopiere standard biblioteksnoder" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Er du sikker på at du vil slette denne node?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Er du sikker på at du vil slette alle noder?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Er du sikker på at du vil slette denne egenskab?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Indholdsparameter kan ikke slettes, mens denne node stadig har et parameter for rækkefølge, begrænsning eller regel." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Er du sikker på at du vil slette denne regel?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Rækkefølge kræver et parameter for indhold" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "Er du sikker på at du vil slette denne komponent?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "Brugerdefineret egenskab..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "Der blev ikke fundet nogen muligheder at vælge imellem" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "Henter indhold for plugin..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "Link stien hertil" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "Genre-ID" + +msgctxt "#30501" +msgid "Country ID" +msgstr "ID for land" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "Studio-ID" + +msgctxt "#30503" +msgid "Director ID" +msgstr "ID for instruktør" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "ID for skuespiller" + +msgctxt "#30505" +msgid "Set ID" +msgstr "ID for sæt" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "ID for mærkater" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "ID for tv-serie" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "ID for kunstner" + +msgctxt "#30509" +msgid "Album ID" +msgstr "ID for album" + +msgctxt "#30510" +msgid "Role ID" +msgstr "ID for rolle" + +msgctxt "#30511" +msgid "Song ID" +msgstr "ID for sang" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "Kun albumkunstnere" + +msgctxt "#30513" +msgid "Show singles" +msgstr "Vis singler" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "Plugin..." diff --git a/plugin.library.node.editor/resources/language/resource.language.de_DE/strings.po b/plugin.library.node.editor/resources/language/resource.language.de_DE/strings.po new file mode 100644 index 0000000000..8ed32555a0 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.de_DE/strings.po @@ -0,0 +1,326 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-07-23 14:15+0000\n" +"Last-Translator: Kai Sommerfeld <kai.sommerfeld@gmx.com>\n" +"Language-Team: German <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/de_de/>\n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.7.2\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Benutzerdefinierte Bibliotheksknoten verwalten." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Benutzerdefinierte Bibliotheksknoten erstellen und bearbeiten." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Inhalt hinzufügen ..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Pfad hinzufügen ..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Sortierung hinzufügen ..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Beschränkung hinzufügen ..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Gruppierung hinzufügen ..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Regel hinzufügen ..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Neuer Knoten ..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Neuer übergeordneter Knoten ..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Bibliotheksknoten auf Standard zurücksetzen ..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "Komponente hinzufügen ..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Manuell bearbeiten ..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videobibliothek" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Musikbibliothek" + +msgctxt "#30100" +msgid "Delete" +msgstr "Löschen" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Beschriftung ändern" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Symbol bearbeiten" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Nach Symbol suchen" + +msgctxt "#30104" +msgid "Move node" +msgstr "Knoten verschieben" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Sichtbarkeit bearbeiten" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Zum Hauptmenü hinzufügen" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Nach Wert suchen" + +msgctxt "#30200" +msgid "Content" +msgstr "Inhalt" + +msgctxt "#30201" +msgid "Order" +msgstr "Sortierung" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Gruppierung" + +msgctxt "#30203" +msgid "Limit" +msgstr "Beschränkung" + +msgctxt "#30204" +msgid "Path" +msgstr "Pfad" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regel" + +msgctxt "#30206" +msgid "Match" +msgstr "Übereinstimmung" + +msgctxt "#30300" +msgid "Set name" +msgstr "Name festlegen" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Sichtbarkeitsbedingung festlegen" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Sortierungsindex festlegen" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Name des neuen übergeordneten Knotens" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Mit Standardeinstellungen starten" + +msgctxt "#30305" +msgid "Field" +msgstr "Feld" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operator" + +msgctxt "#30307" +msgid "Value" +msgstr "Wert" + +msgctxt "#30308" +msgid "Content type" +msgstr "Inhaltstyp" + +msgctxt "#30309" +msgid "Content" +msgstr "Inhalt" + +msgctxt "#30310" +msgid "Group" +msgstr "Gruppe" + +msgctxt "#30311" +msgid "Limit" +msgstr "Beschränkung" + +msgctxt "#30312" +msgid "Path" +msgstr "Pfad" + +msgctxt "#30313" +msgid "Icon" +msgstr "Symbol" + +msgctxt "#30314" +msgid "Order by" +msgstr "Sortieren nach" + +msgctxt "#30315" +msgid "Direction" +msgstr "Richtung" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Name des neuen Knotens" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "Werte werden abgerufen ..." + +msgctxt "#30318" +msgid "Property" +msgstr "Eigenschaft" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Standardbibliotheksknoten können nicht kopiert werden" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Diesen Knoten wirklich löschen?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Wirklich alle Knoten zurücksetzen?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Diese Eigenschaft wirklich löschen?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Inhaltsparameter kann nicht gelöscht werden, solange dieser Knoten noch eine Sortierung, Beschränkung oder Regel enthält." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Diese Regel wirklich löschen?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Sortierung benötigt einen Inhaltsparameter" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "Diese Komponente wirklich löschen?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "Benutzerdefinierte Eigenschaft ..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "Keine Auswahlmöglichkeiten gefunden" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "Plugin-Auflistungen werden abgerufen ..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "Pfad hierauf verlinken" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "Genre-ID" + +msgctxt "#30501" +msgid "Country ID" +msgstr "Landes-ID" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "Studio-ID" + +msgctxt "#30503" +msgid "Director ID" +msgstr "Direktor-ID" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "Darsteller-ID" + +msgctxt "#30505" +msgid "Set ID" +msgstr "Zusammenstellungs-ID" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "Tag-ID" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "TV-Show-ID" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "Interpreten-ID" + +msgctxt "#30509" +msgid "Album ID" +msgstr "Album-ID" + +msgctxt "#30510" +msgid "Role ID" +msgstr "Rollen-ID" + +msgctxt "#30511" +msgid "Song ID" +msgstr "Titel-ID" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "Nur Albumsinterpreten" + +msgctxt "#30513" +msgid "Show singles" +msgstr "Singles anzeigen" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "Plugin ..." + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Sortierung bearbeiten" diff --git a/plugin.library.node.editor/resources/language/resource.language.el_GR/strings.po b/plugin.library.node.editor/resources/language/resource.language.el_GR/strings.po new file mode 100644 index 0000000000..7c9f383163 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.el_GR/strings.po @@ -0,0 +1,326 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Greek (http://www.transifex.com/projects/p/xbmc-addons/language/el/)\n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Συλλογή Βίντεο" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Μουσική Συλλογή" + +msgctxt "#30100" +msgid "Delete" +msgstr "Διαγραφή" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Επεξεργασία ετικέτας" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Επεξεργασία εικονιδίου" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Αναζήτηση εικονιδίου" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Επεξεργασία Ορατότητας" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Προσθήκη στο κεντρικό μενού" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "Περιεχόμενο" + +msgctxt "#30201" +msgid "Order" +msgstr "Σειρά" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Ομαδοποίηση" + +msgctxt "#30203" +msgid "Limit" +msgstr "Όριο" + +msgctxt "#30204" +msgid "Path" +msgstr "Διαδρομή" + +msgctxt "#30205" +msgid "Rule" +msgstr "Κανόνας" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Ορισμός Ονόματος" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Πεδίο" + +msgctxt "#30306" +msgid "Operator" +msgstr "Χειριστής" + +msgctxt "#30307" +msgid "Value" +msgstr "Τιμή" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "Περιεχόμενο" + +msgctxt "#30310" +msgid "Group" +msgstr "Γκρουπ" + +msgctxt "#30311" +msgid "Limit" +msgstr "Όριο" + +msgctxt "#30312" +msgid "Path" +msgstr "Διαδρομή" + +msgctxt "#30313" +msgid "Icon" +msgstr "Εικονίδιο" + +msgctxt "#30314" +msgid "Order by" +msgstr "Σειρά κατά" + +msgctxt "#30315" +msgid "Direction" +msgstr "Κατεύθυνση" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Επεξεργασία Σειράς" diff --git a/plugin.library.node.editor/resources/language/resource.language.en_AU/strings.po b/plugin.library.node.editor/resources/language/resource.language.en_AU/strings.po new file mode 100644 index 0000000000..2bc28e6982 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.en_AU/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (Australia) (http://www.transifex.com/projects/p/xbmc-addons/language/en_AU/)\n" +"Language: en_AU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Video Library" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Music Library" + +msgctxt "#30100" +msgid "Delete" +msgstr "Delete" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Edit label" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Edit icon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Browse for icon" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Path" + +msgctxt "#30205" +msgid "Rule" +msgstr "Rule" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Group" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Path" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Order by" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.en_GB/strings.po b/plugin.library.node.editor/resources/language/resource.language.en_GB/strings.po new file mode 100644 index 0000000000..d735b4fe4c --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.en_GB/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.en_NZ/strings.po b/plugin.library.node.editor/resources/language/resource.language.en_NZ/strings.po new file mode 100644 index 0000000000..88cdb3df64 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.en_NZ/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (New Zealand) (http://www.transifex.com/projects/p/xbmc-addons/language/en_NZ/)\n" +"Language: en_NZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Manage custom library nodes." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Create and edit custom library nodes." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Add content..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Add path..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Add order..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Add limit..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Add grouping..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Add rule..." + +msgctxt "#30006" +msgid "New node..." +msgstr "New node..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "New parent node..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Reset library nodes to default..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Video Library" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Music Library" + +msgctxt "#30100" +msgid "Delete" +msgstr "Delete" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Edit label" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Edit icon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Browse for icon" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Edit visibility" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Add to main menu" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Browse for value" + +msgctxt "#30200" +msgid "Content" +msgstr "Content" + +msgctxt "#30201" +msgid "Order" +msgstr "Order" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Grouping" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limit" + +msgctxt "#30204" +msgid "Path" +msgstr "Path" + +msgctxt "#30205" +msgid "Rule" +msgstr "Rule" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Set name" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Set visibility condition" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Set order index" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Name of new parent node" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Start with defaults" + +msgctxt "#30305" +msgid "Field" +msgstr "Field" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operator" + +msgctxt "#30307" +msgid "Value" +msgstr "Value" + +msgctxt "#30308" +msgid "Content type" +msgstr "Content type" + +msgctxt "#30309" +msgid "Content" +msgstr "Content" + +msgctxt "#30310" +msgid "Group" +msgstr "Group" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limit" + +msgctxt "#30312" +msgid "Path" +msgstr "Path" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Order by" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direction" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Name of new node" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Unable to copy default library nodes" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Are you sure you want to delete this node?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Are you sure you want to reset all nodes?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Are you sure you want to delete this property?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Are you sure you want to delete this rule?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Order requires a Content parameter" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Edit order" diff --git a/plugin.library.node.editor/resources/language/resource.language.en_US/strings.po b/plugin.library.node.editor/resources/language/resource.language.en_US/strings.po new file mode 100644 index 0000000000..8fd0b458fa --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.en_US/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (US) (http://www.transifex.com/projects/p/xbmc-addons/language/en_US/)\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Manage custom library nodes." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Create and edit custom library nodes." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Add content..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Add path..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Add order..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Add limit..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Add grouping..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Add rule..." + +msgctxt "#30006" +msgid "New node..." +msgstr "New node..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "New parent node..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Reset library nodes to default..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Video Library" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Music Library" + +msgctxt "#30100" +msgid "Delete" +msgstr "Delete" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Edit label" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Edit icon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Browse for icon" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Edit visibility" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Add to main menu" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Browse for value" + +msgctxt "#30200" +msgid "Content" +msgstr "Content" + +msgctxt "#30201" +msgid "Order" +msgstr "Order" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Grouping" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limit" + +msgctxt "#30204" +msgid "Path" +msgstr "Path" + +msgctxt "#30205" +msgid "Rule" +msgstr "Rule" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Set name" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Set visibility condition" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Set order index" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Name of new parent node" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Start with defaults" + +msgctxt "#30305" +msgid "Field" +msgstr "Field" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operator" + +msgctxt "#30307" +msgid "Value" +msgstr "Value" + +msgctxt "#30308" +msgid "Content type" +msgstr "Content type" + +msgctxt "#30309" +msgid "Content" +msgstr "Content" + +msgctxt "#30310" +msgid "Group" +msgstr "Group" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limit" + +msgctxt "#30312" +msgid "Path" +msgstr "Path" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Order by" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direction" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Name of new node" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Unable to copy default library nodes" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Are you sure you want to delete this node?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Are you sure you want to reset all nodes?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Are you sure you want to delete this property?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Are you sure you want to delete this rule?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Order requires a Content parameter" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Edit order" diff --git a/plugin.library.node.editor/resources/language/resource.language.eo/strings.po b/plugin.library.node.editor/resources/language/resource.language.eo/strings.po new file mode 100644 index 0000000000..c8d0d4de8a --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.eo/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-08-04 13:29+0000\n" +"Last-Translator: Christian Gade <gade@kodi.tv>\n" +"Language-Team: Esperanto <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/eo/>\n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.7.2\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "Aldoni enhavon..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Aldoni dosierindikon..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Aldoni ordigon..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Aldoni limon..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Aldoni grupigon..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Aldoni regulon..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Nova nodo..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Nova parenta nodo..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Restaŭri nodojn de biblioteko al defaŭlto..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Mane redakti..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videaĵa biblioteko" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Muzika biblioteko" + +msgctxt "#30100" +msgid "Delete" +msgstr "Forigi" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Editii Label" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Redakti piktogramon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Elekti piktogramon" + +msgctxt "#30104" +msgid "Move node" +msgstr "Movi nodon" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Redakti videblecon" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Aldoni al ĉefmenuo" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Elekti valoron" + +msgctxt "#30200" +msgid "Content" +msgstr "Enhavo" + +msgctxt "#30201" +msgid "Order" +msgstr "Ordo" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Grupigado" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limo" + +msgctxt "#30204" +msgid "Path" +msgstr "Dosierindiko" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regulo" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Agordi nomon" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Nomo de nova parenta nodo" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Komenci kun defaŭltaj" + +msgctxt "#30305" +msgid "Field" +msgstr "Kampo" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operatoro" + +msgctxt "#30307" +msgid "Value" +msgstr "Valoro" + +msgctxt "#30308" +msgid "Content type" +msgstr "Enhava speco" + +msgctxt "#30309" +msgid "Content" +msgstr "Enhavo" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupo" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limo" + +msgctxt "#30312" +msgid "Path" +msgstr "Dosierindiko" + +msgctxt "#30313" +msgid "Icon" +msgstr "Piktogramo" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordigi laŭ" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direkto" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Nomo de nova nodo" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "Atribuo" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Neeblas kopii defaŭltajn nodojn de biblioteko" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Ĉu vi certe volas forigi ĉi tiun nodon?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Ĉu vi certe volas restaŭri ĉiujn nodojn?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Ĉu vi certe volas forigi ĉi tiun atribuon?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Ĉu vi certe volas forigi ĉi tiun regulon?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "Ĉu vi certe volas forigi ĉi tiun komponenton?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "Propra atribuo..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "Neniu elekteblaj opcioj trovitaj" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "Montri singlaĵojn" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "Kromprogramo..." diff --git a/plugin.library.node.editor/resources/language/resource.language.es_AR/strings.po b/plugin.library.node.editor/resources/language/resource.language.es_AR/strings.po new file mode 100644 index 0000000000..bffe5b46c0 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.es_AR/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/xbmc-addons/language/es_AR/)\n" +"Language: es_AR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Colección de Video" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Colección de Música" + +msgctxt "#30100" +msgid "Delete" +msgstr "Eliminar" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Editar etiqueta" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Buscar un icono" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Ruta" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupo" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Ruta" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ícono" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordenar por" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.es_ES/strings.po b/plugin.library.node.editor/resources/language/resource.language.es_ES/strings.po new file mode 100644 index 0000000000..3f0badadde --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.es_ES/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/xbmc-addons/language/es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Vídeoteca" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Biblioteca de Música" + +msgctxt "#30100" +msgid "Delete" +msgstr "Eliminar" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Editar etiqueta" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Buscar por icono" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "Contenido" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Ruta" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Campo" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operador" + +msgctxt "#30307" +msgid "Value" +msgstr "Valor" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "Contenido" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupo" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Ruta" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icono" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordenar por" + +msgctxt "#30315" +msgid "Direction" +msgstr "Dirección" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.es_MX/strings.po b/plugin.library.node.editor/resources/language/resource.language.es_MX/strings.po new file mode 100644 index 0000000000..b5e1cacff6 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.es_MX/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-03-26 06:28+0000\n" +"Last-Translator: Edson Armando <edsonarmando78@outlook.com>\n" +"Language-Team: Spanish (Mexico) <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/es_mx/>\n" +"Language: es_MX\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.5.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "Agregar contenido..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Agregar ruta..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Agregar orden..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Agregar límite..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Agregar agrupación..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Agregar regla..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Nuevo nodo..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Nuevo nodo padre..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Reiniciar nodos de biblioteca a valores por defecto..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "Agregar componente..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Editar manualmente..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Biblioteca Vídeo" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Biblioteca de Música" + +msgctxt "#30100" +msgid "Delete" +msgstr "Eliminar" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Editar etiqueta" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Editar ícono" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Buscar ícono" + +msgctxt "#30104" +msgid "Move node" +msgstr "Mover nodo" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Editar visibilidad" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Agregar al menú principal" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Buscar valor" + +msgctxt "#30200" +msgid "Content" +msgstr "Contenido" + +msgctxt "#30201" +msgid "Order" +msgstr "Orden" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Agrupación" + +msgctxt "#30203" +msgid "Limit" +msgstr "Límite" + +msgctxt "#30204" +msgid "Path" +msgstr "Ruta" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regla" + +msgctxt "#30206" +msgid "Match" +msgstr "Coincidencia" + +msgctxt "#30300" +msgid "Set name" +msgstr "Establecer nombre" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Establecer condición de visibilidad" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Establecer índice de orden" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Nombre del nuevo nodo padre" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Iniciar con valores por defecto" + +msgctxt "#30305" +msgid "Field" +msgstr "Campo" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operador" + +msgctxt "#30307" +msgid "Value" +msgstr "Valor" + +msgctxt "#30308" +msgid "Content type" +msgstr "Tipo de contenido" + +msgctxt "#30309" +msgid "Content" +msgstr "Contenido" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupo" + +msgctxt "#30311" +msgid "Limit" +msgstr "Límite" + +msgctxt "#30312" +msgid "Path" +msgstr "Ruta" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ícono" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordenar por" + +msgctxt "#30315" +msgid "Direction" +msgstr "Dirección" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Nombre del nuevo nodo" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "Obteniendo valores..." + +msgctxt "#30318" +msgid "Property" +msgstr "Propiedad" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "No se pudieron copiar los nodos por defecto" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "¿Estás seguro que quieres borrar este nodo?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "¿Estás seguro que quieres reiniciar todos los nodos?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "¿Estás seguro que quieres borrar esta propiedad?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "El parámetro contenido no se puede borrar mientras el nodo aún tenga un parámetro de orden, límite o regla." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "¿Estás seguro que quieres borrar esta regla?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Orden requiere el parámetro contenido" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "¿Estás seguro que quieres borrar este componente?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "Propiedad personalizada..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "No se encontraron opciones para la selección" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "Obteniendo listado del plugin..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "Enlazar directorio aquí" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "ID de género" + +msgctxt "#30501" +msgid "Country ID" +msgstr "ID de país" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "ID de estudio" + +msgctxt "#30503" +msgid "Director ID" +msgstr "ID de director" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "ID de actor" + +msgctxt "#30505" +msgid "Set ID" +msgstr "ID de set" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "ID de etiqueta" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "ID de serie" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "ID de artista" + +msgctxt "#30509" +msgid "Album ID" +msgstr "ID de álbum" + +msgctxt "#30510" +msgid "Role ID" +msgstr "ID de rol" + +msgctxt "#30511" +msgid "Song ID" +msgstr "ID de canción" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "Solo artistas del álbum" + +msgctxt "#30513" +msgid "Show singles" +msgstr "Mostrar sencillos" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "Plugin..." diff --git a/plugin.library.node.editor/resources/language/resource.language.et_EE/strings.po b/plugin.library.node.editor/resources/language/resource.language.et_EE/strings.po new file mode 100644 index 0000000000..872085bd47 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.et_EE/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-07-19 09:21+0000\n" +"Last-Translator: rimasx <riks_12@hot.ee>\n" +"Language-Team: Estonian <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/et_ee/>\n" +"Language: et_EE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.7.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "Lisa sisu..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Lisa rada..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Lisa järjestus..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Lisa limiit..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Lisa reegel..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Uus sõlm..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Uus ülemsõlm..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Taasta meediakogu sõlmede vaikeväärtused..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "Lisa komponent..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Muuda käsitsi..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videokogu" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Muusikakogu" + +msgctxt "#30100" +msgid "Delete" +msgstr "Kustuta" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Muuda silti" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Muuda ikooni" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Sirvi ikoone" + +msgctxt "#30104" +msgid "Move node" +msgstr "Teisalda sõlm" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Muuda nähtavust" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Lisa peamenüüsse" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Sirvi väärtust" + +msgctxt "#30200" +msgid "Content" +msgstr "Sisu" + +msgctxt "#30201" +msgid "Order" +msgstr "Järjestus" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Rühmitamine" + +msgctxt "#30203" +msgid "Limit" +msgstr "Piirid" + +msgctxt "#30204" +msgid "Path" +msgstr "Rada" + +msgctxt "#30205" +msgid "Rule" +msgstr "Reegel" + +msgctxt "#30206" +msgid "Match" +msgstr "Vaste" + +msgctxt "#30300" +msgid "Set name" +msgstr "Anna nimi" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Määra nähtavuse tingimused" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Sea järjestuse indeks" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Uue ülemsõlme nimi" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Alusta vaikeväärtustega" + +msgctxt "#30305" +msgid "Field" +msgstr "Väli" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operaator" + +msgctxt "#30307" +msgid "Value" +msgstr "Väärtus" + +msgctxt "#30308" +msgid "Content type" +msgstr "Sisu tüüp" + +msgctxt "#30309" +msgid "Content" +msgstr "Sisu" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupp" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Rada" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikoon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Järjesta" + +msgctxt "#30315" +msgid "Direction" +msgstr "Suund" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Uue sõlme nimi" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "Väärtuste toomine..." + +msgctxt "#30318" +msgid "Property" +msgstr "Omadus" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Meediakogu vaikesõlmede kopeerimine nurjus" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Kas soovid selle sõlme kustutada?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Kas soovid kõik sõlmed lähtestada?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Kas soovid selle omaduse kustutada?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Kas soovid selle reegli kustutada?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Järjestus nõuab sisu parameetrit" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "Kas soovid selle komponendi kustutada?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "Kohandatud omadus..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "Ühtegi valikut ei leitud" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "Pistikprogrammide loendite toomine..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "Lingi rada siia" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "Žanri ID" + +msgctxt "#30501" +msgid "Country ID" +msgstr "Riigi ID" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "Stuudio ID" + +msgctxt "#30503" +msgid "Director ID" +msgstr "Lavastaja ID" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "Näitleja ID" + +msgctxt "#30505" +msgid "Set ID" +msgstr "Määra ID" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "Sildi ID" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "Seriaali ID" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "Esitaja ID" + +msgctxt "#30509" +msgid "Album ID" +msgstr "Albumi ID" + +msgctxt "#30510" +msgid "Role ID" +msgstr "Rolli ID" + +msgctxt "#30511" +msgid "Song ID" +msgstr "Loo ID" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "Ainult albumi esitajad" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.eu_ES/strings.po b/plugin.library.node.editor/resources/language/resource.language.eu_ES/strings.po new file mode 100644 index 0000000000..118e139f51 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.eu_ES/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Basque (http://www.transifex.com/projects/p/xbmc-addons/language/eu/)\n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Ezabatu" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Editatu etiketa" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Bidea" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Taldea" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Bidea" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordenatu" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.fa_AF/strings.po b/plugin.library.node.editor/resources/language/resource.language.fa_AF/strings.po new file mode 100644 index 0000000000..51a6c3672c --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.fa_AF/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Persian (http://www.transifex.com/projects/p/xbmc-addons/language/fa/)\n" +"Language: fa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "حذف" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.fa_IR/strings.po b/plugin.library.node.editor/resources/language/resource.language.fa_IR/strings.po new file mode 100644 index 0000000000..54ec7d934d --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.fa_IR/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Persian (Iran) (http://www.transifex.com/projects/p/xbmc-addons/language/fa_IR/)\n" +"Language: fa_IR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "کتابخانه ی ویدیو" + +msgctxt "#30092" +msgid "Music Library" +msgstr "کتابخانه ی موسیقی" + +msgctxt "#30100" +msgid "Delete" +msgstr "حذف" + +msgctxt "#30101" +msgid "Edit label" +msgstr "ویرایش برچسب" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "مسیر" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "گروه" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "مسیر" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.fi_FI/strings.po b/plugin.library.node.editor/resources/language/resource.language.fi_FI/strings.po new file mode 100644 index 0000000000..a051ab36df --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.fi_FI/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Finnish (http://www.transifex.com/projects/p/xbmc-addons/language/fi/)\n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videokirjasto" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Musiikkikirjasto" + +msgctxt "#30100" +msgid "Delete" +msgstr "Poista" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Muuta nimeä" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Hae kuvake" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Polku" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Ryhmä" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Polku" + +msgctxt "#30313" +msgid "Icon" +msgstr "Kuvake" + +msgctxt "#30314" +msgid "Order by" +msgstr "Järjestys" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.fo_FO/strings.po b/plugin.library.node.editor/resources/language/resource.language.fo_FO/strings.po new file mode 100644 index 0000000000..b0e0535a52 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.fo_FO/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Faroese (http://www.transifex.com/projects/p/xbmc-addons/language/fo/)\n" +"Language: fo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Strika" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Broyt heiti" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Stíggi" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Bólkur" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Stíggi" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Skipa eftir" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.fr_CA/strings.po b/plugin.library.node.editor/resources/language/resource.language.fr_CA/strings.po new file mode 100644 index 0000000000..d4d8a6ecc0 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.fr_CA/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: French (Canada) (http://www.transifex.com/projects/p/xbmc-addons/language/fr_CA/)\n" +"Language: fr_CA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Gérer des nœuds de médiathèque personnalisés." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Créer et modifier des nœuds de médiathèque personnalisés." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Ajouter du contenu..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Ajouter un chemin..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Ajouter un ordre..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Ajouter une limite..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Ajouter un regroupement..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Ajouter une règle..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Nouveau nœud..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Nouveau nœud parent..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Réinitialiser les nœuds de médiathèque à leurs valeurs par défaut..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Vidéothèque" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Audiothèque" + +msgctxt "#30100" +msgid "Delete" +msgstr "Supprimer" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Modifier l'étiquette" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Modifier l'icône" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Rechercher des icônes" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Modifier la visibilité" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Ajouter au menu principal" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Rechercher une valeur" + +msgctxt "#30200" +msgid "Content" +msgstr "Contenu" + +msgctxt "#30201" +msgid "Order" +msgstr "Ordre" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Regroupement" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limite" + +msgctxt "#30204" +msgid "Path" +msgstr "Chemin" + +msgctxt "#30205" +msgid "Rule" +msgstr "Règle" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Définir un nom" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Définir une condition de visibilité" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Définir l'index d'ordre" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Nom du nouveau nœud parent" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Démarrer avec les valeurs par défaut" + +msgctxt "#30305" +msgid "Field" +msgstr "Champ" + +msgctxt "#30306" +msgid "Operator" +msgstr "Opérateur" + +msgctxt "#30307" +msgid "Value" +msgstr "Valeur" + +msgctxt "#30308" +msgid "Content type" +msgstr "Type de contenu" + +msgctxt "#30309" +msgid "Content" +msgstr "Contenu" + +msgctxt "#30310" +msgid "Group" +msgstr "Groupe" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limite" + +msgctxt "#30312" +msgid "Path" +msgstr "Chemin" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icône" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ranger par" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direction" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Nom du nouveau nœud" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Impossible de copier les nœuds de médiathèque par défaut" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Voulez-vous vraiment supprimer ce nœud?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Voulez-vous vraiment réinitialiser tous les nœuds?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Voulez-vous vraiment supprimer cette propriété?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Le paramètre de contenu ne peut pas être supprimé tant que le nœud à une paramètre d'Ordre, de Limite ou de Règle." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Voulez-vous vraiment supprimer cette règle?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "L'Ordre exige un paramètre de Contenu" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Modifier l'ordre" diff --git a/plugin.library.node.editor/resources/language/resource.language.fr_FR/strings.po b/plugin.library.node.editor/resources/language/resource.language.fr_FR/strings.po new file mode 100644 index 0000000000..a4ebd38cb4 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.fr_FR/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: French (http://www.transifex.com/projects/p/xbmc-addons/language/fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Gére des nœuds de médiathèque personnalisés." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Crée et modifie des nœuds de médiathèque personnalisés." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Ajouter du contenu…" + +msgctxt "#30001" +msgid "Add path..." +msgstr "Ajouter un chemin…" + +msgctxt "#30002" +msgid "Add order..." +msgstr "Ajouter un ordre…" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Ajouter une limite…" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Ajouter un regroupement…" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Ajouter une règle…" + +msgctxt "#30006" +msgid "New node..." +msgstr "Nouveau nœud…" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Nouveau nœud parent…" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Réinitialiser les nœuds de médiathèque aux valeurs par défaut…" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Vidéothèque" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Audiothèque" + +msgctxt "#30100" +msgid "Delete" +msgstr "Supprimer" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Éditer le nom" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Éditer l'icône" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Rechercher des icônes" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Éditer la visibilité" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Ajouter au menu principal" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Chercher une valeur" + +msgctxt "#30200" +msgid "Content" +msgstr "Contenu" + +msgctxt "#30201" +msgid "Order" +msgstr "Ordre" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Regroupement" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limite" + +msgctxt "#30204" +msgid "Path" +msgstr "Chemin" + +msgctxt "#30205" +msgid "Rule" +msgstr "Règle" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Définir un nom" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Définir une condition de visibilité" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Définir un index d'ordre" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Nom du nouveau nœud parent" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Commencer avec les réglages par défaut" + +msgctxt "#30305" +msgid "Field" +msgstr "Champ" + +msgctxt "#30306" +msgid "Operator" +msgstr "Opérateur" + +msgctxt "#30307" +msgid "Value" +msgstr "Valeur" + +msgctxt "#30308" +msgid "Content type" +msgstr "Type de contenu" + +msgctxt "#30309" +msgid "Content" +msgstr "Contenu" + +msgctxt "#30310" +msgid "Group" +msgstr "Groupe" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limite" + +msgctxt "#30312" +msgid "Path" +msgstr "Chemin" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icône" + +msgctxt "#30314" +msgid "Order by" +msgstr "Trier par" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direction" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Nom du nouveau nœud" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Impossible de copier les nœuds de médiathèque par défaut" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Faut-il vraiment supprimer ce nœud ?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Faut-il vraiment réinitialiser tous les nœuds ?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Faut-il vraiment supprimer cette propriété ?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Le paramètre de contenu ne peut pas être supprimé tant que le nœud possède un paramètre d'ordre, de limite ou de règle." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Faut-il vraiment supprimer cette règle ?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "L'ordre requiert un paramètre de Contenu" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Éditer l'ordre" diff --git a/plugin.library.node.editor/resources/language/resource.language.gl_ES/strings.po b/plugin.library.node.editor/resources/language/resource.language.gl_ES/strings.po new file mode 100644 index 0000000000..78bf1bcc5f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.gl_ES/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Galician (http://www.transifex.com/projects/p/xbmc-addons/language/gl/)\n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +msgctxt "#30000" +msgid "Add content..." +msgstr "Engadir contido..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Engadir ruta..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Engadir orde..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Engadir limite..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Engadir regra..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Novo nodo..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Novo nodo pai..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Biblioteca de Vídeo" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Biblioteca de Música" + +msgctxt "#30100" +msgid "Delete" +msgstr "Eliminar" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Editar etiqueta" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Editar icona" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Buscar icona" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Editar visibilidade" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Engadir ó menú principal" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Buscar un valor" + +msgctxt "#30200" +msgid "Content" +msgstr "Contido" + +msgctxt "#30201" +msgid "Order" +msgstr "Orde" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "Límite" + +msgctxt "#30204" +msgid "Path" +msgstr "Ruta" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regra" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Estabelecer nome" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Estabelecer condición de visibilidade" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Campo" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operador" + +msgctxt "#30307" +msgid "Value" +msgstr "Valor" + +msgctxt "#30308" +msgid "Content type" +msgstr "Tipo de contido" + +msgctxt "#30309" +msgid "Content" +msgstr "Contido" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupo" + +msgctxt "#30311" +msgid "Limit" +msgstr "Límite" + +msgctxt "#30312" +msgid "Path" +msgstr "Ruta" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icona" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordear por" + +msgctxt "#30315" +msgid "Direction" +msgstr "Dirección" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Nome do novo nodo" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Tes a certeza de querer eliminar este nodo?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Tes a certeza de querer restabelecer todos os nodos?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Tes a certeza de querer eliminar esta propiedade?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Tes a certeza de querer eliminar esta regra?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Editar orde" diff --git a/plugin.library.node.editor/resources/language/resource.language.he_IL/strings.po b/plugin.library.node.editor/resources/language/resource.language.he_IL/strings.po new file mode 100644 index 0000000000..b699115db9 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.he_IL/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-07-19 09:21+0000\n" +"Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n" +"Language-Team: Hebrew (Israel) <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/he_il/>\n" +"Language: he_IL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Weblate 4.7.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +msgctxt "#30000" +msgid "Add content..." +msgstr "הוספת תוכן ..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "הוספת נתיב ..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "הוספת סדר…" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "הוספת גבול ..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "הוספת הקבצה ..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "הוספת כלל ..." + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "ספריית וידאו" + +msgctxt "#30092" +msgid "Music Library" +msgstr "ספריית מוזיקה" + +msgctxt "#30100" +msgid "Delete" +msgstr "מחיקה" + +msgctxt "#30101" +msgid "Edit label" +msgstr "עריכת תווית" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "עריכת סמל" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "בחירת סמל" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "הוספה לתפריט הראשי" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "איתור ערך" + +msgctxt "#30200" +msgid "Content" +msgstr "תוכן" + +msgctxt "#30201" +msgid "Order" +msgstr "סדר" + +msgctxt "#30202" +msgid "Grouping" +msgstr "קיבוץ" + +msgctxt "#30203" +msgid "Limit" +msgstr "מגבלה" + +msgctxt "#30204" +msgid "Path" +msgstr "נתיב" + +msgctxt "#30205" +msgid "Rule" +msgstr "כלל" + +msgctxt "#30206" +msgid "Match" +msgstr "התאמה" + +msgctxt "#30300" +msgid "Set name" +msgstr "הגדרת שם" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "הגדרת תנאי חשיפה" + +msgctxt "#30302" +msgid "Set order index" +msgstr "הגדרת מפתח סידור" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "התחלה עם ברירת מחדל" + +msgctxt "#30305" +msgid "Field" +msgstr "שדה" + +msgctxt "#30306" +msgid "Operator" +msgstr "אופרטור" + +msgctxt "#30307" +msgid "Value" +msgstr "ערך" + +msgctxt "#30308" +msgid "Content type" +msgstr "סוג תוכן" + +msgctxt "#30309" +msgid "Content" +msgstr "תוכן" + +msgctxt "#30310" +msgid "Group" +msgstr "קבוצה" + +msgctxt "#30311" +msgid "Limit" +msgstr "מגבלה" + +msgctxt "#30312" +msgid "Path" +msgstr "נתיב" + +msgctxt "#30313" +msgid "Icon" +msgstr "אייקון" + +msgctxt "#30314" +msgid "Order by" +msgstr "סדר לפי" + +msgctxt "#30315" +msgid "Direction" +msgstr "כיוון" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "האם אתה בטוח שאתה רוצה למחוק את הנכס הזה?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "האם אתה בטוח שאתה רוצה למחוק את הכלל הזה?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "הצו מחייב פרמטר תוכן" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.hi_IN/strings.po b/plugin.library.node.editor/resources/language/resource.language.hi_IN/strings.po new file mode 100644 index 0000000000..6209c32a91 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.hi_IN/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Hindi (Devanagiri) (http://www.transifex.com/projects/p/xbmc-addons/language/hi/)\n" +"Language: hi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "मिटाना" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "पथ" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "पथ" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.hr_HR/strings.po b/plugin.library.node.editor/resources/language/resource.language.hr_HR/strings.po new file mode 100644 index 0000000000..44c720f7b7 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.hr_HR/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Croatian (http://www.transifex.com/projects/p/xbmc-addons/language/hr/)\n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videoteka" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Fonoteka" + +msgctxt "#30100" +msgid "Delete" +msgstr "Obriši" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Uredi oznaku" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Potraži ikonu" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Putanja" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Polje" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operator" + +msgctxt "#30307" +msgid "Value" +msgstr "Vrijednost" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupa" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Putanja" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikona" + +msgctxt "#30314" +msgid "Order by" +msgstr "Razvrstaj po" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.hu_HU/strings.po b/plugin.library.node.editor/resources/language/resource.language.hu_HU/strings.po new file mode 100644 index 0000000000..8601f04d09 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.hu_HU/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Hungarian (http://www.transifex.com/projects/p/xbmc-addons/language/hu/)\n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videó könytár" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Zene könyvtár" + +msgctxt "#30100" +msgid "Delete" +msgstr "Törlés" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Cím szerkesztése" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Ikon keresése" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "Tartalom" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Útvonal" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Mező" + +msgctxt "#30306" +msgid "Operator" +msgstr "Üzemeltető" + +msgctxt "#30307" +msgid "Value" +msgstr "Érték" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "Tartalom" + +msgctxt "#30310" +msgid "Group" +msgstr "Csoport" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Útvonal" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Rendezés" + +msgctxt "#30315" +msgid "Direction" +msgstr "Irány" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.hy_AM/strings.po b/plugin.library.node.editor/resources/language/resource.language.hy_AM/strings.po new file mode 100644 index 0000000000..51ec89bbed --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.hy_AM/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Armenian (http://www.transifex.com/projects/p/xbmc-addons/language/hy/)\n" +"Language: hy\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Ջնջել" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Ուղի" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Ուղի" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.id_ID/strings.po b/plugin.library.node.editor/resources/language/resource.language.id_ID/strings.po new file mode 100644 index 0000000000..9b394c3def --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.id_ID/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-07-19 09:21+0000\n" +"Last-Translator: liimee <alt3753.7@gmail.com>\n" +"Language-Team: Indonesian <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/id_id/>\n" +"Language: id_ID\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.7.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Hapus" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Sunting label" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Sunting ikon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Cari ikon" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Path" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Atur nama" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grup" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Path" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Order berdasar" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "Apakah Anda ingin menghapus komponen ini?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.is_IS/strings.po b/plugin.library.node.editor/resources/language/resource.language.is_IS/strings.po new file mode 100644 index 0000000000..4b129e1227 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.is_IS/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Icelandic (http://www.transifex.com/projects/p/xbmc-addons/language/is/)\n" +"Language: is\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Myndbandasafn" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Tónlistarsafn" + +msgctxt "#30100" +msgid "Delete" +msgstr "Eyða" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Breyta nafni" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Fletta eftir táknmynd" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Slóð" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "Gildi" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Hópur" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Slóð" + +msgctxt "#30313" +msgid "Icon" +msgstr "Tákn" + +msgctxt "#30314" +msgid "Order by" +msgstr "Raða eftir" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.it_IT/strings.po b/plugin.library.node.editor/resources/language/resource.language.it_IT/strings.po new file mode 100644 index 0000000000..907121c289 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.it_IT/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Italian (http://www.transifex.com/projects/p/xbmc-addons/language/it/)\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Gestisci nodi libreria personalizzati" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Crea e modifica nodi libreria personalizzati." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Aggiungi contenuto..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Aggiungi percorso..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Aggiungi ordine..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Aggiungi limite..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Aggiungi raggruppamento..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Aggiungi regola..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Nuovo nodo..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Nuovo nodo superiore..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Ripristina nodi libreria a predefinito..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Libreria Video" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Libreria Musicale" + +msgctxt "#30100" +msgid "Delete" +msgstr "Elimina" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Modifica etichetta" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Modifica icona" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Cerca icona" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Modifica visibilita'" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Aggiungi a menu' principale" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Cerca valore" + +msgctxt "#30200" +msgid "Content" +msgstr "Contenuto" + +msgctxt "#30201" +msgid "Order" +msgstr "Ordine" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Raggruppamento" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limite" + +msgctxt "#30204" +msgid "Path" +msgstr "Percorso" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regola" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Imposta nome" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Imposta condizione visibilita'" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Imposta ordine indice" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Nome del nuovo nodo superiore" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Parti con predefiniti" + +msgctxt "#30305" +msgid "Field" +msgstr "Campo" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operatore" + +msgctxt "#30307" +msgid "Value" +msgstr "Valore" + +msgctxt "#30308" +msgid "Content type" +msgstr "Tipo contenuto" + +msgctxt "#30309" +msgid "Content" +msgstr "Contenuto" + +msgctxt "#30310" +msgid "Group" +msgstr "Gruppo" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limite" + +msgctxt "#30312" +msgid "Path" +msgstr "Percorso" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icona" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordina per" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direzione" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Nome del nuovo nodo" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Impossibile copiare i nodi libreria predefiniti" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Sei sicuro di voler eliminare questo nodo?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Sei sicuro di voler resettare tutti i nodi?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Sei sicuro di voler cancellare questa proprieta'?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Il parametro del contenuto non può essere cancellato finche' questo nodo ha ancora un parametro Ordine, Limite o Regola." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Sei sicuro di voler cancellare questa regola?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "L'Ordine richiede un parametro Contenuto" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Modifica ordine" diff --git a/plugin.library.node.editor/resources/language/resource.language.ja_JP/strings.po b/plugin.library.node.editor/resources/language/resource.language.ja_JP/strings.po new file mode 100644 index 0000000000..6e6dc1ee48 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ja_JP/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Japanese (http://www.transifex.com/projects/p/xbmc-addons/language/ja/)\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "ビデオライブラリ" + +msgctxt "#30092" +msgid "Music Library" +msgstr "ミュージックライブラリ" + +msgctxt "#30100" +msgid "Delete" +msgstr "削除" + +msgctxt "#30101" +msgid "Edit label" +msgstr "ラベル編集" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "アイコンをブラウズ" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "パス" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "グループ" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "パス" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "並べ替え" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.ko_KR/strings.po b/plugin.library.node.editor/resources/language/resource.language.ko_KR/strings.po new file mode 100644 index 0000000000..b4649b863f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ko_KR/strings.po @@ -0,0 +1,326 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-08-20 04:51+0000\n" +"Last-Translator: Minho Park <parkmino@gmail.com>\n" +"Language-Team: Korean <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/ko_kr/>\n" +"Language: ko_KR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.7.2\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "사용자 정의 라이브러리 노드를 관리합니다." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "사용자 라이브러리 노드를 생성하고 편집합니다." + +msgctxt "#30000" +msgid "Add content..." +msgstr "콘텐츠 추가..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "경로 추가..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "순서 추가..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "한도 추가..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "그룹화 추가..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "규칙 추가..." + +msgctxt "#30006" +msgid "New node..." +msgstr "새 노드..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "새 상위 노드..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "라이브러리 노드를 기본값으로 초기화..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "구성요소 추가..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "수동으로 편집..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "비디오 라이브러리" + +msgctxt "#30092" +msgid "Music Library" +msgstr "음악 라이브러리" + +msgctxt "#30100" +msgid "Delete" +msgstr "삭제" + +msgctxt "#30101" +msgid "Edit label" +msgstr "레이블 수정" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "아이콘 편집" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "아이콘 찾기" + +msgctxt "#30104" +msgid "Move node" +msgstr "노드 이동" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "가시성 수정" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "메인 메뉴에 추가" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "값 찾아보기" + +msgctxt "#30200" +msgid "Content" +msgstr "콘텐츠" + +msgctxt "#30201" +msgid "Order" +msgstr "순서" + +msgctxt "#30202" +msgid "Grouping" +msgstr "그룹화" + +msgctxt "#30203" +msgid "Limit" +msgstr "한계" + +msgctxt "#30204" +msgid "Path" +msgstr "경로" + +msgctxt "#30205" +msgid "Rule" +msgstr "규칙" + +msgctxt "#30206" +msgid "Match" +msgstr "일치" + +msgctxt "#30300" +msgid "Set name" +msgstr "이름 설정" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "가시성 조건 설정" + +msgctxt "#30302" +msgid "Set order index" +msgstr "순서 인덱스 설정" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "새 상위 노드 이름" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "기본값으로 시작" + +msgctxt "#30305" +msgid "Field" +msgstr "항목" + +msgctxt "#30306" +msgid "Operator" +msgstr "운영자" + +msgctxt "#30307" +msgid "Value" +msgstr "값" + +msgctxt "#30308" +msgid "Content type" +msgstr "콘텐츠 종류" + +msgctxt "#30309" +msgid "Content" +msgstr "콘텐츠" + +msgctxt "#30310" +msgid "Group" +msgstr "그룹" + +msgctxt "#30311" +msgid "Limit" +msgstr "한계" + +msgctxt "#30312" +msgid "Path" +msgstr "경로" + +msgctxt "#30313" +msgid "Icon" +msgstr "아이콘" + +msgctxt "#30314" +msgid "Order by" +msgstr "정렬순서" + +msgctxt "#30315" +msgid "Direction" +msgstr "방향" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "새 노느 이름" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "값 검색 중..." + +msgctxt "#30318" +msgid "Property" +msgstr "속성" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "기본 라이브러리 노드를 복사할 수 없습니다" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "이 노드를 삭제하겠습니까?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "모든 노드를 초기화 하겠습니까?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "이 속성을 삭제할까요?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "이 노드에 아직 순서, 한계 또는 통제 매개변수가 있는 동안에는 컨텐트 매개변수를 삭제할 수 없습니다." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "이 규칙을 삭제하겠습니까?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "순서에는 컨텐트 매개변수가 필요합니다" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "이 구성 요소를 삭제할까요?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "사용자 정의 속성..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "선택할 수 있는 옵션이 없습니다" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "플러그인 목록을 가져오는 중..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "연결 경로" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "장르 ID" + +msgctxt "#30501" +msgid "Country ID" +msgstr "국가 ID" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "스튜디오 ID" + +msgctxt "#30503" +msgid "Director ID" +msgstr "감독 ID" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "연기자 ID" + +msgctxt "#30505" +msgid "Set ID" +msgstr "세트 ID" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "태그 ID" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "TV 쇼 ID" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "아티스트 ID" + +msgctxt "#30509" +msgid "Album ID" +msgstr "앨범 ID" + +msgctxt "#30510" +msgid "Role ID" +msgstr "역할 ID" + +msgctxt "#30511" +msgid "Song ID" +msgstr "노래 ID" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "앨범 아티스트만" + +msgctxt "#30513" +msgid "Show singles" +msgstr "싱글 보기" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "플러그인..." + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "순서 편집" diff --git a/plugin.library.node.editor/resources/language/resource.language.lt_LT/strings.po b/plugin.library.node.editor/resources/language/resource.language.lt_LT/strings.po new file mode 100644 index 0000000000..e1adcd753b --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.lt_LT/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Lithuanian (http://www.transifex.com/projects/p/xbmc-addons/language/lt/)\n" +"Language: lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Tvarkykite savus bibliotekos mazgus." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Sukurkite ir keiskite savus bibliotekos mazgus." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Pridėti turinį..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Pridėti kelią..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Pridėti rikiavimą..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Pridėti apribojimą..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "ridėti grupavimą..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Pridėti taisyklę..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Naujas mazgą..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Naujas pagrindinis mazgas..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Atstatyti numatytuosius bibliotekos mazgus..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Vaizdo biblioteka" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Muzikos biblioteka" + +msgctxt "#30100" +msgid "Delete" +msgstr "Šalinti" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Keisti etiketę" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Keisti piktogramą" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Parinkti piktogramą" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Keisti matomumą" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Pridėti į pagrindinį meniu" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Parinkti reikšmę" + +msgctxt "#30200" +msgid "Content" +msgstr "Turinys" + +msgctxt "#30201" +msgid "Order" +msgstr "Rikiavimas" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Grupavimas" + +msgctxt "#30203" +msgid "Limit" +msgstr "Apribojimas" + +msgctxt "#30204" +msgid "Path" +msgstr "Kelias" + +msgctxt "#30205" +msgid "Rule" +msgstr "Taisyklė" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Nustatyti vardą" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Nustatyti matomumo sąlygą" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Nustatyti rikiavimo indeksą" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Naujo pagrindinio mazgo vardas" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Pradėti su numatytomis reikšmėmis" + +msgctxt "#30305" +msgid "Field" +msgstr "Laukas" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operatorius" + +msgctxt "#30307" +msgid "Value" +msgstr "Reikšmė" + +msgctxt "#30308" +msgid "Content type" +msgstr "Turinio tipas" + +msgctxt "#30309" +msgid "Content" +msgstr "Turinys" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupė" + +msgctxt "#30311" +msgid "Limit" +msgstr "Apribojimas" + +msgctxt "#30312" +msgid "Path" +msgstr "Kelias" + +msgctxt "#30313" +msgid "Icon" +msgstr "Piktograma" + +msgctxt "#30314" +msgid "Order by" +msgstr "Rikiuoti pagal" + +msgctxt "#30315" +msgid "Direction" +msgstr "Kryptis" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Naujo mazgo vardas" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Neįmanoma nukopijuoti numatytųjų bibliotekos mazgų" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Ar tikrai norite pašalinti šį mazgą?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Ar tikrai norite atstatyti visus mazgus?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Ar tikrai norite pašalinti šią savybę?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Turinio parametras negali būti pašalintas, kol mazgas vis dar turi rikiavimo, apribojimo ar taisyklės parametrą." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Ar tikrai norite pašalinti šią taisyklę?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Rikiavimui reikalingas turinio parametras" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Keisti rikiavimą" diff --git a/plugin.library.node.editor/resources/language/resource.language.lv_LV/strings.po b/plugin.library.node.editor/resources/language/resource.language.lv_LV/strings.po new file mode 100644 index 0000000000..518bccc936 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.lv_LV/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Latvian (http://www.transifex.com/projects/p/xbmc-addons/language/lv/)\n" +"Language: lv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Dzēst" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Labot etiķeti" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Pārlūkot ikonu" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Ceļš" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupa" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Ceļš" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Sakārto pēc" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.mk_MK/strings.po b/plugin.library.node.editor/resources/language/resource.language.mk_MK/strings.po new file mode 100644 index 0000000000..6f070ac8a0 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.mk_MK/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Macedonian (http://www.transifex.com/projects/p/xbmc-addons/language/mk/)\n" +"Language: mk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Избриши" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Уреди натпис" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Барај икона" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Патека" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Група" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Патека" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Подреди по" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.ml_IN/strings.po b/plugin.library.node.editor/resources/language/resource.language.ml_IN/strings.po new file mode 100644 index 0000000000..84cf0bdd8e --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ml_IN/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Malayalam (http://www.transifex.com/projects/p/xbmc-addons/language/ml/)\n" +"Language: ml\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "കളയുക" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.mn_MN/strings.po b/plugin.library.node.editor/resources/language/resource.language.mn_MN/strings.po new file mode 100644 index 0000000000..5fde5ff1b3 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.mn_MN/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Mongolian (Mongolia) (http://www.transifex.com/projects/p/xbmc-addons/language/mn_MN/)\n" +"Language: mn_MN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Устгах" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Групп" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.ms_MY/strings.po b/plugin.library.node.editor/resources/language/resource.language.ms_MY/strings.po new file mode 100644 index 0000000000..79523b4cc6 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ms_MY/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Malay (http://www.transifex.com/projects/p/xbmc-addons/language/ms/)\n" +"Language: ms\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Pustaka Video" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Pustaka Muzik" + +msgctxt "#30100" +msgid "Delete" +msgstr "Hapus" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Sunting label" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Sunting ikon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "Kandungan" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Laluan" + +msgctxt "#30205" +msgid "Rule" +msgstr "Peraturan" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "Kandungan" + +msgctxt "#30310" +msgid "Group" +msgstr "Kumpulan" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Laluan" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikon" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "Arah" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.mt_MT/strings.po b/plugin.library.node.editor/resources/language/resource.language.mt_MT/strings.po new file mode 100644 index 0000000000..f25d643efb --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.mt_MT/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Maltese (http://www.transifex.com/projects/p/xbmc-addons/language/mt/)\n" +"Language: mt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Ħassar" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Biddel l-Label" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Issortja" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.my_MM/strings.po b/plugin.library.node.editor/resources/language/resource.language.my_MM/strings.po new file mode 100644 index 0000000000..b88aa914d6 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.my_MM/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-05-07 12:40+0000\n" +"Last-Translator: Christian Gade <gade@kodi.tv>\n" +"Language-Team: Burmese <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/my_mm/>\n" +"Language: my_MM\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.6.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "ဖျက်ရန်" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "လမ်းကြောင်း" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "အုပ်စု" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "လမ်းကြောင်း" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.nb_NO/strings.po b/plugin.library.node.editor/resources/language/resource.language.nb_NO/strings.po new file mode 100644 index 0000000000..927b8c037f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.nb_NO/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Norwegian (http://www.transifex.com/projects/p/xbmc-addons/language/no/)\n" +"Language: no\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videobibliotek" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Musikk Bibliotek" + +msgctxt "#30100" +msgid "Delete" +msgstr "Slett" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Rediger plateselskap" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Bla etter ikon" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Sti" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "Verdi" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Gruppe" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Sti" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Sorter etter" + +msgctxt "#30315" +msgid "Direction" +msgstr "Retning" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.nl_NL/strings.po b/plugin.library.node.editor/resources/language/resource.language.nl_NL/strings.po new file mode 100644 index 0000000000..1e9b081be8 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.nl_NL/strings.po @@ -0,0 +1,326 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-08-04 13:29+0000\n" +"Last-Translator: Christian Gade <gade@kodi.tv>\n" +"Language-Team: Dutch <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/nl_nl/>\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.7.2\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Beheer aangepaste bibliotheek nodes." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Creëer en wijzig aangepaste bibliotheek nodes." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Toevoegen inhoud..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Toevoegen pad..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Toevoegen volgorde..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "toevoegen limiet..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "toevoegen groepering..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Toevoegen regel..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Nieuwe node..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Nieuw hoofd node..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Herstel bibliotheek nodes naar standaardwaarden..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videobibliotheek" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Muziekbibliotheek" + +msgctxt "#30100" +msgid "Delete" +msgstr "Verwijderen" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Label aanpassen" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Wijzig icoon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Zoek naar icoon" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Aanpassen zichtbaarheid" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Toevoegen aan het startmenu" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Zoek naar waarde" + +msgctxt "#30200" +msgid "Content" +msgstr "Inhoud" + +msgctxt "#30201" +msgid "Order" +msgstr "Volgorde" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Groepering" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limiet" + +msgctxt "#30204" +msgid "Path" +msgstr "Locatie" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regel" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Instellen naam" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Instellen zichtbaarheid conditie" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Instellen volgorde index" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Naam van nieuwe hoofd node" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Start met standaardwaarden" + +msgctxt "#30305" +msgid "Field" +msgstr "Veld" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operator" + +msgctxt "#30307" +msgid "Value" +msgstr "Waarde" + +msgctxt "#30308" +msgid "Content type" +msgstr "Inhoud type" + +msgctxt "#30309" +msgid "Content" +msgstr "Inhoud" + +msgctxt "#30310" +msgid "Group" +msgstr "Groep" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limiet" + +msgctxt "#30312" +msgid "Path" +msgstr "Locatie" + +msgctxt "#30313" +msgid "Icon" +msgstr "Icoon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Sorteren op" + +msgctxt "#30315" +msgid "Direction" +msgstr "Richting" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Naam van nieuwe node" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Niet gelukt standaard bibliotheek nodes te kopiëren" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Weet u zeker dat u deze node wilt verwijderen?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Wat u zeker dat u alle nodes wilt herstellen?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Wet u zeker dat u eigendom wilt verwijderen?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Inhoudsparameter kan niet worden verwijder omdat deze node nog steeds een volgorde, limiet of regelparameter heeft." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Wet u zeker dat u deze regel wilt verwijderen?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Volgorde vereist een inhoudsparameter" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Wijzig volgorde" diff --git a/plugin.library.node.editor/resources/language/resource.language.pl_PL/strings.po b/plugin.library.node.editor/resources/language/resource.language.pl_PL/strings.po new file mode 100644 index 0000000000..3658d1f4a1 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.pl_PL/strings.po @@ -0,0 +1,326 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-08-04 13:29+0000\n" +"Last-Translator: Marek Adamski <fevbew@wp.pl>\n" +"Language-Team: Polish <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/pl_pl/>\n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.7.2\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Zarządzanie niestandardowymi węzłami biblioteki." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Tworzenie i edytowanie niestandardowych węzłów biblioteki." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Dodaj zawartość..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Dodaj ścieżkę..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Dodaj kolejność..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Dodaj ograniczenie..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Dodaj grupowanie..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Dodaj regułę..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Dodaj węzeł..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Nowy węzeł nadrzędny..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Przywróć domyśle węzły biblioteki..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "Dodaj komponent..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Edytuj ręcznie..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Biblioteka wideo" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Biblioteka muzyki" + +msgctxt "#30100" +msgid "Delete" +msgstr "Usuń" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Edytuj etykietę" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Edytuj ikonę" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Wybierz ikonę" + +msgctxt "#30104" +msgid "Move node" +msgstr "Przenieś węzeł" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Edytuj widoczność" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Dodaj do menu startowego" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Wybierz wartość" + +msgctxt "#30200" +msgid "Content" +msgstr "Zawartość" + +msgctxt "#30201" +msgid "Order" +msgstr "Kolejność" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Grupowanie" + +msgctxt "#30203" +msgid "Limit" +msgstr "Ograniczenie" + +msgctxt "#30204" +msgid "Path" +msgstr "Ścieżka" + +msgctxt "#30205" +msgid "Rule" +msgstr "Reguła" + +msgctxt "#30206" +msgid "Match" +msgstr "Dopasowanie" + +msgctxt "#30300" +msgid "Set name" +msgstr "Ustaw nazwę" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Ustaw warunek widoczności" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Ustaw indeks kolejności" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Nazwa nowego węzła nadrzędnego" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Rozpocznij z domyślnymi" + +msgctxt "#30305" +msgid "Field" +msgstr "Pole" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operator" + +msgctxt "#30307" +msgid "Value" +msgstr "Wartość" + +msgctxt "#30308" +msgid "Content type" +msgstr "Typ zawartości" + +msgctxt "#30309" +msgid "Content" +msgstr "Zawartość" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupa" + +msgctxt "#30311" +msgid "Limit" +msgstr "Ograniczenie" + +msgctxt "#30312" +msgid "Path" +msgstr "Ścieżka" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikona" + +msgctxt "#30314" +msgid "Order by" +msgstr "Sortuj wg" + +msgctxt "#30315" +msgid "Direction" +msgstr "Kierunek" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Nazwa nowego węzła" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "Pobieranie wartości..." + +msgctxt "#30318" +msgid "Property" +msgstr "Własność" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Nieudane kopiowanie domyślnych węzłów biblioteki" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Jesteś pewien, że chcesz usunąć ten węzeł?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Jesteś pewien, że chcesz wyczyścić wszystkie węzły?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Jesteś pewien, że chcesz usunąć tę właściwość?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Parametr zawartości nie może zostać usunięty, gdy ten węzeł ciągle ma parametr kolejności, ograniczenia lub regułę." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Jesteś pewien, że chcesz usunąć tę regułę?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Kolejność wymaga parametru zawartości" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "Czy na pewno chcesz usunąć ten komponent?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "Własność niestandardowa..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "Nie znaleziono opcji do wyboru" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "Pobieranie listy wtyczek..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "Ścieżka łącza tutaj" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "Identyfikator gatunku" + +msgctxt "#30501" +msgid "Country ID" +msgstr "Identyfikator kraju" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "Identyfikator studia" + +msgctxt "#30503" +msgid "Director ID" +msgstr "Identyfikator reżysera" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "Identyfikator aktora" + +msgctxt "#30505" +msgid "Set ID" +msgstr "Identyfikator zestawu" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "Identyfikator znacznika" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "Identyfikator serialu" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "Identyfikator artysty" + +msgctxt "#30509" +msgid "Album ID" +msgstr "Identyfikator albumu" + +msgctxt "#30510" +msgid "Role ID" +msgstr "Identyfikator roli" + +msgctxt "#30511" +msgid "Song ID" +msgstr "Identyfikator utworu" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "Tylko wykonawcy albumów" + +msgctxt "#30513" +msgid "Show singles" +msgstr "Pokazuj single" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "Wtyczka..." + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Edytuj kolejność" diff --git a/plugin.library.node.editor/resources/language/resource.language.pt_BR/strings.po b/plugin.library.node.editor/resources/language/resource.language.pt_BR/strings.po new file mode 100644 index 0000000000..7fc1eba14e --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.pt_BR/strings.po @@ -0,0 +1,326 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-12-08 00:58+0000\n" +"Last-Translator: Fabio <fabioihle+kodi@alunos.utfpr.edu.br>\n" +"Language-Team: Portuguese (Brazil) <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/pt_br/>\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.9.1\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Gerenciar nós customizados da biblioteca." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Crie e edite nós customizados na biblioteca." + +msgctxt "#30000" +msgid "Add content..." +msgstr "Adicionar conteúdo..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Adicionar caminho..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Adicionar ordem..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Adicionar limite..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Adicionar agrupando..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Adicionar regra..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Novo nó..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Novo nó principal..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Resetar os nós da biblioteca para valores padrões..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "Adicionar componente..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Editar manualmente..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Coleção de Vídeo" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Coleção de Músicas" + +msgctxt "#30100" +msgid "Delete" +msgstr "Delete" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Editar etiqueta" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Editar ícone" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Procurar por logo do canal" + +msgctxt "#30104" +msgid "Move node" +msgstr "Mover nó" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Editar visibilidade" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Adicionar ao menu principal" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Procurar pelo valor" + +msgctxt "#30200" +msgid "Content" +msgstr "Conteúdo" + +msgctxt "#30201" +msgid "Order" +msgstr "Ordem" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Agrupando" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limite" + +msgctxt "#30204" +msgid "Path" +msgstr "Caminho" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regra" + +msgctxt "#30206" +msgid "Match" +msgstr "Corresponder" + +msgctxt "#30300" +msgid "Set name" +msgstr "Defina nome" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Defina condição de visibilidade" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Definir ordem de indexação" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Nome do novo nó principal" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Iniciar com valores padrões" + +msgctxt "#30305" +msgid "Field" +msgstr "Campo" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operador" + +msgctxt "#30307" +msgid "Value" +msgstr "Valor" + +msgctxt "#30308" +msgid "Content type" +msgstr "Tipo do Conteúdo" + +msgctxt "#30309" +msgid "Content" +msgstr "Conteúdo" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupo" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limite" + +msgctxt "#30312" +msgid "Path" +msgstr "Caminho" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ícone" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordenar por" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direção" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Nome do novo nó" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "Obtendo valores..." + +msgctxt "#30318" +msgid "Property" +msgstr "Propriedades" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Não foi possível copiar os nós de biblioteca padrão" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Tem certeza que deseja deletar este nó?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Tem certeza que deseja resetar todos os nós?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Tem certeza que deseja deletar esta propriedade?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Parâmetro do conteúdo não pode ser excluído enquanto esse nó ainda tiver um parâmetro de ordem, limite ou regra." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Tem certeza que deseja deletar esta regra?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Ordenar requer um parâmetro de Conteúdo" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "Tem certeza de que deseja excluir este componente?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "Propriedade personalizada..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "Não foram encontradas opções para seleção" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "Obtendo listas de plug-ins..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "Vincular caminho a este" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "ID Gênero" + +msgctxt "#30501" +msgid "Country ID" +msgstr "ID País" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "ID Estúdio" + +msgctxt "#30503" +msgid "Director ID" +msgstr "ID Diretor" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "ID Ator" + +msgctxt "#30505" +msgid "Set ID" +msgstr "ID Set" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "ID Tag" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "ID Série" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "ID Artista" + +msgctxt "#30509" +msgid "Album ID" +msgstr "ID Álbum" + +msgctxt "#30510" +msgid "Role ID" +msgstr "ID Papel" + +msgctxt "#30511" +msgid "Song ID" +msgstr "ID Música" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "Apenas artistas do álbum" + +msgctxt "#30513" +msgid "Show singles" +msgstr "Mostrar singles" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "Plugin..." + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Editar ordem" diff --git a/plugin.library.node.editor/resources/language/resource.language.pt_PT/strings.po b/plugin.library.node.editor/resources/language/resource.language.pt_PT/strings.po new file mode 100644 index 0000000000..7994fe2c8d --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.pt_PT/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Portuguese (http://www.transifex.com/projects/p/xbmc-addons/language/pt/)\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Biblioteca de Vídeo" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Biblioteca de Música" + +msgctxt "#30100" +msgid "Delete" +msgstr "Apagar" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Editar etiqueta" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Procurar ícone" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Caminho" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Campo" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operador" + +msgctxt "#30307" +msgid "Value" +msgstr "Valor" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupo" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Caminho" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ícone" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordenar" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direcção" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.ro_RO/strings.po b/plugin.library.node.editor/resources/language/resource.language.ro_RO/strings.po new file mode 100644 index 0000000000..ed05bd9b12 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ro_RO/strings.po @@ -0,0 +1,325 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Romanian (http://www.transifex.com/projects/p/xbmc-addons/language/ro/)\n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Administrați noduri mediatecă personalizate" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +msgctxt "#30000" +msgid "Add content..." +msgstr "Adăugare conținut..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Adăugare cale..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Adăugare ordonare..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Adăugare limită..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Adăugare grupare..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Adăugare regulă..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Nod nou..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Nod părinte nou..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Mediatecă video" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Mediatecă audio" + +msgctxt "#30100" +msgid "Delete" +msgstr "Șterge" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Modificare etichetă" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Modificare pictogramă" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Răsfoire după pictogramă" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Modificare vizibilitate" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Adaugă la meniul principal" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Răsfoire după valoare" + +msgctxt "#30200" +msgid "Content" +msgstr "Conținut" + +msgctxt "#30201" +msgid "Order" +msgstr "Ordonare" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Grupare" + +msgctxt "#30203" +msgid "Limit" +msgstr "Limită" + +msgctxt "#30204" +msgid "Path" +msgstr "Cale" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regulă" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Definire nume" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Numele noului nod părinte" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Pornește cu cele implicite" + +msgctxt "#30305" +msgid "Field" +msgstr "Câmp" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operator" + +msgctxt "#30307" +msgid "Value" +msgstr "Valoare" + +msgctxt "#30308" +msgid "Content type" +msgstr "Tip conținut" + +msgctxt "#30309" +msgid "Content" +msgstr "Conținut" + +msgctxt "#30310" +msgid "Group" +msgstr "Grup" + +msgctxt "#30311" +msgid "Limit" +msgstr "Limită" + +msgctxt "#30312" +msgid "Path" +msgstr "Cale" + +msgctxt "#30313" +msgid "Icon" +msgstr "Pictogramă" + +msgctxt "#30314" +msgid "Order by" +msgstr "Ordonează după" + +msgctxt "#30315" +msgid "Direction" +msgstr "Direcție" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Numele noului nod" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Sigur doriți să ștergeți această regulă?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" + +#~ msgctxt "#30104" +#~ msgid "Edit order" +#~ msgstr "Modificare ordonare" diff --git a/plugin.library.node.editor/resources/language/resource.language.ru_RU/strings.po b/plugin.library.node.editor/resources/language/resource.language.ru_RU/strings.po new file mode 100644 index 0000000000..276dbf0d87 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ru_RU/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-07-23 14:15+0000\n" +"Last-Translator: vdkbsd <valexgus@gmail.com>\n" +"Language-Team: Russian <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/ru_ru/>\n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.7.2\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Управление узлами пользовательских медиатек." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Создание и редактирование пользовательских узлов медиатеки." + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "Содержимое..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Путь..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Сортировка..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Лимит..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Группирование..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Добавить правило..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Новый узел..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Новый родительский узел..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Сбросить узлы медиатеки на значения по умолчанию..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "Добавить компонент..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Редактировать вручную..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Видеотека" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Музыкальная медиатека" + +msgctxt "#30100" +msgid "Delete" +msgstr "Удалить" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Изменить название" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Изменить значок" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Найти значок" + +msgctxt "#30104" +msgid "Move node" +msgstr "Переместить узел" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Редактировать видимость" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Добавить в главное меню" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Поиск значения" + +msgctxt "#30200" +msgid "Content" +msgstr "Содержимое" + +msgctxt "#30201" +msgid "Order" +msgstr "Сортировка" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Группирование" + +msgctxt "#30203" +msgid "Limit" +msgstr "Лимит" + +msgctxt "#30204" +msgid "Path" +msgstr "Путь" + +msgctxt "#30205" +msgid "Rule" +msgstr "Правило" + +msgctxt "#30206" +msgid "Match" +msgstr "Соответствие" + +msgctxt "#30300" +msgid "Set name" +msgstr "Введите имя" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Введите условие видимости" + +msgctxt "#30302" +msgid "Set order index" +msgstr "Установите индекс сортировки" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "Имя нового родительского узла" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Начните с настроек по умолчанию" + +msgctxt "#30305" +msgid "Field" +msgstr "Поле" + +msgctxt "#30306" +msgid "Operator" +msgstr "Оператор" + +msgctxt "#30307" +msgid "Value" +msgstr "Значение" + +msgctxt "#30308" +msgid "Content type" +msgstr "Тип содержимого" + +msgctxt "#30309" +msgid "Content" +msgstr "Содержимое" + +msgctxt "#30310" +msgid "Group" +msgstr "Группа" + +msgctxt "#30311" +msgid "Limit" +msgstr "Лимит" + +msgctxt "#30312" +msgid "Path" +msgstr "Путь" + +msgctxt "#30313" +msgid "Icon" +msgstr "Иконка" + +msgctxt "#30314" +msgid "Order by" +msgstr "Упорядочить по" + +msgctxt "#30315" +msgid "Direction" +msgstr "Направление" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "Имя нового узла" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "Получение значений..." + +msgctxt "#30318" +msgid "Property" +msgstr "Свойство" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "Невозможно скопировать узлы медиатеки" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "Вы действительно хотите удалить этот узел?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "Вы действительно хотите сбросить все узлы?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "Вы действительно хотите удалить это свойство?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "Параметр содержимого не может быть удален, пока в этом узле установлены значения сортировки, лимита или правила." + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "Вы действительно хотите удалить это правило?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "Необходимо установить значение для содержания" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "Вы действительно хотите удалить этот компонент?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "Пользовательское..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "Не найдено ни одного варианта для выбора" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "Получение списков плагинов..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "Путь по ссылке здесь" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "ID жанра" + +msgctxt "#30501" +msgid "Country ID" +msgstr "ID страны" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "ID студии" + +msgctxt "#30503" +msgid "Director ID" +msgstr "ID режиссёра" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "ID актёра" + +msgctxt "#30505" +msgid "Set ID" +msgstr "ID киноцикла" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "ID метки" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "ID сериала" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "ID исполнителя" + +msgctxt "#30509" +msgid "Album ID" +msgstr "ID альбома" + +msgctxt "#30510" +msgid "Role ID" +msgstr "ID роли" + +msgctxt "#30511" +msgid "Song ID" +msgstr "ID песни" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "Только исполнители альбомов" + +msgctxt "#30513" +msgid "Show singles" +msgstr "Показывать синглы" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "Плагин..." diff --git a/plugin.library.node.editor/resources/language/resource.language.sk_SK/strings.po b/plugin.library.node.editor/resources/language/resource.language.sk_SK/strings.po new file mode 100644 index 0000000000..4d9f57a4bf --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.sk_SK/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Slovak (http://www.transifex.com/projects/p/xbmc-addons/language/sk/)\n" +"Language: sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Hudobná knižnica" + +msgctxt "#30100" +msgid "Delete" +msgstr "Vymazať" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Upraviť názov" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Nájsť cestu k logu" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Umiestnenie" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Skupina" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Umiestnenie" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikona" + +msgctxt "#30314" +msgid "Order by" +msgstr "Usporiadať podľa" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.sl_SI/strings.po b/plugin.library.node.editor/resources/language/resource.language.sl_SI/strings.po new file mode 100644 index 0000000000..8056d99456 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.sl_SI/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Slovenian (http://www.transifex.com/projects/p/xbmc-addons/language/sl/)\n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Izbriši" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Uredi oznako" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Prebrskajte za ikono" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Pot" + +msgctxt "#30205" +msgid "Rule" +msgstr "Pravilo" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Polje" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "Vrednost" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Skupina" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Pot" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikona" + +msgctxt "#30314" +msgid "Order by" +msgstr "Razvrščeno po" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.sq_AL/strings.po b/plugin.library.node.editor/resources/language/resource.language.sq_AL/strings.po new file mode 100644 index 0000000000..e6af73233f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.sq_AL/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Albanian (http://www.transifex.com/projects/p/xbmc-addons/language/sq/)\n" +"Language: sq\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Fshij" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Ndryshoni etiketën" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Vendndodhja" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grup" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Vendndodhja" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Porosi nga" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.sr_RS/strings.po b/plugin.library.node.editor/resources/language/resource.language.sr_RS/strings.po new file mode 100644 index 0000000000..52952c3172 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.sr_RS/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Serbian (Cyrillic) (http://www.transifex.com/projects/p/xbmc-addons/language/sr_RS/)\n" +"Language: sr_RS\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Избриши" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Уреди натпис" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Путања" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Путања" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Сложи по" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.sr_RS@latin/strings.po b/plugin.library.node.editor/resources/language/resource.language.sr_RS@latin/strings.po new file mode 100644 index 0000000000..fb25a63ee7 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.sr_RS@latin/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Serbian (http://www.transifex.com/projects/p/xbmc-addons/language/sr/)\n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Izbriši" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Uredi natpis" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Putanja" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Putanja" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikone" + +msgctxt "#30314" +msgid "Order by" +msgstr "Složi po" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.sv_SE/strings.po b/plugin.library.node.editor/resources/language/resource.language.sv_SE/strings.po new file mode 100644 index 0000000000..1c8950d2d3 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.sv_SE/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-10-25 09:53+0000\n" +"Last-Translator: Prahlis <prahl.tobias@gmail.com>\n" +"Language-Team: Swedish <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/sv_se/>\n" +"Language: sv_SE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Hantera anpassade biblioteksnoder." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "Skapa och redigera anpassade biblioteksnoder." + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "Lägg till innehåll..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Lägg till sökväg..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "Lägg till sortering..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "Lägg till begränsning..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "Lägg till gruppering..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "Lägg till regel..." + +msgctxt "#30006" +msgid "New node..." +msgstr "Ny nod..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "Ny överliggande nod..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "Återställ biblioteksnoder till standard..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "Lägg till komponent..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "Redigera manuellt..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "Videobibliotek" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Musikbibliotek" + +msgctxt "#30100" +msgid "Delete" +msgstr "Ta bort" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Ändra namn" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Redigera ikon" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Bläddra bland ikoner" + +msgctxt "#30104" +msgid "Move node" +msgstr "Flytta nod" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "Redigera synlighet" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "Lägg till i huvudmenyn" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "Bläddra efter värde" + +msgctxt "#30200" +msgid "Content" +msgstr "Innehåll" + +msgctxt "#30201" +msgid "Order" +msgstr "Sortera" + +msgctxt "#30202" +msgid "Grouping" +msgstr "Gruppering" + +msgctxt "#30203" +msgid "Limit" +msgstr "Gräns" + +msgctxt "#30204" +msgid "Path" +msgstr "Sökväg" + +msgctxt "#30205" +msgid "Rule" +msgstr "Regel" + +msgctxt "#30206" +msgid "Match" +msgstr "Matchning" + +msgctxt "#30300" +msgid "Set name" +msgstr "Välj namn" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "Välj synlighetsvillkor" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Fält" + +msgctxt "#30306" +msgid "Operator" +msgstr "Operatör" + +msgctxt "#30307" +msgid "Value" +msgstr "Värde" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "Innehåll" + +msgctxt "#30310" +msgid "Group" +msgstr "Grupp" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Sökväg" + +msgctxt "#30313" +msgid "Icon" +msgstr "Ikon" + +msgctxt "#30314" +msgid "Order by" +msgstr "Sortera efter" + +msgctxt "#30315" +msgid "Direction" +msgstr "Riktning" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.ta_IN/strings.po b/plugin.library.node.editor/resources/language/resource.language.ta_IN/strings.po new file mode 100644 index 0000000000..553b9adf01 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.ta_IN/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Tamil (India) (http://www.transifex.com/projects/p/xbmc-addons/language/ta_IN/)\n" +"Language: ta_IN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "நீக்கு" + +msgctxt "#30101" +msgid "Edit label" +msgstr "சிட்டை திருத்து" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "உருவத்திற்காக உலாவுக" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "பாதை" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "குழு" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "பாதை" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "வரிசைபடி" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.th_TH/strings.po b/plugin.library.node.editor/resources/language/resource.language.th_TH/strings.po new file mode 100644 index 0000000000..33dd8b379c --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.th_TH/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Thai (http://www.transifex.com/projects/p/xbmc-addons/language/th/)\n" +"Language: th\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "แฟ้ม วิดีโอ" + +msgctxt "#30092" +msgid "Music Library" +msgstr "คลังเพลง" + +msgctxt "#30100" +msgid "Delete" +msgstr "ลบ" + +msgctxt "#30101" +msgid "Edit label" +msgstr "แก้ป้ายชื่อ" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "เรียกดูไอคอน" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "เส้นทาง" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "กลุ่ม" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "เส้นทาง" + +msgctxt "#30313" +msgid "Icon" +msgstr "ไอคอน" + +msgctxt "#30314" +msgid "Order by" +msgstr "เรียงลำดับตาม" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.tr_TR/strings.po b/plugin.library.node.editor/resources/language/resource.language.tr_TR/strings.po new file mode 100644 index 0000000000..8797702f2d --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.tr_TR/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Turkish (http://www.transifex.com/projects/p/xbmc-addons/language/tr/)\n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Video Kitaplığı" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Müzik Kitaplığı" + +msgctxt "#30100" +msgid "Delete" +msgstr "Sil" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Etiketi düzenle" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "Simgeyi Düzenle" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Simgeye gözat" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Yol" + +msgctxt "#30205" +msgid "Rule" +msgstr "Kural" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "Alan" + +msgctxt "#30306" +msgid "Operator" +msgstr "İşletici" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Grup" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Yol" + +msgctxt "#30313" +msgid "Icon" +msgstr "Simge" + +msgctxt "#30314" +msgid "Order by" +msgstr "Sırala" + +msgctxt "#30315" +msgid "Direction" +msgstr "Yön" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.uk_UA/strings.po b/plugin.library.node.editor/resources/language/resource.language.uk_UA/strings.po new file mode 100644 index 0000000000..bc76328f0f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.uk_UA/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Ukrainian (http://www.transifex.com/projects/p/xbmc-addons/language/uk/)\n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "Бібліотека відео" + +msgctxt "#30092" +msgid "Music Library" +msgstr "Бібліотека музики" + +msgctxt "#30100" +msgid "Delete" +msgstr "Видалити" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Змінити назву" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "Вибрати значок" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Шлях" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Група" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Шлях" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "Упорядкувати за" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.uz_UZ/strings.po b/plugin.library.node.editor/resources/language/resource.language.uz_UZ/strings.po new file mode 100644 index 0000000000..eba376e36f --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.uz_UZ/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Uzbek (http://www.transifex.com/projects/p/xbmc-addons/language/uz/)\n" +"Language: uz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "O'chirish" + +msgctxt "#30101" +msgid "Edit label" +msgstr "" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Yo'l" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "Guruh" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Yo'l" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.vi_VN/strings.po b/plugin.library.node.editor/resources/language/resource.language.vi_VN/strings.po new file mode 100644 index 0000000000..9a2630a236 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.vi_VN/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-08-20 04:51+0000\n" +"Last-Translator: Nguyễn Trung Hậu <trunghau1712@gmail.com>\n" +"Language-Team: Vietnamese <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/vi_vn/>\n" +"Language: vi_VN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.7.2\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "Quản lý các nút thư viện tùy chỉnh." + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "Thêm nội dung..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "Thêm đường dẫn..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "" + +msgctxt "#30092" +msgid "Music Library" +msgstr "" + +msgctxt "#30100" +msgid "Delete" +msgstr "Xóa" + +msgctxt "#30101" +msgid "Edit label" +msgstr "Nhãn sửa" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "Đường dẫn" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "Đặt tên" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "Bắt đầu theo mặc định" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "Kiểu nội dung" + +msgctxt "#30309" +msgid "Content" +msgstr "Nội dung" + +msgctxt "#30310" +msgid "Group" +msgstr "Nhóm" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "Đường dẫn" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/language/resource.language.zh_CN/strings.po b/plugin.library.node.editor/resources/language/resource.language.zh_CN/strings.po new file mode 100644 index 0000000000..c89ecde1e6 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.zh_CN/strings.po @@ -0,0 +1,323 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2021-09-17 05:30+0000\n" +"Last-Translator: taxigps <taxigps@sina.com>\n" +"Language-Team: Chinese (China) <https://kodi.weblate.cloud/projects/kodi-add-ons-program/plugin-library-node-editor/zh_cn/>\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "管理自定义资料库节点。" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "创建和编辑自定义资料库节点。" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "添加内容..." + +msgctxt "#30001" +msgid "Add path..." +msgstr "添加路径..." + +msgctxt "#30002" +msgid "Add order..." +msgstr "添加顺序..." + +msgctxt "#30003" +msgid "Add limit..." +msgstr "添加限制..." + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "添加组..." + +msgctxt "#30005" +msgid "Add rule..." +msgstr "添加规则..." + +msgctxt "#30006" +msgid "New node..." +msgstr "新节点..." + +msgctxt "#30007" +msgid "New parent node..." +msgstr "新父节点..." + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "将资料库节点重置为默认值..." + +msgctxt "#30009" +msgid "Add component..." +msgstr "添加部件..." + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "人工编辑..." + +msgctxt "#30091" +msgid "Video Library" +msgstr "视频资料库" + +msgctxt "#30092" +msgid "Music Library" +msgstr "音乐资料库" + +msgctxt "#30100" +msgid "Delete" +msgstr "删除" + +msgctxt "#30101" +msgid "Edit label" +msgstr "编辑标签" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "编辑图标" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "浏览图标" + +msgctxt "#30104" +msgid "Move node" +msgstr "移动节点" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "编辑可见性" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "添加到主菜单" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "浏览价值" + +msgctxt "#30200" +msgid "Content" +msgstr "内容" + +msgctxt "#30201" +msgid "Order" +msgstr "顺序" + +msgctxt "#30202" +msgid "Grouping" +msgstr "组" + +msgctxt "#30203" +msgid "Limit" +msgstr "限制" + +msgctxt "#30204" +msgid "Path" +msgstr "路径" + +msgctxt "#30205" +msgid "Rule" +msgstr "规则" + +msgctxt "#30206" +msgid "Match" +msgstr "匹配" + +msgctxt "#30300" +msgid "Set name" +msgstr "设置名称" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "设置可见性条件" + +msgctxt "#30302" +msgid "Set order index" +msgstr "设置排序索引" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "新父节点名称" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "从默认值开始" + +msgctxt "#30305" +msgid "Field" +msgstr "域" + +msgctxt "#30306" +msgid "Operator" +msgstr "算子" + +msgctxt "#30307" +msgid "Value" +msgstr "值" + +msgctxt "#30308" +msgid "Content type" +msgstr "内容类型" + +msgctxt "#30309" +msgid "Content" +msgstr "内容" + +msgctxt "#30310" +msgid "Group" +msgstr "组" + +msgctxt "#30311" +msgid "Limit" +msgstr "限制" + +msgctxt "#30312" +msgid "Path" +msgstr "路径" + +msgctxt "#30313" +msgid "Icon" +msgstr "图标" + +msgctxt "#30314" +msgid "Order by" +msgstr "排序" + +msgctxt "#30315" +msgid "Direction" +msgstr "方向" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "新节点名称" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "检索值..." + +msgctxt "#30318" +msgid "Property" +msgstr "属性" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "无法复制默认资料库节点" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "确定删除此节点吗?" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "确定重置所有节点吗?" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "确定删除此属性吗?" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "当此节点仍具有排序、限制或规则参数时,无法删除内容参数。" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "确定删除此规则吗?" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "排序需要一个内容参数" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "确定删除此部件吗?" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "自定义属性..." + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "找不到可供选择的选项" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "正获取插件列表..." + +msgctxt "#30411" +msgid "Link path to here" +msgstr "链接路径到此处" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "类型 ID" + +msgctxt "#30501" +msgid "Country ID" +msgstr "国家 ID" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "出品人 ID" + +msgctxt "#30503" +msgid "Director ID" +msgstr "导演 ID" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "演员 ID" + +msgctxt "#30505" +msgid "Set ID" +msgstr "电影集 ID" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "标签 ID" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "剧集 ID" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "歌手 ID" + +msgctxt "#30509" +msgid "Album ID" +msgstr "专辑 ID" + +msgctxt "#30510" +msgid "Role ID" +msgstr "角色 ID" + +msgctxt "#30511" +msgid "Song ID" +msgstr "歌曲 ID" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "仅专辑歌手" + +msgctxt "#30513" +msgid "Show singles" +msgstr "显示单曲" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "插件..." diff --git a/plugin.library.node.editor/resources/language/resource.language.zh_TW/strings.po b/plugin.library.node.editor/resources/language/resource.language.zh_TW/strings.po new file mode 100644 index 0000000000..e3b8a22cf9 --- /dev/null +++ b/plugin.library.node.editor/resources/language/resource.language.zh_TW/strings.po @@ -0,0 +1,322 @@ +# Kodi Media Center language file +# Addon Name: Library Node Editor +# Addon id: plugin.library.node.editor +# Addon Provider: Unfledged, Team-Kodi +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Chinese (Traditional) (http://www.transifex.com/projects/p/xbmc-addons/language/zh_TW/)\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "Addon Summary" +msgid "Manage custom library nodes." +msgstr "" + +msgctxt "Addon Description" +msgid "Create and edit custom library nodes." +msgstr "" + +# Menu items +msgctxt "#30000" +msgid "Add content..." +msgstr "" + +msgctxt "#30001" +msgid "Add path..." +msgstr "" + +msgctxt "#30002" +msgid "Add order..." +msgstr "" + +msgctxt "#30003" +msgid "Add limit..." +msgstr "" + +msgctxt "#30004" +msgid "Add grouping..." +msgstr "" + +msgctxt "#30005" +msgid "Add rule..." +msgstr "" + +msgctxt "#30006" +msgid "New node..." +msgstr "" + +msgctxt "#30007" +msgid "New parent node..." +msgstr "" + +msgctxt "#30008" +msgid "Reset library nodes to default..." +msgstr "" + +msgctxt "#30009" +msgid "Add component..." +msgstr "" + +msgctxt "#30010" +msgid "Manually edit..." +msgstr "" + +msgctxt "#30091" +msgid "Video Library" +msgstr "影片資料庫" + +msgctxt "#30092" +msgid "Music Library" +msgstr "音樂資料庫" + +msgctxt "#30100" +msgid "Delete" +msgstr "刪除" + +msgctxt "#30101" +msgid "Edit label" +msgstr "編輯標籤" + +msgctxt "#30102" +msgid "Edit icon" +msgstr "" + +msgctxt "#30103" +msgid "Browse for icon" +msgstr "瀏覽圖示" + +msgctxt "#30104" +msgid "Move node" +msgstr "" + +msgctxt "#30105" +msgid "Edit visibility" +msgstr "" + +msgctxt "#30106" +msgid "Add to main menu" +msgstr "" + +msgctxt "#30107" +msgid "Browse for value" +msgstr "" + +msgctxt "#30200" +msgid "Content" +msgstr "" + +msgctxt "#30201" +msgid "Order" +msgstr "" + +msgctxt "#30202" +msgid "Grouping" +msgstr "" + +msgctxt "#30203" +msgid "Limit" +msgstr "" + +msgctxt "#30204" +msgid "Path" +msgstr "路徑" + +msgctxt "#30205" +msgid "Rule" +msgstr "" + +msgctxt "#30206" +msgid "Match" +msgstr "" + +msgctxt "#30300" +msgid "Set name" +msgstr "" + +msgctxt "#30301" +msgid "Set visibility condition" +msgstr "" + +msgctxt "#30302" +msgid "Set order index" +msgstr "" + +msgctxt "#30303" +msgid "Name of new parent node" +msgstr "" + +msgctxt "#30304" +msgid "Start with defaults" +msgstr "" + +msgctxt "#30305" +msgid "Field" +msgstr "" + +msgctxt "#30306" +msgid "Operator" +msgstr "" + +msgctxt "#30307" +msgid "Value" +msgstr "" + +msgctxt "#30308" +msgid "Content type" +msgstr "" + +msgctxt "#30309" +msgid "Content" +msgstr "" + +msgctxt "#30310" +msgid "Group" +msgstr "群組" + +msgctxt "#30311" +msgid "Limit" +msgstr "" + +msgctxt "#30312" +msgid "Path" +msgstr "路徑" + +msgctxt "#30313" +msgid "Icon" +msgstr "" + +msgctxt "#30314" +msgid "Order by" +msgstr "排序方式" + +msgctxt "#30315" +msgid "Direction" +msgstr "" + +msgctxt "#30316" +msgid "Name of new node" +msgstr "" + +msgctxt "#30317" +msgid "Retrieving values..." +msgstr "" + +msgctxt "#30318" +msgid "Property" +msgstr "" + +msgctxt "#30400" +msgid "Unable to copy default library nodes" +msgstr "" + +msgctxt "#30401" +msgid "Are you sure you want to delete this node?" +msgstr "" + +msgctxt "#30402" +msgid "Are you sure you want to reset all nodes?" +msgstr "" + +msgctxt "#30403" +msgid "Are you sure you want to delete this property?" +msgstr "" + +msgctxt "#30404" +msgid "Content parameter can't be deleted whilst this node still has an Order, Limit or Rule parameter." +msgstr "" + +msgctxt "#30405" +msgid "Are you sure you want to delete this rule?" +msgstr "" + +msgctxt "#30406" +msgid "Order requires a Content parameter" +msgstr "" + +msgctxt "#30407" +msgid "Are you sure you want to delete this component?" +msgstr "" + +msgctxt "#30408" +msgid "Custom property..." +msgstr "" + +msgctxt "#30409" +msgid "No options to select from were found" +msgstr "" + +msgctxt "#30410" +msgid "Getting plugin listings..." +msgstr "" + +msgctxt "#30411" +msgid "Link path to here" +msgstr "" + +msgctxt "#30500" +msgid "Genre ID" +msgstr "" + +msgctxt "#30501" +msgid "Country ID" +msgstr "" + +msgctxt "#30502" +msgid "Studio ID" +msgstr "" + +msgctxt "#30503" +msgid "Director ID" +msgstr "" + +msgctxt "#30504" +msgid "Actor ID" +msgstr "" + +msgctxt "#30505" +msgid "Set ID" +msgstr "" + +msgctxt "#30506" +msgid "Tag ID" +msgstr "" + +msgctxt "#30507" +msgid "TV Show ID" +msgstr "" + +msgctxt "#30508" +msgid "Artist ID" +msgstr "" + +msgctxt "#30509" +msgid "Album ID" +msgstr "" + +msgctxt "#30510" +msgid "Role ID" +msgstr "" + +msgctxt "#30511" +msgid "Song ID" +msgstr "" + +msgctxt "#30512" +msgid "Album artists only" +msgstr "" + +msgctxt "#30513" +msgid "Show singles" +msgstr "" + +msgctxt "#30514" +msgid "Plugin..." +msgstr "" diff --git a/plugin.library.node.editor/resources/lib/__init__.py b/plugin.library.node.editor/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.library.node.editor/resources/lib/addon.py b/plugin.library.node.editor/resources/lib/addon.py new file mode 100644 index 0000000000..5b2e1362ad --- /dev/null +++ b/plugin.library.node.editor/resources/lib/addon.py @@ -0,0 +1,836 @@ +# coding=utf-8 +import os, sys, shutil, unicodedata, re, types + +from resources.lib.common import * + +from html.entities import name2codepoint +from urllib.parse import parse_qs +from urllib.parse import quote, unquote + +import xbmc, xbmcplugin, xbmcgui, xbmcvfs +import xml.etree.ElementTree as xmltree +import urllib +from unidecode import unidecode + +from traceback import print_exc +import json + +from resources.lib import rules, pathrules, viewattrib, orderby, moveNodes + +# character entity reference +CHAR_ENTITY_REXP = re.compile('&(%s);' % '|'.join(name2codepoint)) + +# decimal character reference +DECIMAL_REXP = re.compile('&#(\d+);') + +# hexadecimal character reference +HEX_REXP = re.compile('&#x([\da-fA-F]+);') + +REPLACE1_REXP = re.compile(r'[\']+') +REPLACE2_REXP = re.compile(r'[^-a-z0-9]+') +REMOVE_REXP = re.compile('-{2,}') + + + +class Main: + # MAIN ENTRY POINT + def __init__(self, params, ltype, rule, attrib, pathrule, orderby): + + self._parse_argv() + self.ltype = ltype + self.PARAMS = params + self.RULE = rule + self.ATTRIB = attrib + self.PATHRULE = pathrule + self.ORDERBY = orderby + # If there are no custom library nodes in the profile directory, copy them from the Kodi install + targetDir = os.path.join( xbmcvfs.translatePath( "special://profile" ), "library", ltype ) + if True: + if not os.path.exists( targetDir ): + xbmcvfs.mkdirs( targetDir ) + originDir = os.path.join( xbmcvfs.translatePath( "special://xbmc" ), "system", "library", ltype ) + dirs, files = xbmcvfs.listdir( originDir ) + self.copyNode( dirs, files, targetDir, originDir ) + else: + xbmcgui.Dialog().ok(ADDONNAME, LANGUAGE( 30400 ) ) + print_exc + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + return + # Create data if not exists + if not os.path.exists(DATAPATH): + xbmcvfs.mkdir(DATAPATH) + if "type" in self.PARAMS: + # We're performing a specific action + if self.PARAMS[ "type" ] == "delete": + message = LANGUAGE( 30401 ) + actionpath = unquote(self.PARAMS["actionPath"]) + if self.PARAMS[ "actionPath" ] == targetDir: + # Ask the user is they want to reset all nodes + message = LANGUAGE( 30402 ) + result = xbmcgui.Dialog().yesno(ADDONNAME, message ) + if result: + if actionpath.endswith( ".xml" ): + # Delete single file + xbmcvfs.delete(actionpath) + else: + # Delete folder + self.RULE.deleteAllNodeRules(actionpath) + shutil.rmtree(actionpath) + else: + return + elif self.PARAMS[ "type" ] == "deletenode": + result = xbmcgui.Dialog().yesno(ADDONNAME, LANGUAGE( 30403 ) ) + if result: + self.changeViewElement( self.PARAMS[ "actionPath" ], self.PARAMS[ "node" ], "" ) + elif self.PARAMS[ "type" ] == "editlabel": + if self.PARAMS[ "label" ].isdigit(): + label = xbmc.getLocalizedString( int( self.PARAMS[ "label" ] ) ) + else: + label = self.PARAMS[ "label" ] + # Get new label from keyboard dialog + keyboard = xbmc.Keyboard( label, LANGUAGE( 30300 ), False ) + keyboard.doModal() + if ( keyboard.isConfirmed() ): + newlabel = keyboard.getText() + if newlabel != "" and newlabel != label: + # We've got a new label, update the xml file + self.changeViewElement( self.PARAMS[ "actionPath" ], "label", newlabel ) + else: + return + elif self.PARAMS[ "type" ] == "editvisibility": + currentVisibility = self.getRootAttrib( self.PARAMS[ "actionPath" ], "visible" ) + # Get new visibility from keyboard dialog + keyboard = xbmc.Keyboard( currentVisibility, LANGUAGE( 30301 ), False ) + keyboard.doModal() + if ( keyboard.isConfirmed() ): + newVisibility = keyboard.getText() + if newVisibility != currentVisibility: + # We've got a new label, update the xml file + self.changeRootAttrib( self.PARAMS[ "actionPath" ], "visible", newVisibility ) + else: + return + elif self.PARAMS[ "type" ] == "moveNode": + self.indexCounter = -1 + + # Get existing nodes + nodes = {} + self.listNodes( self.PARAMS[ "actionPath" ], nodes ) + + # Get updated order + newOrder = moveNodes.getNewOrder( nodes, int( self.PARAMS[ "actionItem" ] ) ) + + if newOrder is not None: + # Update the orders + for i, node in enumerate( newOrder, 1 ): + path = unquote( node[ 2 ] ) + if node[ 3 ] == "folder": + path = os.path.join( unquote( node[ 2 ] ), "index.xml" ) + self.changeRootAttrib( path, "order", str( i * 10 ) ) + + elif self.PARAMS[ "type" ] == "newView": + # Get new view name from keyboard dialog + keyboard = xbmc.Keyboard( "", LANGUAGE( 30316 ), False ) + keyboard.doModal() + if ( keyboard.isConfirmed() ): + newView = keyboard.getText() + if newView != "": + # Ensure filename is unique + filename = self.slugify( newView.lower().replace( " ", "" ) ) + if os.path.exists( os.path.join( self.PARAMS[ "actionPath" ], filename + ".xml" ) ): + count = 0 + while os.path.exists( os.path.join( self.PARAMS[ "actionPath" ], filename + "-" + str( count ) + ".xml" ) ): + count += 1 + filename = filename + "-" + str( count ) + # Create a new xml file + tree = xmltree.ElementTree( xmltree.Element( "node" ) ) + root = tree.getroot() + subtree = xmltree.SubElement( root, "label" ).text = newView + # Add any node rules + self.RULE.addAllNodeRules( self.PARAMS[ "actionPath" ], root ) + # Write the xml file + self.indent( root ) + xmlfile = unquote(os.path.join( self.PARAMS[ "actionPath" ], filename + ".xml" )) + if not os.path.exists(xmlfile): + with open(xmlfile, 'a'): + os.utime(xmlfile, None) + tree.write( xmlfile, encoding="UTF-8" ) + else: + return + elif self.PARAMS[ "type" ] == "newNode": + # Get new node name from the keyboard dialog + keyboard = xbmc.Keyboard( "", LANGUAGE( 30303 ), False ) + keyboard.doModal() + if ( keyboard.isConfirmed() ): + newNode = keyboard.getText() + if newNode == "": + return + # Ensure foldername is unique + foldername = self.slugify( newNode.lower().replace( " ", "" ) ) + if os.path.exists( os.path.join( self.PARAMS[ "actionPath" ], foldername + os.pathsep ) ): + count = 0 + while os.path.exists( os.path.join( self.PARAMS[ "actionPath" ], foldername + "-" + str( count ) + os.pathsep ) ): + count += 1 + foldername = foldername + "-" + str( count ) + foldername = unquote(os.path.join( self.PARAMS[ "actionPath" ], foldername )) + # Create new node folder + xbmcvfs.mkdir( foldername ) + # Create a new xml file + tree = xmltree.ElementTree( xmltree.Element( "node" ) ) + root = tree.getroot() + subtree = xmltree.SubElement( root, "label" ).text = newNode + # Ask user if they want to import defaults + if self.ltype.startswith( "video" ): + defaultNames = [ xbmc.getLocalizedString( 231 ), xbmc.getLocalizedString( 342 ), xbmc.getLocalizedString( 20343 ), xbmc.getLocalizedString( 20389 ) ] + defaultValues = [ "", "movies", "tvshows", "musicvideos" ] + selected = xbmcgui.Dialog().select( LANGUAGE( 30304 ), defaultNames ) + else: + selected = 0 + # If the user selected some defaults... + if selected != -1 and selected != 0: + try: + # Copy those defaults across + originDir = os.path.join( xbmcvfs.translatePath( "special://xbmc" ), "system", "library", self.ltype, defaultValues[ selected ] ) + dirs, files = xbmcvfs.listdir( originDir ) + for file in files: + if file != "index.xml": + xbmcvfs.copy( os.path.join( originDir, file), os.path.join( foldername, file ) ) + # Open index.xml and copy values across + index = xmltree.parse( os.path.join( originDir, "index.xml" ) ).getroot() + if "visible" in index.attrib: + root.set( "visible", index.attrib.get( "visible" ) ) + icon = index.find( "icon" ) + if icon is not None: + xmltree.SubElement( root, "icon" ).text = icon.text + except: + print_exc() + # Write the xml file + self.indent( root ) + tree.write( unquote(os.path.join( foldername, "index.xml" )), encoding="UTF-8" ) + else: + return + elif self.PARAMS[ "type" ] == "rule": + # Display list of all elements of a rule + self.RULE.displayRule( self.PARAMS[ "actionPath" ], self.PATH, self.PARAMS[ "rule" ] ) + return + elif self.PARAMS[ "type" ] == "editMatch": + # Editing the field the rule is matched against + self.RULE.editMatch( self.PARAMS[ "actionPath" ], self.PARAMS[ "rule" ], self.PARAMS[ "content"], self.PARAMS[ "default" ] ) + elif self.PARAMS[ "type" ] == "editOperator": + # Editing the operator of a rule + self.RULE.editOperator( self.PARAMS[ "actionPath" ], self.PARAMS[ "rule" ], self.PARAMS[ "group" ], self.PARAMS[ "default" ] ) + elif self.PARAMS[ "type" ] == "editValue": + # Editing the value of a rule + self.RULE.editValue(self.PARAMS["actionPath"], self.PARAMS[ "rule" ] ) + elif self.PARAMS[ "type" ] == "browseValue": + # Browse for the new value of a rule + self.RULE.browse( self.PARAMS[ "actionPath" ], self.PARAMS[ "rule" ], self.PARAMS[ "match" ], self.PARAMS[ "content" ] ) + elif self.PARAMS[ "type" ] == "deleteRule": + # Delete a rule + self.RULE.deleteRule( self.PARAMS[ "actionPath" ], self.PARAMS[ "rule" ] ) + elif self.PARAMS[ "type" ] == "editRulesMatch": + # Editing whether any or all rules must match + self.ATTRIB.editMatch( self.PARAMS[ "actionPath" ] ) + # --- Edit order-by --- + elif self.PARAMS[ "type" ] == "orderby": + # Display all elements of order by + self.ORDERBY.displayOrderBy( self.PARAMS[ "actionPath" ]) + return + elif self.PARAMS[ "type" ] == "editOrderBy": + self.ORDERBY.editOrderBy( self.PARAMS[ "actionPath" ], self.PARAMS[ "content" ], self.PARAMS[ "default" ] ) + elif self.PARAMS[ "type" ] == "editOrderByDirection": + self.ORDERBY.editDirection( self.PARAMS[ "actionPath" ], self.PARAMS[ "default" ] ) + # --- Edit paths --- + elif self.PARAMS[ "type" ] == "addPath": + self.ATTRIB.addPath( self.PARAMS[ "actionPath" ] ) + elif self.PARAMS[ "type" ] == "editPath": + self.ATTRIB.editPath( self.PARAMS[ "actionPath" ], self.PARAMS[ "value" ] ) + elif self.PARAMS[ "type" ] == "pathRule": + self.PATHRULE.displayRule( self.PARAMS[ "actionPath" ], int( self.PARAMS[ "rule" ] ) ) + return + elif self.PARAMS[ "type" ] == "deletePathRule": + self.ATTRIB.deletePathRule( self.PARAMS[ "actionPath" ], int( self.PARAMS[ "rule" ] ) ) + elif self.PARAMS[ "type" ] == "editPathMatch": + # Editing the field the rule is matched against + self.PATHRULE.editMatch( self.PARAMS[ "actionPath" ], int( self.PARAMS[ "rule" ] ) ) + elif self.PARAMS[ "type" ] == "editPathValue": + # Editing the value of a rule + self.PATHRULE.editValue( self.PARAMS[ "actionPath" ], int( self.PARAMS[ "rule" ] ) ) + elif self.PARAMS[ "type" ] == "browsePathValue": + # Browse for the new value of a rule + self.PATHRULE.browse( self.PARAMS[ "actionPath" ], int( self.PARAMS[ "rule" ] ) ) + # --- Edit other attribute of view --- + # > Content + elif self.PARAMS[ "type" ] == "editContent": + self.ATTRIB.editContent( self.PARAMS[ "actionPath" ], "" ) # No default to pass, yet! + # > Grouping + elif self.PARAMS[ "type" ] == "editGroup": + self.ATTRIB.editGroup( self.PARAMS[ "actionPath" ], self.PARAMS[ "content" ], "" ) + # > Limit + elif self.PARAMS[ "type" ] == "editLimit": + self.ATTRIB.editLimit( self.PARAMS[ "actionPath" ], self.PARAMS[ "value" ] ) + # > Icon (also for node) + elif self.PARAMS[ "type" ] == "editIcon": + self.ATTRIB.editIcon( self.PARAMS[ "actionPath" ], self.PARAMS[ "value" ] ) + elif self.PARAMS[ "type" ] == "browseIcon": + self.ATTRIB.browseIcon( self.PARAMS[ "actionPath" ] ) + # Refresh the listings and exit + xbmc.executebuiltin("Container.Refresh") + return + if self.PATH.endswith( ".xml" ): + self.RulesList() + else: + self.NodesList(targetDir) + + def NodesList( self, targetDir ): + # List nodes and views + nodes = {} + self.indexCounter = -1 + if self.PATH != "": + self.listNodes( self.PATH, nodes ) + else: + self.listNodes( targetDir, nodes ) + self.PATH = quote( self.PATH ) + for i, key in enumerate( sorted( nodes ) ): + # 0 = Label + # 1 = Icon + # 2 = Path + # 3 = Type + # 4 = Order + # Localize the label + if nodes[ key ][ 0 ].isdigit(): + label = xbmc.getLocalizedString( int( nodes[ key ][ 0 ] ) ) + else: + label = nodes[ key ][ 0 ] + # Create the listitem + if nodes[ key ][ 3 ] == "folder": + listitem = xbmcgui.ListItem( label="%s >" % ( label ), label2=nodes[ key ][ 4 ] ) + listitem.setArt({"icon": nodes[ key ][ 1 ]}) + else: + listitem = xbmcgui.ListItem( label=label, label2=nodes[ key ][ 4 ] ) + listitem.setArt({"icon": nodes[ key ][ 1 ]}) + # Add context menu items + commands = [] + commandsNode = [] + commandsView = [] + commandsNode.append( ( LANGUAGE(30101), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=editlabel&actionPath=" % self.ltype + os.path.join( nodes[ key ][ 2 ], "index.xml" ) + "&label=" + nodes[ key ][ 0 ] + ")" ) ) + commandsNode.append( ( LANGUAGE(30102), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=editIcon&actionPath=" % self.ltype + os.path.join( nodes[ key ][ 2 ], "index.xml" ) + "&value=" + nodes[ key ][ 1 ] + ")" ) ) + commandsNode.append( ( LANGUAGE(30103), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=browseIcon&actionPath=" % self.ltype + os.path.join( nodes [ key ][ 2 ], "index.xml" ) + ")" ) ) + if self.PATH == "": + commandsNode.append( ( LANGUAGE(30104), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=moveNode&actionPath=" % self.ltype + targetDir + "&actionItem=" + str( i ) + ")" ) ) + else: + commandsNode.append( ( LANGUAGE(30104), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=moveNode&actionPath=" % self.ltype + self.PATH + "&actionItem=" + str( i ) + ")" ) ) + commandsNode.append( ( LANGUAGE(30105), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=editvisibility&actionPath=" % self.ltype + os.path.join( nodes[ key ][ 2 ], "index.xml" ) + ")" ) ) + commandsNode.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=delete&actionPath=" % self.ltype + nodes[ key ][ 2 ] + ")" ) ) + + commandsView.append( ( LANGUAGE(30101), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=editlabel&actionPath=" % self.ltype + nodes[ key ][ 2 ] + "&label=" + nodes[ key ][ 0 ] + ")" ) ) + commandsView.append( ( LANGUAGE(30102), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=editIcon&actionPath=" % self.ltype + nodes[ key ][ 2 ] + "&value=" + nodes[ key ][ 1 ] + ")" ) ) + commandsView.append( ( LANGUAGE(30103), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=browseIcon&actionPath=" % self.ltype + nodes[ key ][ 2 ] + ")" ) ) + if self.PATH == "": + commandsView.append( ( LANGUAGE(30104), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=moveNode&actionPath=" % self.ltype + targetDir + "&actionItem=" + str( i ) + ")" ) ) + else: + commandsView.append( ( LANGUAGE(30104), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=moveNode&actionPath=" % self.ltype + self.PATH + "&actionItem=" + str( i ) + ")" ) ) + commandsView.append( ( LANGUAGE(30105), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=editvisibility&actionPath=" % self.ltype + nodes[ key ][ 2 ] + ")" ) ) + commandsView.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=delete&actionPath=" % self.ltype + nodes[ key ][ 2 ] + ")" ) ) + if nodes[ key ][ 3 ] == "folder": + listitem.addContextMenuItems( commandsNode ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), "plugin://plugin.library.node.editor?ltype=%s&path=" % self.ltype + nodes[ key ][ 2 ], listitem, isFolder=True ) + else: + listitem.addContextMenuItems( commandsView ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), "plugin://plugin.library.node.editor?ltype=%s&path=" % self.ltype + nodes[ key ][ 2 ], listitem, isFolder=True ) + if self.PATH != "": + # Get any rules from the index.xml + rules, nextRule = self.getRules( os.path.join( unquote( self.PATH ), "index.xml" ), True ) + rulecount = 0 + if rules is not None: + for rule in rules: + commands = [] + if rule[ 0 ] == "rule": + # 1 = field + # 2 = operator + # 3 = value (optional) + if len(rule) == 3: + translated = self.RULE.translateRule( [ rule[ 1 ], rule[ 2 ] ] ) + else: + translated = self.RULE.translateRule( [ rule[ 1 ], rule[ 2 ], rule[ 3 ] ] ) + if len(translated) == 2: + listitem = xbmcgui.ListItem( label="%s: %s %s" % ( LANGUAGE(30205), translated[ 0 ][ 0 ], translated[ 1 ][ 0 ] ) ) + else: + listitem = xbmcgui.ListItem( label="%s: %s %s %s" % ( LANGUAGE(30205), translated[ 0 ][ 0 ], translated[ 1 ][ 0 ], translated[ 2 ][ 1 ] ) ) + commands.append( ( LANGUAGE( 30100 ), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=deleteRule&actionPath=" % self.ltype + os.path.join( self.PATH, "index.xml" ) + "&rule=" + str( rulecount ) + ")" ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=rule&actionPath=" % self.ltype + os.path.join( self.PATH, "index.xml" ) + "&rule=" + str( rulecount ) + rulecount += 1 + listitem.addContextMenuItems( commands, replaceItems = True ) + if rule[ 0 ] == "rule" or rule[ 0 ] == "order": + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=True ) + else: + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + # New rule + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=rule&actionPath=" % self.ltype + os.path.join( self.PATH, "index.xml" ) + "&rule=" + str( nextRule), xbmcgui.ListItem( label="* %s" %( LANGUAGE(30005) ) ), isFolder=True ) + showReset = False + if self.PATH == "": + self.PATH = quote( targetDir ) + showReset = True + # New view and node + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=newView&actionPath=" % self.ltype + self.PATH, xbmcgui.ListItem( label="* %s" %( LANGUAGE(30006) ) ), isFolder=False ) + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=newNode&actionPath=" % self.ltype + self.PATH, xbmcgui.ListItem( label="* %s" %( LANGUAGE(30007) ) ), isFolder=False ) + if showReset: + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), "plugin://plugin.library.node.editor?ltype=%s&type=delete&actionPath=" % self.ltype + targetDir, xbmcgui.ListItem( label="* %s" %( LANGUAGE(30008) ) ), isFolder=False ) + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + + def RulesList( self ): + # List rules for specific view + rules, nextRule = self.getRules( self.PATH ) + hasContent = False + content = "" + hasOrder = False + hasGroup = False + hasLimit = False + hasPath = False + splitPath = None + rulecount = 0 + if rules is not None: + for rule in rules: + commands = [] + if rule[ 0 ] == "content": + # 1 = Content + listitem = xbmcgui.ListItem( label="%s: %s" % ( LANGUAGE(30200), self.ATTRIB.translateContent( rule[ 1 ] ) ) ) + commands.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=deletenode&actionPath=" % self.ltype + self.PATH + "&node=content)" ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editContent&actionPath=" % self.ltype + self.PATH + hasContent = True + content = rule[ 1 ] + elif rule[ 0 ] == "order": + # 1 = orderby + # 2 = direction (optional?) + if len( rule ) == 3: + translate = self.ORDERBY.translateOrderBy( [ rule[ 1 ], rule[ 2 ] ] ) + listitem = xbmcgui.ListItem( label="%s: %s (%s)" % ( LANGUAGE(30201), translate[ 0 ][ 0 ], translate[ 1 ][ 0 ] ) ) + else: + translate = self.ORDERBY.translateOrderBy( [ rule[ 1 ], "" ] ) + listitem = xbmcgui.ListItem( label="%s: %s" % ( LANGUAGE(30201), translate[ 0 ][ 0 ] ) ) + commands.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=deletenode&actionPath=" % self.ltype + self.PATH + "&node=order)" ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=orderby&actionPath=" % self.ltype + self.PATH + hasOrder = True + elif rule[ 0 ] == "group": + # 1 = group + listitem = xbmcgui.ListItem( label="%s: %s" % ( LANGUAGE(30202), self.ATTRIB.translateGroup( rule[ 1 ] ) ) ) + commands.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=deletenode&actionPath=" % self.ltype + self.PATH + "&node=group)" ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editGroup&actionPath=" % self.ltype + self.PATH + "&value=" + rule[ 1 ] + "&content=" + content + hasGroup = True + elif rule[ 0 ] == "limit": + # 1 = limit + listitem = xbmcgui.ListItem( label="%s: %s" % ( LANGUAGE(30203), rule[ 1 ] ) ) + commands.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=deletenode&actionPath=" % self.ltype + self.PATH + "&node=limit)" ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editLimit&actionPath=" % self.ltype + self.PATH + "&value=" + rule[ 1 ] + hasLimit = True + elif rule[ 0 ] == "path": + # 1 = path + # Split the path into components + splitPath = self.ATTRIB.splitPath( rule[ 1 ] ) + + # Add each element of the path to the list + for x, component in enumerate( splitPath ): + if x == 0: + # library://path/ + listitem = xbmcgui.ListItem( label="%s: %s" % ( LANGUAGE(30204), self.ATTRIB.translatePath( component ) ) ) + commands.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=deletenode&actionPath=" % self.ltype + self.PATH + "&node=path)" ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=addPath&actionPath=" % self.ltype + self.PATH + + # Get the rules + rules = self.PATHRULE.getRulesForPath( splitPath[ 0 ] ) + if x != 0: + # Specific component + + # Add the listitem generated from the last component we processed + listitem.addContextMenuItems( commands, replaceItems = True ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=True ) + commands = [] + + # Get the rule for this component + componentRule = self.PATHRULE.getMatchingRule( component, rules ) + translatedComponent = self.PATHRULE.translateComponent( componentRule, splitPath[ x ] ) + translatedValue = self.PATHRULE.translateValue( componentRule, splitPath, x ) + + listitem = xbmcgui.ListItem( label="%s: %s" % ( translatedComponent, translatedValue ) ) + commands.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=deletePathRule&actionPath=%s&rule=%d)" %( self.ltype, self.PATH, x ) ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=pathRule&actionPath=%s&rule=%d" % ( self.ltype, self.PATH, x ) + hasPath = True + elif rule[ 0 ] == "rule": + # 1 = field + # 2 = operator + # 3 = value (optional) + # 4 = ruleNum + if len(rule) == 3: + translated = self.RULE.translateRule( [ rule[ 1 ], rule[ 2 ] ] ) + else: + translated = self.RULE.translateRule( [ rule[ 1 ], rule[ 2 ], rule[ 3 ] ] ) + if translated[ 2 ][ 0 ] == "|NONE|": + listitem = xbmcgui.ListItem( label="%s: %s %s" % ( LANGUAGE(30205), translated[ 0 ][ 0 ], translated[ 1 ][ 0 ] ) ) + else: + listitem = xbmcgui.ListItem( label="%s: %s %s %s" % ( LANGUAGE(30205), translated[ 0 ][ 0 ], translated[ 1 ][ 0 ], translated[ 2 ][ 1 ] ) ) + commands.append( ( LANGUAGE(30100), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=deleteRule&actionPath=" % self.ltype + self.PATH + "&rule=" + str( rule[ 4 ] ) + ")" ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=rule&actionPath=" % self.ltype + self.PATH + "&rule=" + str( rule[ 4 ] ) + rulecount += 1 + elif rule[ 0 ] == "match": + # 1 = value + listitem = xbmcgui.ListItem( label="%s: %s" % ( LANGUAGE(30206), self.ATTRIB.translateMatch( rule[ 1 ] ) ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editRulesMatch&actionPath=%s" %( self.ltype, self.PATH ) + hasGroup = True + listitem.addContextMenuItems( commands, replaceItems = True ) + if rule[ 0 ] == "rule" or rule[ 0 ] == "order" or rule[ 0 ] == "path": + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=True ) + else: + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + if not hasContent and not hasPath: + # Add content + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=editContent&actionPath=" % self.ltype + self.PATH, xbmcgui.ListItem( label="* %s" %( LANGUAGE(30000) ) ) ) + if not hasOrder and hasContent: + # Add order + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=orderby&actionPath=" % self.ltype + self.PATH, xbmcgui.ListItem( label="* %s" %( LANGUAGE(30002) ) ), isFolder=True ) + if not hasGroup and hasContent: + # Add group + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=editGroup&actionPath=" % self.ltype + self.PATH + "&content=" + content, xbmcgui.ListItem( label="* %s" %( LANGUAGE(30004) ) ) ) + if not hasLimit and hasContent: + # Add limit + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=editLimit&actionPath=" % self.ltype + self.PATH + "&value=25", xbmcgui.ListItem( label="* %s" %( LANGUAGE(30003) ) ) ) + if not hasPath and not hasContent: + # Add path + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=addPath&actionPath=" % self.ltype + self.PATH, xbmcgui.ListItem( label="* %s" %( LANGUAGE(30001) ) ) ) + if hasContent: + # Add rule + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=rule&actionPath=" % self.ltype + self.PATH + "&rule=" + str( nextRule ), xbmcgui.ListItem( label="* %s" %( LANGUAGE(30005) ) ), isFolder = True ) + if hasPath: + if "plugin://" not in splitPath[0][0]: + # Add component + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=pathRule&actionPath=%s&rule=%d" % ( self.ltype, self.PATH, x + 1 ), xbmcgui.ListItem( label="* %s" %( LANGUAGE(30009) ) ), isFolder = True ) + # Manually edit path + xbmcplugin.addDirectoryItem( int( sys.argv[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s&type=editPath&actionPath=" % self.ltype + self.PATH + "&value=" + quote( rule[ 1 ] ), xbmcgui.ListItem( label="* %s" %( LANGUAGE(30010) ) ), isFolder = True ) + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + + def _parse_argv( self ): + try: + p = parse_qs(sys.argv[2][1:]) + for i in p.keys(): + p[i] = p[i][0] + self.PARAMS = p + except: + p = parse_qs(sys.argv[1]) + for i in p.keys(): + p[i] = p[i][0] + self.PARAMS = p + if "path" in self.PARAMS: + self.PATH = self.PARAMS[ "path" ] + else: + self.PATH = "" + + def getRules( self, actionPath, justRules = False ): + returnVal = [] + try: + # Load the xml file + tree = xmltree.parse( actionPath ) + root = tree.getroot() + if justRules == False: + # Look for a 'content' + content = root.find( "content" ) + if content is not None: + returnVal.append( ( "content", content.text ) ) + # Look for an 'order' + order = root.find( "order" ) + if order is not None: + if "direction" in order.attrib: + returnVal.append( ( "order", order.text, order.attrib.get( "direction" ) ) ) + else: + returnVal.append( ( "order", order.text ) ) + # Look for a 'group' + group = root.find( "group" ) + if group is not None: + returnVal.append( ( "group", group.text ) ) + # Look for a 'limit' + limit = root.find( "limit" ) + if limit is not None: + returnVal.append( ( "limit", limit.text ) ) + # Look for a 'path' + path = root.find( "path" ) + if path is not None: + returnVal.append( ( "path", path.text ) ) + # Save the current length of the returnVal - we'll insert Match here if there are two or more rules + currentLen = len( returnVal ) + # Look for any rules + ruleNum = 0 + if actionPath.endswith( "index.xml" ): + # Load the rules from RULE module + rules = self.RULE.getNodeRules( actionPath ) + if rules is not None: + for rule in rules: + returnVal.append( ( "rule", rule[ 0 ], rule[ 1 ], rule[ 2 ], ruleNum ) ) + ruleNum += 1 + return returnVal, len( rules ) + else: + return returnVal, 0 + else: + rules = root.findall( "rule" ) + # Process the rules + if rules is not None: + for rule in rules: + value = rule.find( "value" ) + if value is not None and value.text is not None: + translated = self.RULE.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), value.text ] ) + if not self.RULE.isNodeRule( translated, actionPath ): + returnVal.append( ( "rule", rule.attrib.get( "field" ), rule.attrib.get( "operator" ), value.text, ruleNum ) ) + else: + translated = self.RULE.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), "" ] ) + if not self.RULE.isNodeRule( translated, actionPath ): + returnVal.append( ( "rule", rule.attrib.get( "field" ), rule.attrib.get( "operator" ), "", ruleNum ) ) + ruleNum += 1 + # Get any current match value if there are more than two rules + # (if there's only one, the match value doesn't matter) + if ruleNum >= 2: + matchRules = "all" + match = root.find( "match" ) + if match is not None: + matchRules = match.text + returnVal.insert( currentLen, ( "match", matchRules ) ) + return returnVal, len( rules ) + return returnVal, 0 + except: + print_exc() + + def listNodes( self, targetDir, nodes ): + dirs, files = xbmcvfs.listdir( targetDir ) + for dir in dirs: + self.parseNode( os.path.join( targetDir, dir ), nodes ) + for file in files: + self.parseItem( os.path.join( targetDir, file ), nodes ) + + def parseNode( self, node, nodes ): + # If the folder we've been passed contains an index.xml, send that file to be processed + if os.path.exists( os.path.join( node, "index.xml" ) ): + # BETA2 ONLY CODE + self.RULE.moveNodeRuleToAppdata( node, os.path.join( node, "index.xml" ) ) + # /BETA2 ONLY CODE + self.parseItem( os.path.join( node, "index.xml" ), nodes, True, node ) + + def parseItem( self, file, nodes, isFolder = False, origFolder = None ): + if not isFolder and file.endswith( "index.xml" ): + return + try: + # Load the xml file + tree = xmltree.parse( file ) + root = tree.getroot() + # Get the item index + if "order" in tree.getroot().attrib: + index = tree.getroot().attrib.get( "order" ) + origIndex = index + while int( index ) in nodes: + index = int( index ) + index += 1 + index = str( index ) + else: + self.indexCounter -= 1 + index = str( self.indexCounter ) + origIndex = "-" + # Get label and icon + label = root.find( "label" ).text + icon = root.find( "icon" ) + if icon is not None: + icon = icon.text + else: + icon = "" + # Add it to our list of nodes + if isFolder: + nodes[ int( index ) ] = [ label, icon, quote( origFolder ), "folder", origIndex ] + else: + nodes[ int( index ) ] = [ label, icon, file, "item", origIndex ] + except: + print_exc() + + def getViewElement( self, file, element, newvalue ): + try: + # Load the file + tree = xmltree.parse( file ) + root = tree.getroot() + # Change the element + node = root.find( element ) + if node is not None: + return node.text + else: + return "" + except: + print_exc() + + def changeViewElement( self, file, element, newvalue ): + try: + # Load the file + tree = xmltree.parse( file ) + root = tree.getroot() + # If the element is content, we can only delete this if there are no + # rules, limits, orders + if element == "content": + rule = root.find( "rule" ) + order = root.find( "order" ) + limit = root.find( "limit" ) + if rule is not None or order is not None or limit is not None: + xbmcgui.Dialog().ok( ADDONNAME, LANGUAGE( 30404 ) ) + return + # Find the element + node = root.find( element ) + if node is not None: + # If we've been passed an empty value, delete the node + if newvalue == "": + root.remove( node ) + else: + node.text = newvalue + else: + # Add a new node + if newvalue != "": + xmltree.SubElement( root, element ).text = newvalue + # Pretty print and save + self.indent( root ) + tree.write( file, encoding="UTF-8" ) + except: + print_exc() + + def getRootAttrib( self, file, attrib ): + try: + # Load the file + tree = xmltree.parse( file ) + root = tree.getroot() + # Find the element + if attrib in root.attrib: + return root.attrib.get( attrib ) + else: + return "" + except: + print_exc() + + def changeRootAttrib( self, file, attrib, newvalue ): + try: + # Load the file + tree = xmltree.parse( file ) + root = tree.getroot() + # If empty newvalue, delete the attribute + if newvalue == "": + if attrib in root.attrib: + root.attrib.pop( attrib ) + else: + # Change or add the attribute + root.set( attrib, newvalue ) + # Pretty print and save + self.indent( root ) + tree.write( file, encoding="UTF-8" ) + except: + print_exc() + + def copyNode(self, dirs, files, target, origin): + for file in files: + success = xbmcvfs.copy( os.path.join( origin, file ), os.path.join( target, file ) ) + for dir in dirs: + nextDirs, nextFiles = xbmcvfs.listdir( os.path.join( origin, dir ) ) + self.copyNode( nextDirs, nextFiles, os.path.join( target, dir ), os.path.join( origin, dir ) ) + + # in-place prettyprint formatter + def indent( self, elem, level=0 ): + i = "\n" + level*"\t" + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + "\t" + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + # Slugify functions + def smart_truncate(string, max_length=0, word_boundaries=False, separator=' '): + string = string.strip(separator) + if not max_length: + return string + if len(string) < max_length: + return string + if not word_boundaries: + return string[:max_length].strip(separator) + if separator not in string: + return string[:max_length] + truncated = '' + for word in string.split(separator): + if word: + next_len = len(truncated) + len(word) + len(separator) + if next_len <= max_length: + truncated += '{0}{1}'.format(word, separator) + if not truncated: + truncated = string[:max_length] + return truncated.strip(separator) + + def slugify(self, text, entities=True, decimal=True, hexadecimal=True, max_length=0, word_boundary=False, separator='-', convertInteger=False): + # Handle integers + if convertInteger and text.isdigit(): + text = "NUM-" + text + # decode unicode ( ??? = Ying Shi Ma) + text = unidecode(text) + # character entity reference + if entities: + text = CHAR_ENTITY_REXP.sub(lambda m: unichr(name2codepoint[m.group(1)]), text) + # decimal character reference + if decimal: + try: + text = DECIMAL_REXP.sub(lambda m: unichr(int(m.group(1))), text) + except: + pass + # hexadecimal character reference + if hexadecimal: + try: + text = HEX_REXP.sub(lambda m: unichr(int(m.group(1), 16)), text) + except: + pass + # translate + text = unicodedata.normalize('NFKD', text) + if sys.version_info < (3,): + text = text.encode('ascii', 'ignore') + # replace unwanted characters + text = REPLACE1_REXP.sub('', text.lower()) # replace ' with nothing instead with - + text = REPLACE2_REXP.sub('-', text.lower()) + # remove redundant - + text = REMOVE_REXP.sub('-', text).strip('-') + # smart truncate if requested + if max_length > 0: + text = smart_truncate(text, max_length, word_boundary, '-') + if separator != '-': + text = text.replace('-', separator) + return text + +def getVideoLibraryLType(): + json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "Settings.GetSettingValue", "params": {"setting": "myvideos.flatten"}}') + json_response = json.loads(json_query) + + if json_response.get('result') and json_response['result'].get('value'): + if json_response['result']['value']: + return "video_flat" + + return "video" + +def run(args): + log('script version %s started' % ADDONVERSION) + ltype = '' + if args[2] == '': + videoltype = getVideoLibraryLType() + xbmcplugin.addDirectoryItem( int( args[ 1 ] ), "plugin://plugin.library.node.editor?ltype=%s" %( videoltype ), xbmcgui.ListItem( label=LANGUAGE(30091) ), isFolder=True ) + xbmcplugin.addDirectoryItem( int( args[ 1 ] ), "plugin://plugin.library.node.editor?ltype=music", xbmcgui.ListItem( label=LANGUAGE(30092) ), isFolder=True ) + xbmcplugin.setContent(int(args[1]), 'files') + xbmcplugin.endOfDirectory(handle=int(args[1])) + else: + params = dict( arg.split( "=" ) for arg in args[ 2 ][1:].split( "&" ) ) + ltype = params['ltype'] + if ltype != '': + RULE = rules.RuleFunctions(ltype) + ATTRIB = viewattrib.ViewAttribFunctions(ltype) + PATHRULE = pathrules.PathRuleFunctions(ltype) + PATHRULE.ATTRIB = ATTRIB + ORDERBY = orderby.OrderByFunctions(ltype) + Main(params, ltype, RULE, ATTRIB, PATHRULE, ORDERBY) + + log('script stopped') diff --git a/plugin.library.node.editor/resources/lib/common.py b/plugin.library.node.editor/resources/lib/common.py new file mode 100644 index 0000000000..fdab1c4869 --- /dev/null +++ b/plugin.library.node.editor/resources/lib/common.py @@ -0,0 +1,17 @@ + +import sys +import os +import xbmc, xbmcvfs, xbmcaddon + +ADDON = xbmcaddon.Addon() +ADDONID = ADDON.getAddonInfo('id') +ADDONVERSION = ADDON.getAddonInfo('version') +LANGUAGE = ADDON.getLocalizedString +CWD = ADDON.getAddonInfo('path') +ADDONNAME = ADDON.getAddonInfo('name') +DATAPATH = xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('profile')) +DEFAULTPATH = os.path.join( CWD, 'resources' ) + +def log(txt): + message = u'%s: %s' % (ADDONID, txt) + xbmc.log(msg=message, level=xbmc.LOGDEBUG) diff --git a/plugin.library.node.editor/resources/lib/moveNodes.py b/plugin.library.node.editor/resources/lib/moveNodes.py new file mode 100644 index 0000000000..e234079dd3 --- /dev/null +++ b/plugin.library.node.editor/resources/lib/moveNodes.py @@ -0,0 +1,98 @@ +# coding=utf-8 +import sys +import xbmc, xbmcaddon, xbmcgui + +from resources.lib.common import * + + +def getNewOrder( currentPositions, indexToMove ): + # Show select dialog + w = ShowDialog( "DialogSelect.xml", CWD, order=currentPositions, focus=indexToMove, windowtitle=LANGUAGE(30104) ) + w.doModal() + newOrder = w.newOrder + del w + + return newOrder + +class ShowDialog( xbmcgui.WindowXMLDialog ): + def __init__( self, *args, **kwargs ): + xbmcgui.WindowXMLDialog.__init__( self ) + self.order = kwargs.get( "order" ) + self.windowtitle = kwargs.get( "windowtitle" ) + self.selectedItem = kwargs.get( "focus" ) + self.newOrder = [] + + def onInit(self): + self.list = self.getControl(3) + + # Set visibility + self.getControl(3).setVisible(True) + self.getControl(3).setEnabled(True) + self.getControl(5).setVisible(False) + self.getControl(6).setVisible(False) + self.getControl(1).setLabel(self.windowtitle) + + # Set Cancel label + self.getControl(7).setLabel(xbmc.getLocalizedString(222)) + + # Add all the items to the list + for i, key in enumerate( sorted( self.order ) ): + # Get the label and localise if necessary + label = self.order[ key ][ 0 ] + if label.isdigit(): + label = xbmc.getLocalizedString( int( label ) ) + if label == "": + label = self.order[ key ][ 0 ] + if self.order[ key ][ 3 ] == "folder": + label = "%s >" %( label ) + + # Create the listitem and add it + listitem = xbmcgui.ListItem( label=label ) + self.list.addItem( listitem ) + + # And add it to the list that we'll eventually return + self.newOrder.append( self.order[ key ] ) + + # If it's the item we're moving, save it separately + if i == self.selectedItem: + self.itemMoving = self.order[ key ] + + # Set focus + self.list.selectItem(self.selectedItem) + self.setFocus(self.list) + + def onAction(self, action): + # Check if the selected item has changed + if self.list.getSelectedPosition() != self.selectedItem: + # Remove the item we're moving from the list of items + self.newOrder.pop( self.selectedItem ) + + # Add the item we're moving at its new location + self.newOrder.insert( self.list.getSelectedPosition(), self.itemMoving ) + + # Update its current current position + self.selectedItem = self.list.getSelectedPosition() + + # Update the labels of all list items + for i in range( len( self.newOrder ) ): + item = self.list.getListItem( i ) + label = self.newOrder[ i ][ 0 ] + if label.isdigit(): + label = xbmc.getLocalizedString( int( label ) ) + if label == "": + label = self.newOrder[ i ][ 0 ] + if self.newOrder[ i ][ 3 ] == "folder": + label = "%s >" %( label ) + if item.getLabel() != label: + item.setLabel( label ) + + if action.getId() in ( 9, 10, 92, 216, 247, 257, 275, 61467, 61448, ): + self.close() + return + + def onClick(self, controlID): + if controlID == 7: + # Cancel button + self.newOrder = None + + self.close() diff --git a/plugin.library.node.editor/resources/lib/orderby.py b/plugin.library.node.editor/resources/lib/orderby.py new file mode 100644 index 0000000000..66b882a4bc --- /dev/null +++ b/plugin.library.node.editor/resources/lib/orderby.py @@ -0,0 +1,195 @@ +# coding=utf-8 +import os, sys +import xbmc, xbmcaddon, xbmcplugin, xbmcgui, xbmcvfs +import xml.etree.ElementTree as xmltree +from traceback import print_exc +from urllib.parse import unquote + +from resources.lib.common import * + +class OrderByFunctions(): + def __init__(self, ltype): + self.ltype = ltype + + def _load_rules( self ): + if self.ltype.startswith('video'): + overridepath = os.path.join( DEFAULTPATH , "videorules.xml" ) + else: + overridepath = os.path.join( DEFAULTPATH , "musicrules.xml" ) + try: + tree = xmltree.parse( overridepath ) + return tree + except: + return None + + def translateOrderBy( self, rule ): + # Load the rules + tree = self._load_rules() + hasValue = True + if rule[ 0 ] == "sorttitle": + rule[ 0 ] = "title" + if rule[ 0 ] != "random": + # Get the field we're ordering by + elems = tree.getroot().find( "matches" ).findall( "match" ) + for elem in elems: + if elem.attrib.get( "name" ) == rule[ 0 ]: + match = xbmc.getLocalizedString( int( elem.find( "label" ).text ) ) + else: + # We'll manually set for random + match = xbmc.getLocalizedString( 590 ) + # Get localization of direction + direction = None + elems = tree.getroot().find( "orderby" ).findall( "type" ) + for elem in elems: + if elem.text == rule[ 1 ]: + direction = xbmc.getLocalizedString( int( elem.attrib.get( "label" ) ) ) + directionVal = rule[ 1 ] + if direction is None: + direction = xbmc.getLocalizedString( int( tree.getroot().find( "orderby" ).find( "type" ).attrib.get( "label" ) ) ) + directionVal = tree.getroot().find( "orderby" ).find( "type" ).text + return [ [ match, rule[ 0 ] ], [ direction, directionVal ] ] + + def displayOrderBy( self, actionPath): + try: + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Get the content type + content = root.find( "content" ).text + # Get the order node + orderby = root.find( "order" ) + if orderby is None: + # There is no orderby element, so add one + self.newOrderBy( tree, actionPath ) + orderby = root.find( "order" ) + match = orderby.text + if "direction" in orderby.attrib: + direction = orderby.attrib.get( "direction" ) + else: + direction = "" + translated = self.translateOrderBy( [match, direction ] ) + listitem = xbmcgui.ListItem( label="%s" % ( translated[ 0 ][ 0 ] ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editOrderBy&actionPath=" % self.ltype + actionPath + "&content=" + content + "&default=" + translated[0][1] + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + listitem = xbmcgui.ListItem( label="%s" % ( translated[ 1 ][ 0 ] ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editOrderByDirection&actionPath=" % self.ltype + actionPath + "&default=" + translated[1][1] + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(int(sys.argv[1])) + except: + print_exc() + + def editOrderBy( self, actionPath, content, default ): + # Load all operator groups + tree = self._load_rules().getroot() + elems = tree.find( "matches" ).findall( "match" ) + selectName = [] + selectValue = [] + # Find the matches for the content we've been passed + for elem in elems: + contentMatch = elem.find( content ) + if contentMatch is not None: + selectName.append( xbmc.getLocalizedString( int( elem.find( "label" ).text ) ) ) + selectValue.append( elem.attrib.get( "name" ) ) + # Add a random element + selectName.append( xbmc.getLocalizedString( 590 ) ) + selectValue.append( "random" ) + # Let the user select an operator + selectedOperator = xbmcgui.Dialog().select( LANGUAGE( 30314 ), selectName ) + # If the user selected no operator... + if selectedOperator == -1: + return + returnVal = selectValue[ selectedOperator ] + if returnVal == "title": + returnVal = "sorttitle" + self.writeUpdatedOrderBy( actionPath, field = returnVal ) + + def editDirection( self, actionPath, direction ): + # Load all directions + tree = self._load_rules().getroot() + elems = tree.find( "orderby" ).findall( "type" ) + selectName = [] + selectValue = [] + # Find the group we've been passed and load its operators + for elem in elems: + selectName.append( xbmc.getLocalizedString( int( elem.attrib.get( "label" ) ) ) ) + selectValue.append( elem.text ) + # Let the user select an operator + selectedOperator = xbmcgui.Dialog().select( LANGUAGE( 30315 ), selectName ) + # If the user selected no operator... + if selectedOperator == -1: + return + self.writeUpdatedOrderBy( actionPath, direction = selectValue[ selectedOperator ] ) + + def writeUpdatedOrderBy( self, actionPath, field = None, direction = None ): + # This function writes an updated orderby rule + try: + # Load the xml file + tree = xmltree.parse( unquote(unquote(actionPath)) ) + root = tree.getroot() + # Get all the rules + orderby = root.find( "order" ) + if field is not None: + orderby.text = field + if direction is not None: + orderby.set( "direction", direction ) + # Save the file + self.indent( root ) + tree.write( unquote(actionPath), encoding="UTF-8" ) + except: + print_exc() + + def newOrderBy( self, tree, actionPath ): + # This function adds a new OrderBy, with default match and direction + try: + # Load the xml file + #tree = xmltree.parse( actionPath ) + root = tree.getroot() + # Get the content type + content = root.find( "content" ) + if content is None: + xbmcgui.Dialog().ok( ADDONNAME, LANGUAGE( 30406 ) ) + return + else: + content = content.text + # Find the default match for this content type + ruleTree = self._load_rules().getroot() + elems = ruleTree.find( "matches" ).findall( "match" ) + match = "title" + for elem in elems: + contentCheck = elem.find( content ) + if contentCheck is not None: + # We've found the first match for this type + match = elem.attrib.get( "name" ) + break + if match == "title": + match = "sorttitle" + # Find the default direction + elem = ruleTree.find( "orderby" ).find( "type" ) + direction = elem.text + # Write the new rule + newRule = xmltree.SubElement( root, "order" ) + newRule.text = match + newRule.set( "direction", direction ) + # Save the file + self.indent( root ) + tree.write( unquote( actionPath ), encoding="UTF-8" ) + except: + print_exc() + + # in-place prettyprint formatter + def indent( self, elem, level=0 ): + i = "\n" + level*"\t" + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + "\t" + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + diff --git a/plugin.library.node.editor/resources/lib/pathrules.py b/plugin.library.node.editor/resources/lib/pathrules.py new file mode 100644 index 0000000000..8d68294347 --- /dev/null +++ b/plugin.library.node.editor/resources/lib/pathrules.py @@ -0,0 +1,377 @@ +# coding=utf-8 +import os, sys, shutil +import xbmc, xbmcaddon, xbmcplugin, xbmcgui, xbmcvfs +import xml.etree.ElementTree as xmltree +import json +from traceback import print_exc +from urllib.parse import quote, unquote + +from resources.lib.common import * + +class PathRuleFunctions(): + def __init__(self, ltype): + self.nodeRules = None + self.ATTRIB = None + self.ltype = ltype + + def _load_rules( self ): + if self.ltype.startswith('video'): + overridepath = os.path.join( DEFAULTPATH , "videorules.xml" ) + else: + overridepath = os.path.join( DEFAULTPATH , "musicrules.xml" ) + try: + tree = xmltree.parse( overridepath ) + return tree + except: + return None + + def translateComponent( self, component, splitPath ): + if component[ 0 ] is None: + return splitPath[ 0 ] + if component[0].isdigit(): + string = LANGUAGE( int( component[ 0 ] ) ) + if string != "": return string + return xbmc.getLocalizedString( int( component[ 0 ] ) ) + else: + return component[ 0 ] + + def translateValue( self, rule, splitPath, ruleNum ): + if splitPath[ ruleNum ][ 1 ] == "": + return "<No value>" + + if rule[ 1 ] == "year" or rule[ 2 ] != "integer" or rule[ 3 ] is None: + return splitPath[ ruleNum ][ 1 ] + + try: + value = int( splitPath[ ruleNum ][ 1 ] ) + except: + return splitPath[ ruleNum ][ 1 ] + + json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "Files.GetDirectory", "params": { "properties": ["title"], "directory": "%s", "media": "files" } }' % rule[ 3 ] ) + json_response = json.loads(json_query) + listings = [] + values = [] + # Add all directories returned by the json query + if json_response.get('result') and json_response['result'].get('files') and json_response['result']['files'] is not None: + for item in json_response['result']['files']: + if "id" in item and item[ "id" ] == value: + return "(%d) %s" %( value, item[ "label" ] ) + + # Didn't find a match + return "%s (%s)" %( splitPath[ ruleNum ][ 1 ], xbmc.getLocalizedString(13205) ) + + def displayRule( self, actionPath, ruleNum ): + try: + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Get the path + path = root.find( "path" ).text + # Split the path element + splitPath = self.ATTRIB.splitPath( path ) + # Get the rules + rules = self.getRulesForPath( splitPath[ 0 ] ) + if len( splitPath ) == int( ruleNum ): + # This rule doesn't exist - create it + self.ATTRIB.writeUpdatedPath( actionPath, ( ruleNum, rules[ 0 ][ 1 ], "" ) ) + rule = rules[ 0 ] + translatedValue = "<No value>" + else: + # Find the matching rule + rule = self.getMatchingRule( splitPath[ ruleNum ], rules ) + translatedValue = self.translateValue( rule, splitPath, ruleNum ) + + #Component + listitem = xbmcgui.ListItem( label="%s" % ( self.translateComponent( rule, splitPath[ruleNum] ) ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editPathMatch&actionPath=%s&rule=%d" %( self.ltype, actionPath, ruleNum ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + + # Value + listitem = xbmcgui.ListItem( label="%s" % ( translatedValue ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editPathValue&actionPath=%s&rule=%s" %( self.ltype, actionPath, ruleNum ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + # Browse + if rule[ 3 ] is not None: + listitem = xbmcgui.ListItem( label=LANGUAGE(30107) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=browsePathValue&actionPath=%s&rule=%s" %( self.ltype, actionPath, ruleNum ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + return + except: + log( "Failed" ) + print_exc() + + def getRulesForPath( self, path ): + # This function gets all valid rules for a given path + # Load the rules + tree = self._load_rules() + subSearch = None + content = [] + elems = tree.getroot().find( "paths" ).findall( "type" ) + for elem in elems: + if elem.attrib.get( "name" ) == path[ 0 ]: + for contentType in elem.findall( "content" ): + content.append( contentType.text ) + subSearch = elem + break + + if path[ 1 ] and subSearch is not None: + for elem in subSearch.findall( "type" ): + if elem.attrib.get( "name" ) == path[ 1 ]: + if elem.find( "content" ): + content = [] + for contentType in elem.findall( "content" ): + content.append( contentType.text ) + break + + rules = [] + for rule in tree.getroot().find( "pathRules" ).findall( "rule" ): + # Can it be browsed + if rule.find( "browse" ) is not None: + browse = rule.find( "browse" ).text + else: + browse = None + if len( content ) == 0: + rules.append( ( rule.attrib.get( "label" ), rule.attrib.get( "name" ), rule.find( "value" ).text, browse ) ) + else: + for contentType in rule.findall( "content" ): + if contentType.text in content: + # If the root of the browse is changed dependant on what we're looking at, replace + # it now with the correct content + if browse is not None and "::root::" in browse: + browse = browse.replace( "::root::", content[ 0 ] ) + rules.append( ( rule.attrib.get( "label" ), rule.attrib.get( "name" ), rule.find( "value" ).text, browse ) ) + + if len( rules ) == 0: + return [ ( None, "property", "string", None ) ] + return rules + + def getMatchingRule( self, component, rules ): + # This function matches a component to its rule + for rule in rules: + if rule[ 1 ] == component[ 0 ]: + return rule + + # No rule matched, return an empty one + return ( None, "property", "string", None ) + + def editMatch( self, actionPath, ruleNum ): + # Change the match element of a path component + + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Get the path + path = root.find( "path" ).text + # Split the path element + splitPath = self.ATTRIB.splitPath( path ) + # Get the rules and current value + rules = self.getRulesForPath( splitPath[ 0 ] ) + currentValue = splitPath[ ruleNum ][ 1 ] + + if rules[ 0 ][ 0 ] is None: + # There are no available choices + self.manuallyEditMatch( actionPath, ruleNum, splitPath[ ruleNum ][ 0 ], currentValue ) + return + + # Build list of rules to choose from + selectName = [] + for rule in rules: + selectName.append( self.translateComponent( rule, None ) ) + + # Add a manual option + selectName.append( LANGUAGE( 30408 ) ) + + # Let the user select an operator + selectedOperator = xbmcgui.Dialog().select( LANGUAGE( 30305 ), selectName ) + # If the user selected no operator... + if selectedOperator == -1: + return + elif selectedOperator == len( rules ): + # User choose custom property + self.manuallyEditMatch( actionPath, ruleNum, splitPath[ ruleNum ][ 0 ], currentValue ) + return + else: + self.ATTRIB.writeUpdatedPath( actionPath, ( ruleNum, rules[ selectedOperator ][ 1 ], currentValue ) ) + + def manuallyEditMatch( self, actionPath, ruleNum, currentName, currentValue ): + type = xbmcgui.INPUT_ALPHANUM + returnVal = xbmcgui.Dialog().input( LANGUAGE( 30318 ), currentName, type=type ) + if returnVal != "": + self.ATTRIB.writeUpdatedPath( actionPath, ( ruleNum, returnVal.decode( "utf-8" ), currentValue ) ) + + def editValue( self, actionPath, ruleNum ): + # Let the user edit the value of a path component + + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Get the path + path = root.find( "path" ).text + # Split the path element + splitPath = self.ATTRIB.splitPath( path ) + # Get the rules and current value + rules = self.getRulesForPath( splitPath[ 0 ] ) + rule = self.getMatchingRule( splitPath[ ruleNum ], rules ) + + if rule[ 2 ] == "boolean": + # Let the user select a boolean + selectedBool = xbmcgui.Dialog().select( LANGUAGE( 30307 ), [ xbmc.getLocalizedString(20122), xbmc.getLocalizedString(20424) ] ) + # If the user selected nothing... + if selectedBool == -1: + return + value = "true" + if selectedBool == 1: + value = "false" + self.ATTRIB.writeUpdatedPath( actionPath, ( ruleNum, splitPath[ ruleNum][ 0 ], value ) ) + else: + type = xbmcgui.INPUT_ALPHANUM + if rule[ 2 ] == "integer": + type = xbmcgui.INPUT_NUMERIC + returnVal = xbmcgui.Dialog().input( LANGUAGE( 30307 ), splitPath[ ruleNum ][ 1 ], type=type ) + if returnVal != "": + self.ATTRIB.writeUpdatedPath( actionPath, ( ruleNum, splitPath[ ruleNum ][ 0 ], returnVal ) ) + + + def browse( self, actionPath, ruleNum ): + # This function launches the browser for the given property type + + pDialog = xbmcgui.DialogProgress() + pDialog.create( ADDONNAME, LANGUAGE( 30317 ) ) + + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Get the path + path = root.find( "path" ).text + # Split the path element + splitPath = self.ATTRIB.splitPath( path ) + # Get the rules and current value + rules = self.getRulesForPath( splitPath[ 0 ] ) + rule = self.getMatchingRule( splitPath[ ruleNum ], rules ) + title = self.translateComponent( rule, splitPath[ ruleNum ] ) + + # Get the path we'll be browsing + browsePath = self.getBrowsePath( splitPath, rule[ 3 ], ruleNum ) + + json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "Files.GetDirectory", "params": { "properties": ["title", "file", "thumbnail"], "directory": "%s", "media": "files" } }' % browsePath ) + json_response = json.loads(json_query) + listings = [] + values = [] + # Add all directories returned by the json query + if json_response.get('result') and json_response['result'].get('files') and json_response['result']['files'] is not None: + total = len( json_response[ 'result' ][ 'files' ] ) + for item in json_response['result']['files']: + if item[ "label" ] == "..": + continue + thumb = None + if item[ "thumbnail" ] is not "": + thumb = item[ "thumbnail" ] + listitem = xbmcgui.ListItem(label=item[ "label" ] ) + listitem.setArt({'icon': thumb}) + listitem.setProperty( "thumbnail", thumb ) + listings.append( listitem ) + if rule[ 2 ] == "integer" and "id" in item: + values.append( str( item[ "id" ] ) ) + else: + values.append( item[ "label" ] ) + + pDialog.close() + + if len( listings ) == 0: + # No browsable options found + xbmcgui.Dialog().ok( ADDONNAME, LANGUAGE( 30409 ) ) + return + + # Show dialog + w = ShowDialog( "DialogSelect.xml", CWD, listing=listings, windowtitle=title ) + w.doModal() + selectedItem = w.result + del w + if selectedItem == "" or selectedItem == -1: + return None + + self.ATTRIB.writeUpdatedPath( actionPath, ( ruleNum, splitPath[ ruleNum ][ 0 ], values[ selectedItem ] ) ) + + def getBrowsePath( self, splitPath, newBase, rule ): + # This function replaces the base path with the browse path, and removes the current + # component + + returnText = "" + + if "::root::" in newBase: + newBase = newBase.replace( "::root::", splitPath[ 0 ][ 0 ] ) + + # Enumarate through everything in the existing path + addedQ = False + for x, component in enumerate( splitPath ): + if x != rule: + # Transfer this component to the new path + if x == 0: + returnText = newBase + elif not addedQ: + returnText += "?%s=%s" %( component[ 0 ], quote(component[1]) ) + addedQ = True + else: + returnText += "&%s=%s" %( component[ 0 ], quote(component[1]) ) + return returnText + + # in-place prettyprint formatter + def indent( self, elem, level=0 ): + i = "\n" + level*"\t" + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + "\t" + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + +# ============================ +# === PRETTY SELECT DIALOG === +# ============================ +class ShowDialog( xbmcgui.WindowXMLDialog ): + def __init__( self, *args, **kwargs ): + xbmcgui.WindowXMLDialog.__init__( self ) + self.listing = kwargs.get( "listing" ) + self.windowtitle = kwargs.get( "windowtitle" ) + self.result = -1 + + def onInit(self): + try: + self.fav_list = self.getControl(6) + self.getControl(3).setVisible(False) + except: + print_exc() + self.fav_list = self.getControl(3) + self.getControl(5).setVisible(False) + self.getControl(1).setLabel(self.windowtitle) + for item in self.listing : + listitem = xbmcgui.ListItem(label=item.getLabel(), label2=item.getLabel2()) + listitem.setArt({'icon': item.getProperty( "icon" ), 'thumb': item.getProperty( "thumbnail" )}) + listitem.setProperty( "Addon.Summary", item.getLabel2() ) + self.fav_list.addItem( listitem ) + self.setFocus(self.fav_list) + + def onAction(self, action): + if action.getId() in ( 9, 10, 92, 216, 247, 257, 275, 61467, 61448, ): + self.result = -1 + self.close() + + def onClick(self, controlID): + if controlID == 6 or controlID == 3: + num = self.fav_list.getSelectedPosition() + self.result = num + else: + self.result = -1 + self.close() + + def onFocus(self, controlID): + pass diff --git a/plugin.library.node.editor/resources/lib/pluginBrowser.py b/plugin.library.node.editor/resources/lib/pluginBrowser.py new file mode 100644 index 0000000000..1b7a8a1ec7 --- /dev/null +++ b/plugin.library.node.editor/resources/lib/pluginBrowser.py @@ -0,0 +1,54 @@ +# coding=utf-8 +import sys +import xbmc, xbmcaddon, xbmcgui +import json + +from resources.lib.common import * + +def getPluginPath( ltype, location = None ): + listings = [] + listingsLabels = [] + + if location is not None: + # If location given, add 'create' item + listings.append( "::CREATE::" ) + listingsLabels.append( LANGUAGE( 30411 ) ) + else: + # If no location, build default + if location is None: + if ltype.startswith( "video" ): + location = "addons://sources/video" + else: + location = "addons://sources/audio" + + # Show a waiting dialog, then get the listings for the directory + dialog = xbmcgui.DialogProgress() + dialog.create( ADDONNAME, LANGUAGE( 30410 ) ) + + json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "Files.GetDirectory", "params": { "properties": ["title", "file", "thumbnail", "episode", "showtitle", "season", "album", "artist", "imdbnumber", "firstaired", "mpaa", "trailer", "studio", "art"], "directory": "' + location + '", "media": "files" } }') + json_response = json.loads(json_query) + + # Add all directories returned by the json query + if json_response.get('result') and json_response['result'].get('files') and json_response['result']['files']: + json_result = json_response['result']['files'] + + for item in json_result: + if item[ "file" ].startswith( "plugin://" ): + listings.append( item[ "file" ] ) + listingsLabels.append( "%s >" %( item[ "label" ] ) ) + + # Close progress dialog + dialog.close() + + selectedItem = xbmcgui.Dialog().select( LANGUAGE( 30309 ), listingsLabels ) + + if selectedItem == -1: + # User cancelled + return None + + selectedAction = listings[ selectedItem ] + if selectedAction == "::CREATE::": + return location + else: + # User has chosen a sub-level to display, add details and re-call this function + return getPluginPath(ltype, selectedAction) diff --git a/plugin.library.node.editor/resources/lib/rules.py b/plugin.library.node.editor/resources/lib/rules.py new file mode 100644 index 0000000000..7bb708d05b --- /dev/null +++ b/plugin.library.node.editor/resources/lib/rules.py @@ -0,0 +1,1128 @@ +# coding=utf-8 +import os, sys, shutil +import xbmc, xbmcaddon, xbmcplugin, xbmcgui, xbmcvfs +import xml.etree.ElementTree as xmltree +import json +from traceback import print_exc +from urllib.parse import quote, unquote + +from resources.lib.common import * + +class RuleFunctions(): + def __init__(self, ltype): + self.nodeRules = None + self.ltype = ltype + + def _load_rules( self ): + if self.ltype.startswith('video'): + overridepath = os.path.join( DEFAULTPATH , "videorules.xml" ) + else: + overridepath = os.path.join( DEFAULTPATH , "musicrules.xml" ) + try: + tree = xmltree.parse( overridepath ) + return tree + except: + return None + + def translateRule( self, rule ): + # Load the rules + tree = self._load_rules() + hasValue = True + elems = tree.getroot().find( "matches" ).findall( "match" ) + for elem in elems: + if elem.attrib.get( "name" ) == rule[ 0 ]: + match = xbmc.getLocalizedString( int( elem.find( "label" ).text ) ) + group = elem.find( "operator" ).text + elems = tree.getroot().find( "operators" ).findall( "group" ) + operator = None + defaultOperator = None + defaultOperatorValue = None + for elem in elems: + if elem.attrib.get( "name" ) == group: + for operators in elem.findall( "operator" ): + if operators.text == rule[ 1 ]: + operator = xbmc.getLocalizedString( int( operators.attrib.get( "label" ) ) ) + if defaultOperator is None: + defaultOperator = xbmc.getLocalizedString( int( operators.attrib.get( "label" ) ) ) + defaultOperatorValue = operators.text + if "option" in elem.attrib: + hasValue = False + # If we didn't match an operator, set it to the default + if operator is None: + operator = defaultOperator + rule[ 1 ] = defaultOperatorValue + if hasValue == False: + return [ [ match, rule[ 0 ] ], [ operator, group, rule[ 1 ] ], [ "|NONE|", "<No value>" ] ] + if len( rule ) == 2 or rule[ 2 ] == "" or rule[ 2 ] is None: + return [ [ match, rule[ 0 ] ], [ operator, group, rule[ 1 ] ], [ "", "<No value>" ] ] + return [ [ match, rule[ 0 ] ], [ operator, group, rule[ 1 ] ], [ rule[ 2 ], rule[ 2 ] ] ] + + def displayRule( self, actionPath, path, ruleNum ): + if actionPath.endswith( "index.xml" ): + # If this is a parent node, call alternative function + self.displayNodeRule( actionPath, ruleNum ) + return + try: + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + actionPath = actionPath + # Get the content type + content = root.find( "content" ) + if content is not None: + content = content.text + else: + content = "NONE" + # Get all the rules + ruleCount = 0 + rules = root.findall( "rule" ) + if len( rules ) == int( ruleNum ): + # This rule doesn't exist - create it + self.newRule( tree, unquote( actionPath ) ) + rules = root.findall( "rule" ) + if rules is not None: + for rule in rules: + if str( ruleCount ) == ruleNum: + value = rule.find( "value" ) + if value is None: + value = "" + else: + value = value.text + translated = self.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), value ] ) + # Rule to change match + listitem = xbmcgui.ListItem( label="%s" % ( translated[ 0 ][ 0 ] ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editMatch&actionPath=" % self.ltype + actionPath + "&content=" + content + "&default=" + translated[ 0 ][ 1 ] + "&rule=" + str( ruleCount ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + listitem = xbmcgui.ListItem( label="%s" % ( translated[ 1 ][ 0 ] ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editOperator&actionPath=" % self.ltype + actionPath + "&group=" + translated[ 1 ][ 1 ] + "&default=" + translated[ 1 ][ 2 ] + "&rule=" + str( ruleCount ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + if not ( translated[ 2 ][ 0 ] ) == "|NONE|": + listitem = xbmcgui.ListItem( label="%s" % ( translated[ 2 ][ 1 ] ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editValue&actionPath=" % self.ltype + actionPath + "&rule=" + str( ruleCount ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + # Check if this match type can be browsed + if self.canBrowse( translated[ 0 ][ 1 ], content ): + #listitem.addContextMenuItems( [(LANGUAGE(30107), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=browseValue&actionPath=" % ltype + actionPath + "&rule=" + str( ruleCount ) + "&match=" + translated[ 0 ][ 1 ] + "&content=" + content + ")" )], replaceItems = True ) + listitem = xbmcgui.ListItem( label=LANGUAGE(30107) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=browseValue&actionPath=" % self.ltype + actionPath + "&rule=" + str( ruleCount ) + "&match=" + translated[ 0 ][ 1 ] + "&content=" + content + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + #self.browse( translated[ 0 ][ 1 ], content ) + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + return + ruleCount = ruleCount + 1 + except: + print_exc() + + def editMatch( self, actionPath, ruleNum, content, default ): + # Load all operator groups + tree = self._load_rules().getroot() + elems = tree.find( "matches" ).findall( "match" ) + selectName = [] + selectValue = [] + # Find the matches for the content we've been passed + for elem in elems: + if content != "NONE": + contentMatch = elem.find( content ) + if contentMatch is not None: + selectName.append( xbmc.getLocalizedString( int( elem.find( "label" ).text ) ) ) + selectValue.append( elem.attrib.get( "name" ) ) + else: + pass + else: + selectName.append( xbmc.getLocalizedString( int( elem.find( "label" ).text ) ) ) + selectValue.append( elem.attrib.get( "name" ) ) + # Let the user select an operator + selectedOperator = xbmcgui.Dialog().select( LANGUAGE( 30305 ), selectName ) + # If the user selected no operator... + if selectedOperator == -1: + return + self.writeUpdatedRule( actionPath, ruleNum, match = selectValue[ selectedOperator ] ) + + def editOperator( self, actionPath, ruleNum, group, default ): + # Load all operator groups + tree = self._load_rules().getroot() + elems = tree.find( "operators" ).findall( "group" ) + selectName = [] + selectValue = [] + # Find the group we've been passed and load its operators + for elem in elems: + if elem.attrib.get( "name" ) == group: + for operators in elem.findall( "operator" ): + selectName.append( xbmc.getLocalizedString( int( operators.attrib.get( "label" ) ) ) ) + selectValue.append( operators.text ) + # Let the user select an operator + selectedOperator = xbmcgui.Dialog().select( LANGUAGE( 30306 ), selectName ) + # If the user selected no operator... + if selectedOperator == -1: + return + self.writeUpdatedRule( actionPath, ruleNum, operator = selectValue[ selectedOperator ] ) + + def editValue( self, actionPath, ruleNum ): + # This function is the entry point for editing the value of a rule + # Because we can't always pass the current value through the uri, we first need + # to retrieve it, and the operator data type + actionPath = unquote(actionPath) + try: + if actionPath.endswith( "index.xml" ): + ( filePath, fileName ) = os.path.split(unquote(actionPath)) + # Load the rules file + if self.ltype.startswith('video'): + tree = xmltree.parse( os.path.join( DATAPATH, "videorules.xml" ) ) + else: + tree = xmltree.parse( os.path.join( DATAPATH, "musicrules.xml" ) ) + root = tree.getroot() + nodes = root.findall( "node" ) + for node in nodes: + if node.attrib.get( "name" ) == filePath: + rules = node.findall( "rule" ) + ruleCount = 0 + for rule in rules: + if ruleCount == int( ruleNum ): + # This is the rule we'll be updating + # Get the current value + curValue = rule.find( "value" ) + if curValue is None: + curValue = "" + else: + curValue = curValue.text + match = rule.attrib.get( "field" ) + operator = rule.attrib.get( "operator" ) + ruleCount += 1 + else: + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Get all the rules + ruleCount = 0 + rules = root.findall( "rule" ) + if rules is not None: + for rule in rules: + if str( ruleCount ) == ruleNum: + # This is the rule we'll be updating + # Get the current value + curValue = rule.find( "value" ) + if curValue is None: + curValue = "" + else: + curValue = curValue.text + match = rule.attrib.get( "field" ) + operator = rule.attrib.get( "operator" ) + ruleCount = ruleCount + 1 + # Now, use the match value to get the group of operators this + # comes from (this will tell us the data type in all types + # but "date") + tree = self._load_rules().getroot() + elems = tree.find( "matches" ).findall( "match" ) + for elem in elems: + if elem.attrib.get( "name" ) == match: + group = elem.find( "operator" ).text + if group == "date": + # We probably should go through the tree again, but we'll just check + # for string ending in "inthelast", and switch the type to numeric + if operator.endswith( "inthelast" ): + group = "numeric" + # Set the type of text entry dialog to be used + if group == "string": + type = xbmcgui.INPUT_ALPHANUM + if group == "numeric": + type = xbmcgui.INPUT_NUMERIC + if group == "time": + type = xbmcgui.INPUT_TIME + if group == "date": + type = xbmcgui.INPUT_DATE + if group == "isornot": + type = xbmcgui.INPUT_ALPHANUM + if group == "seconds": + type = xbmcgui.INPUT_TIME + # album duration is in HH:MM:SS but we can't call that input dialog from python so + # find and strip any seconds that may be in an existing node. Use the HH:MM dialog + # and add the seconds field back on before we write the node + secPos = -1 + if curValue is not None: + secPos = curValue.rfind(":00") + if secPos == -1: + secPos = len( curValue) + if ((curValue is not None) and (secPos >= 4)): + curValue = curValue[0:secPos] + if len( curValue ) < 5: + curValue = " " + curValue + returnVal = xbmcgui.Dialog().input( LANGUAGE( 30307 ), curValue, type=type ) + if ( group == "seconds" and returnVal !="" ): + returnVal += ":00"# Add the seconds if we previously removed them + if returnVal != "": + self.writeUpdatedRule( unquote(actionPath), ruleNum, value=returnVal ) + except: + print_exc() + + def writeUpdatedRule( self, actionPath, ruleNum, match = None, operator = None, value = None ): + # This function writes an updated match, operator or value to a rule + if actionPath.endswith( "index.xml" ): + # This is a parent node rule, so call the relevant function + #( filePath, fileName ) = os.path.split( actionPath ) + #self.editNodeRule( filePath, originalRule, translated ) + self.editNodeRule( unquote(actionPath), ruleNum, match, operator, value ) + return + try: + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Get all the rules + ruleCount = 0 + rules = root.findall( "rule" ) + if rules is not None: + for rule in rules: + if str( ruleCount ) == ruleNum: + # This is the rule we're updating + valueElem = rule.find( "value" ) + if match is None: + match = rule.attrib.get( "field" ) + if operator is None: + operator = rule.attrib.get( "operator" ) + if value is None: + if valueElem is None: + value = "" + else: + value = valueElem.text + if value is None: + value = "" + translated = self.translateRule( [ match, operator, value ] ) + # Update the rule + rule.set( "field", translated[ 0 ][ 1 ] ) + rule.set( "operator", translated[ 1 ][ 2 ] ) + if len( translated ) == 3: + if rule.find( "value" ) == None: + # Create a new rule node + xmltree.SubElement( rule, "value" ).text = translated[ 2 ][ 0 ] + else: + rule.find( "value" ).text = translated[ 2 ][ 0 ] + ruleCount = ruleCount + 1 + # Save the file + self.indent( root ) + tree.write( unquote(actionPath), encoding="UTF-8" ) + except: + print_exc() + + def newRule( self, tree, actionPath ): + # This function adds a new rule, with default match and operator, no value + try: + # Load the xml file + # tree = xmltree.parse( actionPath ) + root = tree.getroot() + # Get the content type + content = root.find( "content" ) + # Find the default match for this content type + ruleTree = self._load_rules().getroot() + elems = ruleTree.find( "matches" ).findall( "match" ) + match = "title" + for elem in elems: + if content is not None: + contentCheck = elem.find( content.text ) + if contentCheck is not None: + # We've found the first match for this type + match = elem.attrib.get( "name" ) + operator = elem.find( "operator" ).text + break + else: + # We've found the first match for this type + match = elem.attrib.get( "name" ) + operator = elem.find( "operator" ).text + break + # Find the default operator for this match + elems = ruleTree.find( "operators" ).findall( "group" ) + for elem in elems: + if elem.attrib.get( "name" ) == operator: + operator = elem.find( "operator" ).text + break + # Write the new rule + newRule = xmltree.SubElement( root, "rule" ) + newRule.set( "field", match ) + newRule.set( "operator", operator ) + xmltree.SubElement( newRule, "value" ) + # Save the file + self.indent( root ) + tree.write( unquote(actionPath), encoding="UTF-8" ) + if actionPath.endswith( "index.xml" ): + ( filePath, fileName ) = os.path.split( actionPath ) + newRule = self.translateRule( [ match, operator, "" ] ) + self.addNodeRule( filePath, newRule ) + except: + print_exc() + + def deleteRule( self, actionPath, ruleNum ): + # This function deletes a rule + result = xbmcgui.Dialog().yesno(ADDONNAME, LANGUAGE( 30405 ) ) + if not result: + return + if actionPath.endswith( "index.xml" ): + # This is a parent node rule, so call the relevant function + #( filePath, fileName ) = os.path.split( actionPath ) + #self.deleteNodeRule( filePath, originalRule ) + self.deleteNodeRule( unquote(actionPath), ruleNum ) + try: + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Get all the rules + ruleCount = 0 + rules = root.findall( "rule" ) + if rules is not None: + for rule in rules: + if str( ruleCount ) == ruleNum: + # This is the rule we want to delete + if actionPath.endswith( "index.xml" ): + # Translate the rule, so we can delete it from the views + valueElem = rule.find( "value" ) + origMatch = rule.attrib.get( "field" ) + origOperator = rule.attrib.get( "operator" ) + if valueElem is None: + origValue = "" + else: + origValue = valueElem.text + originalRule = self.translateRule( [ origMatch, origOperator, origValue ] ) + # Delete the rule + root.remove( rule ) + break + ruleCount = ruleCount + 1 + # Save the file + self.indent( root ) + tree.write( unquote(actionPath), encoding="UTF-8" ) + except: + print_exc() + + # Functions for managing rules in all views + def displayNodeRule( self, actionPath, ruleNum ): + # This function will load and display a parent node rule + # (and create one, if the ruleNum specified doesn't exist) + # Split the actionPath, to make things easier + ( filePath, fileName ) = os.path.split( unquote(actionPath) ) + try: + # Load the rules file + if self.ltype.startswith('video'): + tree = xmltree.parse( os.path.join( DATAPATH, "videorules.xml" ) ) + else: + tree = xmltree.parse( os.path.join( DATAPATH, "musicrules.xml" ) ) + root = tree.getroot() + # Find the relevant node + nodes = root.findall( "node" ) + if nodes is None: + self.newNodeRule( actionPath, ruleNum ) + return + ruleNode = None + for node in nodes: + if node.attrib.get( "name" ) == filePath: + ruleNode = node + break + if ruleNode == None: + self.newNodeRule( actionPath, ruleNum ) + return + # Find the relevant rule + rules = node.findall( "rule" ) + if rules is None or len( rules ) == int( ruleNum ): + self.newNodeRule( actionPath, ruleNum ) + return + ruleCount = 0 + for rule in rules: + if ruleCount == int( ruleNum ): + value = rule.find( "value" ) + if value is None: + value = "" + else: + value = value.text + translated = self.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), value ] ) + actionPath = quote( actionPath ) + # Rule to change match + listitem = xbmcgui.ListItem( label="%s" % ( translated[ 0 ][ 0 ] ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editMatch&actionPath=" % self.ltype + actionPath + "&default=" + translated[ 0 ][ 1 ] + "&rule=" + str( ruleCount ) + "&content=NONE" + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + + listitem = xbmcgui.ListItem( label="%s" % ( translated[ 1 ][ 0 ] ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editOperator&actionPath=" % self.ltype + actionPath + "&group=" + translated[ 1 ][ 1 ] + "&default=" + translated[ 1 ][ 2 ] + "&rule=" + str( ruleCount ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + if not ( translated[ 2 ][ 0 ] ) == "|NONE|": + listitem = xbmcgui.ListItem( label="%s" % ( translated[ 2 ][ 1 ] ) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=editValue&actionPath=" % self.ltype + actionPath + "&rule=" + str( ruleCount ) + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + # Check if this match type can be browsed + if self.canBrowse( translated[ 0 ][ 1 ] ): + #listitem.addContextMenuItems( [(LANGUAGE(30107), "RunPlugin(plugin://plugin.library.node.editor?ltype=%s&type=browseValue&actionPath=" % ltype + actionPath + "&rule=" + str( ruleCount ) + "&match=" + translated[ 0 ][ 1 ] + "&content=" + content + ")" )], replaceItems = True ) + listitem = xbmcgui.ListItem( label=LANGUAGE(30107) ) + action = "plugin://plugin.library.node.editor?ltype=%s&type=browseValue&actionPath=" % self.ltype + actionPath + "&rule=" + str( ruleCount ) + "&match=" + translated[ 0 ][ 1 ] + "&content=NONE" + xbmcplugin.addDirectoryItem( int(sys.argv[ 1 ]), action, listitem, isFolder=False ) + #self.browse( translated[ 0 ][ 1 ], content ) + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) + return + ruleCount += 1 + except: + print_exc() + self.newNodeRule( actionPath, ruleNum ) + return + + def newNodeRule( self, actionPath, ruleNum ): + # This function creates a new node rule, then re-calls the displayNodeRule function + # Split the actionPath, to make things easier + ( filePath, fileName ) = os.path.split( unquote(actionPath) ) + # Open the rules file if it exists, else create it + if self.ltype.startswith('video'): + rulesfile = 'videorules.xml' + else: + rulesfile = 'musicrules.xml' + if os.path.exists( os.path.join( DATAPATH, rulesfile ) ): + tree = xmltree.parse( os.path.join( DATAPATH, rulesfile ) ) + root = tree.getroot() + else: + tree = xmltree.ElementTree( xmltree.Element( "rules" ) ) + root = tree.getroot() + # See if we already have a element for the node we're parsing + nodes = root.findall( "node" ) + ruleNode = None + if nodes is not None: + for node in nodes: + if node.attrib.get( "name" ) == filePath: + ruleNode = node + break + if ruleNode is None: + # We couldn't find an existing element for the node we're parsing - so create one + ruleNode = xmltree.SubElement( root, "node" ) + ruleNode.set( "name", filePath ) + # Create a new rule + ruleTree = self._load_rules().getroot() + elems = ruleTree.find( "matches" ).findall( "match" ) + match = "title" + for elem in elems: + # We've found the first match for this type + match = elem.attrib.get( "name" ) + operator = elem.find( "operator" ).text + break + # Find the default operator for this match + elems = ruleTree.find( "operators" ).findall( "group" ) + for elem in elems: + if elem.attrib.get( "name" ) == operator: + operator = elem.find( "operator" ).text + break + # Write the new rule + newRule = xmltree.SubElement( ruleNode, "rule" ) + newRule.set( "field", match ) + newRule.set( "operator", operator ) + xmltree.SubElement( newRule, "value" ) + # Save the file + self.indent( root ) + tree.write( os.path.join( DATAPATH, rulesfile ), encoding="UTF-8" ) + # Now add the rule to all views within the node + dirs, files = xbmcvfs.listdir( filePath ) + for file in files: + if file == "index.xml": + continue + elif file.endswith( ".xml" ): + filename = os.path.join( filePath, file ) + try: + # Load the xml file + tree = xmltree.parse( filename ) + root = tree.getroot() + rule = xmltree.SubElement( root, "rule" ) + rule.set( "field", match ) + rule.set( "operator", operator ) + xmltree.SubElement( rule, "value" ) + # Save the file + self.indent( root ) + tree.write( filename, encoding="UTF-8" ) + except: + print_exc() + # Re-call the displayNodeRule function + self.displayNodeRule( actionPath, ruleNum ) + + #def editNodeRule( self, actionPath, originalRule, newRule ): + def editNodeRule( self, actionPath, ruleNum, match, operator, value ): + ( filePath, fileName ) = os.path.split( unquote(actionPath) ) + # Update the rule in the rules file + if self.ltype.startswith('video'): + rulesfile = 'videorules.xml' + else: + rulesfile = 'musicrules.xml' + try: + tree = xmltree.parse( os.path.join( DATAPATH, rulesfile ) ) + root = tree.getroot() + nodes = root.findall( "node" ) + for node in nodes: + if node.attrib.get( "name" ) == filePath: + ruleCount = 0 + rules = node.findall( "rule" ) + for rule in rules: + if ruleCount == int( ruleNum ): + # This is the rule we want to update + valueElem = rule.find( "value" ) + if match is None: + match = rule.attrib.get( "field" ) + if operator is None: + operator = rule.attrib.get( "operator" ) + if value is None: + if valueElem is None: + value = "" + else: + value = valueElem.text + if value is None: + value = "" + newRule = self.translateRule( [ match, operator, value ] ) + # Get the original rule + origMatch = rule.attrib.get( "field" ) + origOperator = rule.attrib.get( "operator" ) + if valueElem is None: + origValue = "" + else: + origValue = valueElem.text + originalRule = self.translateRule( [ origMatch, origOperator, origValue ] ) + # Update the rule + rule.set( "field", newRule[ 0 ][ 1 ] ) + rule.set( "operator", newRule[ 1 ][ 2 ] ) + if len( newRule ) == 3: + if rule.find( "value" ) == None: + # Create a new rule node + xmltree.SubElement( rule, "value" ).text = newRule[ 2 ][ 0 ] + else: + rule.find( "value" ).text = newRule[ 2 ][ 0 ] + ruleCount += 1 + # Save the file + self.indent( root ) + tree.write( os.path.join( DATAPATH, rulesfile ), encoding="UTF-8" ) + except: + print_exc() + return + # Now update the views with the new rule + dirs, files = xbmcvfs.listdir( filePath ) + for file in files: + if file == "index.xml": + continue + elif file.endswith( ".xml" ): + filename = os.path.join( filePath, file ) + # List the rules + try: + # Load the xml file + tree = xmltree.parse( filename ) + root = tree.getroot() + # Look for any rules + rules = root.findall( "rule" ) + if rules is not None: + for rule in rules: + value = rule.find( "value" ) + if value is not None and value.text is not None: + translated = self.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), value.text ] ) + else: + translated = self.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), "" ] ) + if originalRule[ 0 ][ 1 ] == translated[ 0 ][ 1 ] and originalRule[ 1 ][ 2 ] == translated[ 1 ][ 2 ] and originalRule[ 2 ][ 0 ] == translated[ 2 ][ 0 ]: + # This is the right rule, update it + rule.set( "field", newRule[ 0 ][ 1 ] ) + rule.set( "operator", newRule[ 1 ][ 2 ] ) + if value is not None: + value.text = newRule[ 2 ][ 0 ] + else: + xmltree.SubElement( rule, "value" ).text = newRule[ 2 ][ 0 ] + break + # Save the file + self.indent( root ) + tree.write( filename, encoding="UTF-8" ) + except: + print_exc() + + #def deleteNodeRule( self, actionPath, originalRule ): + def deleteNodeRule( self, actionPath, ruleNum ): + ( filePath, fileName ) = os.path.split( actionPath ) + # Delete the rule from the rules file + if self.ltype.startswith('video'): + rulesfile = 'videorules.xml' + else: + rulesfile = 'musicrules.xml' + try: + tree = xmltree.parse( os.path.join( DATAPATH, rulesfile ) ) + root = tree.getroot() + nodes = root.findall( "node" ) + for node in nodes: + if node.attrib.get( "name" ) == filePath: + ruleCount = 0 + rules = node.findall( "rule" ) + for rule in rules: + if ruleCount == int( ruleNum ): + # This is the rule we want to delete + # Translate the rule, so we can delete it from the views + valueElem = rule.find( "value" ) + origMatch = rule.attrib.get( "field" ) + origOperator = rule.attrib.get( "operator" ) + if valueElem is None: + origValue = "" + else: + origValue = valueElem.text + originalRule = self.translateRule( [ origMatch, origOperator, origValue ] ) + node.remove( rule ) + ruleCount += 1 + # Save the file + self.indent( root ) + tree.write( os.path.join( DATAPATH, rulesfile ), encoding="UTF-8" ) + except: + print_exc() + return + # Now delete the rule from all the views + dirs, files = xbmcvfs.listdir( filePath ) + for file in files: + if file == "index.xml": + continue + elif file.endswith( ".xml" ): + filename = os.path.join( filePath, file ) + # List the rules + try: + # Load the xml file + tree = xmltree.parse( filename ) + root = tree.getroot() + # Look for any rules + rules = root.findall( "rule" ) + if rules is not None: + for rule in rules: + value = rule.find( "value" ) + if value is not None and value.text is not None: + translated = self.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), value.text ] ) + else: + translated = self.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), "" ] ) + + if originalRule[ 0 ][ 1 ] == translated[ 0 ][ 1 ] and originalRule[ 1 ][ 2 ] == translated[ 1 ][ 2 ] and originalRule[ 2 ][ 0 ] == translated[ 2 ][ 0 ]: + # This is the right rule, delete it + root.remove( rule ) + break + # Save the file + self.indent( root ) + tree.write( filename, encoding="UTF-8" ) + except: + print_exc() + + def deleteAllNodeRules( self, actionPath ): + if self.ltype.startswith('video'): + rulesfile = 'videorules.xml' + else: + rulesfile = 'musicrules.xml' + try: + # Remove all rules for this parent node from the rules file + tree = xmltree.parse( os.path.join( DATAPATH, rulesfile ) ) + root = tree.getroot() + nodes = root.findall( "node" ) + for node in nodes: + if node.attrib.get( "name" ) == actionPath: + root.remove( node ) + # Write the updated file + self.indent( root ) + tree.write( os.path.join( DATAPATH, rulesfile ), encoding="UTF-8" ) + except: + print_exc() + + def isNodeRule( self, viewRule, actionPath ): + if actionPath.endswith( "index.xml" ): + return False + if self.nodeRules is None: + self.nodeRules = [] + # Load all the node rules for current directory + ( filePath, fileName ) = os.path.split( actionPath ) + self.loadNodeRules( filePath ) + # If there are no node rules, return False + if len( self.nodeRules ) == 0: + return False + + # Compare the passed in rule with those in self.nodeRules + count = 0 + for nodeRule in self.nodeRules: + if nodeRule[0] == viewRule[0][1] and nodeRule[1] == viewRule[1][2] and nodeRule[2] == viewRule[2][0]: + # Rule matches + self.nodeRules.pop( count ) + return True + count += 1 + return False + + def addAllNodeRules( self, actionPath, root ): + if self.nodeRules is None: + self.loadNodeRules( actionPath ) + if len( self.nodeRules ) == 0: + return + for nodeRule in self.nodeRules: + rule = xmltree.SubElement( root, "rule" ) + rule.set( "field", nodeRule[ 0 ] ) + rule.set( "operator", nodeRule[ 1 ] ) + xmltree.SubElement( rule, "value" ).text = nodeRule[ 2 ] + + def getNodeRules( self, actionPath ): + ( filePath, fileName ) = os.path.split( actionPath ) + if self.nodeRules is None: + self.loadNodeRules( filePath ) + if len( self.nodeRules ) == 0: + return None + else: + return self.nodeRules + + def loadNodeRules( self, actionPath ): + self.nodeRules = [] + # Load all the node rules for current directory + #actionPath = os.path.join( actionPath, "index.xml" ) + if self.ltype.startswith('video'): + filename = os.path.join( DATAPATH, "videorules.xml" ) + else: + filename = os.path.join( DATAPATH, "musicrules.xml" ) + if os.path.exists( filename ): + try: + # Load the xml file + tree = xmltree.parse( filename ) + root = tree.getroot() + # Find the node element for this path + nodes = root.findall( "node" ) + if nodes is None: + return + ruleNode = None + for node in nodes: + if node.attrib.get( "name" ) == actionPath: + ruleNode = node + break + if ruleNode is None: + # There don't appear to be any rules + return + # Look for any rules + rules = ruleNode.findall( "rule" ) + if rules is not None: + for rule in rules: + value = rule.find( "value" ) + if value is not None and value.text is not None: + translated = self.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), value.text ] ) + else: + translated = self.translateRule( [ rule.attrib.get( "field" ), rule.attrib.get( "operator" ), "" ] ) + # Save the rule + self.nodeRules.append( [ translated[0][1], translated[1][2], translated[2][0] ] ) + except: + print_exc() + + def moveNodeRuleToAppdata( self, path, actionPath ): + #BETA2 ONLY CODE + # This function will move any parent node rules out of the index.xml, and into the rules file in the plugins appdata folder + # Open the rules file if it exists, else create it + if self.ltype.startswith('video'): + rulesfile = 'videorules.xml' + else: + rulesfile = 'musicrules.xml' + if os.path.exists( os.path.join( DATAPATH, rulesfile ) ): + ruleTree = xmltree.parse( os.path.join( DATAPATH, rulesfile ) ) + ruleRoot = ruleTree.getroot() + else: + ruleTree = xmltree.ElementTree( xmltree.Element( "rules" ) ) + ruleRoot = ruleTree.getroot() + # See if we already have a element for the node we're parsing + nodes = ruleRoot.findall( "node" ) + ruleNode = None + if nodes is not None: + for node in nodes: + if node.attrib.get( "name" ) == path: + ruleNode = node + break + if ruleNode is None: + # We couldn't find an existing element for the node we're parsing - so create one + ruleNode = xmltree.SubElement( ruleRoot, "node" ) + ruleNode.set( "name", path ) + try: + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + rules = root.findall( "rule" ) + if rules is not None: + for rule in rules: + # Create a new rule in the ruleTree + newRule = xmltree.SubElement( ruleNode, "rule" ) + newRule.set( "field", rule.attrib.get( "field" ) ) + newRule.set( "operator", rule.attrib.get( "operator" ) ) + value = rule.find( "value" ) + if value is not None: + xmltree.SubElement( newRule, "value" ).text = value.text + # Delete the rule from the tree + root.remove( rule ) + # Write both files + self.indent( root ) + tree.write( unquote(actionPath), encoding="UTF-8" ) + self.indent( ruleRoot ) + ruleTree.write( os.path.join( DATAPATH, rulesfile ), encoding="UTF-8" ) + except: + print_exc() + #/BETA2 ONLY CODE + + # Functions for browsing for value + def canBrowse( self, match, content = None ): + # Check whether the match type allows browsing + if content == "NONE": + content = None + # Load the rules + tree = self._load_rules() + elems = tree.getroot().find( "matches" ).findall( "match" ) + for elem in elems: + if elem.attrib.get( "name" ) == match: + canBrowse = elem.find( "browse" ) + if canBrowse is None: + # This match type is marked as non-browsable + return False + if content is None: + # If we haven't been passed a content type, allow to browse all + return True + canBrowse = elem.find( content ) + if canBrowse is None: + # We can't browse for this content type + return False + # We can browse this content type + return True + return False + + def browse( self, actionPath, ruleNum, match, content = None ): + # This function launches the browser for the given match and content type + if content is None or content == "" or content == "NONE": + if match != "path" and match != "playlist": + # No content parameter passed, so check what contents are valid for + # this type + tree = self._load_rules() + elems = tree.getroot().find( "matches" ).findall( "match" ) + matches = {} + for elem in elems: + if elem.attrib.get( "name" ) == match: + if self.ltype.startswith('video'): + matches["movies"] = elem.find( "movies" ) + matches["tvshows"] = elem.find( "tvshows" ) + matches["episodes"] = elem.find( "episodes" ) + matches["musicvideos"] = elem.find( "musicvideos" ) + else: + matches["artists"] = elem.find( "artists" ) + matches["albums"] = elem.find( "albums" ) + matches["songs"] = elem.find( "songs" ) + break + matchesList = [] + matchesValue = [] + # Generate a list of the available content types + elems = tree.getroot().find( "content" ).findall( "type" ) + for elem in elems: + if matches[ elem.text ] is not None: + matchesList.append( xbmc.getLocalizedString( int( elem.attrib.get( "label" ) ) ) ) + matchesValue.append( elem.text ) + if len( matchesList ) == 0: + return + if len( matchesList ) == 1: + # Only one returned, no point offering a choice of content type + content = matchesValue[ 0 ] + else: + # Display a select dialog so user can choose their content + selectedContent = xbmcgui.Dialog().select( LANGUAGE( 30308 ), matchesList ) + # If the user selected nothing... + if selectedContent == -1: + return + content = matchesValue[ selectedContent ] + if match == "title": + self.createBrowseNode( content, None ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "tvshow": + if content == "episodes": + content = "tvshows" + self.createBrowseNode( content, None ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "genre": + self.createBrowseNode( content, "genres" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "album": + self.createBrowseNode( content, "none" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "country": + self.createBrowseNode( content, "countries" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "year": + if content == "episodes": + content = "tvshows" + self.createBrowseNode( content, "years" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "artist": + self.createBrowseNode( content, "artists" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "director": + self.createBrowseNode( content, "directors" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "actor": + if content == "episodes": + content = "tvshows" + self.createBrowseNode( content, "actors" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "studio": + self.createBrowseNode( content, "studios" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "path": + returnVal = xbmcgui.Dialog().browse(0, self.niceMatchName( match ), self.ltype ) + elif match == "set": + self.createBrowseNode( content, "sets" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "tag": + self.createBrowseNode( content, "tags" ) + returnVal = self.browser( self.niceMatchName( match ) ) + elif match == "playlist": + returnVal = self.browserPlaylist( self.niceMatchName( match ) ) + elif match == "virtualfolder": + returnVal = self.browserPlaylist( self.niceMatchName( match ) ) + elif match == "albumartist": + self.createBrowseNode( content, "artists" ) + returnVal = self.browser( self.niceMatchName( match ) ) + try: + # Delete any fake node + xbmcvfs.delete( os.path.join( xbmcvfs.translatePath( "special://profile" ), "library", self.ltype, "plugin.library.node.editor", "temp.xml" ) ) + except: + print_exc() + self.writeUpdatedRule( actionPath, ruleNum, value = returnVal ) + + def niceMatchName( self, match ): + # This function retrieves the translated label for a given match + tree = self._load_rules() + elems = tree.getroot().find( "matches" ).findall( "match" ) + matches = {} + for elem in elems: + if elem.attrib.get( "name" ) == match: + return xbmc.getLocalizedString( int( elem.find( "label" ).text ) ) + + def createBrowseNode( self, content, grouping = None ): + # This function creates a fake node which we'll use for browsing + targetDir = os.path.join( xbmcvfs.translatePath( "special://profile" ), "library", self.ltype, "plugin.library.node.editor" ) + if not os.path.exists( targetDir ): + xbmcvfs.mkdirs( targetDir ) + # Create a new etree + tree = xmltree.ElementTree(xmltree.Element( "node" ) ) + root = tree.getroot() + root.set( "type", "filter" ) + xmltree.SubElement( root, "label" ).text = "Fake node used for browsing" + xmltree.SubElement( root, "content" ).text = content + if grouping is not None: + xmltree.SubElement( root, "group" ).text = grouping + else: + order = xmltree.SubElement( root, "order" ) + order.text = "sorttitle" + order.set( "direction", "ascending" ) + self.indent( root ) + tree.write( os.path.join( targetDir, "temp.xml" ), encoding="UTF-8" ) + + def browser( self, title ): + # Browser instance used by majority of browses + json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "Files.GetDirectory", "params": { "properties": ["title", "file", "genre", "studio", "director", "thumbnail"], "directory": "library://%s/plugin.library.node.editor/temp.xml", "media": "files" } }' % self.ltype) + json_response = json.loads(json_query) + listings = [] + values = [] + # Add all directories returned by the json query + if json_response.get('result') and json_response['result'].get('files') and json_response['result']['files'] is not None: + for item in json_response['result']['files']: + if item[ "label" ] == "..": + continue + thumb = None + if item[ "thumbnail" ] is not "": + thumb = item[ "thumbnail" ] + + if "videodb://" not in item["file"]: + if title.lower() == "genre": + for genre in item["genre"]: + label = genre + elif title.lower() == "studios": + for studio in item["studio"]: + label = studio + elif title.lower() == "director": + for director in item["director"]: + label = director + else: + label = item["label"] + else: + label = item["label"] + + if label not in values: + listitem = xbmcgui.ListItem(label=label) + if thumb: + listitem.setArt({'icon': thumb, 'thumb': thumb}) + listitem.setProperty( "thumbnail", thumb ) + listings.append( listitem ) + values.append( label ) + # Show dialog + w = ShowDialog( "DialogSelect.xml", CWD, listing=listings, windowtitle=title ) + w.doModal() + selectedItem = w.result + del w + if selectedItem == "" or selectedItem == -1: + return None + return values[ selectedItem ] + + def browserPlaylist( self, title ): + # Browser instance used by playlists + json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "id": 0, "method": "Files.GetDirectory", "params": { "properties": ["title", "file", "thumbnail"], "directory": "special://%splaylists/", "media": "files" } }' % self.ltype) + json_response = json.loads(json_query) + listings = [] + values = [] + # Add all directories returned by the json query + if json_response.get('result') and json_response['result'].get('files') and json_response['result']['files'] is not None: + for item in json_response['result']['files']: + if item[ "label" ] == "..": + continue + thumb = None + if item[ "thumbnail" ] is not "": + thumb = item[ "thumbnail" ] + listitem = xbmcgui.ListItem(label=item[ "label" ]) + listitem.setArt({"icon": thumb, "thumbnail": thumb}) + listitem.setProperty( "thumbnail", thumb ) + listings.append( listitem ) + values.append( item[ "label" ] ) + # Show dialog + w = ShowDialog( "DialogSelect.xml", CWD, listing=listings, windowtitle=title ) + w.doModal() + selectedItem = w.result + del w + if selectedItem == "" or selectedItem == -1: + return None + return values[ selectedItem ] + + # in-place prettyprint formatter + def indent( self, elem, level=0 ): + i = "\n" + level*"\t" + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + "\t" + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + +# ============================ +# === PRETTY SELECT DIALOG === +# ============================ +class ShowDialog( xbmcgui.WindowXMLDialog ): + def __init__( self, *args, **kwargs ): + xbmcgui.WindowXMLDialog.__init__( self ) + self.listing = kwargs.get( "listing" ) + self.windowtitle = kwargs.get( "windowtitle" ) + self.result = -1 + + def onInit(self): + try: + self.fav_list = self.getControl(6) + self.getControl(3).setVisible(False) + except: + print_exc() + self.fav_list = self.getControl(3) + self.getControl(5).setVisible(False) + self.getControl(1).setLabel(self.windowtitle) + for item in self.listing : + listitem = xbmcgui.ListItem(label=item.getLabel(), label2=item.getLabel2()) + listitem.setArt({"icon": item.getProperty( "icon" ), "thumbnail": item.getProperty( "thumbnail" )}) + listitem.setProperty( "Addon.Summary", item.getLabel2() ) + self.fav_list.addItem( listitem ) + self.setFocus(self.fav_list) + + def onAction(self, action): + if action.getId() in ( 9, 10, 92, 216, 247, 257, 275, 61467, 61448, ): + self.result = -1 + self.close() + + def onClick(self, controlID): + if controlID == 6 or controlID == 3: + num = self.fav_list.getSelectedPosition() + self.result = num + else: + self.result = -1 + self.close() + + def onFocus(self, controlID): + pass diff --git a/plugin.library.node.editor/resources/lib/viewattrib.py b/plugin.library.node.editor/resources/lib/viewattrib.py new file mode 100644 index 0000000000..1c93688963 --- /dev/null +++ b/plugin.library.node.editor/resources/lib/viewattrib.py @@ -0,0 +1,353 @@ +# coding=utf-8 +import os, sys +import xbmc, xbmcaddon, xbmcplugin, xbmcgui, xbmcvfs +import xml.etree.ElementTree as xmltree +import json +from traceback import print_exc +from urllib.parse import quote, unquote + +from resources.lib.common import * +from resources.lib import pluginBrowser + +class ViewAttribFunctions(): + def __init__(self, ltype): + self.ltype = ltype + + def _load_rules( self ): + if self.ltype.startswith('video'): + overridepath = os.path.join( DEFAULTPATH , "videorules.xml" ) + else: + overridepath = os.path.join( DEFAULTPATH , "musicrules.xml" ) + try: + tree = xmltree.parse( overridepath ) + return tree + except: + return None + + def translateContent( self, content ): + # Load the rules + tree = self._load_rules() + hasValue = True + elems = tree.getroot().find( "content" ).findall( "type" ) + for elem in elems: + if elem.text == content: + return xbmc.getLocalizedString( int( elem.attrib.get( "label" ) ) ) + return None + + def editContent( self, actionPath, default ): + # Load all the rules + tree = self._load_rules().getroot() + elems = tree.find( "content" ).findall( "type" ) + selectName = [] + selectValue = [] + # Find all the content types + for elem in elems: + selectName.append( xbmc.getLocalizedString( int( elem.attrib.get( "label" ) ) ) ) + selectValue.append( elem.text ) + # Let the user select a content type + selectedContent = xbmcgui.Dialog().select( LANGUAGE( 30309 ), selectName ) + # If the user selected no operator... + if selectedContent == -1: + return + self.writeUpdatedRule( actionPath, "content", selectValue[ selectedContent ], addFilter = True ) + + def translateGroup( self, grouping ): + # Load the rules + tree = self._load_rules() + hasValue = True + elems = tree.getroot().find( "groupings" ).findall( "grouping" ) + for elem in elems: + if elem.attrib.get( "name" ) == grouping: + return xbmc.getLocalizedString( int( elem.find( "label" ).text ) ) + return None + + def editGroup( self, actionPath, content, default ): + # Load all the rules + tree = self._load_rules().getroot() + elems = tree.find( "groupings" ).findall( "grouping" ) + selectName = [] + selectValue = [] + # Find all the content types + for elem in elems: + checkContent = elem.find( content ) + if checkContent is not None: + selectName.append( xbmc.getLocalizedString( int( elem.find( "label" ).text ) ) ) + selectValue.append( elem.attrib.get( "name" ) ) + # Let the user select a content type + selectedGrouping = xbmcgui.Dialog().select( LANGUAGE( 30310 ), selectName ) + # If the user selected no operator... + if selectedGrouping == -1: + return + self.writeUpdatedRule( actionPath, "group", selectValue[ selectedGrouping ] ) + + def addLimit( self, actionPath ): + # Load all the rules + try: + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Add a new content tag + newContent = xmltree.SubElement( root, "limit" ) + newContent.text = "25" + # Save the file + self.indent( root ) + tree.write( unquote(actionPath), encoding="UTF-8" ) + except: + print_exc() + + def editLimit( self, actionPath, curValue ): + returnVal = xbmcgui.Dialog().input( LANGUAGE( 30311 ), curValue, type=xbmcgui.INPUT_NUMERIC ) + if returnVal != "": + self.writeUpdatedRule( actionPath, "limit", returnVal ) + + def addPath( self, actionPath ): + # Load all the rules + tree = self._load_rules().getroot() + elems = tree.find( "paths" ).findall( "type" ) + selectName = [] + selectValue = [] + # Find all the path types + for elem in elems: + selectName.append( xbmc.getLocalizedString( int( elem.attrib.get( "label" ) ) ) ) + selectValue.append( elem.attrib.get( "name" ) ) + # Find any sub-path types + for subElem in elem.findall( "type" ): + selectName.append( " - %s" %( xbmc.getLocalizedString( int( subElem.attrib.get( "label" ) ) ) ) ) + selectValue.append( "%s/%s" %( elem.attrib.get( "name" ), subElem.attrib.get( "name" ) ) ) + + # Add option to select a plugin + selectName.append( LANGUAGE( 30514 ) ) + selectValue.append( "::PLUGIN::" ) + + # Let the user select a path + selectedContent = xbmcgui.Dialog().select( LANGUAGE( 30309 ), selectName ) + # If the user selected no operator... + if selectedContent == -1: + return + if selectValue[ selectedContent ] == "::PLUGIN::": + # The user has asked to browse for a plugin + path = pluginBrowser.getPluginPath(self.ltype) + if path is not None: + # User has selected a plugin + self.writeUpdatedPath( actionPath, (0, path), addFolder = True) + else: + # The user has chosen a specific path + self.writeUpdatedPath( actionPath, (0, selectValue[ selectedContent ] ), addFolder = True ) + + def editPath( self, actionPath, curValue ): + returnVal = xbmcgui.Dialog().input( LANGUAGE( 30312 ), curValue, type=xbmcgui.INPUT_ALPHANUM ) + if returnVal != "": + self.writeUpdatedRule( actionPath, "path", returnVal ) + + def editIcon( self, actionPath, curValue ): + returnVal = xbmcgui.Dialog().input( LANGUAGE( 30313 ), curValue, type=xbmcgui.INPUT_ALPHANUM ) + if returnVal != "": + self.writeUpdatedRule( actionPath, "icon", returnVal ) + + def browseIcon( self, actionPath ): + returnVal = xbmcgui.Dialog().browse( 2, LANGUAGE( 30313 ), "files", useThumbs = True ) + if returnVal: + self.writeUpdatedRule( actionPath, "icon", returnVal ) + + def writeUpdatedRule( self, actionPath, attrib, value, addFilter = False ): + # This function writes an updated match, operator or value to a rule + try: + # Load the xml file + tree = xmltree.parse( actionPath ) + root = tree.getroot() + # Add type="filter" if requested + if addFilter: + root.set( "type", "filter" ) + # Find the attribute and update it + elem = root.find( attrib ) + if elem is None: + # There's no existing attribute with this name, so create one + elem = xmltree.SubElement( root, attrib ) + elem.text = value + # Save the file + self.indent( root ) + tree.write( actionPath, encoding="UTF-8" ) + except: + print_exc() + + def writeUpdatedPath( self, actionPath, newComponent, addFolder = False ): + # This functions writes an updated path + try: + # Load the xml file + tree = xmltree.parse( unquote(actionPath) ) + root = tree.getroot() + # Add type="folder" if requested + if addFolder: + root.set( "type", "folder" ) + # Find the current path element + elem = root.find( "path" ) + if elem is None: + # There's no existing path element, so create one + elem = xmltree.SubElement( root, "path" ) + # Get the split version of the path + splitPath = self.splitPath( elem.text ) + elem.text = "" + if len( splitPath ) == 0: + # If the splitPath is empty, add our new component straight away + elem.text = "%s/" %( newComponent[ 1 ] ) + elif newComponent[ 0 ] == 0 and newComponent[ 1 ].startswith( "plugin://"): + # We've been passed a plugin, so only want to write that plugin + elem.text = newComponent[ 1 ] + else: + # Enumarate through everything in the existing path + for x, component in enumerate( splitPath ): + if x != newComponent[ 0 ]: + # Transfer this component to the new path + if x == 0: + elem.text = self.joinPath( component ) + elif x == 1: + elem.text += "?%s=%s" %( component[ 0 ], quote(component[1]) ) + else: + elem.text += "&%s=%s" %( component[ 0 ], quote(component[1]) ) + else: + # Add our new component + if x == 0: + elem.text = "%s/" %( newComponent[ 1 ] ) + elif x == 1: + elem.text += "?%s=%s" %( newComponent[ 1 ], quote(newComponent[2]) ) + else: + elem.text += "&%s=%s" %( newComponent[ 1 ], quote(newComponent[2]) ) + # Check that we added it + if x < newComponent[ 0 ]: + if newComponent[ 0 ] == 1: + elem.text += "?%s=%s" %( newComponent[ 1 ], quote(newComponent[2]) ) + else: + elem.text += "&%s=%s" %( newComponent[ 1 ], quote(newComponent[2]) ) + # Save the file + self.indent( root ) + tree.write( unquote(actionPath), encoding="UTF-8" ) + except: + print_exc() + + def deletePathRule( self, actionPath, rule ): + # This function deletes a rule from a path component + result = xbmcgui.Dialog().yesno(ADDONNAME, LANGUAGE( 30407 ) ) + if not result: + return + + try: + # Load the xml file + tree = xmltree.parse( actionPath ) + root = tree.getroot() + # Find the current path element + elem = root.find( "path" ) + # Get the split version of the path + splitPath = self.splitPath( elem.text ) + elem.text = "" + + # Enumarate through everything in the existing path + addedQ = False + for x, component in enumerate( splitPath ): + if x != rule: + if x == 0: + elem.text = self.joinPath( component ) + elif not addedQ: + elem.text += "?%s=%s" %( component[ 0 ], quote(component[1]) ) + addedQ = True + else: + elem.text += "&%s=%s" %( component[ 0 ], quote(component[1]) ) + # Save the file + self.indent( root ) + tree.write( actionPath, encoding="UTF-8" ) + except: + print_exc() + + def splitPath( self, completePath ): + # This function returns an array of the different components of a path + # [library]://[primary path]/[secondary path]/?attribute1=value1&attribute2=value2... + # [( , )] [( , )] [( , )]... + splitPath = [] + + # If completePath is empty, return an empty list + if completePath is None: + return [] + + # If it's a plugin, then we don't want to split it as its unlikely the user will want to edit individual components + if completePath.startswith( "plugin://" ): + return [ ( completePath, None ) ] + + # Split, get the library://primarypath/[secondarypath] + split = completePath.rsplit( "/", 1 ) + if split[ 0 ].count( "/" ) == 3: + # There's a secondary path + paths = split[ 0 ].rsplit( "/", 1 ) + splitPath.append( ( paths[0], paths[1] ) ) + else: + splitPath.append( ( split[ 0 ], None ) ) + + + # Now split the components + if len( split ) != 1 and split[ 1 ].startswith( "?" ): + for component in split[ 1 ][ 1: ].split( "&" ): + componentSplit = component.split( "=" ) + splitPath.append( ( componentSplit[ 0 ], unquote( componentSplit[1]) ) ) + + return splitPath + + def joinPath( self, components ): + # This function rejoins the library://path/subpath components of a path + returnPath = "%s/" %( components[ 0 ] ) + if components[ 1 ] is not None: + returnPath += "%s/" %( components[ 1 ] ) + return returnPath + + + def translatePath( self, path ): + # Load the rules + tree = self._load_rules() + subSearch = None + translated = [ path[ 0 ], path[ 1 ] ] + elems = tree.getroot().find( "paths" ).findall( "type" ) + for elem in elems: + if elem.attrib.get( "name" ) == path[ 0 ]: + translated[ 0 ] = xbmc.getLocalizedString( int( elem.attrib.get( "label" ) ) ) + subSearch = elem + break + + if path[ 1 ] and subSearch is not None: + for elem in subSearch.findall( "type" ): + if elem.attrib.get( "name" ) == path[ 1 ]: + translated[ 1 ] = xbmc.getLocalizedString( int( elem.attrib.get( "label" ) ) ) + break + + returnString = translated[ 0 ] + if translated[ 1 ]: + returnString += " - %s" %( translated[ 1 ] ) + + return returnString + + def translateMatch( self, value ): + if value == "any": + return xbmc.getLocalizedString(21426).capitalize() + else: + return xbmc.getLocalizedString(21425).capitalize() + + def editMatch( self, actionPath ): + selectName = [ xbmc.getLocalizedString(21425).capitalize(), xbmc.getLocalizedString(21426).capitalize() ] + selectValue = [ "all", "any" ] + # Let the user select wether any or all rules need match + selectedMatch = xbmcgui.Dialog().select( LANGUAGE( 30310 ), selectName ) + # If the user made no selection... + if selectedMatch == -1: + return + self.writeUpdatedRule( actionPath, "match", selectValue[ selectedMatch ] ) + + # in-place prettyprint formatter + def indent( self, elem, level=0 ): + i = "\n" + level*"\t" + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + "\t" + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i diff --git a/plugin.library.node.editor/resources/musicrules.xml b/plugin.library.node.editor/resources/musicrules.xml new file mode 100644 index 0000000000..322cfcddc9 --- /dev/null +++ b/plugin.library.node.editor/resources/musicrules.xml @@ -0,0 +1,364 @@ +<rules> + <matches> + <match name="artist"> + <label>557</label> + <artists>True</artists> + <albums>True</albums> + <songs>True</songs> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="genre"> + <label>515</label> + <artists>True</artists> + <albums>True</albums> + <songs>True</songs> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="moods"> + <label>175</label> + <artists>True</artists> + <albums>True</albums> + <songs>True</songs> + <operator>string</operator> + </match> + <match name="styles"> + <label>176</label> + <artists>True</artists> + <albums>True</albums> + <operator>string</operator> + </match> + <match name="instruments"> + <label>21892</label> + <artists>True</artists> + <operator>string</operator> + </match> + <match name="biography"> + <label>21887</label> + <artists>True</artists> + <operator>string</operator> + </match> + <match name="born"> + <label>21893</label> + <artists>True</artists> + <operator>string</operator> + </match> + <match name="formed"> + <label>21894</label> + <artists>True</artists> + <operator>string</operator> + </match> + <match name="disbanded"> + <label>21896</label> + <artists>True</artists> + <operator>string</operator> + </match> + <match name="died"> + <label>21897</label> + <artists>True</artists> + <operator>string</operator> + </match> + <match name="playlist"> + <label>559</label> + <artists>True</artists> + <albums>True</albums> + <songs>True</songs> + <operator>isornot</operator> + <browse>True</browse> + </match> + <match name="virtualfolder"> + <label>614</label> + <artists>True</artists> + <albums>True</albums> + <songs>True</songs> + <operator>isornot</operator> + <browse>True</browse> + </match> + <match name="album"> + <label>558</label> + <albums>True</albums> + <songs>True</songs> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="albumartist"> + <label>566</label> + <albums>True</albums> + <songs>True</songs> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="year"> + <label>345</label> + <albums>True</albums> + <songs>True</songs> + <operator>numeric</operator> + <browse>True</browse> + </match> + <match name="review"> + <label>183</label> + <albums>True</albums> + <operator>string</operator> + </match> + <match name="themes"> + <label>21895</label> + <albums>True</albums> + <operator>string</operator> + </match> + <match name="type"> + <label>564</label> + <albums>True</albums> + <operator>string</operator> + </match> + <match name="label"> + <label>21899</label> + <albums>True</albums> + <operator>string</operator> + </match> + <match name="rating"> + <label>563</label> + <albums>True</albums> + <songs>True</songs> + <operator>numeric</operator> + </match> + <match name="playcount"> + <label>567</label> + <albums>True</albums> + <songs>True</songs> + <operator>numeric</operator> + </match> + <match name="title"> + <label>556</label> + <songs>True</songs> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="duration"> + <label>180</label> + <songs>True</songs> + <operator>time</operator> + </match> + <match name="track"> + <label>554</label> + <songs>True</songs> + <operator>numeric</operator> + </match> + <match name="file"> + <label>561</label> + <songs>True</songs> + <operator>string</operator> + </match> + <match name="path"> + <label>573</label> + <albums>True</albums> + <artists>True</artists> + <songs>True</songs> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="lastplayed"> + <label>568</label> + <albums>True</albums> + <songs>True</songs> + <operator>date</operator> + </match> + <match name="comment"> + <label>569</label> + <songs>True</songs> + <operator>string</operator> + </match> + <match name="totaldiscs"> + <label>38077</label> + <albums>True</albums> + <operator>numeric</operator> + </match> + <match name="samplerate"> + <label>613</label> + <songs>True</songs> + <operator>numeric</operator> + </match> + <match name="bitrate"> + <label>623</label> + <songs>True</songs> + <operator>numeric</operator> + </match> + <match name="channels"> + <label>253</label> + <songs>True</songs> + <operator>numeric</operator> + </match> + <match name="bpm"> + <label>38080</label> + <songs>True</songs> + <operator>numeric</operator> + </match> + <match name="albumstatus"> + <label>38081</label> + <albums>True</albums> + <operator>string</operator> + </match> + <match name="albumduration"> + <label>180</label> + <albums>True</albums> + <operator>seconds</operator> + </match> + </matches> + <operators> + <group name="string"> + <operator label="21400">contains</operator> + <operator label="21401">doesnotcontain</operator> + <operator label="21404">startswith</operator> + <operator label="21405">endswith</operator> + <operator label="21402">is</operator> + <operator label="21403">isnot</operator> + </group> + <group name="numeric"> + <operator label="21402">is</operator> + <operator label="21403">isnot</operator> + <operator label="21406">greaterthan</operator> + <operator label="21407">lessthan</operator> + </group> + <group name="time"> + <operator label="21402">is</operator> + <operator label="21403">isnot</operator> + <operator label="21406">greaterthan</operator> + <operator label="21407">lessthan</operator> + </group> + <group name="date"> + <operator label="21408">after</operator> + <operator label="21409">before</operator> + <operator label="21410" type="integer">inthelast</operator> + <operator label="21411" type="integer">notinthelast</operator> + </group> + <group name="isornot"> + <operator label="21402">is</operator> + <operator label="21403">isnot</operator> + </group> + </operators> + <groupings> + <grouping name="none"> + <label>16018</label> + <artists>True</artists> + </grouping> + <grouping name="genres"> + <label>135</label> + <artists>True</artists> + </grouping> + <grouping name="years"> + <label>652</label> + <albums>True</albums> + </grouping> + </groupings> + <orderby> + <type label="21430">ascending</type> + <type label="21431">descending</type> + </orderby> + <content> + <type label="133">artists</type> + <type label="132">albums</type> + <type label="134">songs</type> + </content> + + <paths> + <type label="135" name="musicdb://genres"/> + <type label="133" name="musicdb://artists"/> + <type label="132" name="musicdb://albums"/> + <type label="1050" name="musicdb://singles"/> + <type label="134" name="musicdb://songs"/> + <type label="29994" name="musicdb://roles"/> + <type label="271" name="musicdb://top100"> + <type label="134" name="songs"/> + <type label="132" name="albums"/> + </type> + <type label="359" name="musicdb://recentlyaddedalbums"/> + <type label="517" name="musicdb://recentlyplayedalbums"/> + <type label="521" name="musicdb://compilations"/> + <type label="652" name="musicdb://years"/> + <type label="744" name="sources://music"> + <content>None</content> + </type> + <type label="136" name="special://videoplaylists"> + <content>None</content> + </type> + </paths> + + <pathRules> + <rule name="roleid" label="30510"> + <value>integer</value> + <content>artists</content> + <content>albums</content> + <browse>musicdb://roles/</browse> + </rule> + <rule name="role" label="38033"> + <value>string</value> + <content>artists</content> + <content>albums</content> + <browse>musicdb://roles/</browse> + </rule> + <rule name="artistid" label="30508"> + <value>integer</value> + <content>artists</content> + <content>albums</content> + <content>songs</content> + <content>singles</content> + <browse>musicdb://artists/</browse> + </rule> + <rule name="genreid" label="30500"> + <value>integer</value> + <content>artists</content> + <content>albums</content> + <content>songs</content> + <content>singles</content> + <browse>musicdb://genres/</browse> + </rule> + <rule name="genre" label="515"> + <value>string</value> + <content>artists</content> + <content>albums</content> + <content>songs</content> + <content>singles</content> + <browse>musicdb://genres/</browse> + </rule> + <rule name="albumid" label="30509"> + <value>integer</value> + <content>artists</content> + <content>songs</content> + <content>singles</content> + <browse>musicdb://albums/</browse> + </rule> + <rule name="album" label="558"> + <value>string</value> + <content>artists</content> + <content>songs</content> + <content>singles</content> + <browse>musicdb://albums/</browse> + </rule> + <rule name="songid" label="30511"> + <value>integer</value> + <content>artists</content> + <browse>musicdb://songs/</browse> + </rule> + <rule name="albumartistsonly" label="30512"> + <value>boolean</value> + <content>artists</content> + </rule> + <rule name="year" label="345"> + <value>integer</value> + <content>albums</content> + <content>songs</content> + <content>singles</content> + <browse>musicdb://years/</browse> + </rule> + <rule name="compilation" label="521"> + <value>boolean</value> + <content>albums</content> + <content>songs</content> + <content>singles</content> + </rule> + <rule name="showsingles" label="30513"> + <value>boolean</value> + <content>albums</content> + </rule> + </pathRules> +</rules> diff --git a/plugin.library.node.editor/resources/videorules.xml b/plugin.library.node.editor/resources/videorules.xml new file mode 100644 index 0000000000..21926f7d7f --- /dev/null +++ b/plugin.library.node.editor/resources/videorules.xml @@ -0,0 +1,529 @@ +<rules> + <matches> + <match name="title"> + <label>556</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="tvshow"> + <label>20364</label> + <episodes>True</episodes> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="plot"> + <label>207</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>string</operator> + </match> + <match name="status"> + <label>126</label> + <tvshows>True</tvshows> + <operator>string</operator> + </match> + <match name="votes"> + <movies>True</movies> + <label>205</label> + <tvshows>True</tvshows> + <episodes>True</episodes> + <operator>string</operator> + </match> + <match name="plotoutline"> + <label>203</label> + <movies>True</movies> + <operator>string</operator> + </match> + <match name="tagline"> + <label>202</label> + <movies>True</movies> + <operator>string</operator> + </match> + <match name="rating"> + <label>563</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <operator>numeric</operator> + </match> + <match name="time"> + <label>180</label> + <movies>True</movies> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>time</operator> + </match> + <match name="writers"> + <label>20417</label> + <movies>True</movies> + <episodes>True</episodes> + <operator>string</operator> + </match> + <match name="airdate"> + <label>20416</label> + <episodes>True</episodes> + <operator>date</operator> + </match> + <match name="playcount"> + <label>567</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>numeric</operator> + </match> + <match name="lastplayed"> + <label>568</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>date</operator> + </match> + <match name="inprogress"> + <label>575</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <operator>boolean</operator> + </match> + <match name="genre"> + <label>515</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="album"> + <label>558</label> + <musicvideos>True</musicvideos> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="country"> + <label>574</label> + <movies>True</movies> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="year"> + <label>562</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>numeric</operator> + <browse>True</browse> + </match> + <match name="artist"> + <label>557</label> + <musicvideos>True</musicvideos> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="director"> + <label>20339</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="actor"> + <label>20337</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="episode"> + <label>20359</label> + <episodes>True</episodes> + <operator>numeric</operator> + </match> + <match name="season"> + <label>20373</label> + <episodes>True</episodes> + <operator>numeric</operator> + </match> + <match name="numepisodes"> + <label>20360</label> + <tvshows>True</tvshows> + <operator>numeric</operator> + </match> + <match name="numwatched"> + <label>21457</label> + <tvshows>True</tvshows> + <operator>numeric</operator> + </match> + <match name="mpaarating"> + <label>20074</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <operator>string</operator> + </match> + <match name="top250"> + <label>13409</label> + <movies>True</movies> + <operator>numeric</operator> + </match> + <match name="studio"> + <label>20388</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="hastrailer"> + <label>20423</label> + <movies>True</movies> + <operator>boolean</operator> + </match> + <match name="filename"> + <label>561</label> + <movies>True</movies> + <tvshows>True</tvshows> + <musicvideos>True</musicvideos> + <operator>string</operator> + </match> + <match name="path"> + <label>573</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="set"> + <label>20457</label> + <movies>True</movies> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="tag"> + <label>20459</label> + <movies>True</movies> + <tvshows>True</tvshows> + <musicvideos>True</musicvideos> + <operator>string</operator> + <browse>True</browse> + </match> + <match name="dateadded"> + <label>570</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>date</operator> + </match> + <match name="videoresolution"> + <label>21443</label> + <movies>True</movies> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>numeric</operator> + </match> + <match name="audiochannels"> + <label>21444</label> + <movies>True</movies> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>string</operator> + </match> + <match name="videocodec"> + <label>21445</label> + <movies>True</movies> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>isornot</operator> + </match> + <match name="audiocodec"> + <label>21446</label> + <movies>True</movies> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>isornot</operator> + </match> + <match name="audiolanguage"> + <label>21447</label> + <movies>True</movies> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>isornot</operator> + </match> + <match name="subtitlelanguage"> + <label>21448</label> + <movies>True</movies> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>isornot</operator> + </match> + <match name="videoaspect"> + <label>21374</label> + <movies>True</movies> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>numeric</operator> + </match> + <match name="playlist"> + <label>559</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>isornot</operator> + <browse>True</browse> + </match> + <match name="virtualfolder"> + <label>614</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + <operator>isornot</operator> + <browse>True</browse> + </match> + </matches> + <operators> + <group name="string"> + <operator label="21400">contains</operator> + <operator label="21401">doesnotcontain</operator> + <operator label="21404">startswith</operator> + <operator label="21405">endswith</operator> + <operator label="21402">is</operator> + <operator label="21403">isnot</operator> + </group> + <group name="numeric"> + <operator label="21402">is</operator> + <operator label="21403">isnot</operator> + <operator label="21406">greaterthan</operator> + <operator label="21407">lessthan</operator> + </group> + <group name="time"> + <operator label="21402">is</operator> + <operator label="21403">isnot</operator> + <operator label="21406">greaterthan</operator> + <operator label="21407">lessthan</operator> + </group> + <group name="date"> + <operator label="21408">after</operator> + <operator label="21409">before</operator> + <operator label="21410" type="integer">inthelast</operator> + <operator label="21411" type="integer">notinthelast</operator> + </group> + <group name="boolean" option="novalue"> + <operator label="20122">true</operator> + <operator label="20424">false</operator> + </group> + <group name="isornot"> + <operator label="21402">is</operator> + <operator label="21403">isnot</operator> + </group> + </operators> + <groupings> + <grouping name="none"> + <label>16018</label> + <movies>True</movies> + </grouping> + <grouping name="genres"> + <label>135</label> + <movies>True</movies> + <tvshows>True</tvshows> + <musicvideos>True</musicvideos> + </grouping> + <grouping name="years"> + <label>652</label> + <movies>True</movies> + <tvshows>True</tvshows> + <musicvideos>True</musicvideos> + </grouping> + <grouping name="actors"> + <label>344</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + </grouping> + <grouping name="artists"> + <label>133</label> + <musicvideos>True</musicvideos> + </grouping> + <grouping name="directors"> + <label>20348</label> + <movies>True</movies> + <tvshows>True</tvshows> + <episodes>True</episodes> + <musicvideos>True</musicvideos> + </grouping> + <grouping name="studios"> + <label>20388</label> + <movies>True</movies> + <tvshows>True</tvshows> + <musicvideos>True</musicvideos> + </grouping> + <grouping name="countries"> + <label>20451</label> + <movies>True</movies> + </grouping> + <grouping name="sets"> + <label>20434</label> + <movies>True</movies> + </grouping> + <grouping name="tags"> + <label>20459</label> + <movies>True</movies> + <tvshows>True</tvshows> + <musicvideos>True</musicvideos> + </grouping> + </groupings> + <orderby> + <type label="21430">ascending</type> + <type label="21431">descending</type> + </orderby> + <content> + <type label="342">movies</type> + <type label="20343">tvshows</type> + <type label="20360">episodes</type> + <type label="20389">musicvideos</type> + </content> + + <paths> + <type label="342" name="videodb://movies"> + <content>movies</content> + <type label="135" name="genres"/> + <type label="369" name="titles"/> + <type label="652" name="years"/> + <type label="344" name="actors"/> + <type label="20348" name="directors"/> + <type label="20388" name="studios"/> + <type label="20434" name="sets"/> + <type label="20451" name="countries"/> + <type label="20459" name="tags"/> + </type> + <type label="20343" name="videodb://tvshows"> + <content>tvshows</content> + <type label="135" name="genres"/> + <type label="369" name="titles"/> + <type label="652" name="years"/> + <type label="344" name="actors"/> + <type label="20388" name="studios"/> + <type label="20459" name="tags"/> + </type> + <type label="20389" name="videodb://musicvideos"> + <content>musicvideos</content> + <type label="135" name="genres"/> + <type label="369" name="titles"/> + <type label="652" name="years"/> + <type label="133" name="artists"/> + <type label="132" name="albums"/> + <type label="20348" name="directors"/> + <type label="20388" name="studios"/> + <type label="20459" name="tags"/> + </type> + <type label="20386" name="videodb://recentlyaddedmovies"> + <content>movies</content> + </type> + <type label="20387" name="videodb://recentlyaddedepisodes"> + <content>tvshows</content> + </type> + <type label="20390" name="videodb://recentlyaddedmusicvideos"> + <content>musicvideos</content> + </type> + <type label="626" name="videodb://inprogresstvshows"> + <content>tvshows</content> + </type> + <type label="744" name="sources://video"> + <content>None</content> + </type> + <type label="136" name="special://videoplaylists"> + <content>None</content> + </type> + <type label="1037" name="addons://sources/video"> + <content>None</content> + </type> + </paths> + <pathRules> + <rule label="30500" name="genreid"> + <value>integer</value> + <content>movies</content> + <content>tvshows</content> + <content>musicvideos</content> + <browse>videodb://::root::/genres/</browse> + </rule> + <rule label="30501" name="countryid"> + <value>integer</value> + <content>movies</content> + <browse>videodb://movies/countries/</browse> + </rule> + <rule label="30502" name="studioid"> + <value>integer</value> + <content>movies</content> + <content>tvshows</content> + <content>musicvideos</content> + <browse>videodb://::root::/studios/</browse> + </rule> + <rule label="30503" name="directorid"> + <value>integer</value> + <content>movies</content> + <content>tvshows</content> + <content>musicvideos</content> + <browse>videodb://::root::/directors/</browse> + </rule> + <rule label="345" name="year"> + <value>integer</value> + <content>movies</content> + <content>tvshows</content> + <content>musicvideos</content> + <browse>videodb://::root::/years/</browse> + </rule> + <rule label="30504" name="actorid"> + <value>integer</value> + <content>movies</content> + <content>tvshows</content> + <browse>videodb://::root::/actors/</browse> + </rule> + <rule label="30505" name="setid"> + <value>integer</value> + <content>movies</content> + <browse>videodb://::root::/sets/</browse> + </rule> + <rule label="30506" name="tagid"> + <value>integer</value> + <content>movies</content> + <content>tvshows</content> + <browse>videodb://::root::/tags/</browse> + </rule> + <rule label="30507" name="tvshowid"> + <value>integer</value> + <content>tvshows</content> + <browse>videodb://tvshows/</browse> + </rule> + <rule label="20373" name="season"> + <value>integer</value> + <content>tvshows</content> + </rule> + <rule label="30508" name="artistid"> + <value>integer</value> + <content>musicvideos</content> + <browse>videodb://musicvideos/artists/</browse> + </rule> + <rule label="30509" name="albumid"> + <value>integer</value> + <content>musicvideos</content> + <browse>videodb://musicvideos/albums/</browse> + </rule> + </pathRules> +</rules> diff --git a/plugin.onedrive/LICENSE.txt b/plugin.onedrive/LICENSE.txt new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/plugin.onedrive/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/plugin.onedrive/README.md b/plugin.onedrive/README.md new file mode 100644 index 0000000000..755213b315 --- /dev/null +++ b/plugin.onedrive/README.md @@ -0,0 +1,15 @@ +# OneDrive KODI Add-on +OneDrive for KODI + +Play all your media from Microsoft OneDrive including Videos, Music and Pictures. +* Unlimited personal or business accounts +* Playback your music and videos. Listing of videos with thumbnails. +* Use OneDrive as a source. +* Subtitles can be assigned automatically if a .str file exists next to the video. +* Export your videos to your library (.strm files). You can export your music too, but kodi won't support it yet. It's a Kodi issue for now. +* Show your photos individually or run a slideshow of them. Listing of pictures with thumbnails. +* Auto-Refreshed slideshow. +* Use of OAuth 2 login. You don't have to write your user/password within the add-on. Use the login process in your browser. +* Extremely fast. Using the Microsoft Graph API + +This program is not affiliated with or sponsored by Microsoft. diff --git a/plugin.onedrive/addon.xml b/plugin.onedrive/addon.xml new file mode 100644 index 0000000000..4ea5016d2a --- /dev/null +++ b/plugin.onedrive/addon.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<addon id="plugin.onedrive" name="OneDrive" version="2.3.0" provider-name="Carlos Guzman (cguZZman)"> + <requires> + <import addon="xbmc.python" version="3.0.0" /> + <import addon="script.module.clouddrive.common" version="1.4.0"/> + </requires> + <extension point="xbmc.python.pluginsource" library="entrypoint.py"> + <provides>image audio video</provides> + </extension> + <extension point="xbmc.service" library="service.py" /> + <extension point="xbmc.addon.metadata"> + <platform>all</platform> + <summary lang="en_GB">Microsoft OneDrive for KODI</summary> + <description lang="en_GB"> +Play all your media from OneDrive including Videos, Music and Pictures. + - Unlimited number of personal or business accounts. + - Search over your drive. + - Auto-Refreshed slideshow. + - Export your videos to your library (.strm files) + - Use OneDrive as a source + - This program is not affiliated with or sponsored by Microsoft. + </description> + <summary lang="he_IL">כונן OneDrive של מיקרוסופט עבור קודי</summary> + <description lang="he_IL"> +הפעל את כל המדיה שלך מ- OneDrive כולל וידאו, מוסיקה ותמונות. +  - מספר בלתי מוגבל של חשבונות אישיים או עסקיים. +  - חיפוש ומציאת הכונן שלך. +  - ריענון מצגת אוטומטית. +  - ייצוא קטעי הווידאו לספרייה שלך (קבצי .strm) +  - שימוש ב-OneDrive כמקור +  - תוכנית זו אינה מזוהה עם או בחסות מיקרוסופט. + </description> + <license>GPL-3.0-or-later</license> + <source>https://github.com/cguZZman/plugin.onedrive</source> + <forum>https://github.com/cguZZman/plugin.onedrive/issues</forum> + <website>https://addons.kodi.tv/show/plugin.onedrive</website> + <assets> + <icon>icon.png</icon> + <fanart>fanart.jpg</fanart> + </assets> + <news> +v2.3.0 released Jan 21, 2023: +- Kodi 20 fix + </news> + <disclaimer lang="en_GB"> +This cloud drive addon uses a third-party authentication mechanism commonly known as OAuth 2.0. +If you want to know more about OAuth 2.0 you can visit the following pages: +- https://oauth.net/2/ +- https://developers.google.com/identity/protocols/OAuth2 +- https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth + +Kodi and myself take no responsibility or liability. + +The authentication server URL is specified in Settings / Advanced / Sign-in Server. The Sign-in Server implements the OAuth 2.0 protocol. +The complete source code of the Sign-in Server can be download here: https://github.com/cguZZman/drive-login +You can clone the project and host it in your own server. + </disclaimer> + </extension> +</addon> diff --git a/plugin.onedrive/entrypoint.py b/plugin.onedrive/entrypoint.py new file mode 100644 index 0000000000..b426082f4b --- /dev/null +++ b/plugin.onedrive/entrypoint.py @@ -0,0 +1,21 @@ +#------------------------------------------------------------------------------- +# Copyright (C) 2017 Carlos Guzman (cguZZman) carlosguzmang@protonmail.com +# +# This file is part of OneDrive for Kodi +# +# OneDrive for Kodi 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. +# +# Cloud Drive Common Module for Kodi 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 <http://www.gnu.org/licenses/>. +#------------------------------------------------------------------------------- + +from resources.lib.addon import OneDriveAddon +OneDriveAddon().route() diff --git a/plugin.onedrive/fanart.jpg b/plugin.onedrive/fanart.jpg new file mode 100644 index 0000000000..59c89d3957 Binary files /dev/null and b/plugin.onedrive/fanart.jpg differ diff --git a/plugin.onedrive/icon.png b/plugin.onedrive/icon.png new file mode 100644 index 0000000000..7b8a1d940b Binary files /dev/null and b/plugin.onedrive/icon.png differ diff --git a/plugin.onedrive/resources/__init__.py b/plugin.onedrive/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.onedrive/resources/language/resource.language.en_gb/strings.po b/plugin.onedrive/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..7e72200536 --- /dev/null +++ b/plugin.onedrive/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,121 @@ +# Kodi Media Center language file +# Addon Name: OneDrive +# Addon id: plugin.onedrive +# Addon Provider: Carlos Guzman (cguZZman) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Player Service" +msgstr "" + +msgctxt "#32001" +msgid "Export Service" +msgstr "" + +msgctxt "#32002" +msgid "Source Service" +msgstr "" + +msgctxt "#32003" +msgid "Before export to library, remove previous files and folders" +msgstr "" + +msgctxt "#32004" +msgid "Automatically set subtitles from cloud drive" +msgstr "" + +msgctxt "#32005" +msgid "Auto-Refreshed slideshow" +msgstr "" + +msgctxt "#32006" +msgid "Refresh interval in minutes" +msgstr "" + +msgctxt "#32007" +msgid "Special: Photos" +msgstr "" + +msgctxt "#32008" +msgid "Special: Camera Roll" +msgstr "" + +msgctxt "#32009" +msgid "Special: Music" +msgstr "" + +msgctxt "#32010" +msgid "Recursive auto-refreshed slideshow" +msgstr "" + +msgctxt "#32011" +msgid "Resume playing when resume point exists in library" +msgstr "" + +msgctxt "#32012" +msgid "Open Cloud Drive Common Settings..." +msgstr "" + +msgctxt "#32017" +msgid "Ask to resume if resume point exists in library" +msgstr "" + +msgctxt "#32018" +msgid "Save resume points and watched status in library" +msgstr "" + +msgctxt "#32019" +msgid "Do not include filename extension in a .strm" +msgstr "" + +msgctxt "#32020" +msgid "Hide exporting progress dialog" +msgstr "" + +msgctxt "#32030" +msgid "Collaboration" +msgstr "" + +msgctxt "#32031" +msgid "Report errors automatically to help resolve them quickly" +msgstr "" + +msgctxt "#32032" +msgid "Advanced" +msgstr "" + +msgctxt "#32033" +msgid "Sign-in Server" +msgstr "" + +msgctxt "#32034" +msgid "Cache expiration time (in minutes)" +msgstr "" + +msgctxt "#32035" +msgid "Clear cache now" +msgstr "" + +msgctxt "#32067" +msgid "Services" +msgstr "" + +msgctxt "#32068" +msgid "Allow using OneDrive as a source" +msgstr "" + +msgctxt "#32069" +msgid " Source server port - http://localhost:<port>/source" +msgstr "" \ No newline at end of file diff --git a/plugin.onedrive/resources/language/resource.language.he_il/strings.po b/plugin.onedrive/resources/language/resource.language.he_il/strings.po new file mode 100644 index 0000000000..dbe8f8051a --- /dev/null +++ b/plugin.onedrive/resources/language/resource.language.he_il/strings.po @@ -0,0 +1,70 @@ +# Kodi Media Center language file +# Addon Name: OneDrive +# Addon id: plugin.onedrive +# Addon Provider: Carlos Guzman (cguZZman) +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2017-10-19 11:41+0300\n" +"Last-Translator: A. Dambledore\n" +"Language-Team: Eng2Heb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: he_IL\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Account" +msgstr "חשבון" + +msgctxt "#32001" +msgid "Video Library Export Folder" +msgstr "תיקיית ייצוא ספריית וידאו" + +msgctxt "#32002" +msgid "Music Library Export Folder" +msgstr "תיקיית ייצוא ספריית מוזיקה" + +msgctxt "#32003" +msgid "Before export to library, remove previous files and folders" +msgstr "לפני ייצוא לספריה, הסר את הקבצים והתיקיות הקודמים" + +msgctxt "#32004" +msgid "When playing videos, set the subtitle file located next to the video (.srt)" +msgstr "בעת ניגון וידאו, הגדר את קובץ הכתוביות הממוקם ליד הווידאו (.srt)" + +msgctxt "#32005" +msgid "Auto-Refreshed slideshow" +msgstr "מצגת עם רענון אוטומטי" + +msgctxt "#32006" +msgid "Refresh interval in minutes" +msgstr "מרווח זמן לרענון בדקות" + +msgctxt "#32007" +msgid "Special: Photos" +msgstr "מיוחד: תמונות" + +msgctxt "#32008" +msgid "Special: Camera Roll" +msgstr "מיוחד: סיבוב המצלמה" + +msgctxt "#32009" +msgid "Special: Music" +msgstr "מיוחד: מוסיקה" + +msgctxt "#32010" +msgid "Recursive auto-refreshed slideshow" +msgstr "רענן מצגת באופן רקורסיבי ואוטומטי" + +msgctxt "#32011" +msgid "Common Settings" +msgstr "הגדרות נפוצות" + +msgctxt "#32012" +msgid "Open Cloud Drive Common Settings..." +msgstr "פתח הגדרות נפוצות של כונן ענן ..." + diff --git a/plugin.onedrive/resources/lib/__init__.py b/plugin.onedrive/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.onedrive/resources/lib/addon.py b/plugin.onedrive/resources/lib/addon.py new file mode 100644 index 0000000000..a36dc7684d --- /dev/null +++ b/plugin.onedrive/resources/lib/addon.py @@ -0,0 +1,68 @@ +#------------------------------------------------------------------------------- +# Copyright (C) 2017 Carlos Guzman (cguZZman) carlosguzmang@protonmail.com +# +# This file is part of OneDrive for Kodi +# +# OneDrive for Kodi 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. +# +# Cloud Drive Common Module for Kodi 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 <http://www.gnu.org/licenses/>. +#------------------------------------------------------------------------------- + +import urllib + +from clouddrive.common.ui.addon import CloudDriveAddon +from clouddrive.common.utils import Utils +from resources.lib.provider.onedrive import OneDrive + +class OneDriveAddon(CloudDriveAddon): + _provider = OneDrive() + _action = None + + def __init__(self): + super(OneDriveAddon, self).__init__() + + def get_provider(self): + return self._provider + + def get_custom_drive_folders(self, driveid): + drive = self._account_manager.get_by_driveid('drive', driveid) + drive_folders = [] + if drive['type'] == 'personal': + if self._content_type == 'image': + path = 'special/photos' + params = {'action': '_slideshow', 'content_type': self._content_type, 'driveid': driveid, 'path': path} + context_options = [(self._common_addon.getLocalizedString(32032), 'RunPlugin('+self._addon_url + '?' + urllib.parse.urlencode(params)+')')] + drive_folders.append({'name' : self._addon.getLocalizedString(32007), 'path' : path, 'context_options': context_options}) + + path = 'special/cameraroll' + params['path'] = path + context_options = [(self._common_addon.getLocalizedString(32032), 'RunPlugin('+self._addon_url + '?' + urllib.parse.urlencode(params)+')')] + drive_folders.append({'name' : self._addon.getLocalizedString(32008), 'path' : path, 'context_options': context_options}) + elif self._content_type == 'audio': + drive_folders.append({'name' : self._addon.getLocalizedString(32009), 'path' : 'special/music'}) + drive_folders.append({'name' : self._common_addon.getLocalizedString(32053), 'path' : 'recent'}) + if drive['type'] != 'documentLibrary': + drive_folders.append({'name' : self._common_addon.getLocalizedString(32058), 'path' : 'sharedWithMe'}) + return drive_folders + + def _rename_action(self): + if self._action == 'open_drive_folder': + self._addon_params['path'] = Utils.get_safe_value(self._addon_params, 'folder') + self._action = Utils.get_safe_value({ + 'open_folder': '_list_folder', + 'open_drive': '_list_drive', + 'open_drive_folder': '_list_folder' + }, self._action, self._action) + +if __name__ == '__main__': + OneDriveAddon().route() + diff --git a/plugin.onedrive/resources/lib/provider/__init__.py b/plugin.onedrive/resources/lib/provider/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.onedrive/resources/lib/provider/onedrive.py b/plugin.onedrive/resources/lib/provider/onedrive.py new file mode 100644 index 0000000000..62d39447bb --- /dev/null +++ b/plugin.onedrive/resources/lib/provider/onedrive.py @@ -0,0 +1,229 @@ +#------------------------------------------------------------------------------- +# Copyright (C) 2017 Carlos Guzman (cguZZman) carlosguzmang@protonmail.com +# +# This file is part of OneDrive for Kodi +# +# OneDrive for Kodi 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. +# +# Cloud Drive Common Module for Kodi 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 <http://www.gnu.org/licenses/>. +#------------------------------------------------------------------------------- + +from clouddrive.common.remote.provider import Provider +from clouddrive.common.utils import Utils +from clouddrive.common.exception import RequestException, ExceptionUtils + +import urllib +from urllib.error import HTTPError + +class OneDrive(Provider): + _extra_parameters = {'expand': 'thumbnails'} + + def __init__(self, source_mode = False): + super(OneDrive, self).__init__('onedrive', source_mode) + + def _get_api_url(self): + return 'https://graph.microsoft.com/v1.0' + + def _get_request_headers(self): + return None + + def get_account(self, request_params=None, access_tokens=None): + me = self.get('/me/', request_params=request_params, access_tokens=access_tokens) + if not me: + raise Exception('NoAccountInfo') + return { 'id' : me['id'], 'name' : me['displayName']} + + def get_drives(self, request_params=None, access_tokens=None): + drives = [] + drives_id_list =[] + try: + response = self.get('/drives', request_params=request_params, access_tokens=access_tokens) + for drive in response['value']: + drives_id_list.append(drive['id']) + drives.append({ + 'id' : drive['id'], + 'name' : Utils.get_safe_value(drive, 'name', ''), + 'type' : drive['driveType'] + }) + except RequestException as ex: + httpex = ExceptionUtils.extract_exception(ex, HTTPError) + if not httpex or httpex.code != 403: + raise ex + + response = self.get('/me/drives', request_params=request_params, access_tokens=access_tokens) + for drive in response['value']: + if not drive['id'] in drives_id_list: + drives_id_list.append(drive['id']) + drives.append({ + 'id' : drive['id'], + 'name' : Utils.get_safe_value(drive, 'name', ''), + 'type' : drive['driveType'] + }) + return drives + + def get_drive_type_name(self, drive_type): + if drive_type == 'personal': + return 'OneDrive Personal' + elif drive_type == 'business': + return 'OneDrive for Business' + elif drive_type == 'documentLibrary': + return ' SharePoint Document Library' + return drive_type + + def get_folder_items(self, item_driveid=None, item_id=None, path=None, on_items_page_completed=None, include_download_info=False, on_before_add_item=None): + item_driveid = Utils.default(item_driveid, self._driveid) + if item_id: + files = self.get('/drives/'+item_driveid+'/items/' + item_id + '/children', parameters = self._extra_parameters) + elif path == 'sharedWithMe' or path == 'recent': + files = self.get('/drives/'+self._driveid+'/' + path) + else: + if path == '/': + path = 'root' + else: + parts = path.split('/') + if len(parts) > 1 and not parts[0]: + path = 'root:'+path+':' + files = self.get('/drives/'+self._driveid+'/' + path + '/children', parameters = self._extra_parameters) + if self.cancel_operation(): + return + return self.process_files(files, on_items_page_completed, include_download_info, on_before_add_item=on_before_add_item) + + def process_files(self, files, on_items_page_completed=None, include_download_info=False, extra_info=None, on_before_add_item=None): + items = [] + for f in files['value']: + f = Utils.get_safe_value(f, 'remoteItem', f) + item = self._extract_item(f, include_download_info) + if on_before_add_item: + on_before_add_item(item) + items.append(item) + if on_items_page_completed: + on_items_page_completed(items) + if type(extra_info) is dict: + if '@odata.deltaLink' in files: + extra_info['change_token'] = files['@odata.deltaLink'] + + if '@odata.nextLink' in files: + next_files = self.get(files['@odata.nextLink']) + if self.cancel_operation(): + return + items.extend(self.process_files(next_files, on_items_page_completed, include_download_info, extra_info, on_before_add_item)) + return items + + def _extract_item(self, f, include_download_info=False): + name = Utils.get_safe_value(f, 'name', '') + parent_reference = Utils.get_safe_value(f, 'parentReference', {}) + item = { + 'id': f['id'], + 'name': name, + 'name_extension' : Utils.get_extension(name), + 'drive_id' : Utils.get_safe_value(parent_reference, 'driveId'), + 'parent' : Utils.get_safe_value(parent_reference, 'id'), + 'mimetype' : Utils.get_safe_value(Utils.get_safe_value(f, 'file', {}), 'mimeType'), + 'last_modified_date' : Utils.get_safe_value(f,'lastModifiedDateTime'), + 'size': Utils.get_safe_value(f, 'size', 0), + 'description': Utils.get_safe_value(f, 'description', ''), + 'deleted': 'deleted' in f + } + if 'folder' in f: + item['folder'] = { + 'child_count' : Utils.get_safe_value(f['folder'],'childCount',0) + } + if 'video' in f: + video = f['video'] + item['video'] = { + 'width' : Utils.get_safe_value(video,'width', 0), + 'height' : Utils.get_safe_value(video, 'height', 0), + 'duration' : Utils.get_safe_value(video, 'duration', 0) /1000 + } + if 'audio' in f: + audio = f['audio'] + item['audio'] = { + 'tracknumber' : Utils.get_safe_value(audio, 'track'), + 'discnumber' : Utils.get_safe_value(audio, 'disc'), + 'duration' : int(Utils.get_safe_value(audio, 'duration') or '0') / 1000, + 'year' : Utils.get_safe_value(audio, 'year'), + 'genre' : Utils.get_safe_value(audio, 'genre'), + 'album': Utils.get_safe_value(audio, 'album'), + 'artist': Utils.get_safe_value(audio, 'artist'), + 'title': Utils.get_safe_value(audio, 'title') + } + if 'image' in f or 'photo' in f: + item['image'] = { + 'size' : Utils.get_safe_value(f, 'size', 0) + } + if 'thumbnails' in f and type(f['thumbnails']) == list and len(f['thumbnails']) > 0: + thumbnails = f['thumbnails'][0] + item['thumbnail'] = Utils.get_safe_value(Utils.get_safe_value(thumbnails, 'large', {}), 'url', '') + if include_download_info: + item['download_info'] = { + 'url' : Utils.get_safe_value(f,'@microsoft.graph.downloadUrl') + } + return item + + def search(self, query, item_driveid=None, item_id=None, on_items_page_completed=None): + item_driveid = Utils.default(item_driveid, self._driveid) + url = '/drives/' + if item_id: + url += item_driveid+'/items/' + item_id + else: + url += self._driveid + url += '/search(q=\''+urllib.parse.quote(Utils.str(query))+'\')' + self._extra_parameters['filter'] = 'file ne null' + files = self.get(url, parameters = self._extra_parameters) + if self.cancel_operation(): + return + return self.process_files(files, on_items_page_completed) + + def get_subtitles(self, parent, name, item_driveid=None, include_download_info=False): + item_driveid = Utils.default(item_driveid, self._driveid) + subtitles = [] + search_url = '/drives/'+item_driveid+'/items/' + parent + '/search(q=\''+urllib.parse.quote(Utils.str(Utils.remove_extension(name)).replace("'","''"))+'\')' + files = self.get(search_url) + for f in files['value']: + subtitle = self._extract_item(f, include_download_info) + if subtitle['name_extension'].lower() in ('srt','idx','sub','sbv','ass','ssa','smi'): + subtitles.append(subtitle) + return subtitles + + def get_item(self, item_driveid=None, item_id=None, path=None, find_subtitles=False, include_download_info=False): + item_driveid = Utils.default(item_driveid, self._driveid) + if item_id: + f = self.get('/drives/'+item_driveid+'/items/' + item_id, parameters = self._extra_parameters) + elif path == 'sharedWithMe' or path == 'recent': + return + else: + if path == '/': + path = 'root' + else: + parts = path.split('/') + if len(parts) > 1 and not parts[0]: + path = 'root:'+path+':' + f = self.get('/drives/'+self._driveid+'/' + path, parameters = self._extra_parameters) + + item = self._extract_item(f, include_download_info) + if find_subtitles: + subtitles = self.get_subtitles(item['parent'], item['name'], item_driveid, include_download_info) + if subtitles: + item['subtitles'] = subtitles + return item + + def changes(self): + f = self.get(Utils.default(self.get_change_token(), '/drives/'+self._driveid+'/root/delta?token=latest'), request_params = {'on_exception': self.on_exception}) + extra_info = {} + changes = self.process_files(f, include_download_info=True, extra_info=extra_info) + self.persist_change_token(Utils.get_safe_value(extra_info, 'change_token')) + return changes + + def on_exception(self, request, e): + ex = ExceptionUtils.extract_exception(e, HTTPError) + if ex and ex.code == 404: + self.persist_change_token(None) \ No newline at end of file diff --git a/plugin.onedrive/resources/settings.xml b/plugin.onedrive/resources/settings.xml new file mode 100644 index 0000000000..9b9694d1cb --- /dev/null +++ b/plugin.onedrive/resources/settings.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<settings > + <category label="32067"> + <setting label="32000" type="lsep"/> + <setting label="32004" type="bool" id="set_subtitle" default="true" value="true"/> + <setting label="32011" type="bool" id="resume_playing" default="true" visible="!String.IsEmpty(window(home).property(iskrypton))" /> + <setting label="32018" type="bool" id="save_resume_watched" default="true" visible="!String.IsEmpty(window(home).property(iskrypton))" /> + <setting label="32017" type="bool" id="ask_resume" default="true" visible="String.IsEmpty(window(home).property(iskrypton))" /> + <setting label="32001" type="lsep"/> + <setting label="32003" type="bool" id="clean_folder" default="true" value="true"/> + <setting label="32019" type="bool" id="no_extension_strm" default="false" value="false"/> + <setting label="32020" type="bool" id="hide_export_progress" default="false" value="false"/> + <setting label="32002" type="lsep"/> + <setting label="32068" type="bool" id="allow_directory_listing" default="true"/> + <setting label="32069" type="number" id="port_directory_listing" default="8586"/> + <setting label="32005" type="lsep"/> + <setting label="32006" type="number" id="slideshow_refresh_interval" default="5" value="5"/> + <setting label="32010" type="bool" id="slideshow_recursive" default="false" value="false"/> + </category> + <category label="32032"> + <setting label="32033" type="text" id="sign-in-server" default="https://drive-login.herokuapp.com"/> + <setting label="32034" type="number" id="cache-expiration-time" default="5"/> + <setting label="32035" type="action" option="close" action="RunPlugin(plugin://plugin.onedrive/?action=_clear_cache)"/> + <setting label="32012" type="action" option="close" action="RunPlugin(plugin://plugin.onedrive/?action=_open_common_settings)"/> + </category> + <category label="32030"> + <setting label="32031" type="bool" id="report_error" default="false"/> + </category> +</settings> diff --git a/plugin.onedrive/service.py b/plugin.onedrive/service.py new file mode 100644 index 0000000000..a53b1fea62 --- /dev/null +++ b/plugin.onedrive/service.py @@ -0,0 +1,30 @@ +#------------------------------------------------------------------------------- +# Copyright (C) 2017 Carlos Guzman (cguZZman) carlosguzmang@protonmail.com +# +# This file is part of OneDrive for Kodi +# +# OneDrive for Kodi 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. +# +# Cloud Drive Common Module for Kodi 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 <http://www.gnu.org/licenses/>. +#------------------------------------------------------------------------------- + +from clouddrive.common.service.download import DownloadService +from clouddrive.common.service.source import SourceService +from clouddrive.common.service.utils import ServiceUtil +from resources.lib.provider.onedrive import OneDrive +from clouddrive.common.service.export import ExportService +from clouddrive.common.service.player import PlayerService + + +if __name__ == '__main__': + ServiceUtil.run([DownloadService(OneDrive), SourceService(OneDrive), + ExportService(OneDrive), PlayerService(OneDrive)]) \ No newline at end of file diff --git a/plugin.picture.googlephotos/LICENSE.txt b/plugin.picture.googlephotos/LICENSE.txt new file mode 100644 index 0000000000..f288702d2f --- /dev/null +++ b/plugin.picture.googlephotos/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/plugin.picture.googlephotos/README.md b/plugin.picture.googlephotos/README.md new file mode 100644 index 0000000000..b1630c940c --- /dev/null +++ b/plugin.picture.googlephotos/README.md @@ -0,0 +1,23 @@ +# Google Photos Kodi Addon +View Google Photos on Kodi! +* Unlimited accounts +* Shared Album Support +* Custom Filter Support +* Video Playback (Exists but broken due to Photos API limitations) + +This program is not affiliated with or sponsored by Google. + +## Installation Instructions +### Method 1 +Use official KODI Repository +Add-ons -> Install from repository -> Kodi Add-on Repository -> Picture add-ons -> Google Photos -> Install + +### Method 2 +From zip +1. Download zip from releases. +2. Add-ons -> Install from zip file -> Locate the file -> Click OK. + + +Note: Instructions to generate client credentials can be found at https://photos-kodi-addon.onrender.com/credentialsguide + + diff --git a/plugin.picture.googlephotos/addon.xml b/plugin.picture.googlephotos/addon.xml new file mode 100644 index 0000000000..97624927ea --- /dev/null +++ b/plugin.picture.googlephotos/addon.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<addon id="plugin.picture.googlephotos" version="2.0.0" name="Google Photos" provider-name="Pranjal Singhal"> + <requires> + <import addon="xbmc.python" version="3.0.0" /> + <import addon="script.module.requests" /> + <import addon="script.module.pyqrcode" /> + </requires> + + <extension point="xbmc.python.pluginsource" library="main.py"> + <provides>image</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <summary lang="en_GB">View Google Photos on Kodi!</summary> + <description lang="en_GB">See all your photos and videos on Kodi. +- Unlimited number of accounts. +- Custom Filter support +- With Pagination for small load times +Video Seeking does not work due to limitations in the API. If video playback is failing, try the solution given in https://forum.kodi.tv/showthread.php?tid=361046 </description> + <disclaimer lang="en_GB"> +Google Photos for Kodi uses a third-party authentication mechanism commonly known as OAuth 2.0. +If you want to know more about OAuth 2.0 you can visit the following pages: +- https://oauth.net/2/ +- https://developers.google.com/identity/protocols/OAuth2 + +Kodi and I take no responsibility or liability. + +The authentication server URL is specified in Settings / Sign-in Server. The Sign-in Server implements the OAuth 2.0 protocol. +The complete source code of the Sign-in Server can be download here: https://github.com/pransin/Device-Authorization-Grant-Proxy-Server +You can clone the project and host it in your own server. + </disclaimer> + <license>GPL-3.0-or-later</license> + <assets> + <icon>icon.png</icon> + <fanart>fanart.jpg</fanart> + </assets> + + <news>v2.0.0 (2022-09-14) + CONTAINS BREAKING CHANGES. WILL REMOVE SAVED ACCOUNTS. + - Changed sign-in server to photos-kodi-login.onrender.com + - Client credentials must be entered in the settings to use the addon + Note: I will update the addon if I find a way to simplify the login procedure + </news> + </extension> +</addon> \ No newline at end of file diff --git a/plugin.picture.googlephotos/fanart.jpg b/plugin.picture.googlephotos/fanart.jpg new file mode 100644 index 0000000000..954a333874 Binary files /dev/null and b/plugin.picture.googlephotos/fanart.jpg differ diff --git a/plugin.picture.googlephotos/icon.png b/plugin.picture.googlephotos/icon.png new file mode 100644 index 0000000000..a58dce6527 Binary files /dev/null and b/plugin.picture.googlephotos/icon.png differ diff --git a/plugin.picture.googlephotos/main.py b/plugin.picture.googlephotos/main.py new file mode 100644 index 0000000000..0001b65e80 --- /dev/null +++ b/plugin.picture.googlephotos/main.py @@ -0,0 +1,417 @@ +import json +import sys +import requests +from pathlib import Path +from resources.lib import auth +import resources.lib.config as config +from urllib import parse +import xbmc +import xbmcaddon +import xbmcvfs +import xbmcgui +import xbmcplugin +import threading +import traceback +import time + +from resources.lib.auth import read_credentials, get_device_code +import resources.lib.dialogs as dialogs +# from resources.lib.ui.custom_filter_dialog import FilterDialog +import resources.lib.utils as utils + +base_url = sys.argv[0] +addon_handle = int(sys.argv[1]) +qs = sys.argv[2][1:] +args = parse.parse_qs(qs) + +mode = args.get('mode') +token = None # Access token + +# Addon dir path +__addon__ = xbmcaddon.Addon() +# addon_path = __addon__.getAddonInfo('path') +profile_path = xbmcvfs.translatePath( + __addon__.getAddonInfo('profile')) +token_folder = Path(profile_path + config.token_folder) +token_path = None +if args.get('token_filename'): + token_path = Path(token_folder / args.get('token_filename')[0]) +xbmcplugin.setContent(addon_handle, 'images') + + +def new_account(): + xbmc.log('Executing new_account function', xbmc.LOGDEBUG) + + # Open dialog + baseUrl = __addon__.getSettingString('baseUrl') + login_dialog = dialogs.QRDialogProgress.create() + login_dialog.show() + # Gives enough time for window to initialize before sending the device code request + xbmc.sleep(10) + # Get User Code from auth server + code_json = get_device_code() + expires_at = time.time() + 0.99 * float(code_json["expires_in"]) + login_dialog.update( + int(code_json["expires_in"]), code=code_json["userCode"]) + last_req_time = time.time() + + # Update progress dialog indicating time left for complete + while not login_dialog.iscanceled(): + time_left = round(expires_at - time.time()) + login_dialog.update(time_left=time_left) + xbmc.sleep(1000) + + status_code = -1 # Indicates no request sent + # Check if last request is sufficiently old + if (time_left > 0) and (time.time() - last_req_time >= code_json["interval"]): + status_code = auth.fetch_and_save_token( + code_json['deviceCode'], token_folder) + last_req_time = time.time() + if status_code == 200: + xbmcgui.Dialog().notification(__addon__.getLocalizedString(30424), + __addon__.getLocalizedString(30402), xbmcgui.NOTIFICATION_INFO, 3000) + break + + if status_code == 403: # 403 indicates rate limiting + xbmc.sleep(1000) + + # Refresh Code if time is over + if (time_left < 0) or (status_code and (status_code == 400)): + code_json = get_device_code() + expires_at = time.time() + 0.99 * float(code_json["expires_in"]) + login_dialog.update( + int(code_json["expires_in"]), code_json["userCode"]) + xbmcgui.Dialog().notification(__addon__.getLocalizedString(30424), + __addon__.getLocalizedString(30403), xbmcgui.NOTIFICATION_INFO, 3000) + login_dialog.close() + xbmc.executebuiltin('Container.Refresh') + + +def remove_account(): + xbmcvfs.delete(str(token_path)) + xbmc.executebuiltin('Container.Refresh') + + +def remove_all_accounts(): + token_folder.mkdir(parents=True, exist_ok=True) + for file in token_folder.iterdir(): + xbmcvfs.delete(str(file)) + + +def list_options(): + items = [] + + # Third string in the tuples are API endpoint's route + modes = [(__addon__.getLocalizedString(30404), 'list_media', 'all'), + (__addon__.getLocalizedString(30405), 'list_albums', 'albums'), + (__addon__.getLocalizedString(30406), 'list_albums', 'sharedAlbums'), + (__addon__.getLocalizedString(30407), 'custom_filter')] + + for mode in modes: + if len(mode) == 2: + url = utils.build_url( + base_url, {'mode': mode[1], 'token_filename': token_path.name}) + else: + url = utils.build_url( + base_url, {'mode': mode[1], 'type': mode[2], 'token_filename': token_path.name}) + li = xbmcgui.ListItem(mode[0]) + items.append((url, li, True)) # (url, listitem[, isFolder]) + + xbmcplugin.addDirectoryItems(addon_handle, items) + xbmcplugin.endOfDirectory(addon_handle) + + +def refresh(): + ''' + Refreshes the current window + IMP: Pass previous query string as argument in the url with key prev_q + ''' + id = xbmcgui.getCurrentWindowId() + pos = int(xbmc.getInfoLabel(f'Container.CurrentItem')) + cont_path = utils.build_url( + base_url, {'call_type': 1}, args.get('prev_q')[0]) + xbmc.executebuiltin(f'Container.Update({cont_path})') + + # get active window + win = xbmcgui.Window(id) + cid = win.getFocusId() + # Check if window is fully loaded + while not xbmc.getCondVisibility(f'Control.IsVisible({cid})'): + xbmc.sleep(150) + # Focus on the last focused position + xbmc.executebuiltin(f'SetFocus({cid},{pos},absolute)') + + +def get_items(pageToken=None) -> dict: + + # Prepare request + params = {'pageSize': 100} + if pageToken: + params['pageToken'] = pageToken + headers = {'Authorization': 'Bearer ' + token} + + # Check type of required listing + list_type = args.get('type')[0] + if list_type == 'all': + error = __addon__.getLocalizedString(30408) + res = requests.get(config.service_endpoint + '/mediaItems', + headers=headers, params=params) + elif list_type == 'album': + error = __addon__.getLocalizedString(30409) + params['albumId'] = args.get('id')[0] + res = requests.post(config.service_endpoint + '/mediaItems:search', + headers=headers, data=params) + elif list_type == 'filter': + # params + params['filters'] = utils.buildFilter(__addon__) + if not bool(params['filters']): + return None + error = __addon__.getLocalizedString(30410) + res = requests.post(config.service_endpoint + '/mediaItems:search', + headers=headers, json=params) + if res.status_code != 200: + dialog = xbmcgui.Dialog() + dialog.notification( + __addon__.getLocalizedString(30411), f'{error}. {__addon__.getLocalizedString(30412)}:{res.status_code}', xbmcgui.NOTIFICATION_ERROR, 3000) + return None + return res.json() + + +def get_items_bg(result, path): + if 'nextPageToken' in result[-1]: + pageToken = result[-1]["nextPageToken"] + media = get_items(pageToken) + if not media: + return None + utils.storeData(path, media) + + +def list_media(): + # For all photo directories + + path = profile_path + config.media_filename + if not args.get('call_type'): + xbmcvfs.delete(path) + + result = utils.loadData(path) + if not result: + media = get_items() + if media: + utils.storeData(path, media) + result = [media] + else: + xbmcgui.Dialog().notification(__addon__.getLocalizedString(30425), + __addon__.getLocalizedString(30413), xbmcgui.NOTIFICATION_INFO, 3000) + return + # List for media + items = [] + for item in result: + items += create_media_list(item) + + # Add list to directory + xbmcplugin.addDirectoryItems( + addon_handle, items, totalItems=len(items)) + + if 'nextPageToken' in result[-1]: + url = utils.build_url(base_url, {'mode': 'refresh', 'prev_q': qs}) + li = xbmcgui.ListItem(__addon__.getLocalizedString(30414)) + li.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(addon_handle, url, li) + if args.get('call_type'): + updateListing = True + else: + updateListing = False + xbmcplugin.endOfDirectory( + addon_handle, updateListing=updateListing, cacheToDisc=False) + + if 'nextPageToken' in result[-1]: + loader = threading.Thread(target=get_items_bg, args=( + result, path,)) + loader.start() + loader.join() + + +def create_media_list(media: dict): + # Creates a list of (url, li) from media dictionary + items = [] + for item in media.get("mediaItems", {}): + item_type = item["mimeType"].split('/')[0] + li = xbmcgui.ListItem(item["filename"]) + url = None + h = xbmcgui.getScreenHeight() + w = xbmcgui.getScreenWidth() + if item_type == 'image': + # thumb_url = item["baseUrl"] + f'=w{w}-h{h}' + thumb_url = item["baseUrl"] + f'=w{960}-h{540}' + img_url = item["baseUrl"] + f'=w{w}-h{h}' + li.setArt( + {'icon': 'DefaultIconInfo.png'}) + url = img_url + elif item_type == 'video': + vid_url = item["baseUrl"] + '=dv' + thumb_url = item["baseUrl"] + f'=w{w}-h{h}' + url = utils.build_url( + base_url, {'mode': 'play_video', 'path': vid_url, + 'status': item["mediaMetadata"]["video"]["status"], 'token_filename': token_path.name}) + li.setArt({'thumb': thumb_url, 'icon': thumb_url}) + li.setProperty('IsPlayable', 'true') + else: + continue + li.setProperty('MimeType', item["mimeType"]) + items.append((url, li)) + return items + + +def play_video(): + # https://kodi.wiki/view/HOW-TO:Video_addon + if args.get('status')[0] != 'READY': + xbmcgui.Dialog().notification(__addon__.getLocalizedString(30411), + __addon__.getLocalizedString(30415), xbmcgui.NOTIFICATION_ERROR, 3000) + else: + # Create a playable item with a path to play. + play_item = xbmcgui.ListItem(path=args.get('path')[0]) + # Pass the item to the Kodi player. + xbmcplugin.setResolvedUrl(addon_handle, True, listitem=play_item) + + +def custom_filter(): + __addon__.openSettings() + url = utils.build_url( + base_url, {'mode': 'list_media', 'type': 'filter'}, qs) + li = xbmcgui.ListItem(__addon__.getLocalizedString(30416)) + li.setProperty('isPlayable', 'false') + xbmcplugin.addDirectoryItem(addon_handle, url, li, isFolder=True) + xbmcplugin.endOfDirectory(addon_handle) + + +def list_albums(): + # For shared albums and regular albums + # https://developers.google.com/photos/library/guides/list + + request_type = args.get('type')[0] + # Request for listing albums or sharedAlbums + params = {} + if args.get('pageToken'): + params['pageToken'] = args.get('pageToken')[0] + headers = {'Authorization': 'Bearer ' + token} + res = requests.get(config.service_endpoint + f'/{request_type}', + headers=headers, params=params) + + # Parse response + a_num = 1 # For albums without name TODO: a_num should be in nextPage URL + if res.status_code != 200: + dialog = xbmcgui.Dialog() + dialog.notification( + 'Error', f'{__addon__.getLocalizedString(30417)}{res.status_code}', xbmcgui.NOTIFICATION_ERROR, 3000) + else: + album_data = res.json() # { albums, nextPageToken} + items = [] + if album_data: + for album in album_data[request_type]: + url = utils.build_url( + base_url, {'mode': 'list_media', 'type': 'album', 'id': album["id"], 'token_filename': token_path.name}) + if 'title' in album: + li = xbmcgui.ListItem(album["title"]) + else: + li = xbmcgui.ListItem( + f'{__addon__.getLocalizedString(30418)} {a_num}') + a_num += 1 + thumb_url = album["coverPhotoBaseUrl"] + \ + f'=w{xbmcgui.getScreenWidth()}-h{xbmcgui.getScreenHeight()}' + li.setArt( + {'thumb': thumb_url}) + items.append((url, li, True)) + + xbmcplugin.addDirectoryItems( + addon_handle, items, totalItems=len(items)) + + # Pagination + if 'nextPageToken' in album_data: + url = utils.build_url( + base_url, {'mode': 'list_albums', 'pageToken': album_data['nextPageToken'], 'token_filename': token_path.name, 'type': request_type}) + li = xbmcgui.ListItem(__addon__.getLocalizedString(30419)) + xbmcplugin.addDirectoryItem(addon_handle, url, li, True) + + xbmcplugin.endOfDirectory(addon_handle) + +# Modify this function to force changes after update + + +def make_changes(): + version_file = profile_path + 'info' + xbmcvfs.mkdirs(profile_path) + with open(version_file, 'w+') as file: + try: + past_info = json.load(file) + except: + past_info = {} + + if bool(past_info) or past_info.get('version') != __addon__.getAddonInfo('version'): + past_info['version'] = __addon__.getAddonInfo('version') + __addon__.setSettingString( + "baseUrl", "https://photos-kodi-addon.onrender.com") + with open(version_file, 'w') as file: + json.dump(past_info, file, default=str) + + +def display_page_content(): + if mode is None: + # Display logged in accounts + token_folder.mkdir(parents=True, exist_ok=True) + for file in token_folder.iterdir(): + try: + creds = read_credentials(file) + except: + xbmc.log(str(traceback.format_exc()), xbmc.LOGDEBUG) + err_dialog = xbmcgui.Dialog() + err_dialog.notification(__addon__.getLocalizedString(30411), + __addon__.getLocalizedString(30422), + xbmcgui.NOTIFICATION_ERROR, 3000) + exit() + email = creds["email"] + url = utils.build_url( + base_url, {'mode': 'list_options', 'token_filename': file.name}) + li = xbmcgui.ListItem(email) + removePath = utils.build_url( + base_url, {'mode': 'remove_account', 'email': email, 'token_filename': file.name}) + contextItems = [(__addon__.getLocalizedString( + 30420), f'RunPlugin({removePath})')] + li.addContextMenuItems(contextItems) + xbmcplugin.addDirectoryItem(handle=addon_handle, url=url, + listitem=li, isFolder=True) + # Add Account Button + url = utils.build_url(base_url, {'mode': 'new_account'}) + li = xbmcgui.ListItem(__addon__.getLocalizedString(30421)) + xbmcplugin.addDirectoryItem(handle=addon_handle, url=url, + listitem=li) + xbmcplugin.endOfDirectory(addon_handle) + else: + # Read account credentials if present + if token_path and mode[0] != 'remove_account': + try: + creds = read_credentials(token_path) + except: + err_dialog = xbmcgui.Dialog() + err_dialog.notification(__addon__.getLocalizedString(30411), + __addon__.getLocalizedString(30422), + xbmcgui.NOTIFICATION_ERROR, 3000) + exit() + global token + token = creds["token"] + eval(mode[0] + '()') # Fire up the actual function + + +make_changes() +# check for credentials +if not __addon__.getSettingString('client_id') or not __addon__.getSettingString('client_secret'): + open_settings_dialog = xbmcgui.Dialog().ok(__addon__.getLocalizedString(30428), + __addon__.getLocalizedString(30429)) + remove_all_accounts() + __addon__.openSettings() +else: + display_page_content() + # Updates: + # Slideshow + + # Not on list + # Video seeking - Not possible due to API limitations diff --git a/plugin.picture.googlephotos/resources/language/resource.language.en_gb/strings.po b/plugin.picture.googlephotos/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..53ce1850ab --- /dev/null +++ b/plugin.picture.googlephotos/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,343 @@ +# Kodi Media Center language file +# Addon Name: Google Photos +# Addon id: plugin.picture.googlephotos +# Addon Provider: Pranjal Singhal +msgid "" +msgstr "" + +#settings screen settings.xml +#setting category +msgctxt "#30100" +msgid "Sign-in Server" +msgstr "" + +msgctxt "#30101" +msgid "Base URL" +msgstr "" + +msgctxt "#30102" +msgid "Device Code Subroute" +msgstr "" + +msgctxt "#30103" +msgid "Access Token Subroute" +msgstr "" + +msgctxt "#30104" +msgid "Refresh Token Subroute" +msgstr "" + +msgctxt "#30105" +msgid "Client ID " +msgstr "" + +msgctxt "#30106" +msgid "Client Secret" +msgstr "" + +msgctxt "#30107" +msgid "Client Credentials" +msgstr "" + +#cache file name +msgctxt "#30107" +msgid "media_db" +msgstr "" + +msgctxt "#30108" +msgid "accounts/" +msgstr "" + +msgctxt "#30109" +msgid "Sign-in Details" +msgstr "" + +# filters +msgctxt "#30200" +msgid "Filter" +msgstr "" + +msgctxt "#30201" +msgid "Date Filter" +msgstr "" + +msgctxt "#30202" +msgid "Start Date" +msgstr "" + +msgctxt "#30203" +msgid "End Date" +msgstr "" + +msgctxt "#30203" +msgid "End Date" +msgstr "" + +msgctxt "#30204" +msgid "Content Filter" +msgstr "" + +msgctxt "#30205" +msgid "Content type" +msgstr "" + +msgctxt "#30206" +msgid "Media Filter" +msgstr "" + +msgctxt "#30207" +msgid "Media type" +msgstr "" + +msgctxt "#30208" +msgid "All Media" +msgstr "" + +msgctxt "#30209" +msgid "Video" +msgstr "" + +msgctxt "#30210" +msgid "Photo" +msgstr "" + +msgctxt "#30211" +msgid "Feature Filter" +msgstr "" + +msgctxt "#30212" +msgid "Show only favourites" +msgstr "" + +msgctxt "#30213" +msgid "Use date filter" +msgstr "" + +msgctxt "#30400" +msgid "Enter the following code at " +msgstr "" + +msgctxt "#30401" +msgid "Authenticate" +msgstr "" + +msgctxt "#30402" +msgid "Account added successfully" +msgstr "" + +msgctxt "#30403" +msgid "Code refreshed" +msgstr "" + +msgctxt "#30404" +msgid "All Media" +msgstr "" + +msgctxt "#30405" +msgid "All Albums" +msgstr "" + +msgctxt "#30406" +msgid "Shared Albums" +msgstr "" + +msgctxt "#30407" +msgid "Custom Filter" +msgstr "" + +msgctxt "#30408" +msgid "Unable to retrieve media items from Google" +msgstr "" + +msgctxt "#30409" +msgid "Unable to load album items" +msgstr "" + +msgctxt "#30410" +msgid "Error in filtering photos" +msgstr "" + +msgctxt "#30411" +msgid "Error" +msgstr "" + +msgctxt "#30412" +msgid "Error Code" +msgstr "" + +msgctxt "#30413" +msgid "No Media Item to load" +msgstr "" + +msgctxt "#30414" +msgid "Show more..." +msgstr "" + +msgctxt "#30415" +msgid "Video is not ready for viewing yet" +msgstr "" + +msgctxt "#30416" +msgid "Display Filtered Results" +msgstr "" + +msgctxt "#30417" +msgid "Unable to retrieve album list. Error Code:" +msgstr "" + +msgctxt "#30418" +msgid "Nameless album" +msgstr "" + +msgctxt "#30419" +msgid "Next Page" +msgstr "" + +msgctxt "#30420" +msgid "Remove Account" +msgstr "" + +msgctxt "#30421" +msgid "Add New Account" +msgstr "" + +msgctxt "#30422" +msgid "No response from proxy server. Try again." +msgstr "" + +msgctxt "#30423" +msgid "Loading..." +msgstr "" + +msgctxt "#30424" +msgid "Success" +msgstr "" + +msgctxt "#30425" +msgid "No items" +msgstr "" + +msgctxt "#30426" +msgid "Expires in" +msgstr "" + +msgctxt "#30427" +msgid "seconds" +msgstr "" + +msgctxt "#30428" +msgid "Credentials Required" +msgstr "" + +msgctxt "#30429" +msgid "Please input client ID and secret in settings. Follow the instructions at www.photos-kodi-addon.onrender.com/credentialsguide to generate credentials." +msgstr "" +# No need most probably +# # Content filter +# msgctxt "#30301" +# msgid "None" +# msgstr "" + +# msgctxt "#30302" +# msgid "Landscapes" +# msgstr "" + +# msgctxt "#30303" +# msgid "Receipts" +# msgstr "" + +# msgctxt "#30304" +# msgid "Cityscapes" +# msgstr "" + +# msgctxt "#30305" +# msgid "Landmarks" +# msgstr "" + +# msgctxt "#30306" +# msgid "Selfies" +# msgstr "" + +# msgctxt "#30307" +# msgid "People" +# msgstr "" + +# msgctxt "#30308" +# msgid "Pets" +# msgstr "" + +# msgctxt "#30309" +# msgid "Weddings" +# msgstr "" + +# msgctxt "#30310" +# msgid "Birthdays" +# msgstr "" + +# msgctxt "#30311" +# msgid "Documents" +# msgstr "" + +# msgctxt "#30312" +# msgid "Travel" +# msgstr "" + +# msgctxt "#30313" +# msgid "Animals" +# msgstr "" + +# msgctxt "#30314" +# msgid "Food" +# msgstr "" + +# msgctxt "#30315" +# msgid "Sport" +# msgstr "" + +# msgctxt "#30316" +# msgid "Night" +# msgstr "" + +# msgctxt "#30317" +# msgid "Performances" +# msgstr "" + +# msgctxt "#30318" +# msgid "Whiteboards" +# msgstr "" + +# msgctxt "#30319" +# msgid "Screenshots" +# msgstr "" + +# msgctxt "#30320" +# msgid "Utility" +# msgstr "" + +# msgctxt "#30321" +# msgid "Arts" +# msgstr "" + +# msgctxt "#30322" +# msgid "Crafts" +# msgstr "" + +# msgctxt "#30323" +# msgid "Fashion" +# msgstr "" + +# msgctxt "#303024" +# msgid "Houses" +# msgstr "" + +# msgctxt "#303025" +# msgid "Gardens" +# msgstr "" + +# msgctxt "#303026" +# msgid "Flowers" +# msgstr "" + +# msgctxt "#303027" +# msgid "Holidays" +# msgstr "" \ No newline at end of file diff --git a/plugin.picture.googlephotos/resources/lib/__init__.py b/plugin.picture.googlephotos/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.picture.googlephotos/resources/lib/auth.py b/plugin.picture.googlephotos/resources/lib/auth.py new file mode 100644 index 0000000000..0cee93dd01 --- /dev/null +++ b/plugin.picture.googlephotos/resources/lib/auth.py @@ -0,0 +1,139 @@ +import datetime +import time +from pathlib import Path +from . import config +from resources.lib.utils import join_path +import requests +import json +import xbmcaddon +import xbmc + + +SCOPES = ['https://www.googleapis.com/auth/photoslibrary.readonly', + 'email', + 'openid'] + + +# The file token.json stores the user's access and refresh tokens, and is +# created automatically when the authorization flow completes for the first +# time. + +__addon__ = xbmcaddon.Addon() +clientCreds = { + 'clientId': __addon__.getSettingString('client_id'), + 'clientSecret': __addon__.getSettingString('client_secret') +} + + +def get_device_code(): + # On success, returns json containing user code, device code etc. and None on failure + # Exception Possible + path = join_path(__addon__.getSettingString( + 'baseUrl'), __addon__.getSettingString('deviceCodeUrl')) + + # Exchange client credentials for device code and user code + res = requests.post(path, data=clientCreds) + if res.status_code != 200: + xbmc.log(f'GP: {str(res.status_code)}', xbmc.LOGDEBUG) + return None + return res.json() + + +def fetch_and_save_token(device_code, tf_path): + ''' + Takes in token folder path + Fetch token from the auth server and save in token.json if successful + Returns: 200 if successful + 202 if user has not completed login on other device + 403 if server asks to slow down + In any other case server response is returned. + P.S. This function does not continously poll the auth server. It needs to be repeatedly called by the caller code. + ''' + pl_path = Path(tf_path) # Pathlib path + token_url = join_path(__addon__.getSettingString( + 'baseUrl'), __addon__.getSettingString('tokenUrl')) + res = requests.post(token_url, data={ + 'deviceCode': device_code, 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code'}) + if res.status_code == 202 or res.status_code == 403: + return res.status_code + if res.status_code != 200: + xbmc.log(res.text, xbmc.LOGDEBUG) + return res.status_code + + token_data = res.json() + # Fetch email and a unique identifier(called sub) from Google + headers = {'Authorization': 'Bearer ' + token_data["access_token"]} + openid_res = requests.get(config.email_url, headers=headers) + if openid_res.status_code == 200: + res_json = openid_res.json() + email = res_json["email"] + sub_json = res_json["sub"] + ".json" + else: + raise Exception( + f"Unknown response from server: {openid_res.status_code}") + # Construct token + token = { + "token": token_data["access_token"], + "refresh_token": token_data["refresh_token"], + "email": email, + "scopes": SCOPES, + "expiry": datetime.datetime.utcnow() + + datetime.timedelta(seconds=token_data["expires_in"]) + } + # Write to sub.json + pl_path.mkdir(parents=True, exist_ok=True) + with open(pl_path / sub_json, mode='w') as token_json: + json.dump(token, token_json, default=str) + return 200 + + +def refresh_access_token(creds, path): + # Returns new access token and expiry + refresh_url = join_path(__addon__.getSettingString( + 'baseUrl'), __addon__.getSettingString('refreshUrl')) + + data = { + 'refresh_token': creds["refresh_token"], + 'grant_type': 'refresh_token' + } + res = requests.post(refresh_url, data={**data, **clientCreds}) + if res.status_code != 200: + return res.status_code + token_data = res.json() + creds["token"] = token_data["access_token"] + creds["expiry"] = (datetime.datetime.utcnow() + + datetime.timedelta(seconds=token_data["expires_in"])) + with open(path, mode='w') as token_json: + json.dump(creds, token_json, default=str) + return 200 + + +def read_credentials(path): + ''' + Reads credentials from the provided file path + Refreshes and saves the credentials if expired + Returns: credentials json if successful + None if the file does not exist + ''' + pl_path = Path(path) # Pathlib path + if not pl_path.exists(): + return None + creds = None + # Read creds + with open(path, mode='r') as token_json: + creds = json.load(token_json) + + # Refresh creds if expired + expiry = creds["expiry"] + format = "%Y-%m-%d %H:%M:%S.%f" + try: + exp = datetime.datetime.strptime(expiry, format) + except TypeError: + exp = datetime.datetime(*(time.strptime(expiry, format)[0:6])) + if exp < datetime.datetime.utcnow(): + status = refresh_access_token( + creds, path) # Refreshes creds + if status != 200: + xbmc.log(str(status), xbmc.LOGDEBUG) + + return creds diff --git a/plugin.picture.googlephotos/resources/lib/config.py b/plugin.picture.googlephotos/resources/lib/config.py new file mode 100644 index 0000000000..43c2c07a57 --- /dev/null +++ b/plugin.picture.googlephotos/resources/lib/config.py @@ -0,0 +1,8 @@ +# Endpoint to get user email (https://accounts.google.com/.well-known/openid-configuration) +email_url = 'https://openidconnect.googleapis.com/v1/userinfo' + +# Google Photos API +service_endpoint = 'https://photoslibrary.googleapis.com/v1' + +token_folder = 'accounts/' +media_filename = 'media_db' diff --git a/plugin.picture.googlephotos/resources/lib/dialogs.py b/plugin.picture.googlephotos/resources/lib/dialogs.py new file mode 100644 index 0000000000..9c4b804452 --- /dev/null +++ b/plugin.picture.googlephotos/resources/lib/dialogs.py @@ -0,0 +1,74 @@ +from time import time +import xbmcgui +import xbmcvfs +import xbmcaddon +import os + + +# https://github.com/cguZZman/script.module.clouddrive.common/blob/master/clouddrive/common/ui/dialog.py +class QRDialogProgress(xbmcgui.WindowXMLDialog): + _heading_control = 1000 + _qr_control = 1001 + _text_control = 1002 + _cancel_btn_control = 1003 + addon = xbmcaddon.Addon() + + def __init__(self, *args, **kwargs): + self.heading = QRDialogProgress.addon.getLocalizedString(30401) + self.qr_code = "" + self.line1 = QRDialogProgress.addon.getLocalizedString( + 30400) + QRDialogProgress.addon.getSettingString('baseUrl') + ' :' + self.line2 = QRDialogProgress.addon.getLocalizedString(30423) + self.line3 = QRDialogProgress.addon.getLocalizedString(30426) + self.line4 = QRDialogProgress.addon.getLocalizedString(30427) + self.time_left = 0 + self._image_path = None + self.canceled = False + + def __del__(self): + xbmcvfs.delete(self._image_path) + pass + + @staticmethod + def create(): + return QRDialogProgress("pin-dialog.xml", QRDialogProgress.addon.getAddonInfo('path'), "default", "1080i") + + def iscanceled(self): + return self.canceled + + def onInit(self): + self.getControl(self._heading_control).setLabel(self.heading) + self.update(self.time_left, self.line2) + + def update(self, time_left=None, code=None): + if time_left: + self.time_left = time_left + + if code: + self.line2 = code + self.qr_code = os.path.join(QRDialogProgress.addon.getSettingString( + 'baseUrl'), f'authenticate?code={code}') + + text = f'{self.line1}[CR][COLOR red][B]{self.line2}[/B][/COLOR][CR][CR]{self.line3} {self.time_left} {self.line4}.' + self.getControl(self._text_control).setText(text) + self.updateQr() + self.setFocus(self.getControl(self._cancel_btn_control)) + + def onClick(self, control_id): + if control_id == self._cancel_btn_control: + self.canceled = True + self.close() + + def onAction(self, action): + if action.getId() == xbmcgui.ACTION_PREVIOUS_MENU or action.getId() == xbmcgui.ACTION_NAV_BACK: + self.canceled = True + super(QRDialogProgress, self).onAction(action) + + def updateQr(self): + import pyqrcode + self._image_path = os.path.join(xbmcvfs.translatePath( + self.addon.getAddonInfo('profile')), "qr.png") + qrcode = pyqrcode.create(self.qr_code) + qrcode.png(self._image_path, scale=10) + del qrcode + self.getControl(self._qr_control).setImage(self._image_path) diff --git a/plugin.picture.googlephotos/resources/lib/utils.py b/plugin.picture.googlephotos/resources/lib/utils.py new file mode 100644 index 0000000000..e5f8872eb8 --- /dev/null +++ b/plugin.picture.googlephotos/resources/lib/utils.py @@ -0,0 +1,107 @@ +from urllib import parse +import pickle +import os + + +def build_url(base_url, query: dict, qs=None) -> str: + ''' + Options: query to be encoded + qs to be modified with items from the query dict + Assumes single value for a key + Returns a url + ''' + if qs: + qdict = dict(parse.parse_qsl(qs)) + for key, val in query.items(): + qdict[key] = val + return base_url + '?' + parse.urlencode(qdict) + return base_url + '?' + parse.urlencode(query) + +def join_path(*args): + ''' + Joins path parts and strips extra slashes + ''' + return '/'.join(s.strip('/') for s in args) +def sleep_time(time=300): + ''' + Input: Time to complete authentication + Return: time to sleep(in msec) + ''' + return (time * 10) + +# Portability not guaranteed +# check xbmcvfs + + +def loadData(path): + # For caching links + if not os.path.exists(path): + return None + data = [] + with open(path, 'rb') as fr: + try: + while True: + data.append(pickle.load(fr)) + except EOFError: + pass + return data + + +def storeData(path, db: dict): + # Appends data to file + dbfile = open(path, 'ab') + # source, destination + pickle.dump(db, dbfile) + dbfile.close() + + +def buildFilter(__addon__): + is_date_filter_applied = __addon__.getSettingBool('date_filter') + start_date = __addon__.getSettingString('start_date') + end_date = __addon__.getSettingString('end_date') + media_type = __addon__.getSettingString('media_filter') + content_type = __addon__.getSettingString('content_filter') + favourites = __addon__.getSettingBool('favourites') + filters = {} + if (is_date_filter_applied and (start_date < end_date)): + sd = start_date.split('-') + ed = end_date.split('-') + filters['dateFilter'] = { + "ranges": [ + { + "startDate": { + "year": sd[0], + "month": sd[1], + "day": sd[2] + }, + "endDate":{ + "year": ed[0], + "month": ed[1], + "day": ed[2] + } + } + ] + } + + if media_type != 'All Media': + filters['mediaTypeFilter'] = { + 'mediaTypes': [ + media_type.upper().replace(' ', '_') + ] + } + + if content_type != 'None': + filters['contentFilter'] = { + 'includedContentCategories': [ + content_type.upper() + ] + } + + if favourites: + filters['featureFilter'] = { + "includedFeatures": [ + 'FAVOURITES' + ] + } + + return filters diff --git a/plugin.picture.googlephotos/resources/settings.xml b/plugin.picture.googlephotos/resources/settings.xml new file mode 100644 index 0000000000..ab6223bec4 --- /dev/null +++ b/plugin.picture.googlephotos/resources/settings.xml @@ -0,0 +1,159 @@ +<?xml version="1.0"?> +<settings version="1"> + <section id="plugin.picture.googlephotos"> + + <category id="signin" label="30109" help=""> + <group id="4" label="30107"> + <setting id="client_id" type="string" label="30105" help=""> + <level>0</level> + <constraints> + <allowempty>true</allowempty> + </constraints> + <control type="edit" format="string"></control> + </setting> + <setting id="client_secret" type="string" label="30106" help=""> + <level>0</level> + <constraints> + <allowempty>true</allowempty> + </constraints> + <control type="edit" format="string"></control> + </setting> + </group> + + <group id="5" label="30100"> + <setting id="baseUrl" type="string" label="30101" help=""> + <level>0</level> + <default>https://photos-kodi-addon.onrender.com</default> + <constraints> + <allowempty>false</allowempty> + </constraints> + <control type="edit" format="string"></control> + </setting> + + <setting id="deviceCodeUrl" type="string" label="30102" help=""> + <level>3</level> + <default>/device/code</default> + <constraints> + <allowempty>false</allowempty> + </constraints> + <control type="edit" format="string"></control> + </setting> + + <setting id="tokenUrl" type="string" label="30103" help=""> + <level>3</level> + <default>/token</default> + <constraints> + <allowempty>false</allowempty> + </constraints> + <control type="edit" format="urlencoded"></control> + </setting> + + <setting id="refreshUrl" type="string" label="30104" help=""> + <level>3</level> + <default>/refresh</default> + <constraints> + <allowempty>false</allowempty> + </constraints> + <control type="edit" format="string"></control> + </setting> + </group> + </category> + + <category id="filters" label="30200" help=""> + <group id="1" label="30201"> + <setting id="date_filter" type="boolean" label="30213" help=""> + <level>0</level> + <default>false</default> + <control type="toggle" /> + </setting> + <setting id="start_date" type="date" label="30202" help=""> + <level>0</level> + <default>1700-01-01</default> + <dependencies> + <dependency type="enable" setting="date_filter">true</dependency> + </dependencies> + <control type="button" format="date"> + <heading>30202</heading> + </control> + </setting> + <setting id="end_date" type="date" label="30203" help=""> + <level>0</level> + <default>2200-01-01</default> + <dependencies> + <dependency type="enable" setting="date_filter">true</dependency> + </dependencies> + <control type="button" format="date"> + <heading>30203</heading> + </control> + </setting> + </group> + + <group id="2" label="30206"> + <setting id="media_filter" type="string" label="30207" help=""> + <level>0</level> + <default>All Media</default> + <constraints> + <options> + <option>All Media</option> + <option>Video</option> + <option>Photo</option> + </options> + <allowempty>true</allowempty> + </constraints> + <control type="list" format="string"> + <heading>30207</heading> + </control> + </setting> + </group> + + <!-- Check for multiselect control --> + <group id="3" label="30204"> + <setting id="content_filter" type="string" label="30205" help=""> + <level>0</level> + <default>None</default> + <constraints> + <options> + <option>None</option> + <option>Landscapes</option> + <option>Receipts</option> + <option>Cityscapes</option> + <option>Landmarks</option> + <option>Selfies</option> + <option>People</option> + <option>Pets</option> + <option>Weddings</option> + <option>Birthdays</option> + <option>Documents</option> + <option>Travel</option> + <option>Animals</option> + <option>Food</option> + <option>Sport</option> + <option>Night</option> + <option>Performances</option> + <option>Whiteboards</option> + <option>Screenshots</option> + <option>Utility</option> + <option>Arts</option> + <option>Crafts</option> + <option>Fashion</option> + <option>Houses</option> + <option>Gardens</option> + <option>Flowers</option> + <option>Holidays</option> + </options> + <allowempty>false</allowempty> + </constraints> + <control type="list" format="string"> + <heading>30205</heading> + </control> + </setting> + <setting id="favourites" type="boolean" label="30212" help=""> + <level>0</level> + <default>false</default> + <control type="toggle" /> + </setting> + </group> + + </category> + </section> +</settings> \ No newline at end of file diff --git a/plugin.picture.googlephotos/resources/skins/default/1080i/pin-dialog.xml b/plugin.picture.googlephotos/resources/skins/default/1080i/pin-dialog.xml new file mode 100644 index 0000000000..09cc50c78b --- /dev/null +++ b/plugin.picture.googlephotos/resources/skins/default/1080i/pin-dialog.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<window> + <coordinates> + <left>385</left> + <top>315</top> + </coordinates> + <controls> + <animation type="WindowOpen" reversible="false"> + <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="back" time="240" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="back" time="240" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> + <control type="image"> + <left>-1920</left> + <top>-1080</top> + <width>5760</width> + <height>3240</height> + <animation effect="fade" start="0" end="100" time="300">WindowOpen</animation> + <animation effect="fade" start="100" end="0" time="200">WindowClose</animation> + <texture colordiffuse="C2FFFFFF">black.png</texture> + </control> + <!-- Dialog panel --> + <control type="image"> + <left>0</left> + <top>0</top> + <width>1150</width> + <height>450</height> + <texture border="2">dialog-bg.png</texture> + </control> + <!-- Title bar --> + <control type="image"> + <left>0</left> + <top>0</top> + <width>1150</width> + <height>70</height> + <texture colordiffuse="FF147A96" border="2">white.png</texture> + </control> + <!-- Title label --> + <control type="label" id="1000"> + <textoffsetx>20</textoffsetx> + <left>0</left> + <top>0</top> + <width>1150</width> + <height>70</height> + <font>font30_title</font> + <align>left</align> + <aligny>center</aligny> + <shadowcolor>FF000000</shadowcolor> + </control> + <!-- QR image --> + <control type="image" id="1001"> + <left>20</left> + <top>90</top> + <width>340</width> + <height>340</height> + <aspectratio aligny="center" align="center">keep</aspectratio> + </control> + <!-- Text panel --> + <control type="textbox" id="1002"> + <left>380</left> + <top>90</top> + <width>770</width> + <height>340</height> + <font>font12_title</font> + <align>left</align> + <aligny>top</aligny> + <shadowcolor>FF000000</shadowcolor> + </control> + <!-- Cancel button --> + <control type="button" id="1003"> + <left>850</left> + <top>350</top> + <width>300</width> + <height>100</height> + <font>font12_title</font> + <textcolor>FFFFFFFF</textcolor> + <label>222</label> + <align>center</align> + <aligny>center</aligny> + </control> + </controls> +</window> \ No newline at end of file diff --git a/plugin.picture.googlephotos/resources/skins/default/media/black.png b/plugin.picture.googlephotos/resources/skins/default/media/black.png new file mode 100644 index 0000000000..2ff1770d53 Binary files /dev/null and b/plugin.picture.googlephotos/resources/skins/default/media/black.png differ diff --git a/plugin.picture.googlephotos/resources/skins/default/media/dialog-bg.png b/plugin.picture.googlephotos/resources/skins/default/media/dialog-bg.png new file mode 100644 index 0000000000..e8c13ebd70 Binary files /dev/null and b/plugin.picture.googlephotos/resources/skins/default/media/dialog-bg.png differ diff --git a/plugin.picture.googlephotos/resources/skins/default/media/white.png b/plugin.picture.googlephotos/resources/skins/default/media/white.png new file mode 100644 index 0000000000..528c66f6e8 Binary files /dev/null and b/plugin.picture.googlephotos/resources/skins/default/media/white.png differ diff --git a/plugin.program.AML/AUTHORS.md b/plugin.program.AML/AUTHORS.md new file mode 100644 index 0000000000..77d49a5062 --- /dev/null +++ b/plugin.program.AML/AUTHORS.md @@ -0,0 +1,11 @@ +## Advanced MAME Launcher + +Advanced MAME Launcher was coded from scratch by Wintermute0110 <wintermute0110@gmail.com>. +The first public released version of Advanced MAME Launcher was 0.9.0 on Jan 2017. + +[Kodi forum thread](http://forum.kodi.tv/showthread.php?tid=304186) + +### AML contributors + + * PDF rendering code taken from **PDF Reader** addon by i96751414. + [PDF Reader source code](https://github.com/i96751414/plugin.image.pdfreader) diff --git a/plugin.program.AML/LICENSE.txt b/plugin.program.AML/LICENSE.txt new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/plugin.program.AML/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.program.AML/NOTES.md b/plugin.program.AML/NOTES.md new file mode 100644 index 0000000000..21df0f1429 --- /dev/null +++ b/plugin.program.AML/NOTES.md @@ -0,0 +1,352 @@ +[AML python2..master comparison](https://github.com/Wintermute0110/plugin.program.AML.dev/compare/python2..master) + +[AML master..python2 comparison](https://github.com/Wintermute0110/plugin.program.AML.dev/compare/master..python2) + +[Kodi Documentation (codedocs)](https://codedocs.xyz/xbmc/xbmc/) + +[RA buildbot cores](https://buildbot.libretro.com/nightly/) + +[RA MAME 2003 core controls](https://docs.libretro.com/library/mame_2003/) + +MAME 2003 Plus RA core write the file `mame2003-plus.xml` in `SAVES_DIR/mame2003-plus/` directory. + +## AML and Python 2 (Kodi Krypton and Leia) / Python 3 (Kodi Matrix) + + * AML releases `0.9.x` and `0.10.x` will be **Python 2** for Kodi Krypton and Kodi Leia. **Python 2** code will be in branch `python2`. + + * AML releases `1.x.y` will be **Pyhton 3** for Kodi Matrix and up. **Pyhton 3** code will be in branch `master`. + + * From now on (August 2020), focus will be on release `1.x.y`. Make some features from **Pyhton 3** will be backported to **Python 2**. + +## Porting Python 2 to Pyhton 3 ## + +**TODO** + +Remove tasks once finished. + + * Create a function in `disk_IO.py` to write text files, arguments filename and slist. Use this function to write al reports and text files. Use `io.open`. + + * Call function in `utils_kodi` to display text window. + +**Language specific issues** + + * The `urlparse` module is renamed to `urllib.parse` in Python 3. + + * The `StringIO` modules is gone. Use `io.StringIO` and `io.BytesIO`. + + * Python 2 unicode object is str object in Python 3. Python 2 str object is bytes in Python 3. + + * Python 2 dict.iteritems() must be converted to dict.items() + + [StackOverflow: When should iteritems() be used instead of items()?](https://stackoverflow.com/questions/13998492/when-should-iteritems-be-used-instead-of-items) + + * Python 2 iterator method .next() is built-in function next(iterator) or .__next__() method in Python 3. + + * `xml.etree.cElementTree` is deprecated. It will be used automatically by `xml.etree.ElementTree` whenever available. + + * Use `io.open()` and not built-in `open()` in Python 2. `io.open()` in Python 2 supports the `encoding` argument and it's compatible with Python 3 `io.open()`. Moreover, in Python 3 `open()` is an alias of `io.open()`. + +**Kodi specific issues** + + * AML Python 2 is Krypton compatible. This means in Python 3 all the API updates can be applied. + + * Kodi functions now take/return Unicode strings (str type in Python 3) + + [Kodi six](https://github.com/romanvm/kodi.six) + + * Leia change: Addon setting functions getSettingBool(), getSettingInt(), etc. + + * Matrix change: Addon settings *should* be converted. The old settings are deprecated, quoting from the wiki: "Deprecated - Addons submitted to the Kodi 19 Matrix (and up) can use the new setting format. See Add-on_settings_conversion." + + [Kodi wiki: Add-on settings](https://kodi.wiki/view/Add-on_settings) [Kodi wiki: Addon settings conversion](https://kodi.wiki/view/Add-on_settings_conversion) [Kodi Matrix alpha 1, addon settings do not show](https://forum.kodi.tv/showthread.php?tid=356245) + + * Matrix change: `XBMC.RunPlugin({}?command={})` must be changed to `RunPlugin({}?command={})`. + + * Matrix change: `xbmcgui.Dialog().yesno()` add support for custom button. + + * Matrix change: `xbmcgui.Dialog().yesno()`, `.cancel()`, `.ok()`, `xbmcgui.DialogProgress().create()`, `.update()`, Removed Line2, Line3. + + All progress dialogs (search for `pDialog.create()` in the code) must use the new `KodiProgressDialog()` class. + +**Travis errors** + +Travis suggest using `list(dic.items())` instead of `dic.items()` when iterating the keys and values of a dictionary in Python 3. However, this can be harmful for performance! + +``` +- for m_name, r_name in catalog_dic.items(): ++ for m_name, r_name in list(catalog_dic.items()): + sl.append('<machine>') +``` + +[Stack overflow: Difference between iterate dictionary.items() vs list(dictionary.items())](https://stackoverflow.com/questions/63706787/difference-between-iterate-dictionary-items-vs-listdictionary-items) + +**References** + +[The Conservative Python 3 Porting Guide](https://portingguide.readthedocs.io/en/latest/index.html) + +[Kodi forum: Changes to the python API for Kodi Matrix](https://forum.kodi.tv/showthread.php?tid=344263) + +[Kodi forum: Changes to the python API for Kodi Leia](https://forum.kodi.tv/showthread.php?tid=303073) + +[Processing Text Files in Python 3](http://python-notes.curiousefficiency.org/en/latest/python3/text_file_processing.html) + +## Installing multiple Kodi versions in Windows for development ## + + 1. Download and then run the executable installer file for the version of Kodi you want to install. **You must change the installation location from the default location.** + + 2. Find the `Kodi.exe` application in the folder you just installed. Right-click the application, and choose ‘Create shortcut’. + + 3. Right-click the shortcut you created, and choose ‘Properties’. In the ‘Target’ text field, add the argument `-p` after the file location. + + 4. If you create another shortcut and fail to add the `-p` switch, or start this portable version of Kodi in a different way, then the default userdata folder will be created, and might overwrite your standard installation of Kodi, if you have one. + + 5. The Kodi folder which you nominated above will be used to host the data folder (where Kodi stores scripts, plugins, skins and userdata) in a subfolder named `portable_data`. `portable_data` is mapped to `special://home/`. + +**References** + +[Kodi Wiki: HOW-TO:Install_Kodi_for_Windows](https://kodi.wiki/view/HOW-TO:Install_Kodi_for_Windows#Portable_Mode) + +## Installing multiple Kodi versions in Linux for development ## + +WRITE ME. + +**References** + +[Kodi forum: Development with several Kodi versions and userdata directories](https://forum.kodi.tv/showthread.php?tid=356152) + +## Publishing AML into the Kodi repository (Tortoise Git) ## + +**Setup** + +First make sure the remote repository is OK. In `Tortoise Git`, `Settings`, in the `Remote` option there should be a remote named `upstream` with URL `https://github.com/xbmc/repo-plugins.git`. + +**Updating repository** + +Suppose we want to update the branch `krypton`. Use `Git show log` to make sure the repository is on the `krypton` branch. + +To update the working copy with the contents of upstream use `Pull` with remote `upstream` and remote branch `krypton`. + +**Update addon** + +Create a branch with `Create branch...`. The branch name must be `plugin.program.AML`. Make the description the same as the branch name. Use the `Switch/Checkout...` command to switch to the new branch. + +Make sure the repository is on the branch `plugin.program.AML`. Make the changes to update the addon and then do a single commit named `[plugin.program.AML] x.y.z`. + +Push the branch `plugin.program.AML` to the remote `origin`. Finally, open the pull request in Github. + +**Updating the pull request** + +Updating your pull request can be done by applying your changes and squashing them in the already present commit. + +**References** + +[Kodi xbmc-repoplugins: CONTRIBUTING](https://github.com/xbmc/repo-plugins/blob/master/CONTRIBUTING.md) + +## Kodi repository Travis rules ## + + * Screenshots maximum file size is 750 KB. + +## Resolution table ## + +| Name | Resolution | Notes | +|----------------|---------------|------------------------------------------------------------| +| SDTV 480i NTSC | ` 704 x 480` | AR 4:3 NTSC, 720 x 480 full frame with horizontal blanking | +| SDTV 576i PAL | ` 704 x 576` | AR 4:3 NTSC, 720 x 576 full frame with horizontal blanking | +| Standard HD | `1280 x 720` | AR 16:9 | +| Full HD | `1920 x 1080` | AR 16:9, informally referred as 2K | +| 4K Ultra HD | `3840 x 2160` | AR 16:9 | +| 8K Ultra HD | `7680 x 4320` | AR 16:9 | + + * In **SDTV** the pixel aspect ratio (PAR) is not square, and the PAR changes depending on the display aspect ratio (DAR). In other words, the SDTV resolution for 4:3 DAR or 16:9 DAR is the same, what changes is the pixel aspect ratio and hence the physical size of the display. + +## MAME implicit/explicit ROM merging ## + +ClrMAME Pro merges clone ROMs implicitly if a ROM with same CRC exists in the parent set. There is some info in PD forum about this. + +## Known media types in MAME ### + +Machines may have more than one media type. In this case, a number is appended at the end. For +example, a machine with 2 cartriged slots has `cartridge1` and `cartridge2`. + +| Name | Short name | Machine example | +|------------|------------|------------------| +| cartridge | cart | 32x, sms, smspal | +| cassete | cass | | +| floppydisk | flop | | +| quickload | quick | | +| snapshot | dump | | +| harddisk | hard | | +| cdrom | cdrm | | +| printer | prin | | + +## Cartridges ### + +Most consoles have only one cartridge slot, for example `32x`. + +``` +<machine name="32x" sourcefile="megadriv.cpp"> +... +<device type="cartridge" tag="cartslot" mandatory="1" interface="_32x_cart"> + <instance name="cartridge" briefname="cart"/> + <extension name="32x"/> + <extension name="bin"/> +</device> +``` + +Device name and its brief version can be used at command line to launch a specific program/game. + +``` +mame 32x -cartridge foo1.32x +mame 32x -cart foo1.32x +``` + +A machine may have more than one cartridge slot, for example `abc110`. + +``` +<machine name="abc110" sourcefile="bbc.cpp" cloneof="bbcbp" romof="bbcbp"> +... +<device type="cartridge" tag="exp_rom1" interface="bbc_cart"> + <instance name="cartridge1" briefname="cart1"/> + <extension name="bin"/> + <extension name="rom"/> +</device> +<device type="cartridge" tag="exp_rom2" interface="bbc_cart"> + <instance name="cartridge2" briefname="cart2"/> + <extension name="bin"/> + <extension name="rom"/> +</device> +... +``` + +Launching command example. + +``` +mame abc110 -cart1 foo1.bin -cart2 foo2.bin +``` + +## Launching Software Lists ## + +Example of machines with SL: `32x`. + +``` +<machine name="32x" sourcefile="megadriv.cpp"> +... + <device type="cartridge" tag="cartslot" mandatory="1" interface="_32x_cart"> + <instance name="cartridge" briefname="cart"/> + <extension name="32x"/> + <extension name="bin"/> + </device> + <slot name="cartslot"> + </slot> + <softwarelist name="32x" status="original" filter="NTSC-U" /> +</machine> +``` + +**References** + +[MESS wiki: HOWTO](http://mess.redump.net/mess/howto) + +[MESS wiki: Software List Format](http://mess.redump.net/mess/swlist_format) + +## Special SL items in Software Lists ## + +### Implicit ROM merging ### + +Software List XMLs do not have the ROM `merge` attribute. However, ClrMAME Pro merges SL +clone ROMs implicitly if a ROM with same CRC exists in the parent set. + +SL `sms`, item `teddyboy` and `teddyboyc`. + +### SL ROMS with `loadflag` attribute ### + +MAME 0.196, SL `neogeo`, item `aof` "Art of Fighting / Ryuuko no Ken (NGM-044 ~ NGH-044)". + +``` +<software name="aof"> + <description>Art of Fighting / Ryuuko no Ken (NGM-044 ~ NGH-044)</description> + <year>1992</year> + <publisher>SNK</publisher> + <info name="serial" value="NGM-044 (MVS), NGH-044 (AES)"/> + <info name="release" value="19920924 (MVS), 19921211 (AES)"/> + <info name="alt_title" value="龍虎の拳"/> + <sharedfeat name="release" value="MVS,AES" /> + <sharedfeat name="compatibility" value="MVS,AES" /> + <part name="cart" interface="neo_cart"> + <dataarea name="maincpu" width="16" endianness="big" size="0x100000"> + <!-- TC534200 --> + <rom loadflag="load16_word_swap" name="044-p1.p1" offset="0x000000" size="0x080000" crc="ca9f7a6d" sha1="4d28ef86696f7e832510a66d3e8eb6c93b5b91a1" /> + </dataarea> + <dataarea name="fixed" size="0x040000"> + <!-- TC531000 --> + <rom offset="0x000000" size="0x020000" name="044-s1.s1" crc="89903f39" sha1="a04a0c244a5d5c7a595fcf649107969635a6a8b6" /> + </dataarea> + <dataarea name="audiocpu" size="0x020000"> + <!-- TC531001 --> + <rom offset="0x000000" size="0x020000" name="044-m1.m1" crc="0987e4bb" sha1="8fae4b7fac09d46d4727928e609ed9d3711dbded" /> + </dataarea> + <dataarea name="ymsnd" size="0x400000"> + <!-- TC5316200 --> + <rom name="044-v2.v2" offset="0x000000" size="0x200000" crc="3ec632ea" sha1="e3f413f580b57f70d2dae16dbdacb797884d3fce" /> + <!-- TC5316200 --> + <rom name="044-v4.v4" offset="0x200000" size="0x200000" crc="4b0f8e23" sha1="105da0cc5ba19869c7147fba8b177500758c232b" /> + </dataarea> + <dataarea name="sprites" size="0x800000"> + <!-- TC5316200 --> + <rom loadflag="load16_byte" name="044-c1.c1" offset="0x000000" size="0x100000" crc="ddab98a7" sha1="f20eb81ec431268798c142c482146c1545af1c24" /> + <rom size="0x100000" offset="0x400000" loadflag="continue" /> + <!-- TC5316200 --> + <rom loadflag="load16_byte" name="044-c2.c2" offset="0x000001" size="0x100000" crc="d8ccd575" sha1="f697263fe92164e274bf34c55327b3d4a158b332" /> + <rom size="0x100000" offset="0x400001" loadflag="continue" /> + <!-- TC5316200 --> + <rom loadflag="load16_byte" name="044-c3.c3" offset="0x200000" size="0x100000" crc="403e898a" sha1="dd5888f8b24a33b2c1f483316fe80c17849ccfc4" /> + <rom size="0x100000" offset="0x600000" loadflag="continue" /> + <!-- TC5316200 --> + <rom loadflag="load16_byte" name="044-c4.c4" offset="0x200001" size="0x100000" crc="6235fbaa" sha1="9090e337d7beed25ba81ae0708d0aeb57e6cf405" /> + <rom size="0x100000" offset="0x600001" loadflag="continue" /> + </dataarea> + </part> +</software> +``` + +## AML memory consumption in Windows ## + +Windows 7 Ultimate version 6.1 64 bit, Kodi Krypton 17.6. MAME 0.197, Peak Working Set (Memory) +Kodi is restarted before each test. +Options: no ROM/Asset cache, with SLs. + +Build all databases, no INIs/DATs -> 863 MB +Build all databases, with INIs/DATs, with ROM/Asset cache, OPTION_COMPACT_JSON -> 866 MB +Build all databases, with INIs/DATs, with ROM/Asset cache -> 914 MB +Build all databases, with INIs/DATs -> 916 MB +Build MAME database, with INIs/DATs -> 916 MB +Build MAME Audit database, with INIs/DATs -> 938 MB +Build MAME database, with INIs/DATs, OPTION_COMPACT_JSON -> 870 MB +Build MAME Audit database, with INIs/DATs, OPTION_COMPACT_JSON -> 905 MB + +I coded an interative JSON writer. See [Stackoverflow: memoryerror-using-json-dumps](https://stackoverflow.com/questions/24239613/memoryerror-using-json-dumps). Options: no ROM/Asset cache, with SLs, with INIs/DATs. + +Build all databases, older fast writer, OPTION_COMPACT_JSON -> Peak memory 867 MB +``` +fs_write_JSON_file() "C:\Kodi\userdata\addon_data\plugin.program.AML\MAME_DB_main.json" +fs_write_JSON_file() Writing time 8.258000 s +fs_write_JSON_file() "C:\Kodi\userdata\addon_data\plugin.program.AML\MAME_DB_render.json" +fs_write_JSON_file() Writing time 2.981000 s +fs_write_JSON_file() "C:\Kodi\userdata\addon_data\plugin.program.AML\MAME_DB_roms.json" +fs_write_JSON_file() Writing time 13.724000 s +fs_write_JSON_file() "C:\Kodi\userdata\addon_data\plugin.program.AML\ROM_Audit_DB.json" +fs_write_JSON_file() Writing time 12.347000 s +``` + +Build all databases, newer slow writer, OPTION_COMPACT_JSON -> Peak memory 621 MB +``` +fs_write_JSON_file_lowmem() "C:\Kodi\userdata\addon_data\plugin.program.AML\MAME_DB_main.json" +fs_write_JSON_file_lowmem() Writing time 13.526000 s +fs_write_JSON_file_lowmem() "C:\Kodi\userdata\addon_data\plugin.program.AML\MAME_DB_render.json" +fs_write_JSON_file_lowmem() Writing time 4.979000 s +fs_write_JSON_file_lowmem() "C:\Kodi\userdata\addon_data\plugin.program.AML\MAME_DB_roms.json" +fs_write_JSON_file_lowmem() Writing time 22.240000 s +fs_write_JSON_file_lowmem() "C:\Kodi\userdata\addon_data\plugin.program.AML\ROM_Audit_DB.json" +fs_write_JSON_file_lowmem() Writing time 20.663000 s +``` + +The iterative JSON encoder consumes much less memory and is about twice as slow. diff --git a/plugin.program.AML/README.md b/plugin.program.AML/README.md new file mode 100644 index 0000000000..ebc291c0dd --- /dev/null +++ b/plugin.program.AML/README.md @@ -0,0 +1,88 @@ +# Advanced MAME Launcher # + +*Advanced MAME Launcher* is an advanced MAME front end for Kodi media center. AML has support for both +MAME archade machines and Software Lists. AML supports `Merged`, `Split` and `Non-merged` ROM sets and +has the ability to fully audit your ROM and CHD collection. + +## Getting Started guide and Documentation ## + +A *Getting Started* guide with installation instructions and more information about AML can be +found in the [Advanced MAME Launcher thread] in the Kodi forum. Feel free to ask there any +AML-related question you may have. + +You may also find some documentation is in the [Advanced MAME Launcher wiki] in Github. Note that currently +this guide is far from complete and I will try to improve it soon. + +[Advanced MAME Launcher thread]: https://forum.kodi.tv/showthread.php?tid=304186 + +[Advanced MAME Launcher wiki]: https://github.com/Wintermute0110/plugin.program.AML.dev/wiki + +## Screenshot gallery ## + +All the screenshots have been taken using the skin [Estuary AEL MOD](https://forum.kodi.tv/showthread.php?tid=287826&pid=2398922#pid2398922). Kodi skins may not show all AML metadata and artwork. + +**Addon main window** + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_01_main_window.jpg) + +**Browsing MAME machines** + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_02_MAME_pclone_list.jpg) + +**Browsing Software Lists** + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_03_SL_pclone_list.jpg) + +**Fanart and 3D Box generation** + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_04_MAME_fanart.jpg) + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_05_SL_fanart.jpg) + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_06_MAME_3dbox.jpg) + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_07_SL_3dbox.jpg) + +**Audit and ROM browser** + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_09_MAME_ROMs_db.jpg) + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_10_MAME_Audit_db.jpg) + +![](https://raw.githubusercontent.com/Wintermute0110/plugin.program.AML.dev/master/media/shot_11_MAME_Audit_machine.jpg) + +## Installing the latest released version ## + +Advanced MAME Launcher is now available in the +[Kodi Official Addon repository](https://kodi.tv/addon/plugins-program-add-ons/advanced-mame-launcher-0). +To install the latests release AML version follow the instructions in the +[Kodi wiki](https://kodi.wiki/view/Add-on_manager). Advanced MAME Launcher is inside the +category **Program add-ons**. + +## Installing the latest development version ## + +The development version of AML is a separate addon from the stable version. Both can be +coinstalled on the same Kodi machine and won't interfere with each other. In other words, +changing settings in the development version will not affect your stable AML installation. + +The name of the AML stable version is **Advanced MAME Launcher** and the name of the +development version is **Advanced MAME Launcher (dev version)**. + +**IMPORTANT** If you are using Kodi Matrix use the `master` branch. If you are using Kodi Krypton or Kodi Leia use the `python2` branch. To change the branch use the drop-down button on top of the page. The default branch is `master`. + +It is important that you follow this instructions or the Advanced MAME Launcher development +version won't work well. + + 1) In this page click on the green button `Clone or Download` --> `Download ZIP` + + 2) Uncompress this ZIP file. This will create a folder named `plugin.program.AML.dev-master` or `plugin.program.AML.dev-python2` + + 3) Rename that folder to `plugin.program.AML.dev`. + + 4) Compress that folder again into a ZIP file named `plugin.program.AML.dev.zip`. + + 5) In Kodi, use that ZIP file (and not the original one) to install the addon. + + 6) If you get a warning message dialog `For security, installation of add-ons from + unknown sources is disabled.` then click on `Settings` button and then activate + the option `Unknown sources`. diff --git a/plugin.program.AML/SKINNING.md b/plugin.program.AML/SKINNING.md new file mode 100644 index 0000000000..0012c48966 --- /dev/null +++ b/plugin.program.AML/SKINNING.md @@ -0,0 +1,64 @@ +# Advanced MAME Launcher metadata and artwork model # + +AML metadata/assets model is as much compatible with [Advanced Emulator Launcher] as possible. + +[Advanced Emulator Launcher]: http://github.com/Wintermute0110/plugin.program.advanced.emulator.launcher/ + +## MAME machine metadata labels ## + +| Metadata name | setInfo label | setProperty label | Infolabel | +|---------------|---------------|-------------------|--------------------------------------| +| Title | title | | `$INFO[ListItem.Label]` | +| Year | year | | `$INFO[ListItem.Year]` | +| Genre | genre | | `$INFO[ListItem.Genre]` | +| Manufacturer | studio | | `$INFO[ListItem.Studio]` | +| Plot | plot | | `$INFO[ListItem.Plot]` | +| NPlayers | | nplayers | `$INFO[ListItem.Property(nplayers)]` | +| Platform | | platform | `$INFO[ListItem.Property(platform)]` | +| History.DAT | | history | `$INFO[ListItem.Property(history)]` | + +## MAME machine asset labels ## + +| Asset name | setArt label | setInfo label | Infolabel | +|------------|--------------|---------------|----------------------------------| +| Title | title | | `$INFO[ListItem.Art(title)]` | +| Snap | snap | | `$INFO[ListItem.Art(snap)]` | +| Cabinet | boxfront | | `$INFO[ListItem.Art(boxfront)]` | +| CPanel | boxback | | `$INFO[ListItem.Art(boxback)]` | +| PCB | cartridge | | `$INFO[ListItem.Art(cartridge)]` | +| Flyer | flyer | | `$INFO[ListItem.Art(flyer)]` | +| 3D Box | 3dbox | | `$INFO[ListItem.Art(3dbox)]` | +| Icon | icon | | `$INFO[ListItem.Icon]` | +| Fanart | fanart | | `$INFO[ListItem.Fanart]` | +| Marquee | banner | | `$INFO[ListItem.Art(banner)]` | +| Clearlogo | clearlogo | | `$INFO[ListItem.Art(clearlogo)]` | +| Flyer | poster | | `$INFO[ListItem.Art(poster)]` | +| Trailer | | trailer | `$INFO[ListItem.trailer]` | + +## MAME machine asset availability ## + +| Artwork site | Title | Snap | Preview | Boss | End | GameOver | HowTo | Logo | Scores | Select | +|-------------------|-------|-------|---------|------|-----|----------|-------|------|--------|--------| +| [Pleasuredome] | YES | YES | YES | YES | YES | YES | YES | YES | YES | YES | +| [ProgrrettoSNAPS] | YES | YES | YES | YES | YES | YES | YES | YES | YES | YES | + + +| Artwork site | Versus | Cabinet | CPanel | Flyer | Icon | Marquee | PCB | Manual | Trailer | +|-------------------|--------|---------|--------|--------|------|---------|-----|--------|---------| +| [Pleasuredome] | YES | YES | YES | YES | YES | YES | YES | YES | YES | +| [ProgrrettoSNAPS] | YES | YES | YES | YES | YES | YES | YES | YES | YES | + + +## Software Lists asset availability ## + +| Artwork site | Title | Snap | Fanart | Banner | Boxfront | Boxback | Manual | Trailer | +|-------------------|--------|------|--------|--------|----------|----------|--------|---------| +| [Pleasuredome] | YES | YES | NO | NO | YES | NO | YES | YES | +| [ProgrrettoSNAPS] | YES | YES | NO | NO | YES | NO | YES | YES | + + * Many consoles/computers have the same artwork as arcade. For MAME, both arcade and + consoles/computers are just "machines". For example, CPanel of MegaDrive is the + SEGA 3 button joystick. + +[Pleasuredome]: http://www.pleasuredome.org.uk/ +[ProgrrettoSNAPS]: http://www.progettosnaps.net diff --git a/plugin.program.AML/addon.py b/plugin.program.AML/addon.py new file mode 100644 index 0000000000..2784c77f7b --- /dev/null +++ b/plugin.program.AML/addon.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher main script file. + +# --- Modules/packages in this plugin --- +import resources.main + +# --- Python standard library --- +import sys + +# ------------------------------------------------------------------------------------------------- +# main() +# ------------------------------------------------------------------------------------------------- +# In Kodi Leia the submodules are cached and only the entry point is interpreted on every addon +# call. sys.argv must be propagated into submodules. For addon development, caching of submodules +# must be disabled in addon.xml. +# +# Put the main bulk of the code in files inside /resources/, which is a package directory. +# This way, the Python interpreter will precompile them into bytecode (files PYC/PYO) so +# loading time is faster compared to PY files. +# See http://www.network-theory.co.uk/docs/pytut/CompiledPythonfiles.html +# +resources.main.run_plugin(sys.argv) diff --git a/plugin.program.AML/addon.xml b/plugin.program.AML/addon.xml new file mode 100644 index 0000000000..df941b4018 --- /dev/null +++ b/plugin.program.AML/addon.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<addon id="plugin.program.AML" name="Advanced MAME Launcher" version="1.0.2" provider-name="Wintermute0110"> + <requires> + <!-- + Check main addon versions in https://github.com/xbmc/xbmc/tree/master/addons + Change the branch according to the Kodi version. + Kodi 17 Krypton | xbmc.python 2.25.0 | script.module.pil 1.1.7 + Kodi 18 Leia | xbmc.python 2.26.0 | script.module.pil 1.1.7 + Kodi 19 Matrix | xbmc.python 3.0.0 | script.module.pil 5.1.0 + --> + <import addon="xbmc.python" version="3.0.0" /> + <import addon="script.module.pil" version="5.1.0" /> + </requires> + <extension point="xbmc.python.pluginsource" library="addon.py"> + <provides>executable game</provides> + </extension> + <extension point="xbmc.addon.metadata"> + <!-- + This must be commented when releasing the addon. Kodi must be restarted if this setting is changed. + See https://github.com/xbmc/xbmc/pull/13814 + --> + <!-- <reuselanguageinvoker>false</reuselanguageinvoker> --> + <summary lang="en_GB">Kodi meets MAME!</summary> + <description lang="en_GB">MAME front-end for Kodi that supports both MAME machines and Software Lists. For help look at the Advanced MAME Launcher thread in the Kodi forum.</description> + <platform>android freebsd ios linux osx windows</platform> + <license>GPL-2.0-only</license> + <email>wintermute0110@gmail.com</email> + <forum>https://forum.kodi.tv/showthread.php?tid=304186</forum> + <website>https://github.com/Wintermute0110/plugin.program.AML.dev/wiki</website> + <source>https://github.com/Wintermute0110/plugin.program.AML.dev/</source> + <assets> + <icon>media/icon.png</icon> + <fanart>media/fanart.jpg</fanart> + <screenshot>media/shot_01_main_window.jpg</screenshot> + <screenshot>media/shot_02_MAME_pclone_list.jpg</screenshot> + <screenshot>media/shot_03_SL_pclone_list.jpg</screenshot> + <screenshot>media/shot_04_MAME_fanart.jpg</screenshot> + <screenshot>media/shot_05_SL_fanart.jpg</screenshot> + <screenshot>media/shot_06_MAME_3dbox.jpg</screenshot> + <screenshot>media/shot_07_SL_3dbox.jpg</screenshot> + <screenshot>media/shot_08_MAME_History_viewer.jpg</screenshot> + <screenshot>media/shot_09_MAME_ROMs_db.jpg</screenshot> + <screenshot>media/shot_10_MAME_Audit_db.jpg</screenshot> + <screenshot>media/shot_11_MAME_Audit_machine.jpg</screenshot> + </assets> + <news>See changelog.txt file in https://github.com/Wintermute0110/plugin.program.AML.dev/ for information about the latest release.</news> + </extension> +</addon> diff --git a/plugin.program.AML/changelog.txt b/plugin.program.AML/changelog.txt new file mode 100644 index 0000000000..d19108a70d --- /dev/null +++ b/plugin.program.AML/changelog.txt @@ -0,0 +1,684 @@ +[B]AML ideas and planned features (more futuristic ideas)[/B] + +WIP [CORE] JSON database files (specially MAME_DB_main.json, MAME_DB_render.json and + MAME_assets.json) could be compressed with DEFLATE to reduce the size on disk. They + will decompressed on the fly. I have to test if this is faster than no compression at all. + Use low values of compression in the deflate algorithm. Maximum compression is very + slow but less compression is fast and may reduce the JSON file size a lot. + + According to Zachmorris, MessagePack is the fastest serialization method in Python 3. + + The module xmltodict is also very interesting and the performance very good in Python 3. + + See https://forum.kodi.tv/showthread.php?tid=329315&pid=2975588#pid2975588 + See https://github.com/vsergeev/u-msgpack-python + See https://github.com/martinblech/xmltodict + +WIP [AUDIT] Samples distributed with MAME are uncompressed and not stored into ZIP files. + The MAME audit engine must take this into account. + See comments in mame_scan_MAME_ROMs() @mame.py + +WIP [CORE] AML/AEL must report the aspect ratio of artwork to the skin. + How to implement this? Using PIL? + I think best way is that core developers expose the image aspect ratio to the + skin using infolabels. + + +[B]AML ideas and planned features (read to implement)[/B] + +WIP [DOCS] Improve the AML wiki in Github and move documentation from Kodi forum to + Github wiki. + +WIP [CORE] Implement the "Read-only launchers". ROLs are XML files that include the + launcher information as well as the ROMs information. ROLs are generated with AML, + can be exported as XML and then used with AEL. AEL will handle ROLs as read-only + (cannot be edited). If a ROL should be changed, then the XML is edited or regenerated + and then imported again into AML. + +WIP [MANUALS] Increase the number of supported PDF filters. + This may require A LOT of Python coding. + +WIP [MANUALS] Support for CBZ/CBR manuals. + In theory Kodi VFS is able to read RAR files. + Wait until this is implemented in AEL and then port it from AEL into AML? + +WIP [CORE] Recursive context menus. + Pay attention to the select() bug in Kodi Krypton. + This requires Kodi version detection and having different code for Krypton/Leia. + Start with the context menu in which recursiveness ir more useful. + +WIP [GRAPHICS] Optimize the generation of 3D boxes. + Cache the background images used in all boxes. + +WIP [GRAPHICS] Respect the aspect ratio in 3D boxes. For example, if the poster in the + front size of the 3D box is square the 3D box must be square, do not strech the + poster texture. ScreenScraper respects the aspect ratio of the 3D boxes, have + a look at examples there (for example, see Mega Drive 3D box and PSX 3D box). + +WIP Retroarch core MAME 2003 Plus allows to save the MAME XML file needed by AML. + In this core, the XML is extracted using the MAME internal menu, which must be + opened with the Retroarch core options menu. The XML file is saved in the SAVES + directory with name mame2003-plus/mame2003-plus.xml + Exploit this to give support to MAME 2003 and then Android. According to + Chrisism, Retroarch is currently the best way to use MAME in Android. + TTBOMK, currently it is the only core to support this feature. Investigate if there + are more Retroarch cores with this feature. + + See https://github.com/libretro/mame2003-libretro/pull/348 + See https://github.com/libretro/mame2003-plus-libretro/pull/8 + See https://github.com/libretro/mame2010-libretro/pull/123 + See https://github.com/libretro/mame2010-libretro/tree/master/metadata + + +[B]Advanced MAME Launcher | version 1.0.2 | 18 June 2021[/B] + +NOTE Version was bumped to 1.0.2 to keep feature-sync with 0.10.2 + +FEATURE [CORE] Synched utils.py and misc.py with AEL. + +FEATURE [CORE] Support new history.dat XML format. + See https://forum.kodi.tv/showthread.php?tid=304186&pid=3029005#pid3029005 + + +[B]Advanced MAME Launcher | version 1.0.1 | non-released[/B] + +NOTE This version was never uploaded to the Kodi repo. + +FIX [CORE] Fix crash when building Controls (Expanded) catalog in MAME 2003 Plus mode. + Also fixed Controls (Expanded) and Controls (Compact) catalogues for empty controls. + +FIX [CORE] Fix SKIN_SHOW_* launchers. + +FIX [CORE] Fix bug in mame_update_MAME_MostPlay_objects(). + + +[B]Advanced MAME Launcher | version 1.0.0 | 27 November 2020[/B] + +FEATURE [CORE] Port the addon to Kodi Matrix and Python 3. + +FEATURE [CORE] xbmc.translatePath is deprecated in Matrix, use xbmcvfs.translatePath instead. + +FIX [MANUALS] The pdfwr library is not working well with Python 3. PDF image extraction + have been disabled until this issue is fixed. + + +[B]Advanced MAME Launcher | version 0.10.2 | xx June 2021[/B] + +NOTE Code synched with 1.0.2. + +FEATURE [CORE] Synched utils.py and misc.py with AEL. + +FEATURE [CORE] Support new history.dat XML format. + See https://forum.kodi.tv/showthread.php?tid=304186&pid=3029005#pid3029005 + + +[B]Advanced MAME Launcher | version 0.10.1 | 04 February 2021[/B] + +FIX [CORE] Fix crash when building Controls (Expanded) catalog in MAME 2003 Plus mode. + Also fixed Controls (Expanded) and Controls (Compact) catalogues for empty controls. + +FIX [CORE] Fix SKIN_SHOW_* launchers. + +FIX [CORE] Fix bug in mame_update_MAME_MostPlay_objects(). + + +[B]Advanced MAME Launcher | version 0.10.0 | 27 November 2020[/B] + +FEATURE [CORE] Big code refactoring to prepare Kodi Python API changes. + +FEATURE [CORE] Include Software Lists that have no associated MAME machines in "Machines + by Software List" filter. + +FEATURE [CORE] Create branch python2 and place series 0.10.x into this branch. + +FEATURE [CORE] Support Retroarch MAME 2003 Plus. + +FEATURE [FILTERS] Implement filtering options NoMissingROMs, NoMissingCHDs and NoMissingSamples. + See https://forum.kodi.tv/showthread.php?tid=304186&pid=2964109#pid2964109 + +FEATURE [CORE] Different colours for different filters in the root window. + +FEATURE [CORE] In the Setup Plugin CM, Build Fanarts and 3D boxes: add an option to + build all the Fanarts and 3D boxes at once. + +FEATURE [CORE] Reorganised the Setup Plugin context menu a bit. + +FEATURE [CORE] Export MAME info with billyc999s XML format. + +FEATURE [CORE] New Utility "Show machines with biggest ROMs" + +FEATURE [CORE] New Utility "Show machines with smallest ROMs" + +FEATURE [CORE] Disable Kodi screensaver when launching MAME and reenable after MAME finishes. + See https://forum.kodi.tv/showthread.php?tid=304186&pid=2934194#pid2934194 + +FIX [CORE] Fixed crash when creating ROM audit database when MAME CHD set was SPLIT. + See https://forum.kodi.tv/showthread.php?tid=304186&pid=2975566#pid2975566 + +FIX [CORE] Fix parsing of mameinfo.dat 0.226. + + +[B]Advanced MAME Launcher | version 0.9.12 | 10 February 2020[/B] + +FEATURE Create a new infolabel $INFO[ListItem.Property(history)] which shows the contents + of history.dat for a machine. Add an option to include the contents of History.DAT + in the asset database. + Once this is tested remove the code to include History.DAT in the plot. + See https://forum.kodi.tv/showthread.php?tid=304186&pid=2916270#pid2916270 + +FEATURE Option to hide MAME machine flags. + Contributed by Rychem28. + +FEATURE Option to hide SL item flags. + Contributed by Rychem28. + +FEATURE Option to display only SL items with ROMs/CHDs available. + Contributed by Rychem28. + +FIX Fixed parsing of History.dat 2.17. + See https://forum.kodi.tv/showthread.php?tid=304186&pid=2913592#pid2913592 + + +[B]Advanced MAME Launcher | version 0.9.11 | 26 November 2019[/B] + +FEATURE [CORE] For Software Lists, instead of showing the number of ROMs show the number + of Items in Flat display mode or Parents in Parent/Clone display mode. + +FEATURE [CORE] Support for MASH's Alltime.ini. + +FEATURE [CORE] Improvements in the JSON index of DAT files. Use dictionaries and not list + to reduce JSON file size and speed up data access. + +FEATURE [CORE] Improved main statistics. + +FEATURE [CORE] Improve some Software List long names until they are fixed in MAME repository. + +FIX Fixed conversion to int of addon version str in fs_AML_version_str_to_int() + +FIX New history.dat makes AML to crash because one entry could be for more than one + machine. In addition, the same description can be used over several SLs. + + +[B]Advanced MAME Launcher | version 0.9.10 | 10 May 2019[/B] + +FEATURE Option to disable Software Lists at all in Vanilla MAME mode. + +FEATURE Management of MAME Favourite objects (delete missing, delete single). + +FEATURE Management of SL Favourite objects (delete missing, delete single). + +FEATURE Initial support for Retroarch MAME 2003 Plus core. + +FEATURE [CORE] Ability to export DAT files in Logiqx format for MAME ROMs and MAME CHDs. + +FEATURE [CORE] Differentiate between Non-merged and Fully non-merged MAME ROM sets. + Non-merged sets do not include BIOS and device ROMs. + Fully non-merged sets include BIOS and devices ROMs (every ROM required to run each + machine). Pleasuredome ROM sets are Fully non-merged. + This needs more testing. + +FEATURE [FILTERING] Test the filters for errors. For example, test that all the drivers + defined in <Driver> exist. Another example, test that the genres defined in + <Genre> and the controls defined in <Controls> exist. + +FEATURE [FILTERING] Warn the user if errors found in the XML filter definition, maybe in + a report the user can read later. + +FEATURE [FILTERING] Make a filter report the user can read. Now, only the number of machines + after filtering is reported. + +FEATURE [GRAPHICS] Generate MAME machines and SL items 3D Boxes. + +FEATURE [GRAPHICS] Refactoring of the Fanart and 3D Box code generation. Creation of graphics.py. + +FEATURE [GRAPHICS] Only generate 3D Boxes if both Fyler and Clearlogo (MAME) or Boxfront (SL) + are present. Avoid having empty 3D boxes. + +FEATURE [CORE] Improved filter "Machines by Display Type" and removed filter + "Machines by Display Rotation." + +FEATURE [CORE] New catalog filter "Machines by Display VSync freq." Inspired by + MASH's MAMEINFO Vsync.ini. + +FEATURE [CORE] New catalog filter "Machines by Display Resolution" Inspired by + MASH's MAMEINFO Screen.ini. + +FEATURE [CORE] New catalog filter "Machines by CPU" Inspired by + MASH's MAMEINFO CPU.ini. + +FEATURE [CORE] Rewrite the INI loading engine. + See comments in header of function mame_load_INI_datfile(). + +FEATURE [CORE] New INI file Artwork.ini (produced by MASH's MAMEINFO). + Catalog filter "Machines by Artwork". + Note that Artwork.ini places the same machine in different categories. Maybe other + INIs do the same and currently AML does not support this. This requires + some changes in AML engine. + +FEATURE New catalog "Version added" + +FEATURE [CORE] New INI file Category.ini (produced by MASH's MAMEINFO). + Catalog filter "Machines by Category". + +FEATURE [CORE] New "All in one (Extract, Build and Scan)" options: + 1) Delete current to "All in one (Extract, Build, Scan)" + 2) Add "All in one (Extract, Build, Scan, Filters)" + 3) Add "All in one (Extract, Build, Scan, Filters, Audit)" + +FEATURE [CORE] By request of Rufoo, support MAME 3dboxes. + +FIX Fixed a couple of crashes when executing context menus, thanks to Dax9. + See https://forum.kodi.tv/showthread.php?tid=304186&pid=2838180#pid2838180 + +FIX Fix the render_skin_*() functions. These are called by skins to get a list of + filters. + + +[B]Advanced MAME Launcher | version 0.9.9 | 22 March 2019[/B] + +FEATURE [CORE] Improve statistics of working, non-working games, etc. for the Main filters. + +FEATURE [CORE] [LEIA] Check out the ListItem constructor offscreen parameter in Leia. + The offscreen parameter increases the speed a bit. + This requires Kodi version detection and having different code for Krypton/Leia. + See https://forum.kodi.tv/showthread.php?tid=329315&pid=2711937#pid2711937 + and https://forum.kodi.tv/showthread.php?tid=307394&pid=2531524 + +FEATURE [CORE] [LEIA] Use the new API function ListItem.setProperties({p1:v1, p2:v2, ...}) + This requires Kodi version detection and having different code for Krypton/Leia. + See https://forum.kodi.tv/showthread.php?tid=332283 + +FEATURE [FANARTS] Set an order to print fanart assets. This will allow to have images printed + on top of each other. + +FEATURE [CORE] Improve the Samples scanner and report. + +FEATURE [CORE] MAME and SL plots build timestamp. + +FEATURE [CORE] MAME and SL Fanart build timestamp. + +FEATURE [CORE] MAME render and asset cache build timestamp. + +FEATURE [CORE] Move the utilities from the addon settings into the root menu. + +FEATURE [CORE] Move the report viewer from the context menu to the root menu. The context + menu is a little bit overloaded and this will alleviate the situation. + +FEATURE [CORE] Use xbmcplugin.addDirectoryItems() instead of xbmcplugin.addDirectoryItem(). + According to the docs "Large lists benefit over using the standard addDirectoryItem(). + You may call this more than once to add items in chunks." + +FEATURE [CORE] Configuring every DAT and INI file one by one is tedious. Instead, define + a directory where the DATs must be placed and pick the files from there automatically. + +FEATURE [CORE] Add plots for entries in the root window. + +FEATURE [CORE] Renamed the "Machines by Score" filter to "Machines by Rating", to avoid + confusing with scores in games. + +FEATURE [CORE] Implement context menu "All in one (Extract, Build and Scan)" + +FEATURE [CORE] Refactoring and code cleaning of the addon setup functions (DB build, scanner). + +FEATURE [CORE] Remove redundant fields from MAME DB 'coins' and 'control_type'. Use new + 'input' data structure to replace them. + +FEATURE [CORE] At least two directories for samples are needed. MAME includes a default samples + directory with some samples used by some machines. Both directories must be configured + in mame.ini in order to get all the samples working. + + NOTE This feature was cancelled. Only one Samples directory. If the user wants to have + a complete Good audit, then the samples shipped with MAME must be compressed and + the ZIP file placed in the unique samples directory. In any case, only three machines + are affected. + +FEATURE [FILTERING] Implement <Include>, <Exclude> and <Change> tags. + This will require more work than expected. Currently, only parent machines are filtered + and clone machines are added after the filtering process. However, to implement + Include, Exclude and Change tags, all machines must be included in the filter list. + This will require modification of the filter render engine (must be rendered always in + flat mode and not in parent/clone mode). + +FEATURE [MANUALS] Progress bar when extracting PDF pages. + +FEATURE [MANUALS] When displaying manuals use cached extracted images if they exist. + When the manual is extracted, create a small JSON file with the timestamp of + the extraction so it can be compared with the timestamp of the PDF file to + test if images must be extracted again. + +FIX In MAME 0.206, some clone merged ROMs are not present in the parent machine, only + in the BIOS. For example, in machine adonisce (clone of adonis). Before 0.206, such + ROMs were also present on the parent machine. This change in behaviour crashed AML. + +FIX Fixed the AML addon and MAME numerical versioning scheme. + +FIX Fix crash in "Build MAME databases". + See https://forum.kodi.tv/showthread.php?tid=304186&pid=2822949#pid2822949 + +FIX Some Software List ROMs are compressed using non-ASCII characters and this make + the audit engine to crash. I have to investigate how to fix this issue. + Maybe use the chardet library https://github.com/Wintermute0110/chardet/tree/master/chardet + I think this should reported creating an issue in MAME project in Github. + Problematic SL ROM example: SL ibm5170, item wordfndr + https://github.com/mamedev/mame/blob/master/hash/ibm5170.xml#L7521 + The current implementation just ignores non-ASCII files and the audit fails for those + SL items. + + +[B]Advanced MAME Launcher | version 0.9.8 | 23 June 2018[/B] + +FEATURE [DOCS] Documentation in README.md improved. + +FEATURE [LEIA] Kodi Leia will cache the Python interpreter which means submodules will only + be executed once and cached. sys.argv must be propagated from the entry point code + into the submodules. + See https://github.com/xbmc/xbmc/pull/13814 + and https://forum.kodi.tv/showthread.php?tid=303073&pid=2729071#pid2729071 + +FIX Changed source code files to remove BOM. This is necessary to pass Travis tests of + Kodi official repo. + +FIX ActivateWindow(busydialog) and Dialog.Close(busydialog) have been deprecated. + Use DialogProgress() for all operations. + See https://github.com/xbmc/xbmc/pull/13958 + and https://github.com/xbmc/xbmc/pull/13954 + and https://github.com/xbmc/xbmc/pull/10699 + +FIX Do not use the xbmc.Player() in launcher addons. Instead, use functions like + xbmc.getCondVisibility("Player.HasMedia"), xbmc.executebuiltin("PlayerControl(stop)"), etc. + Change proposed by enen92. + See https://github.com/xbmc/repo-plugins/pull/1886#discussion_r196591764 + + +[B]Advanced MAME Launcher | version 0.9.7 | 09 June 2018[/B] + +FEATURE Implemented settings "display_rom_available" and "display_chd_available". + +FEATURE [LAUNCH] Implement "Action on Kodi playing media", "After/before launch delay (ms)", and + "Suspend/resume Kodi audio engine". + See https://github.com/Wintermute0110/plugin.program.AML/issues/3 + +FEATURE [MAME FILTERING] Improve the Custom Filters (add more filtering options as defined + in the reference filter file `AML-MAME-filters-reference.xml`). + +FEATURE [CORE] Render the `In Favourites` flag for MAME machines. + +FEATURE [CORE] Optimize the rendering of ROMs in 3 steps: a) Loading, b) Processing and c) Rendering. + Processing computes all data to render ROMs and Rendering calls Kodi functions. This will + allow to measure how long does it take to call the Kodi functions for ListItem generation. + +FEATURE [CORE] Reduce the memory consumption during the database generation. + By default use the options OPTION_COMPACT_JSON = True and OPTION_LOWMEM_WRITE_JSON = True + See https://stackoverflow.com/questions/24239613/memoryerror-using-json-dumps + +FIX Fix crash when executing "Check/Update all objects" if Favourites are empty. + + +[B]Advanced MAME Launcher | version 0.9.6 | 25 May 2018[/B] + +FEATURE Improve the user experience when the addon is just installed. Check if databases + have been built, check for errors, etc. + +FEATURE Add a isMature field to MAME DB. Take the mature information from mature.ini included + in the Catver.ini ZIP file. + +FEATURE Option in settings to completely hide Mature machines and filter categories. + +FEATURE Asset hashed database. This will speed up launching MAME machines. Note that the asset + DB must be opened for the Most Played and Recently Played DBs. + +FEATURE Prettify the "Display rotation" filter (use Horizontal/Vertical instead of numbers). + +FEATURE Include number of buttons in controls and some other control information. + +FEATURE Add the Samples of each machine to the ROM database. + +FEATURE Audit the ROM samples inside ZIP files. + +FEATURE Implement "Most played MAME machines" + +FEATURE Implement "Recently played MAME machines" + +FEATURE Option in settings to update all MAME and SL Favourite ROMs. Useful for plugin upgrades. + +FEATURE Implement "Most played SL ROMs" + +FEATURE Implement "Recently played SL ROMs" + + +[B]Advanced MAME Launcher | version 0.9.5 | 11 May 2018[/B] + +FEATURE Option to disable the ROM and asset caches. + +FEATURE CRC32 hash collision detector for MAME and SL ROMs. + +FEATURE MAME ROM and asset cache disable by default. They may be enabled by user that want to + increase the loading speed. This will be very useful for develpment because + cache rebuilding takes a long time. + +FEATURE Check if AML configuration is OK or not, and warn the user about warnings/errors. + +FEATURE Improved PDF manual rendering. Use the library pdfrw for image extraction. + +FEATURE Clean ROM cache before rebuilding cache. + +FEATURE Clean asset cache before rebuilding cache. + +FEATURE Clean filters directory before rebuilding custom filters. + +FEATURE MAME audit statistics. + +FEATURE SL audit statistics. + +FEATURE Support for SL Merged ROM/CHD sets (currently only Split). + +FEATURE Added audit timestamps (MAME machines and Software Lists). + +FEATURE Move driver aristmk5.cpp (Aristocrat gambling machines) from Standard to Unusual. + Also, adp.cpp, mpu4vid.cpp, cubo.cpp, sfbonus.cpp, peplus.cpp. + +FIX Software List ROM size was stored as string and not as int. This made the SL Audit to + completely fail. + +FIX Fixed audit of MAME machine ROMs (wrong function name). + +FIX Lots of fixes to MAME ROM audit engine. + +FIX Lots of fixes to Software Lists audit engine. + + +[B]Advanced MAME Launcher | version 0.9.4 | 29 March 2018[/B] + +FEATURE File cache for SL ROMs/CHDs and SL assets. + +FEATURE Port the file scanner cache from AEL to AML. This will increase the scanning speed a lot! + Also, this will allow supporting more image types (currently only PNG), manual + types (currently only PDF) and trailer types (currently MP4 only). + +FEATURE Create an AEL virtual launcher in XML from any AML filter. + +FEATURE Use proper Software List name in "Machines by Software List" filter. + +FEATURE Use proper short name in "Machines by MAME short name" filter. + +FEATURE Clean Render and main machine JSON files. Currently, there are repeated fields on both + databases like nplayers. + +FEATURE Move flags and plot from the render database to the assets database. Flags are modified + by the scanner only and plot generated after the scanner. If flags and plot are in + the asset DB, the ROM cache and hashed DB must be regenerated after the database building + only and not always like now. + +FEATURE Render PDF manuals consisting of image scans (99% of game manuals are scans of images). + Thank you very much to i96751414 for allowing use of his PDF reader addon code. + Have a look at the PDF reader addon https://forum.kodi.tv/showthread.php?tid=187421 + and https://github.com/i96751414/plugin.image.pdfreader + This initial implementation somewhat works for some PDFs but code can be improved a lot. + +FEATURE Create a hased database for all catalog filter combination. This will require the + creation of about 5000 json files but will make AEL as fast as possible. + +FEATURE Hashed database for assets, in a similar fashion to the catalog ROM hashed database. + +FEATURE Make a ROM cache and a assets cache for the MAME filters. That will increase the + loading speed of the MAME filters a lot. + +FEATURE Support MAME artwork by Mr. Do's. Note that Clones use Parent's artwork automatically. + +FEATURE Use Parent/Clone substituted artwork in MAME. For example, most trailers are only available + for the Parent machine and can be used by Clone machines. + +FEATURE Use Parent/Clone substituted artwork in Software Lists. + +FEATURE Build Fanarts from other pieces of artwork for Software List items. + +FEATURE Build Fanarts from other pieces of artwork for MAME machines. + +FEATURE Test MAME and SL Fanart building. + +FEATURE Custom MAME filters, using XML files. Merge some of the functionality of NARS into AML. + First, give to support to filter by driver. Later, more filters can be added. + +FEATURE "Browse by MAME short name" and "Browse by MAME long name" alphabetical catalogs. + +FEATURE Renamed plugin from plugin.program.advanced.MAME.launcher to plugin.program.AML. + Shorter name, shorter databases, higher speed. + +FEATURE Some skin helper commands to display widgets. + +FEATURE Support bestgames.ini and series.ini. + +FEATURE Generate machine plot from MAME XML information. + +FEATURE New Main filters Normal and Unusual. + +FEATURE Show ROMs of a MAME machine that should be in a ZIP file. Supports Merged, Split and + Non-merged sets, CHDs, BIOS and Devices with ROMs. + +FEATURE Audit MAME ROMs for all machines. + +FEATURE Show SL ROMs of a SL entry. Supports Merged, Split and Non-merged sets and SL CHDs. + +FEATURE Audit SL ROMs. + +FEATURE Display MAMEINFO.DAT information. + +FEATURE Display HISTORY.DAT in information. + +FEATURE Display gameinit.dat in information. + +FEATURE Display command.dat in information. + +FEATURE At launching, do not check ROMs for machines which doesn't have ROMs. + Requires loading machines database, which will slow down launching process a lot! + A hashed database of machines is necessary to speed up plugin. + Better solution for now: do not do any check. Let MAME fail if there are ROM/CHD errors. + +FEATURE Allow user to choose default assets as AEL does in addon seetings. + +FEATURE Trailer support in MAME machines and Software Lists. + +FEATURE Manage MAME Favourites context menu. + +FEATURE Manage SL Favourites context menu. + +FEATURE Create a hased database for main ROM database and Audit ROM database. + + +[B]Advanced MAME Launcher | version 0.9.3 | 30 May 2017[/B] + +FEATURE Ability to choose default Icon and Fanart for MAME and SL ROMs in addon settings. + +FEATURE "Parent only" view mode. + +FEATURE Plugin speed has been increased a lot owing to a brand new database design. + +FEATURE Unified catalog system and new machine rendering method. + Requires wiping of ADDON_DATA_DIR to avoid problems. + +FEATURE Properties can be configured for every individual list in AML. + +FEATURE New Status Device flag. Marks wheter a device is mandatory or not. + +FEATURE Show database statistics. + +FEATURE Favourite MAME machines. + +FEATURE Favourite Software Lists ROMs. + +FEATURE Scan SL assets/artwork. + +FEATURE Manage MAME Favourites. + + +[B]Advanced MAME Launcher | version 0.9.2 | 12 February 2017[/B] + +FEATURE Ability to sort cataloged filters by number of machines. + +FEATURE New Main Filter "Machines with no ROMs". + +FEATURE Launch parents with no clones from the parents list in Catalogued filters. + +FEATURE Use a fancy name for well-known MAME drivers. + +FEATURE On filter `Machines by Software List`, substitute short SL name by the proper SL name. + +FEATURE Display MAME stdout/stderr. + +FEATURE Scan Software Lists. + +FEATURE Launch machines with software lists. + +FIX Use SORT_METHOD_LABEL_IGNORE_FOLDERS insead of SORT_METHOD_LABEL. This avoids folders + to be rendered first when sorting listitems alpahbetically. + + +[B]Advanced MAME Launcher | version 0.9.1 | 04 February 2017[/B] + +FEATURE AML only works on Krypton now. Updated addon.xml with new fields. + +FEATURE Add support for nplayers.ini. + +FEATURE Count machines in "Extract MAME.xml" step and not in "Build MAME database" step. + +FEATURE Print the number of clones each machine has. In general, print the number of items + on a submenu. + +FEATURE Add catalog by Devices. This will help launching software list machines. + +FEATURE In a parent list, if there is not clones, then add the ability to launch games from the + parent list. Only coded for indexed machines and not for cataloged machines. + See http://forums.bannister.org/ubbthreads.php?ubb=showflat&Number=108507#Post108507 + +FEATURE Switch in settings to diplay Working machines only. + See http://forum.kodi.tv/showthread.php?tid=304186&pid=2506150#pid2506150 + +FEATURE Improved categories in "Machines by Control Type catalog". + +FIX "I get an error whenever trying to open any "Ball & Paddle" category. I'm pretty sure this + is due to the ampersand, because all the other categories I've tried work. This issue doesn't + affect ROMs with an ampersand in their name, like Cloak & Dagger." + See http://forum.kodi.tv/showthread.php?tid=304186&pid=2506150#pid2506150 + + Problem was that the '&' in the Kodi URL was not escaped. + + +[B]Advanced MAME Launcher | version 0.9.0 | 15 January 2017[/B] + + Initial release + +FEATURE Extract MAME.xml from MAME executable. Tested only on Linux. + +FEATURE Generate main MAME machine database, indices and catalogs from MAME.xml. + +FEATURE Scan ROMs and tell the user about Have/Missing ROMs. + +FEATURE Launch MAME non-Software List (arcade) machines. + +FEATURE Scan CHDs and samples. + +FEATURE Scan assets and build assets database. + +FEATURE Display MAME machine metadata/artwork. + +FEATURE Build Software List catalog. diff --git a/plugin.program.AML/filters/AML-MAME-filters-reference.xml b/plugin.program.AML/filters/AML-MAME-filters-reference.xml new file mode 100644 index 0000000000..d724ebd2fc --- /dev/null +++ b/plugin.program.AML/filters/AML-MAME-filters-reference.xml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Advanced MAME Launcher XML MAME filter reference. +--> +<Advanced_MAME_Launcher_custom_filters> +<MAMEFilter> + <Name>Example filter name</Name> + + <!-- + Device machines are removed automatically. + Separate multiple options with ',' + Multiple <Options> tags allowed. + --> + <Options>NoClones, NoCoin, NoCoinLess, NoROMs, NoCHDs, NoSamples</Options> + <Options>NoMature, NoBIOS, NoMechanical, NoImperfect, NoNonworking</Options> + <Options>NoVertical, NoHorizontal</Options> + + <!-- + The following options rely on the ROM/CHD scanner. + --> + <Options>NoMissingROMs, NoMissingCHDs, NoMissingSamples</Options> + + <!-- + As shown in "Machines by Driver" filter. + Search is case sensitive. + If multiple <Driver> tags only the last one is used. + Literals with spaces must be enclosed in quotes ' or double quotes ". + LSP parser operators: and, or, not, has, lacks, '(', ')', literal. + --> + <Driver>has cps1.cpp or has cps2.cpp</Driver> + + <!-- + As shown in "Manufacturer" filter. + Search is case sensitive. + If multiple <Manufacturer> tags only last one is used. + SP operators: and, or, not, has, lacks, literal. + --> + <Manufacturer>has Konami or has Namco</Manufacturer> + + <!-- + As shown in "Machines by Category (Genre)" filter. + Search is case sensitive. + If multiple <Genre> tags only last one is used. + Literals with spaces must be enclosed in quotes ' or double quotes ". + LSP parser operators: and, or, not, has, lacks, '(', ')', literal. + --> + <Genre>has Driving and has "Board Game"</Genre> + + <!-- + As shown in "Machines by Controls (Compact)" filter. + Search is case sensitive. + If multiple <Controls> tags only last one is used. + Literals with spaces must be enclosed in quotes ' or double quotes ". + LSP parser operators: and, or, not, has, lacks, '(', ')', literal. + --> + <Controls>lacks Mahjong and lacks Gambling and lacks Hanafuda</Controls> + + <!-- + As shown in "Machines by Pluggable Devices (Compact)" filter. + Search is case sensitive. + If multiple <PluggableDevices> tags only last one is used. + Literals with spaces must be enclosed in quotes ' or double quotes ". + LSP parser operators: and, or, not, has, lacks, '(', ')', literal. + --> + <PluggableDevices>has Cartridge and has Harddisk</PluggableDevices> + + <!-- + As shown in "Machines by Year" filter. + If multiple <Year> tags only last one is used. + Invalid year strings (for example '19??') get converted to 0. + In XML files: + '>' must be '>' + '<' must be '<' + '>=' must be '>=' + '<=' must be '<=' + YP operators: ==, !=, >, <, >=, <=, and, or, not, (, ), literal. + --> + <Year>year >= 1990 and year < 2000</Year> + + <!-- + Separate multiple machines with ','. + Multiple <Include> tags allowed. + --> + <Include>005, 100lions, 10yard</Include> + + <!-- + Separate multiple machines with ','. + Multiple <Exclude> tags allowed. + --> + <Exclude>005, 100lions, 10yard</Exclude> + + <!-- + Only 1 machine per <Change> tag. + There may be multiple <Change> tags. + --> + <Change>dino with dinoj</Change> +</MAMEFilter> +</Advanced_MAME_Launcher_custom_filters> diff --git a/plugin.program.AML/filters/AML-MAME-filters.xml b/plugin.program.AML/filters/AML-MAME-filters.xml new file mode 100644 index 0000000000..7d2ab7fd00 --- /dev/null +++ b/plugin.program.AML/filters/AML-MAME-filters.xml @@ -0,0 +1,321 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Example Advanced MAME Launcher custom filters +--> +<Advanced_MAME_Launcher_custom_filters> + +<!-- Driver definitions --> +<DEFINE name="CAPCOM_CPS_DRIVERS">has cps1.cpp or has cps2.cpp or has cps3.cpp</DEFINE> +<DEFINE name="NEOGEO_MVS_DRIVER">has neogeo.cpp</DEFINE> + +<!-- Genre definitions --> +<DEFINE name="STANDARD_GENRES">has "Ball & Paddle" or has Climbing or has Driving or has Fighter or has Maze or has MultiGame or has Multiplay or has Platform or has Puzzle or has Shooter or has Sports or has Whac-A-Mole</DEFINE> + +<!-- Control definitions --> +<DEFINE name="STANDARD_CONTROLS">has Dial or has Joy or has Lightgun or has "Only Buttons" or has Paddle or has Pedal or has Positional or has Stick or has Trackball</DEFINE> + +<!-- Manufacturer/board filters --> +<MAMEFilter> + <Name>Atari arcade games</Name> + <Plot>Arcade games by [COLOR orange]Atari[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Atari</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Capcom arcade games</Name> + <Plot>Arcade games by [COLOR orange]Capcom[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Capcom</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Capcom CPS board</Name> + <Plot>Machines in [COLOR orange]Capcom CPS[/COLOR] arcade boards.</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Driver>CAPCOM_CPS_DRIVERS</Driver> +</MAMEFilter> + +<MAMEFilter> + <Name>Cave arcade games</Name> + <Plot>Machines in [COLOR orange]Cave[/COLOR] arcade boards.</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Cave</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Data East arcade games</Name> + <Plot>Arcade games by [COLOR orange]Data East[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has "Data East"</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Gaelco arcade games</Name> + <Plot>Arcade games by [COLOR orange]Gaelco[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Gaelco</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>IGS arcade games</Name> + <Plot>Arcade games by [COLOR orange]IGS[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Igs or has IGS</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Irem arcade games</Name> + <Plot>Arcade games by [COLOR orange]Irem[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Irem</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Jaleco arcade games</Name> + <Plot>Arcade games by [COLOR orange]Jaleco[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Jaleco</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Kaneko arcade games</Name> + <Plot>Arcade games by [COLOR orange]Kaneko[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Kaneko</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Konami arcade games</Name> + <Plot>Arcade games by [COLOR orange]Konami[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Konami</Manufacturer> + + <!-- Include 2 player version of 4 player machines --> + <Include>simpsons2p, tmht2p</Include> + + <!-- Include 2 player versions and remove 4 player versions --> + <!-- + <Include>simpsons2p, tmht2p</Include> + <Exclude>simpsons, tmnt</Exclude> + --> + + <!-- Or swap the 4 player versions with the 2 player versions --> + <!-- + <Change>simpsons with simpsons2p</Change> + <Change>tmnt with tmht2p</Change> + --> +</MAMEFilter> + +<MAMEFilter> + <Name>Mitchell arcade games</Name> + <Plot>Arcade games by [COLOR orange]Mitchell[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Mitchell</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Midway arcade games</Name> + <Plot>Arcade games by [COLOR orange]Midway[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Midway</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Namco arcade games</Name> + <Plot>Arcade games by [COLOR orange]Namco[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Namco</Manufacturer> + + <!-- Include notable clone games --> + <Include>pacman</Include> +</MAMEFilter> + +<MAMEFilter> + <Name>Nintendo arcade games</Name> + <Plot>Arcade games by [COLOR orange]Nintendo[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Nintendo</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Sammy arcade games</Name> + <Plot>Arcade games by [COLOR orange]Sammy[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Sammy</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>SEGA arcade games</Name> + <Plot>Arcade games by [COLOR orange]SEGA[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Sega or has SEGA</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Seta arcade games</Name> + <Plot>Arcade games by [COLOR orange]Seta[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Seta or has SETA</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>SNK arcade games</Name> + <Plot>Arcade games by [COLOR orange]SNK[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has SNK or has Snk</Manufacturer> + <!-- Exclude the Neo Geo BIOS machines --> + <Exclude>ng_mv1, ng_mv2f, ng_mv4f, neogeo</Exclude> +</MAMEFilter> + +<MAMEFilter> + <Name>SNK NeoGeo MVS board</Name> + <Plot>Machines in [COLOR orange]SNK Neo Geo MVS[/COLOR] arcade board.</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Driver>NEOGEO_MVS_DRIVER</Driver> + <!-- Exclude the Neo Geo BIOS machines --> + <Exclude>ng_mv1, ng_mv2f, ng_mv4f, neogeo</Exclude> +</MAMEFilter> + +<MAMEFilter> + <Name>TAD Corporation arcade games</Name> + <Plot>Arcade games by [COLOR orange]TAD Corporation[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Tad or has TAD</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Taito arcade games</Name> + <Plot>Arcade games by [COLOR orange]Taito[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Taito</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Tecmo arcade games</Name> + <Plot>Arcade games by [COLOR orange]Tecmo[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Tecmo</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Technos arcade games</Name> + <Plot>Arcade games by [COLOR orange]Technos[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Technos</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Toaplan arcade games</Name> + <Plot>Arcade games by [COLOR orange]Toaplan[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Toaplan</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Visco arcade games</Name> + <Plot>Arcade games by [COLOR orange]Visco[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Visco</Manufacturer> +</MAMEFilter> + +<MAMEFilter> + <Name>Williams arcade games</Name> + <Plot>Arcade games by [COLOR orange]Williams[/COLOR].</Plot> + <Options>NoClones, NoCoinLess, NoBIOS, NoMechanical, NoNonworking</Options> + <Manufacturer>has Williams</Manufacturer> +</MAMEFilter> + +<!-- Family game definitions --> +<MAMEFilter> + <Name>Family games from the 70s</Name> + <Plot>Arcade machines from the 1970s with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> + <Year>year > 1000 and year < 1980</Year> +</MAMEFilter> + +<MAMEFilter> + <Name>Family games from the 80s</Name> + <Plot>Arcade machines from 1980 to 1984 with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> + <Year>year >= 1980 and year < 1985</Year> + <!-- Include notable clone games --> + <Include>pacman</Include> +</MAMEFilter> + +<MAMEFilter> + <Name>Family games from the 85s</Name> + <Plot>Arcade machines from 1985 to 1989 with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> + <Year>year >= 1985 and year < 1990</Year> +</MAMEFilter> + +<MAMEFilter> + <Name>Family games from the 90s</Name> + <Plot>Arcade machines from 1990 to 1994 with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> + <Year>year >= 1990 and year < 1995</Year> +</MAMEFilter> + +<MAMEFilter> + <Name>Family games from the 95s</Name> + <Plot>Arcade machines from 1995 to 1999 with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> + <Year>year >= 1995 and year < 2000</Year> +</MAMEFilter> + +<MAMEFilter> + <Name>Family games from the 00s</Name> + <Plot>Arcade machines from the 2000s with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> + <Year>year >= 2000 and year < 2010</Year> +</MAMEFilter> + +<MAMEFilter> + <Name>Family games from the 10s</Name> + <Plot>Arcade machines from the 2010s an up with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> + <Year>year >= 2010</Year> +</MAMEFilter> + +<MAMEFilter> + <Name>Family games with Horizonal Screen</Name> + <Plot>Arcade machines with a Horizontal screen with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Options>NoVertical</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> +</MAMEFilter> + +<MAMEFilter> + <Name>Family games with Vertical Screen</Name> + <Plot>Arcade machines with a Vertical screen with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Options>NoHorizontal</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> +</MAMEFilter> + +<MAMEFilter> + <Name>Complete machines</Name> + <Plot>Complete arcade machines, that is, machines with no missing ROMs, CHDs, or Samples, with standard genres and controls, excluding mature, BIOSes, mechanical and non-working machines.</Plot> + <Options>NoClones, NoCoinLess, NoMature, NoBIOS, NoMechanical, NoNonworking</Options> + <Options>NoMissingROMs, NoMissingCHDs, NoMissingSamples</Options> + <Genre>STANDARD_GENRES</Genre> + <Controls>STANDARD_CONTROLS</Controls> +</MAMEFilter> +</Advanced_MAME_Launcher_custom_filters> diff --git a/plugin.program.AML/fonts/Inconsolata.otf b/plugin.program.AML/fonts/Inconsolata.otf new file mode 100644 index 0000000000..348889828d Binary files /dev/null and b/plugin.program.AML/fonts/Inconsolata.otf differ diff --git a/plugin.program.AML/media/MAME_assets/dino_PCB.png b/plugin.program.AML/media/MAME_assets/dino_PCB.png new file mode 100644 index 0000000000..030fa7664f Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_PCB.png differ diff --git a/plugin.program.AML/media/MAME_assets/dino_artpreview.png b/plugin.program.AML/media/MAME_assets/dino_artpreview.png new file mode 100644 index 0000000000..cb682baa63 Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_artpreview.png differ diff --git a/plugin.program.AML/media/MAME_assets/dino_cabinet.png b/plugin.program.AML/media/MAME_assets/dino_cabinet.png new file mode 100644 index 0000000000..fc8c85dd79 Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_cabinet.png differ diff --git a/plugin.program.AML/media/MAME_assets/dino_clearlogo.png b/plugin.program.AML/media/MAME_assets/dino_clearlogo.png new file mode 100644 index 0000000000..33f0323e52 Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_clearlogo.png differ diff --git a/plugin.program.AML/media/MAME_assets/dino_cpanel.png b/plugin.program.AML/media/MAME_assets/dino_cpanel.png new file mode 100644 index 0000000000..d2a9edac19 Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_cpanel.png differ diff --git a/plugin.program.AML/media/MAME_assets/dino_flyer.png b/plugin.program.AML/media/MAME_assets/dino_flyer.png new file mode 100644 index 0000000000..de392cbf39 Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_flyer.png differ diff --git a/plugin.program.AML/media/MAME_assets/dino_marquee.png b/plugin.program.AML/media/MAME_assets/dino_marquee.png new file mode 100644 index 0000000000..18d88ac4bc Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_marquee.png differ diff --git a/plugin.program.AML/media/MAME_assets/dino_snap.png b/plugin.program.AML/media/MAME_assets/dino_snap.png new file mode 100644 index 0000000000..d7925d6e56 Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_snap.png differ diff --git a/plugin.program.AML/media/MAME_assets/dino_title.png b/plugin.program.AML/media/MAME_assets/dino_title.png new file mode 100644 index 0000000000..87f969cc1f Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/dino_title.png differ diff --git a/plugin.program.AML/media/MAME_assets/mslug_clearlogo.png b/plugin.program.AML/media/MAME_assets/mslug_clearlogo.png new file mode 100644 index 0000000000..c7daa06ae7 Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/mslug_clearlogo.png differ diff --git a/plugin.program.AML/media/MAME_assets/mslug_flyer.png b/plugin.program.AML/media/MAME_assets/mslug_flyer.png new file mode 100644 index 0000000000..71079cdb0b Binary files /dev/null and b/plugin.program.AML/media/MAME_assets/mslug_flyer.png differ diff --git a/plugin.program.AML/media/MAME_clearlogo.png b/plugin.program.AML/media/MAME_clearlogo.png new file mode 100644 index 0000000000..f305131424 Binary files /dev/null and b/plugin.program.AML/media/MAME_clearlogo.png differ diff --git a/plugin.program.AML/media/SL_assets/doom_boxfront.png b/plugin.program.AML/media/SL_assets/doom_boxfront.png new file mode 100644 index 0000000000..9e79ed9aa5 Binary files /dev/null and b/plugin.program.AML/media/SL_assets/doom_boxfront.png differ diff --git a/plugin.program.AML/media/SL_assets/doom_clearlogo.png b/plugin.program.AML/media/SL_assets/doom_clearlogo.png new file mode 100644 index 0000000000..a535c7192d Binary files /dev/null and b/plugin.program.AML/media/SL_assets/doom_clearlogo.png differ diff --git a/plugin.program.AML/media/SL_assets/doom_snap.png b/plugin.program.AML/media/SL_assets/doom_snap.png new file mode 100644 index 0000000000..733da4e258 Binary files /dev/null and b/plugin.program.AML/media/SL_assets/doom_snap.png differ diff --git a/plugin.program.AML/media/SL_assets/doom_title.png b/plugin.program.AML/media/SL_assets/doom_title.png new file mode 100644 index 0000000000..c9d2a18e9a Binary files /dev/null and b/plugin.program.AML/media/SL_assets/doom_title.png differ diff --git a/plugin.program.AML/media/SL_assets/sonic3_boxfront.png b/plugin.program.AML/media/SL_assets/sonic3_boxfront.png new file mode 100644 index 0000000000..3ada3bc1f6 Binary files /dev/null and b/plugin.program.AML/media/SL_assets/sonic3_boxfront.png differ diff --git a/plugin.program.AML/media/SL_assets/sonic3_clearlogo.png b/plugin.program.AML/media/SL_assets/sonic3_clearlogo.png new file mode 100644 index 0000000000..12c3aef5bd Binary files /dev/null and b/plugin.program.AML/media/SL_assets/sonic3_clearlogo.png differ diff --git a/plugin.program.AML/media/fanart.jpg b/plugin.program.AML/media/fanart.jpg new file mode 100644 index 0000000000..b7e2ba3927 Binary files /dev/null and b/plugin.program.AML/media/fanart.jpg differ diff --git a/plugin.program.AML/media/icon.png b/plugin.program.AML/media/icon.png new file mode 100644 index 0000000000..9ba2defd59 Binary files /dev/null and b/plugin.program.AML/media/icon.png differ diff --git a/plugin.program.AML/media/shot_01_main_window.jpg b/plugin.program.AML/media/shot_01_main_window.jpg new file mode 100644 index 0000000000..ee8b3586f0 Binary files /dev/null and b/plugin.program.AML/media/shot_01_main_window.jpg differ diff --git a/plugin.program.AML/media/shot_02_MAME_pclone_list.jpg b/plugin.program.AML/media/shot_02_MAME_pclone_list.jpg new file mode 100644 index 0000000000..afb799908b Binary files /dev/null and b/plugin.program.AML/media/shot_02_MAME_pclone_list.jpg differ diff --git a/plugin.program.AML/media/shot_03_SL_pclone_list.jpg b/plugin.program.AML/media/shot_03_SL_pclone_list.jpg new file mode 100644 index 0000000000..fd183ffd1b Binary files /dev/null and b/plugin.program.AML/media/shot_03_SL_pclone_list.jpg differ diff --git a/plugin.program.AML/media/shot_04_MAME_fanart.jpg b/plugin.program.AML/media/shot_04_MAME_fanart.jpg new file mode 100644 index 0000000000..f420780a94 Binary files /dev/null and b/plugin.program.AML/media/shot_04_MAME_fanart.jpg differ diff --git a/plugin.program.AML/media/shot_05_SL_fanart.jpg b/plugin.program.AML/media/shot_05_SL_fanart.jpg new file mode 100644 index 0000000000..4900e557f6 Binary files /dev/null and b/plugin.program.AML/media/shot_05_SL_fanart.jpg differ diff --git a/plugin.program.AML/media/shot_06_MAME_3dbox.jpg b/plugin.program.AML/media/shot_06_MAME_3dbox.jpg new file mode 100644 index 0000000000..da9ea9ed2e Binary files /dev/null and b/plugin.program.AML/media/shot_06_MAME_3dbox.jpg differ diff --git a/plugin.program.AML/media/shot_07_SL_3dbox.jpg b/plugin.program.AML/media/shot_07_SL_3dbox.jpg new file mode 100644 index 0000000000..5126a0343d Binary files /dev/null and b/plugin.program.AML/media/shot_07_SL_3dbox.jpg differ diff --git a/plugin.program.AML/media/shot_08_MAME_History_viewer.jpg b/plugin.program.AML/media/shot_08_MAME_History_viewer.jpg new file mode 100644 index 0000000000..c469d9a041 Binary files /dev/null and b/plugin.program.AML/media/shot_08_MAME_History_viewer.jpg differ diff --git a/plugin.program.AML/media/shot_09_MAME_ROMs_db.jpg b/plugin.program.AML/media/shot_09_MAME_ROMs_db.jpg new file mode 100644 index 0000000000..f69e25905d Binary files /dev/null and b/plugin.program.AML/media/shot_09_MAME_ROMs_db.jpg differ diff --git a/plugin.program.AML/media/shot_10_MAME_Audit_db.jpg b/plugin.program.AML/media/shot_10_MAME_Audit_db.jpg new file mode 100644 index 0000000000..01831f9706 Binary files /dev/null and b/plugin.program.AML/media/shot_10_MAME_Audit_db.jpg differ diff --git a/plugin.program.AML/media/shot_11_MAME_Audit_machine.jpg b/plugin.program.AML/media/shot_11_MAME_Audit_machine.jpg new file mode 100644 index 0000000000..935f3d9dae Binary files /dev/null and b/plugin.program.AML/media/shot_11_MAME_Audit_machine.jpg differ diff --git a/plugin.program.AML/pdfrw/LICENSE.txt b/plugin.program.AML/pdfrw/LICENSE.txt new file mode 100644 index 0000000000..8250f252bc --- /dev/null +++ b/plugin.program.AML/pdfrw/LICENSE.txt @@ -0,0 +1,75 @@ +pdfrw (github.com/pmaupin/pdfrw) + +The majority of pdfrw was written by Patrick Maupin and is licensed +under the MIT license (reproduced below). Other contributors include +Attila Tajti and Nerijus Mika. It appears that some of the decompression +code was based on the decompressor from PyPDF2, which was written by +Mathieu Fenniak and licensed under the BSD license (also reproduced below). + +Please add any missing authors here: + +Copyright (c) 2006-2017 Patrick Maupin. All rights reserved. +Copyright (c) 2006 Mathieu Fenniak. All rights reserved. +Copyright (c) 2010 Attila Tajti. All rights reserved. +Copyright (c) 2012 Nerijus Mika. All rights reserved. +Copyright (c) 2015 Bastien Gandouet. All rights reserved. +Copyright (c) 2015 Tzerjen Wei. All rights reserved. +Copyright (c) 2015 Jorj X. McKie. All rights reserved. +Copyright (c) 2015 Nicholas Devenish. All rights reserved. +Copyright (c) 2015-2016 Jonatan Dellagostin. All rights reserved. +Copyright (c) 2016-2017 Thomas Kluyver. All rights reserved. +Copyright (c) 2016 James Laird-Wah. All rights reserved. +Copyright (c) 2016 Marcus Brinkmann. All rights reserved. +Copyright (c) 2016 Edward Betts. All rights reserved. +Copyright (c) 2016 Patrick Mazulo. All rights reserved. +Copyright (c) 2017 Haochen Wu. All rights reserved. +Copyright (c) 2017 Jon Lund Steffensen. All rights reserved. +Copyright (c) 2017 Henddher Pedroza. All rights reserved. + + +MIT License: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +BSD License: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/plugin.program.AML/pdfrw/README.rst b/plugin.program.AML/pdfrw/README.rst new file mode 100644 index 0000000000..f41fb76128 --- /dev/null +++ b/plugin.program.AML/pdfrw/README.rst @@ -0,0 +1,803 @@ +================== +pdfrw 0.4 +================== + +:Author: Patrick Maupin + +.. contents:: + :backlinks: none + +.. sectnum:: + +Introduction +============ + +**pdfrw** is a Python library and utility that reads and writes PDF files: + +* Version 0.4 is tested and works on Python 2.6, 2.7, 3.3, 3.4, 3.5, and 3.6 +* Operations include subsetting, merging, rotating, modifying metadata, etc. +* The fastest pure Python PDF parser available +* Has been used for years by a printer in pre-press production +* Can be used with rst2pdf to faithfully reproduce vector images +* Can be used either standalone, or in conjunction with `reportlab`__ + to reuse existing PDFs in new ones +* Permissively licensed + +__ http://www.reportlab.org/ + + +pdfrw will faithfully reproduce vector formats without +rasterization, so the rst2pdf package has used pdfrw +for PDF and SVG images by default since March 2010. + +pdfrw can also be used in conjunction with reportlab, in order +to re-use portions of existing PDFs in new PDFs created with +reportlab. + + +Examples +========= + +The library comes with several examples that show operation both with +and without reportlab. + + +All examples +------------------ + +The examples directory has a few scripts which use the library. +Note that if these examples do not work with your PDF, you should +try to use pdftk to uncompress and/or unencrypt them first. + +* `4up.py`__ will shrink pages down and place 4 of them on + each output page. +* `alter.py`__ shows an example of modifying metadata, without + altering the structure of the PDF. +* `booklet.py`__ shows an example of creating a 2-up output + suitable for printing and folding (e.g on tabloid size paper). +* `cat.py`__ shows an example of concatenating multiple PDFs together. +* `extract.py`__ will extract images and Form XObjects (embedded pages) + from existing PDFs to make them easier to use and refer to from + new PDFs (e.g. with reportlab or rst2pdf). +* `poster.py`__ increases the size of a PDF so it can be printed + as a poster. +* `print_two.py`__ Allows creation of 8.5 X 5.5" booklets by slicing + 8.5 X 11" paper apart after printing. +* `rotate.py`__ Rotates all or selected pages in a PDF. +* `subset.py`__ Creates a new PDF with only a subset of pages from the + original. +* `unspread.py`__ Takes a 2-up PDF, and splits out pages. +* `watermark.py`__ Adds a watermark PDF image over or under all the pages + of a PDF. +* `rl1/4up.py`__ Another 4up example, using reportlab canvas for output. +* `rl1/booklet.py`__ Another booklet example, using reportlab canvas for + output. +* `rl1/subset.py`__ Another subsetting example, using reportlab canvas for + output. +* `rl1/platypus_pdf_template.py`__ Another watermarking example, using + reportlab canvas and generated output for the document. Contributed + by user asannes. +* `rl2`__ Experimental code for parsing graphics. Needs work. +* `subset_booklets.py`__ shows an example of creating a full printable pdf + version in a more professional and pratical way ( take a look at + http://www.wikihow.com/Bind-a-Book ) + +__ https://github.com/pmaupin/pdfrw/tree/master/examples/4up.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/alter.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/booklet.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/cat.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/extract.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/poster.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/print_two.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/rotate.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/subset.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/unspread.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/watermark.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/rl1/4up.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/rl1/booklet.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/rl1/subset.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/rl1/platypus_pdf_template.py +__ https://github.com/pmaupin/pdfrw/tree/master/examples/rl2/ +__ https://github.com/pmaupin/pdfrw/tree/master/examples/subset_booklets.py + +Notes on selected examples +------------------------------------ + +Reorganizing pages and placing them two-up +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A printer with a fancy printer and/or a full-up copy of Acrobat can +easily turn your small PDF into a little booklet (for example, print 4 +letter-sized pages on a single 11" x 17"). + +But that assumes several things, including that the personnel know how +to operate the hardware and software. `booklet.py`__ lets you turn your PDF +into a preformatted booklet, to give them fewer chances to mess it up. + +__ https://github.com/pmaupin/pdfrw/tree/master/examples/booklet.py + +Adding or modifying metadata +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `cat.py`__ example will accept multiple input files on the command +line, concatenate them and output them to output.pdf, after adding some +nonsensical metadata to the output PDF file. + +__ https://github.com/pmaupin/pdfrw/tree/master/examples/cat.py + +The `alter.py`__ example alters a single metadata item in a PDF, +and writes the result to a new PDF. + +__ https://github.com/pmaupin/pdfrw/tree/master/examples/alter.py + + +One difference is that, since **cat** is creating a new PDF structure, +and **alter** is attempting to modify an existing PDF structure, the +PDF produced by alter (and also by watermark.py) *should* be +more faithful to the original (except for the desired changes). + +For example, the alter.py navigation should be left intact, whereas with +cat.py it will be stripped. + + +Rotating and doubling +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you ever want to print something that is like a small booklet, but +needs to be spiral bound, you either have to do some fancy rearranging, +or just waste half your paper. + +The `print_two.py`__ example program will, for example, make two side-by-side +copies each page of of your PDF on a each output sheet. + +__ https://github.com/pmaupin/pdfrw/tree/master/examples/print_two.py + +But, every other page is flipped, so that you can print double-sided and +the pages will line up properly and be pre-collated. + +Graphics stream parsing proof of concept +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `copy.py`__ script shows a simple example of reading in a PDF, and +using the decodegraphics.py module to try to write the same information +out to a new PDF through a reportlab canvas. (If you know about reportlab, +you know that if you can faithfully render a PDF to a reportlab canvas, you +can do pretty much anything else with that PDF you want.) This kind of +low level manipulation should be done only if you really need to. +decodegraphics is really more than a proof of concept than anything +else. For most cases, just use the Form XObject capability, as shown in +the examples/rl1/booklet.py demo. + +__ https://github.com/pmaupin/pdfrw/tree/master/examples/rl2/copy.py + +pdfrw philosophy +================== + +Core library +------------- + +The philosophy of the library portion of pdfrw is to provide intuitive +functions to read, manipulate, and write PDF files. There should be +minimal leakage between abstraction layers, although getting useful +work done makes "pure" functionality separation difficult. + +A key concept supported by the library is the use of Form XObjects, +which allow easy embedding of pieces of one PDF into another. + +Addition of core support to the library is typically done carefully +and thoughtfully, so as not to clutter it up with too many special +cases. + +There are a lot of incorrectly formatted PDFs floating around; support +for these is added in some cases. The decision is often based on what +acroread and okular do with the PDFs; if they can display them properly, +then eventually pdfrw should, too, if it is not too difficult or costly. + +Contributions are welcome; one user has contributed some decompression +filters and the ability to process PDF 1.5 stream objects. Additional +functionality that would obviously be useful includes additional +decompression filters, the ability to process password-protected PDFs, +and the ability to output linearized PDFs. + +Examples +-------- + +The philosophy of the examples is to provide small, easily-understood +examples that showcase pdfrw functionality. + + +PDF files and Python +====================== + +Introduction +------------ + +In general, PDF files conceptually map quite well to Python. The major +objects to think about are: + +- **strings**. Most things are strings. These also often decompose + naturally into +- **lists of tokens**. Tokens can be combined to create higher-level + objects like +- **arrays** and +- **dictionaries** and +- **Contents streams** (which can be more streams of tokens) + +Difficulties +------------ + +The apparent primary difficulty in mapping PDF files to Python is the +PDF file concept of "indirect objects." Indirect objects provide +the efficiency of allowing a single piece of data to be referred to +from more than one containing object, but probably more importantly, +indirect objects provide a way to get around the chicken and egg +problem of circular object references when mapping arbitrary data +structures to files. To flatten out a circular reference, an indirect +object is *referred to* instead of being *directly included* in another +object. PDF files have a global mechanism for locating indirect objects, +and they all have two reference numbers (a reference number and a +"generation" number, in case you wanted to append to the PDF file +rather than just rewriting the whole thing). + +pdfrw automatically handles indirect references on reading in a PDF +file. When pdfrw encounters an indirect PDF file object, the +corresponding Python object it creates will have an 'indirect' attribute +with a value of True. When writing a PDF file, if you have created +arbitrary data, you just need to make sure that circular references are +broken up by putting an attribute named 'indirect' which evaluates to +True on at least one object in every cycle. + +Another PDF file concept that doesn't quite map to regular Python is a +"stream". Streams are dictionaries which each have an associated +unformatted data block. pdfrw handles streams by placing a special +attribute on a subclassed dictionary. + +Usage Model +----------- + +The usage model for pdfrw treats most objects as strings (it takes their +string representation when writing them to a file). The two main +exceptions are the PdfArray object and the PdfDict object. + +PdfArray is a subclass of list with two special features. First, +an 'indirect' attribute allows a PdfArray to be written out as +an indirect PDF object. Second, pdfrw reads files lazily, so +PdfArray knows about, and resolves references to other indirect +objects on an as-needed basis. + +PdfDict is a subclass of dict that also has an indirect attribute +and lazy reference resolution as well. (And the subclassed +IndirectPdfDict has indirect automatically set True). + +But PdfDict also has an optional associated stream. The stream object +defaults to None, but if you assign a stream to the dict, it will +automatically set the PDF /Length attribute for the dictionary. + +Finally, since PdfDict instances are indexed by PdfName objects (which +always start with a /) and since most (all?) standard Adobe PdfName +objects use names formatted like "/CamelCase", it makes sense to allow +access to dictionary elements via object attribute accesses as well as +object index accesses. So usage of PdfDict objects is normally via +attribute access, although non-standard names (though still with a +leading slash) can be accessed via dictionary index lookup. + +Reading PDFs +~~~~~~~~~~~~~~~ + +The PdfReader object is a subclass of PdfDict, which allows easy access +to an entire document:: + + >>> from pdfrw import PdfReader + >>> x = PdfReader('source.pdf') + >>> x.keys() + ['/Info', '/Size', '/Root'] + >>> x.Info + {'/Producer': '(cairo 1.8.6 (http://cairographics.org))', + '/Creator': '(cairo 1.8.6 (http://cairographics.org))'} + >>> x.Root.keys() + ['/Type', '/Pages'] + +Info, Size, and Root are retrieved from the trailer of the PDF file. + +In addition to the tree structure, pdfrw creates a special attribute +named *pages*, that is a list of all the pages in the document. pdfrw +creates the *pages* attribute as a simplification for the user, because +the PDF format allows arbitrarily complicated nested dictionaries to +describe the page order. Each entry in the *pages* list is the PdfDict +object for one of the pages in the file, in order. + +:: + + >>> len(x.pages) + 1 + >>> x.pages[0] + {'/Parent': {'/Kids': [{...}], '/Type': '/Pages', '/Count': '1'}, + '/Contents': {'/Length': '11260', '/Filter': None}, + '/Resources': ... (Lots more stuff snipped) + >>> x.pages[0].Contents + {'/Length': '11260', '/Filter': None} + >>> x.pages[0].Contents.stream + 'q\n1 1 1 rg /a0 gs\n0 0 0 RG 0.657436 + w\n0 J\n0 j\n[] 0.0 d\n4 M q' ... (Lots more stuff snipped) + +Writing PDFs +~~~~~~~~~~~~~~~ + +As you can see, it is quite easy to dig down into a PDF document. But +what about when it's time to write it out? + +:: + + >>> from pdfrw import PdfWriter + >>> y = PdfWriter() + >>> y.addpage(x.pages[0]) + >>> y.write('result.pdf') + +That's all it takes to create a new PDF. You may still need to read the +`Adobe PDF reference manual`__ to figure out what needs to go *into* +the PDF, but at least you don't have to sweat actually building it +and getting the file offsets right. + +__ http://www.adobe.com/devnet/acrobat/pdfs/pdf_reference_1-7.pdf + +Manipulating PDFs in memory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For the most part, pdfrw tries to be agnostic about the contents of +PDF files, and support them as containers, but to do useful work, +something a little higher-level is required, so pdfrw works to +understand a bit about the contents of the containers. For example: + +- PDF pages. pdfrw knows enough to find the pages in PDF files you read + in, and to write a set of pages back out to a new PDF file. +- Form XObjects. pdfrw can take any page or rectangle on a page, and + convert it to a Form XObject, suitable for use inside another PDF + file. It knows enough about these to perform scaling, rotation, + and positioning. +- reportlab objects. pdfrw can recursively create a set of reportlab + objects from its internal object format. This allows, for example, + Form XObjects to be used inside reportlab, so that you can reuse + content from an existing PDF file when building a new PDF with + reportlab. + +There are several examples that demonstrate these features in +the example code directory. + +Missing features +~~~~~~~~~~~~~~~~~~~~~~~ + +Even as a pure PDF container library, pdfrw comes up a bit short. It +does not currently support: + +- Most compression/decompression filters +- encryption + +`pdftk`__ is a wonderful command-line +tool that can convert your PDFs to remove encryption and compression. +However, in most cases, you can do a lot of useful work with PDFs +without actually removing compression, because only certain elements +inside PDFs are actually compressed. + +__ https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/ + +Library internals +================== + +Introduction +------------ + +**pdfrw** currently consists of 19 modules organized into a main +package and one sub-package. + +The `__init.py__`__ module does the usual thing of importing a few +major attributes from some of the submodules, and the `errors.py`__ +module supports logging and exception generation. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/__init__.py +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/errors.py + + +PDF object model support +-------------------------- + +The `objects`__ sub-package contains one module for each of the +internal representations of the kinds of basic objects that exist +in a PDF file, with the `objects/__init__.py`__ module in that +package simply gathering them up and making them available to the +main pdfrw package. + +One feature that all the PDF object classes have in common is the +inclusion of an 'indirect' attribute. If 'indirect' exists and evaluates +to True, then when the object is written out, it is written out as an +indirect object. That is to say, it is addressable in the PDF file, and +could be referenced by any number (including zero) of container objects. +This indirect object capability saves space in PDF files by allowing +objects such as fonts to be referenced from multiple pages, and also +allows PDF files to contain internal circular references. This latter +capability is used, for example, when each page object has a "parent" +object in its dictionary. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/objects/ +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/objects/__init__.py + +Ordinary objects +~~~~~~~~~~~~~~~~ + +The `objects/pdfobject.py`__ module contains the PdfObject class, which is +a subclass of str, and is the catch-all object for any PDF file elements +that are not explicitly represented by other objects, as described below. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/objects/pdfobject.py + +Name objects +~~~~~~~~~~~~ + +The `objects/pdfname.py`__ module contains the PdfName singleton object, +which will convert a string into a PDF name by prepending a slash. It can +be used either by calling it or getting an attribute, e.g.:: + + PdfName.Rotate == PdfName('Rotate') == PdfObject('/Rotate') + +In the example above, there is a slight difference between the objects +returned from PdfName, and the object returned from PdfObject. The +PdfName objects are actually objects of class "BasePdfName". This +is important, because only these may be used as keys in PdfDict objects. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/objects/pdfname.py + +String objects +~~~~~~~~~~~~~~ + +The `objects/pdfstring.py`__ +module contains the PdfString class, which is a subclass of str that is +used to represent encoded strings in a PDF file. The class has encode +and decode methods for the strings. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/objects/pdfstring.py + + +Array objects +~~~~~~~~~~~~~ + +The `objects/pdfarray.py`__ +module contains the PdfArray class, which is a subclass of list that is +used to represent arrays in a PDF file. A regular list could be used +instead, but use of the PdfArray class allows for an indirect attribute +to be set, and also allows for proxying of unresolved indirect objects +(that haven't been read in yet) in a manner that is transparent to pdfrw +clients. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/objects/pdfarray.py + +Dict objects +~~~~~~~~~~~~ + +The `objects/pdfdict.py`__ +module contains the PdfDict class, which is a subclass of dict that is +used to represent dictionaries in a PDF file. A regular dict could be +used instead, but the PdfDict class matches the requirements of PDF +files more closely: + +* Transparent (from the library client's viewpoint) proxying + of unresolved indirect objects +* Return of None for non-existent keys (like dict.get) +* Mapping of attribute accesses to the dict itself + (pdfdict.Foo == pdfdict[NameObject('Foo')]) +* Automatic management of following stream and /Length attributes + for content dictionaries +* Indirect attribute +* Other attributes may be set for private internal use of the + library and/or its clients. +* Support for searching parent dictionaries for PDF "inheritable" + attributes. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/objects/pdfdict.py + +If a PdfDict has an associated data stream in the PDF file, the stream +is accessed via the 'stream' (all lower-case) attribute. Setting the +stream attribute on the PdfDict will automatically set the /Length attribute +as well. If that is not what is desired (for example if the the stream +is compressed), then _stream (same name with an underscore) may be used +to associate the stream with the PdfDict without setting the length. + +To set private attributes (that will not be written out to a new PDF +file) on a dictionary, use the 'private' attribute:: + + mydict.private.foo = 1 + +Once the attribute is set, it may be accessed directly as an attribute +of the dictionary:: + + foo = mydict.foo + +Some attributes of PDF pages are "inheritable." That is, they may +belong to a parent dictionary (or a parent of a parent dictionary, etc.) +The "inheritable" attribute allows for easy discovery of these:: + + mediabox = mypage.inheritable.MediaBox + + +Proxy objects +~~~~~~~~~~~~~ + +The `objects/pdfindirect.py`__ +module contains the PdfIndirect class, which is a non-transparent proxy +object for PDF objects that have not yet been read in and resolved from +a file. Although these are non-transparent inside the library, client code +should never see one of these -- they exist inside the PdfArray and PdfDict +container types, but are resolved before being returned to a client of +those types. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/objects/pdfindirect.py + + +File reading, tokenization and parsing +-------------------------------------- + +`pdfreader.py`__ +contains the PdfReader class, which can read a PDF file (or be passed a +file object or already read string) and parse it. It uses the PdfTokens +class in `tokens.py`__ for low-level tokenization. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/pdfreader.py +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/tokens.py + + +The PdfReader class does not, in general, parse into containers (e.g. +inside the content streams). There is a proof of concept for doing that +inside the examples/rl2 subdirectory, but that is slow and not well-developed, +and not useful for most applications. + +An instance of the PdfReader class is an instance of a PdfDict -- the +trailer dictionary of the PDF file, to be exact. It will have a private +attribute set on it that is named 'pages' that is a list containing all +the pages in the file. + +When instantiating a PdfReader object, there are options available +for decompressing all the objects in the file. pdfrw does not currently +have very many options for decompression, so this is not all that useful, +except in the specific case of compressed object streams. + +Also, there are no options for decryption yet. If you have PDF files +that are encrypted or heavily compressed, you may find that using another +program like pdftk on them can make them readable by pdfrw. + +In general, the objects are read from the file lazily, but this is not +currently true with compressed object streams -- all of these are decompressed +and read in when the PdfReader is instantiated. + + +File output +----------- + +`pdfwriter.py`__ +contains the PdfWriter class, which can create and output a PDF file. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/pdfwriter.py + +There are a few options available when creating and using this class. + +In the simplest case, an instance of PdfWriter is instantiated, and +then pages are added to it from one or more source files (or created +programmatically), and then the write method is called to dump the +results out to a file. + +If you have a source PDF and do not want to disturb the structure +of it too badly, then you may pass its trailer directly to PdfWriter +rather than letting PdfWriter construct one for you. There is an +example of this (alter.py) in the examples directory. + + +Advanced features +----------------- + +`buildxobj.py`__ +contains functions to build Form XObjects out of pages or rectangles on +pages. These may be reused in new PDFs essentially as if they were images. + +buildxobj is careful to cache any page used so that it only appears in +the output once. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/buildxobj.py + + +`toreportlab.py`__ +provides the makerl function, which will translate pdfrw objects into a +format which can be used with `reportlab <http://www.reportlab.org/>`__. +It is normally used in conjunction with buildxobj, to be able to reuse +parts of existing PDFs when using reportlab. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/toreportlab.py + + +`pagemerge.py`__ builds on the foundation laid by buildxobj. It +contains classes to create a new page (or overlay an existing page) +using one or more rectangles from other pages. There are examples +showing its use for watermarking, scaling, 4-up output, splitting +each page in 2, etc. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/pagemerge.py + +`findobjs.py`__ contains code that can find specific kinds of objects +inside a PDF file. The extract.py example uses this module to create +a new PDF that places each image and Form XObject from a source PDF onto +its own page, e.g. for easy reuse with some of the other examples or +with reportlab. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/findobjs.py + + +Miscellaneous +---------------- + +`compress.py`__ and `uncompress.py`__ +contains compression and decompression functions. Very few filters are +currently supported, so an external tool like pdftk might be good if you +require the ability to decompress (or, for that matter, decrypt) PDF +files. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/compress.py +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/uncompress.py + + +`py23_diffs.py`__ contains code to help manage the differences between +Python 2 and Python 3. + +__ https://github.com/pmaupin/pdfrw/tree/master/pdfrw/py23_diffs.py + +Testing +=============== + +The tests associated with pdfrw require a large number of PDFs, +which are not distributed with the library. + +To run the tests: + +* Download or clone the full package from github.com/pmaupin/pdfrw +* cd into the tests directory, and then clone the package + github.com/pmaupin/static_pdfs into a subdirectory (also named + static_pdfs). +* Now the tests may be run from tests directory using unittest, or + py.test, or nose. +* travisci is used at github, and runs the tests with py.test + +.. code-block:: bash + $ pip install pytest + $ pip install reportlab + $ pwd + <...>/pdfrw/tests + $ git clone https://github.com/pmaupin/static_pdfs + $ ln -s ../pdfrw + $ pytest + +To run a single test-case: + +.. code-block:: bash + $ pytest test_roundtrip.py -k "test_compress_9f98322c243fe67726d56ccfa8e0885b.pdf" + +Other libraries +===================== + +Pure Python +----------- + +- `reportlab <http://www.reportlab.org/>`__ + + reportlab is must-have software if you want to programmatically + generate arbitrary PDFs. + +- `pyPdf <https://github.com/mstamy2/PyPDF2>`__ + + pyPdf is, in some ways, very full-featured. It can do decompression + and decryption and seems to know a lot about items inside at least + some kinds of PDF files. In comparison, pdfrw knows less about + specific PDF file features (such as metadata), but focuses on trying + to have a more Pythonic API for mapping the PDF file container + syntax to Python, and (IMO) has a simpler and better PDF file + parser. The Form XObject capability of pdfrw means that, in many + cases, it does not actually need to decompress objects -- they + can be left compressed. + +- `pdftools <http://www.boddie.org.uk/david/Projects/Python/pdftools/index.html>`__ + + pdftools feels large and I fell asleep trying to figure out how it + all fit together, but many others have done useful things with it. + +- `pagecatcher <http://www.reportlab.com/docs/pagecatcher-ds.pdf>`__ + + My understanding is that pagecatcher would have done exactly what I + wanted when I built pdfrw. But I was on a zero budget, so I've never + had the pleasure of experiencing pagecatcher. I do, however, use and + like `reportlab <http://www.reportlab.org/>`__ (open source, from + the people who make pagecatcher) so I'm sure pagecatcher is great, + better documented and much more full-featured than pdfrw. + +- `pdfminer <http://www.unixuser.org/~euske/python/pdfminer/index.html>`__ + + This looks like a useful, actively-developed program. It is quite + large, but then, it is trying to actively comprehend a full PDF + document. From the website: + + "PDFMiner is a suite of programs that help extracting and analyzing + text data of PDF documents. Unlike other PDF-related tools, it + allows to obtain the exact location of texts in a page, as well as + other extra information such as font information or ruled lines. It + includes a PDF converter that can transform PDF files into other + text formats (such as HTML). It has an extensible PDF parser that + can be used for other purposes instead of text analysis." + +non-pure-Python libraries +------------------------- + +- `pyPoppler <https://launchpad.net/poppler-python/>`__ can read PDF + files. +- `pycairo <http://www.cairographics.org/pycairo/>`__ can write PDF + files. +- `PyMuPDF <https://github.com/rk700/PyMuPDF>`_ high performance rendering + of PDF, (Open)XPS, CBZ and EPUB + +Other tools +----------- + +- `pdftk <https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/>`__ is a wonderful command + line tool for basic PDF manipulation. It complements pdfrw extremely + well, supporting many operations such as decryption and decompression + that pdfrw cannot do. +- `MuPDF <http://www.mupdf.com/>`_ is a free top performance PDF, (Open)XPS, CBZ and EPUB rendering library + that also comes with some command line tools. One of those, ``mutool``, has big overlaps with pdftk's - + except it is up to 10 times faster. + +Release information +======================= + +Revisions: + +0.4 -- Released 18 September, 2017 + + - Python 3.6 added to test matrix + - Proper unicode support for text strings in PDFs added + - buildxobj fixes allow better support creating form XObjects + out of compressed pages in some cases + - Compression fixes for Python 3+ + - New subset_booklets.py example + - Bug with non-compressed indices into compressed object streams fixed + - Bug with distinguishing compressed object stream first objects fixed + - Better error reporting added for some invalid PDFs (e.g. when reading + past the end of file) + - Better scrubbing of old bookmark information when writing PDFs, to + remove dangling references + - Refactoring of pdfwriter, including updating API, to allow future + enhancements for things like incremental writing + - Minor tokenizer speedup + - Some flate decompressor bugs fixed + - Compression and decompression tests added + - Tests for new unicode handling added + - PdfReader.readpages() recursion error (issue #92) fixed. + - Initial crypt filter support added + + +0.3 -- Released 19 October, 2016. + + - Python 3.5 added to test matrix + - Better support under Python 3.x for in-memory PDF file-like objects + - Some pagemerge and Unicode patches added + - Changes to logging allow better coexistence with other packages + - Fix for "from pdfrw import \*" + - New fancy_watermark.py example shows off capabilities of pagemerge.py + - metadata.py example renamed to cat.py + + +0.2 -- Released 21 June, 2015. Supports Python 2.6, 2.7, 3.3, and 3.4. + + - Several bugs have been fixed + - New regression test functionally tests core with dozens of + PDFs, and also tests examples. + - Core has been ported and tested on Python3 by round-tripping + several difficult files and observing binary matching results + across the different Python versions. + - Still only minimal support for compression and no support + for encryption or newer PDF features. (pdftk is useful + to put PDFs in a form that pdfrw can use.) + +0.1 -- Released to PyPI in 2012. Supports Python 2.5 - 2.7 + diff --git a/plugin.program.AML/pdfrw/pdfrw/__init__.py b/plugin.program.AML/pdfrw/pdfrw/__init__.py new file mode 100644 index 0000000000..cf7644a286 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/__init__.py @@ -0,0 +1,23 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +from .pdfwriter import PdfWriter +from .pdfreader import PdfReader +from .objects import (PdfObject, PdfName, PdfArray, + PdfDict, IndirectPdfDict, PdfString) +from .tokens import PdfTokens +from .errors import PdfParseError +from .pagemerge import PageMerge + +__version__ = '0.4' + +# Add a tiny bit of compatibility to pyPdf + +PdfFileReader = PdfReader +PdfFileWriter = PdfWriter + +__all__ = """PdfWriter PdfReader PdfObject PdfName PdfArray + PdfTokens PdfParseError PdfDict IndirectPdfDict + PdfString PageMerge""".split() + diff --git a/plugin.program.AML/pdfrw/pdfrw/buildxobj.py b/plugin.program.AML/pdfrw/pdfrw/buildxobj.py new file mode 100644 index 0000000000..f132795323 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/buildxobj.py @@ -0,0 +1,363 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' + +This module contains code to build PDF "Form XObjects". + +A Form XObject allows a fragment from one PDF file to be cleanly +included in another PDF file. + +Reference for syntax: "Parameters for opening PDF files" from SDK 8.1 + + http://www.adobe.com/devnet/acrobat/pdfs/pdf_open_parameters.pdf + + supported 'page=xxx', 'viewrect=<left>,<top>,<width>,<height>' + + Also supported by this, but not by Adobe: + 'rotate=xxx' where xxx in [0, 90, 180, 270] + + Units are in points + + +Reference for content: Adobe PDF reference, sixth edition, version 1.7 + + http://www.adobe.com/devnet/acrobat/pdfs/pdf_reference_1-7.pdf + + Form xobjects discussed chapter 4.9, page 355 +''' + +from .objects import PdfDict, PdfArray, PdfName +from .pdfreader import PdfReader +from .errors import log, PdfNotImplementedError +from .py23_diffs import iteritems +from .uncompress import uncompress +from .compress import compress + + +class ViewInfo(object): + ''' Instantiate ViewInfo with a uri, and it will parse out + the filename, page, and viewrect into object attributes. + + Note 1: + Viewrects follow the adobe definition. (See reference + above). They are arrays of 4 numbers: + + - Distance from left of document in points + - Distance from top (NOT bottom) of document in points + - Width of rectangle in points + - Height of rectangle in points + + Note 2: + For simplicity, Viewrects can also be specified + in fractions of the document. If every number in + the viewrect is between 0 and 1 inclusive, then + viewrect elements 0 and 2 are multiplied by the + mediabox width before use, and viewrect elements + 1 and 3 are multiplied by the mediabox height before + use. + + Note 3: + By default, an XObject based on the view will be + cacheable. It should not be cacheable if the XObject + will be subsequently modified. + ''' + doc = None + docname = None + page = None + viewrect = None + rotate = None + cacheable = True + + def __init__(self, pageinfo='', **kw): + pageinfo = pageinfo.split('#', 1) + if len(pageinfo) == 2: + pageinfo[1:] = pageinfo[1].replace('&', '#').split('#') + for key in 'page viewrect'.split(): + if pageinfo[0].startswith(key + '='): + break + else: + self.docname = pageinfo.pop(0) + for item in pageinfo: + key, value = item.split('=') + key = key.strip() + value = value.replace(',', ' ').split() + if key in ('page', 'rotate'): + assert len(value) == 1 + setattr(self, key, int(value[0])) + elif key == 'viewrect': + assert len(value) == 4 + setattr(self, key, [float(x) for x in value]) + else: + log.error('Unknown option: %s', key) + for key, value in iteritems(kw): + assert hasattr(self, key), key + setattr(self, key, value) + + +def get_rotation(rotate): + ''' Return clockwise rotation code: + 0 = unrotated + 1 = 90 degrees + 2 = 180 degrees + 3 = 270 degrees + ''' + try: + rotate = int(rotate) + except (ValueError, TypeError): + return 0 + if rotate % 90 != 0: + return 0 + return rotate // 90 + + +def rotate_point(point, rotation): + ''' Rotate an (x,y) coordinate clockwise by a + rotation code specifying a multiple of 90 degrees. + ''' + if rotation & 1: + point = point[1], -point[0] + if rotation & 2: + point = -point[0], -point[1] + return point + + +def rotate_rect(rect, rotation): + ''' Rotate both points within the rectangle, then normalize + the rectangle by returning the new lower left, then new + upper right. + ''' + rect = rotate_point(rect[:2], rotation) + rotate_point(rect[2:], rotation) + return (min(rect[0], rect[2]), min(rect[1], rect[3]), + max(rect[0], rect[2]), max(rect[1], rect[3])) + + +def getrects(inheritable, pageinfo, rotation): + ''' Given the inheritable attributes of a page and + the desired pageinfo rectangle, return the page's + media box and the calculated boundary (clip) box. + ''' + mbox = tuple([float(x) for x in inheritable.MediaBox]) + cbox = tuple([float(x) for x in (inheritable.CropBox or mbox)]) + vrect = pageinfo.viewrect + if vrect is not None: + # Rotate the media box to match what the user sees, + # figure out the clipping box, then rotate back + mleft, mbot, mright, mtop = rotate_rect(cbox, rotation) + x, y, w, h = vrect + + # Support operations in fractions of a page + if 0 <= min(vrect) < max(vrect) <= 1: + mw = mright - mleft + mh = mtop - mbot + x *= mw + w *= mw + y *= mh + h *= mh + + cleft = mleft + x + ctop = mtop - y + cright = cleft + w + cbot = ctop - h + cbox = (max(mleft, cleft), max(mbot, cbot), + min(mright, cright), min(mtop, ctop)) + cbox = rotate_rect(cbox, -rotation) + return mbox, cbox + + +def _build_cache(contents, allow_compressed): + ''' Build a new dictionary holding the stream, + and save it along with private cache info. + Assumes validity has been pre-checked if + we have a non-None xobj_copy. + + Also, the spec says nothing about nested arrays, + so we assume those don't exist until we see one + in the wild. + ''' + try: + xobj_copy = contents.xobj_copy + except AttributeError: + # Should have a PdfArray here... + array = contents + private = contents + else: + # Should have a PdfDict here -- might or might not have cache copy + if xobj_copy is not None: + return xobj_copy + array = [contents] + private = contents.private + + # If we don't allow compressed objects, OR if we have multiple compressed + # objects, we try to decompress them, and fail if we cannot do that. + + if not allow_compressed or len(array) > 1: + keys = set(x[0] for cdict in array for x in iteritems(cdict)) + was_compressed = len(keys) > 1 + if was_compressed: + # Make copies of the objects before we uncompress them. + array = [PdfDict(x) for x in array] + if not uncompress(array): + raise PdfNotImplementedError( + 'Xobjects with these compression parameters not supported: %s' % + keys) + + xobj_copy = PdfDict(array[0]) + xobj_copy.private.xobj_cachedict = {} + private.xobj_copy = xobj_copy + + if len(array) > 1: + newstream = '\n'.join(x.stream for x in array) + newlength = sum(int(x.Length) for x in array) + len(array) - 1 + assert newlength == len(newstream) + xobj_copy.stream = newstream + if was_compressed and allow_compressed: + compress(xobj_copy) + + return xobj_copy + + +def _cache_xobj(contents, resources, mbox, bbox, rotation, cacheable=True): + ''' Return a cached Form XObject, or create a new one and cache it. + Adds private members x, y, w, h + ''' + cachedict = contents.xobj_cachedict + cachekey = mbox, bbox, rotation + result = cachedict.get(cachekey) if cacheable else None + if result is None: + # If we are not getting a full page, or if we are going to + # modify the results, first retrieve an underlying Form XObject + # that represents the entire page, so that we are not copying + # the full page data into the new file multiple times + func = (_get_fullpage, _get_subpage)[mbox != bbox or not cacheable] + result = PdfDict( + func(contents, resources, mbox), + Type=PdfName.XObject, + Subtype=PdfName.Form, + FormType=1, + BBox=PdfArray(bbox), + ) + rect = bbox + if rotation: + matrix = (rotate_point((1, 0), rotation) + + rotate_point((0, 1), rotation)) + result.Matrix = PdfArray(matrix + (0, 0)) + rect = rotate_rect(rect, rotation) + + private = result.private + private.x = rect[0] + private.y = rect[1] + private.w = rect[2] - rect[0] + private.h = rect[3] - rect[1] + if cacheable: + cachedict[cachekey] = result + return result + + +def _get_fullpage(contents, resources, mbox): + ''' fullpage is easy. Just copy the contents, + set up the resources, and let _cache_xobj handle the + rest. + ''' + return PdfDict(contents, Resources=resources) + + +def _get_subpage(contents, resources, mbox): + ''' subpages *could* be as easy as full pages, but we + choose to complicate life by creating a Form XObject + for the page, and then one that references it for + the subpage, on the off-chance that we want multiple + items from the page. + ''' + return PdfDict( + stream='/FullPage Do\n', + Resources=PdfDict( + XObject=PdfDict( + FullPage=_cache_xobj(contents, resources, mbox, mbox, 0) + ) + ) + ) + + +def pagexobj(page, viewinfo=ViewInfo(), allow_compressed=True): + ''' pagexobj creates and returns a Form XObject for + a given view within a page (Defaults to entire page.) + + pagexobj is passed a page and a viewrect. + ''' + inheritable = page.inheritable + resources = inheritable.Resources + rotation = get_rotation(inheritable.Rotate) + mbox, bbox = getrects(inheritable, viewinfo, rotation) + rotation += get_rotation(viewinfo.rotate) + contents = _build_cache(page.Contents, allow_compressed) + return _cache_xobj(contents, resources, mbox, bbox, rotation, + viewinfo.cacheable) + + +def docxobj(pageinfo, doc=None, allow_compressed=True): + ''' docinfo reads a page out of a document and uses + pagexobj to create the Form XObject based on + the page. + + This is a convenience function for things like + rst2pdf that want to be able to pass in textual + filename/location descriptors and don't want to + know about using PdfReader. + + Can work standalone, or in conjunction with + the CacheXObj class (below). + + ''' + if not isinstance(pageinfo, ViewInfo): + pageinfo = ViewInfo(pageinfo) + + # If we're explicitly passed a document, + # make sure we don't have one implicitly as well. + # If no implicit or explicit doc, then read one in + # from the filename. + if doc is not None: + assert pageinfo.doc is None + pageinfo.doc = doc + elif pageinfo.doc is not None: + doc = pageinfo.doc + else: + doc = pageinfo.doc = PdfReader(pageinfo.docname, + decompress=not allow_compressed) + assert isinstance(doc, PdfReader) + + sourcepage = doc.pages[(pageinfo.page or 1) - 1] + return pagexobj(sourcepage, pageinfo, allow_compressed) + + +class CacheXObj(object): + ''' Use to keep from reparsing files over and over, + and to keep from making the output too much + bigger than it ought to be by replicating + unnecessary object copies. + + This is a convenience function for things like + rst2pdf that want to be able to pass in textual + filename/location descriptors and don't want to + know about using PdfReader. + ''' + def __init__(self, decompress=False): + ''' Set decompress true if you need + the Form XObjects to be decompressed. + Will decompress what it can and scream + about the rest. + ''' + self.cached_pdfs = {} + self.decompress = decompress + + def load(self, sourcename): + ''' Load a Form XObject from a uri + ''' + info = ViewInfo(sourcename) + fname = info.docname + pcache = self.cached_pdfs + doc = pcache.get(fname) + if doc is None: + doc = pcache[fname] = PdfReader(fname, decompress=self.decompress) + return docxobj(info, doc, allow_compressed=not self.decompress) diff --git a/plugin.program.AML/pdfrw/pdfrw/compress.py b/plugin.program.AML/pdfrw/pdfrw/compress.py new file mode 100644 index 0000000000..b7b4e756be --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/compress.py @@ -0,0 +1,27 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' +Currently, this sad little file only knows how to compress +using the flate (zlib) algorithm. Maybe more later, but it's +not a priority for me... +''' + +from .objects import PdfName +from .uncompress import streamobjects +from .py23_diffs import zlib, convert_load, convert_store + + +def compress(mylist): + flate = PdfName.FlateDecode + for obj in streamobjects(mylist): + ftype = obj.Filter + if ftype is not None: + continue + oldstr = obj.stream + newstr = convert_load(zlib.compress(convert_store(oldstr))) + if len(newstr) < len(oldstr) + 30: + obj.stream = newstr + obj.Filter = flate + obj.DecodeParms = None diff --git a/plugin.program.AML/pdfrw/pdfrw/crypt.py b/plugin.program.AML/pdfrw/pdfrw/crypt.py new file mode 100644 index 0000000000..dc00676cbf --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/crypt.py @@ -0,0 +1,150 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2017 Jon Lund Steffensen +# MIT license -- See LICENSE.txt for details + +from __future__ import division + +import hashlib +import struct + +try: + from Crypto.Cipher import ARC4, AES + HAS_CRYPTO = True +except ImportError: + HAS_CRYPTO = False + +from .objects import PdfDict, PdfName + +_PASSWORD_PAD = ( + '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08' + '..\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz') + + +def streamobjects(mylist, isinstance=isinstance, PdfDict=PdfDict): + for obj in mylist: + if isinstance(obj, PdfDict) and obj.stream is not None: + yield obj + + +def create_key(password, doc): + """Create an encryption key (Algorithm 2 in PDF spec).""" + key_size = int(doc.Encrypt.Length or 40) // 8 + padded_pass = (password + _PASSWORD_PAD)[:32] + hasher = hashlib.md5() + hasher.update(padded_pass) + hasher.update(doc.Encrypt.O.to_bytes()) + hasher.update(struct.pack('<i', int(doc.Encrypt.P))) + hasher.update(doc.ID[0].to_bytes()) + temp_hash = hasher.digest() + + if int(doc.Encrypt.R or 0) >= 3: + for _ in range(50): + temp_hash = hashlib.md5(temp_hash[:key_size]).digest() + + return temp_hash[:key_size] + + +def create_user_hash(key, doc): + """Create the user password hash (Algorithm 4/5).""" + revision = int(doc.Encrypt.R or 0) + if revision < 3: + cipher = ARC4.new(key) + return cipher.encrypt(_PASSWORD_PAD) + else: + hasher = hashlib.md5() + hasher.update(_PASSWORD_PAD) + hasher.update(doc.ID[0].to_bytes()) + temp_hash = hasher.digest() + + for i in range(20): + temp_key = ''.join(chr(i ^ ord(x)) for x in key) + cipher = ARC4.new(temp_key) + temp_hash = cipher.encrypt(temp_hash) + + return temp_hash + + +def check_user_password(key, doc): + """Check that the user password is correct (Algorithm 6).""" + expect_user_hash = create_user_hash(key, doc) + revision = int(doc.Encrypt.R or 0) + if revision < 3: + return doc.Encrypt.U.to_bytes() == expect_user_hash + else: + return doc.Encrypt.U.to_bytes()[:16] == expect_user_hash + + +class AESCryptFilter(object): + """Crypt filter corresponding to /AESV2.""" + def __init__(self, key): + self._key = key + + def decrypt_data(self, num, gen, data): + """Decrypt data (string/stream) using key (Algorithm 1).""" + key_extension = struct.pack('<i', num)[:3] + key_extension += struct.pack('<i', gen)[:2] + key_extension += 'sAlT' + temp_key = self._key + key_extension + temp_key = hashlib.md5(temp_key).digest() + + iv = data[:AES.block_size] + cipher = AES.new(temp_key, AES.MODE_CBC, iv) + decrypted = cipher.decrypt(data[AES.block_size:]) + + # Remove padding + pad_size = ord(decrypted[-1]) + assert 1 <= pad_size <= 16 + return decrypted[:-pad_size] + + +class RC4CryptFilter(object): + """Crypt filter corresponding to /V2.""" + def __init__(self, key): + self._key = key + + def decrypt_data(self, num, gen, data): + """Decrypt data (string/stream) using key (Algorithm 1).""" + new_key_size = min(len(self._key) + 5, 16) + key_extension = struct.pack('<i', num)[:3] + key_extension += struct.pack('<i', gen)[:2] + temp_key = self._key + key_extension + temp_key = hashlib.md5(temp_key).digest()[:new_key_size] + + cipher = ARC4.new(temp_key) + return cipher.decrypt(data) + + +class IdentityCryptFilter(object): + """Identity crypt filter (pass through with no encryption).""" + def decrypt_data(self, num, gen, data): + return data + + +def decrypt_objects(objects, default_filter, filters): + """Decrypt list of stream objects. + + The parameter default_filter specifies the default filter to use. The + filters parameter is a dictionary of alternate filters to use when the + object specfies an alternate filter locally. + """ + for obj in streamobjects(objects): + if getattr(obj, 'decrypted', False): + continue + + filter = default_filter + + # Check whether a locally defined crypt filter should override the + # default filter. + ftype = obj.Filter + if ftype is not None: + if not isinstance(ftype, list): + ftype = [ftype] + if len(ftype) >= 1 and ftype[0] == PdfName.Crypt: + ftype = ftype[1:] + parms = obj.DecodeParms or obj.DP + filter = filters[parms.Name] + + num, gen = obj.indirect + obj.stream = filter.decrypt_data(num, gen, obj.stream) + obj.private.decrypted = True + obj.Filter = ftype or None diff --git a/plugin.program.AML/pdfrw/pdfrw/errors.py b/plugin.program.AML/pdfrw/pdfrw/errors.py new file mode 100644 index 0000000000..ef6ab7d58b --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/errors.py @@ -0,0 +1,41 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' +PDF Exceptions and error handling +''' + +import logging + + +fmt = logging.Formatter('[%(levelname)s] %(filename)s:%(lineno)d %(message)s') + +handler = logging.StreamHandler() +handler.setFormatter(fmt) + +log = logging.getLogger('pdfrw') +log.setLevel(logging.WARNING) +log.addHandler(handler) + + +class PdfError(Exception): + "Abstract base class of exceptions thrown by this module" + + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return self.msg + + +class PdfParseError(PdfError): + "Error thrown by parser/tokenizer" + + +class PdfOutputError(PdfError): + "Error thrown by PDF writer" + + +class PdfNotImplementedError(PdfError): + "Error thrown on missing features" diff --git a/plugin.program.AML/pdfrw/pdfrw/findobjs.py b/plugin.program.AML/pdfrw/pdfrw/findobjs.py new file mode 100644 index 0000000000..67d33a08f0 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/findobjs.py @@ -0,0 +1,137 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' This module contains a function to find all the XObjects + in a document, and another function that will wrap them + in page objects. +''' + +from .objects import PdfDict, PdfArray, PdfName + + +def find_objects(source, valid_types=(PdfName.XObject, None), + valid_subtypes=(PdfName.Form, PdfName.Image), + no_follow=(PdfName.Parent,), + isinstance=isinstance, id=id, sorted=sorted, + reversed=reversed, PdfDict=PdfDict): + ''' + Find all the objects of a particular kind in a document + or array. Defaults to looking for Form and Image XObjects. + + This could be done recursively, but some PDFs + are quite deeply nested, so we do it without + recursion. + + Note that we don't know exactly where things appear on pages, + but we aim for a sort order that is (a) mostly in document order, + and (b) reproducible. For arrays, objects are processed in + array order, and for dicts, they are processed in key order. + ''' + container = (PdfDict, PdfArray) + + # Allow passing a list of pages, or a dict + if isinstance(source, PdfDict): + source = [source] + else: + source = list(source) + + visited = set() + source.reverse() + while source: + obj = source.pop() + if not isinstance(obj, container): + continue + myid = id(obj) + if myid in visited: + continue + visited.add(myid) + if isinstance(obj, PdfDict): + if obj.Type in valid_types and obj.Subtype in valid_subtypes: + yield obj + obj = [y for (x, y) in sorted(obj.iteritems()) + if x not in no_follow] + else: + # TODO: This forces resolution of any indirect objects in + # the array. It may not be necessary. Don't know if + # reversed() does any voodoo underneath the hood. + # It's cheap enough for now, but might be removeable. + obj and obj[0] + source.extend(reversed(obj)) + + +def wrap_object(obj, width, margin): + ''' Wrap an xobj in its own page object. + ''' + fmt = 'q %s 0 0 %s %s %s cm /MyImage Do Q' + contents = PdfDict(indirect=True) + subtype = obj.Subtype + if subtype == PdfName.Form: + contents._stream = obj.stream + contents.Length = obj.Length + contents.Filter = obj.Filter + contents.DecodeParms = obj.DecodeParms + resources = obj.Resources + mbox = obj.BBox + elif subtype == PdfName.Image: # Image + xoffset = margin[0] + yoffset = margin[1] + cw = width - margin[0] - margin[2] + iw, ih = float(obj.Width), float(obj.Height) + ch = 1.0 * cw / iw * ih + height = ch + margin[1] + margin[3] + p = tuple(('%.9f' % x).rstrip('0').rstrip('.') for x in (cw, ch, xoffset, yoffset)) + contents.stream = fmt % p + resources = PdfDict(XObject=PdfDict(MyImage=obj)) + mbox = PdfArray((0, 0, width, height)) + else: + raise TypeError("Expected Form or Image XObject") + + return PdfDict( + indirect=True, + Type=PdfName.Page, + MediaBox=mbox, + Resources=resources, + Contents=contents, + ) + + +def trivial_xobjs(maxignore=300): + ''' Ignore XObjects that trivially contain other XObjects. + ''' + ignore = set('q Q cm Do'.split()) + Image = PdfName.Image + + def check(obj): + if obj.Subtype == Image: + return False + s = obj.stream + if len(s) < maxignore: + s = (x for x in s.split() if not x.startswith('/') and + x not in ignore) + s = (x.replace('.', '').replace('-', '') for x in s) + if not [x for x in s if not x.isdigit()]: + return True + return check + + +def page_per_xobj(xobj_iter, width=8.5 * 72, margin=0.0 * 72, + image_only=False, ignore=trivial_xobjs(), + wrap_object=wrap_object): + ''' page_per_xobj wraps every XObj found + in its own page object. + width and margin are used to set image sizes. + ''' + try: + iter(margin) + except: + margin = [margin] + while len(margin) < 4: + margin *= 2 + + if isinstance(xobj_iter, (list, dict)): + xobj_iter = find_objects(xobj_iter) + for obj in xobj_iter: + if not ignore(obj): + if not image_only or obj.Subtype == PdfName.IMage: + yield wrap_object(obj, width, margin) diff --git a/plugin.program.AML/pdfrw/pdfrw/objects/__init__.py b/plugin.program.AML/pdfrw/pdfrw/objects/__init__.py new file mode 100644 index 0000000000..879e0ef6cf --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/objects/__init__.py @@ -0,0 +1,19 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' +Objects that can occur in PDF files. The most important +objects are arrays and dicts. Either of these can be +indirect or not, and dicts could have an associated +stream. +''' +from .pdfname import PdfName +from .pdfdict import PdfDict, IndirectPdfDict +from .pdfarray import PdfArray +from .pdfobject import PdfObject +from .pdfstring import PdfString +from .pdfindirect import PdfIndirect + +__all__ = """PdfName PdfDict IndirectPdfDict PdfArray + PdfObject PdfString PdfIndirect""".split() diff --git a/plugin.program.AML/pdfrw/pdfrw/objects/pdfarray.py b/plugin.program.AML/pdfrw/pdfrw/objects/pdfarray.py new file mode 100644 index 0000000000..e15f4ad69d --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/objects/pdfarray.py @@ -0,0 +1,71 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +from .pdfindirect import PdfIndirect +from .pdfobject import PdfObject + + +def _resolved(): + pass + + +class PdfArray(list): + ''' A PdfArray maps the PDF file array object into a Python list. + It has an indirect attribute which defaults to False. + ''' + indirect = False + + def __init__(self, source=[]): + self._resolve = self._resolver + self.extend(source) + + def _resolver(self, isinstance=isinstance, enumerate=enumerate, + listiter=list.__iter__, PdfIndirect=PdfIndirect, + resolved=_resolved, PdfNull=PdfObject('null')): + for index, value in enumerate(list.__iter__(self)): + if isinstance(value, PdfIndirect): + value = value.real_value() + if value is None: + value = PdfNull + self[index] = value + self._resolve = resolved + + def __getitem__(self, index, listget=list.__getitem__): + self._resolve() + return listget(self, index) + + try: + def __getslice__(self, i, j, listget=list.__getslice__): + self._resolve() + return listget(self, i, j) + except AttributeError: + pass + + def __iter__(self, listiter=list.__iter__): + self._resolve() + return listiter(self) + + def count(self, item): + self._resolve() + return list.count(self, item) + + def index(self, item): + self._resolve() + return list.index(self, item) + + def remove(self, item): + self._resolve() + return list.remove(self, item) + + def sort(self, *args, **kw): + self._resolve() + return list.sort(self, *args, **kw) + + def pop(self, *args): + self._resolve() + return list.pop(self, *args) + + def __reversed__(self): + self._resolve() + return list.__reversed__(self) diff --git a/plugin.program.AML/pdfrw/pdfrw/objects/pdfdict.py b/plugin.program.AML/pdfrw/pdfrw/objects/pdfdict.py new file mode 100644 index 0000000000..888fc83ac7 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/objects/pdfdict.py @@ -0,0 +1,241 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +from .pdfname import PdfName, BasePdfName +from .pdfindirect import PdfIndirect +from .pdfobject import PdfObject +from ..py23_diffs import iteritems +from ..errors import PdfParseError + + +class _DictSearch(object): + ''' Used to search for inheritable attributes. + ''' + + def __init__(self, basedict): + self.basedict = basedict + + def __getattr__(self, name, PdfName=PdfName): + return self[PdfName(name)] + + def __getitem__(self, name, set=set, getattr=getattr, id=id): + visited = set() + mydict = self.basedict + while 1: + value = mydict[name] + if value is not None: + return value + myid = id(mydict) + assert myid not in visited + visited.add(myid) + mydict = mydict.Parent + if mydict is None: + return + + +class _Private(object): + ''' Used to store private attributes (not output to PDF files) + on PdfDict classes + ''' + + def __init__(self, pdfdict): + vars(self)['pdfdict'] = pdfdict + + def __setattr__(self, name, value): + vars(self.pdfdict)[name] = value + + +class PdfDict(dict): + ''' PdfDict objects are subclassed dictionaries + with the following features: + + - Every key in the dictionary starts with "/" + + - A dictionary item can be deleted by assigning it to None + + - Keys that (after the initial "/") conform to Python + naming conventions can also be accessed (set and retrieved) + as attributes of the dictionary. E.g. mydict.Page is the + same thing as mydict['/Page'] + + - Private attributes (not in the PDF space) can be set + on the dictionary object attribute dictionary by using + the private attribute: + + mydict.private.foo = 3 + mydict.foo = 5 + x = mydict.foo # x will now contain 3 + y = mydict['/foo'] # y will now contain 5 + + Most standard adobe dictionary keys start with an upper case letter, + so to avoid conflicts, it is best to start private attributes with + lower case letters. + + - PdfDicts have the following read-only properties: + + - private -- as discussed above, provides write access to + dictionary's attributes + - inheritable -- this creates and returns a "view" attribute + that will search through the object hierarchy for + any desired attribute, such as /Rotate or /MediaBox + + - PdfDicts also have the following special attributes: + - indirect is not stored in the PDF dictionary, but in the object's + attribute dictionary + - stream is also stored in the object's attribute dictionary + and will also update the stream length. + - _stream will store in the object's attribute dictionary without + updating the stream length. + + It is possible, for example, to have a PDF name such as "/indirect" + or "/stream", but you cannot access such a name as an attribute: + + mydict.indirect -- accesses object's attribute dictionary + mydict["/indirect"] -- accesses actual PDF dictionary + ''' + indirect = False + stream = None + + _special = dict(indirect=('indirect', False), + stream=('stream', True), + _stream=('stream', False), + ) + + def __setitem__(self, name, value, setter=dict.__setitem__, + BasePdfName=BasePdfName, isinstance=isinstance): + if not isinstance(name, BasePdfName): + raise PdfParseError('Dict key %s is not a PdfName' % repr(name)) + if value is not None: + setter(self, name, value) + elif name in self: + del self[name] + + def __init__(self, *args, **kw): + if args: + if len(args) == 1: + args = args[0] + self.update(args) + if isinstance(args, PdfDict): + self.indirect = args.indirect + self._stream = args.stream + for key, value in iteritems(kw): + setattr(self, key, value) + + def __getattr__(self, name, PdfName=PdfName): + ''' If the attribute doesn't exist on the dictionary object, + try to slap a '/' in front of it and get it out + of the actual dictionary itself. + ''' + return self.get(PdfName(name)) + + def get(self, key, dictget=dict.get, isinstance=isinstance, + PdfIndirect=PdfIndirect): + ''' Get a value out of the dictionary, + after resolving any indirect objects. + ''' + value = dictget(self, key) + if isinstance(value, PdfIndirect): + # We used to use self[key] here, but that does an + # unwanted check on the type of the key (github issue #98). + # Python will keep the old key object in the dictionary, + # so that check is not necessary. + value = value.real_value() + if value is not None: + dict.__setitem__(self, key, value) + else: + del self[key] + return value + + def __getitem__(self, key): + return self.get(key) + + def __setattr__(self, name, value, special=_special.get, + PdfName=PdfName, vars=vars): + ''' Set an attribute on the dictionary. Handle the keywords + indirect, stream, and _stream specially (for content objects) + ''' + info = special(name) + if info is None: + self[PdfName(name)] = value + else: + name, setlen = info + vars(self)[name] = value + if setlen: + notnone = value is not None + self.Length = notnone and PdfObject(len(value)) or None + + def iteritems(self, dictiter=iteritems, + isinstance=isinstance, PdfIndirect=PdfIndirect, + BasePdfName=BasePdfName): + ''' Iterate over the dictionary, resolving any unresolved objects + ''' + for key, value in list(dictiter(self)): + if isinstance(value, PdfIndirect): + self[key] = value = value.real_value() + if value is not None: + if not isinstance(key, BasePdfName): + raise PdfParseError('Dict key %s is not a PdfName' % + repr(key)) + yield key, value + + def items(self): + return list(self.iteritems()) + + def itervalues(self): + for key, value in self.iteritems(): + yield value + + def values(self): + return list((value for key, value in self.iteritems())) + + def keys(self): + return list((key for key, value in self.iteritems())) + + def __iter__(self): + for key, value in self.iteritems(): + yield key + + def iterkeys(self): + return iter(self) + + def copy(self): + return type(self)(self) + + def pop(self, key): + value = self.get(key) + del self[key] + return value + + def popitem(self): + key, value = dict.pop(self) + if isinstance(value, PdfIndirect): + value = value.real_value() + return value + + def inheritable(self): + ''' Search through ancestors as needed for inheritable + dictionary items. + NOTE: You might think it would be a good idea + to cache this class, but then you'd have to worry + about it pointing to the wrong dictionary if you + made a copy of the object... + ''' + return _DictSearch(self) + inheritable = property(inheritable) + + def private(self): + ''' Allows setting private metadata for use in + processing (not sent to PDF file). + See note on inheritable + ''' + return _Private(self) + private = property(private) + + +class IndirectPdfDict(PdfDict): + ''' IndirectPdfDict is a convenience class. You could + create a direct PdfDict and then set indirect = True on it, + or you could just create an IndirectPdfDict. + ''' + indirect = True diff --git a/plugin.program.AML/pdfrw/pdfrw/objects/pdfindirect.py b/plugin.program.AML/pdfrw/pdfrw/objects/pdfindirect.py new file mode 100644 index 0000000000..4df8ac327d --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/objects/pdfindirect.py @@ -0,0 +1,22 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + + +class _NotLoaded(object): + pass + + +class PdfIndirect(tuple): + ''' A placeholder for an object that hasn't been read in yet. + The object itself is the (object number, generation number) tuple. + The attributes include information about where the object is + referenced from and the file object to retrieve the real object from. + ''' + value = _NotLoaded + + def real_value(self, NotLoaded=_NotLoaded): + value = self.value + if value is NotLoaded: + value = self.value = self._loader(self) + return value diff --git a/plugin.program.AML/pdfrw/pdfrw/objects/pdfname.py b/plugin.program.AML/pdfrw/pdfrw/objects/pdfname.py new file mode 100644 index 0000000000..28a146448c --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/objects/pdfname.py @@ -0,0 +1,81 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +import re + +from ..errors import log + +warn = log.warning + + +class BasePdfName(str): + ''' A PdfName is an identifier that starts with + a slash. + + If a PdfName has illegal space or delimiter characters, + then it will be decorated with an "encoded" attribute that + has those characters properly escaped as #<hex><hex> + + The "encoded" attribute is what is sent out to a PDF file, + the non-encoded main object is what is compared for equality + in a PDF dictionary. + ''' + + indirect = False + encoded = None + + whitespace = '\x00 \t\f\r\n' + delimiters = '()<>{}[]/%' + forbidden = list(whitespace) + list('\\' + x for x in delimiters) + remap = dict((x, '#%02X' % ord(x)) for x in (whitespace + delimiters)) + split_to_encode = re.compile('(%s)' % '|'.join(forbidden)).split + split_to_decode = re.compile(r'\#([0-9A-Fa-f]{2})').split + + def __new__(cls, name, pre_encoded=True, remap=remap, + join=''.join, new=str.__new__, chr=chr, int=int, + split_to_encode=split_to_encode, + split_to_decode=split_to_decode, + ): + ''' We can build a PdfName from scratch, or from + a pre-encoded name (e.g. coming in from a file). + ''' + # Optimization for normal case + if name[1:].isalnum(): + return new(cls, name) + encoded = name + if pre_encoded: + if '#' in name: + substrs = split_to_decode(name) + substrs[1::2] = (chr(int(x, 16)) for x in substrs[1::2]) + name = join(substrs) + else: + encoded = split_to_encode(encoded) + encoded[3::2] = (remap[x] for x in encoded[3::2]) + encoded = join(encoded) + self = new(cls, name) + if encoded != name: + self.encoded = encoded + return self + + +# We could have used a metaclass, but this matches what +# we were doing historically. + +class PdfName(object): + ''' Two simple ways to get a PDF name from a string: + + x = PdfName.FooBar + x = pdfName('FooBar') + + Either technique will return "/FooBar" + + ''' + + def __getattr__(self, name, BasePdfName=BasePdfName): + return BasePdfName('/' + name, False) + + def __call__(self, name, BasePdfName=BasePdfName): + return BasePdfName('/' + name, False) + +PdfName = PdfName() diff --git a/plugin.program.AML/pdfrw/pdfrw/objects/pdfobject.py b/plugin.program.AML/pdfrw/pdfrw/objects/pdfobject.py new file mode 100644 index 0000000000..7317395688 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/objects/pdfobject.py @@ -0,0 +1,11 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + + +class PdfObject(str): + ''' A PdfObject is a textual representation of any PDF file object + other than an array, dict or string. It has an indirect attribute + which defaults to False. + ''' + indirect = False diff --git a/plugin.program.AML/pdfrw/pdfrw/objects/pdfstring.py b/plugin.program.AML/pdfrw/pdfrw/objects/pdfstring.py new file mode 100644 index 0000000000..906f30e9f2 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/objects/pdfstring.py @@ -0,0 +1,553 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2017 Patrick Maupin, Austin, Texas +# 2016 James Laird-Wah, Sydney, Australia +# MIT license -- See LICENSE.txt for details + +""" + +================================ +PdfString encoding and decoding +================================ + +Introduction +============= + + +This module handles encoding and decoding of PDF strings. PDF strings +are described in the PDF 1.7 reference manual, mostly in chapter 3 +(sections 3.2 and 3.8) and chapter 5. + +PDF strings are used in the document structure itself, and also inside +the stream of page contents dictionaries. + +A PDF string can represent pure binary data (e.g. for a font or an +image), or text, or glyph indices. For Western fonts, the glyph indices +usually correspond to ASCII, but that is not guaranteed. (When it does +happen, it makes examination of raw PDF data a lot easier.) + +The specification defines PDF string encoding at two different levels. +At the bottom, it defines ways to encode arbitrary bytes so that a PDF +tokenizer can understand they are a string of some sort, and can figure +out where the string begins and ends. (That is all the tokenizer itself +cares about.) Above that level, if the string represents text, the +specification defines ways to encode Unicode text into raw bytes, before +the byte encoding is performed. + +There are two ways to do the byte encoding, and two ways to do the text +(Unicode) encoding. + +Encoding bytes into PDF strings +================================ + +Adobe calls the two ways to encode bytes into strings "Literal strings" +and "Hexadecimal strings." + +Literal strings +------------------ + +A literal string is delimited by ASCII parentheses ("(" and ")"), and a +hexadecimal string is delimited by ASCII less-than and greater-than +signs ("<" and ">"). + +A literal string may encode bytes almost unmolested. The caveat is +that if a byte has the same value as a parenthesis, it must be escaped +so that the tokenizer knows the string is not finished. This is accomplished +by using the ASCII backslash ("\") as an escape character. Of course, +now any backslash appearing in the data must likewise be escaped. + +Hexadecimal strings +--------------------- + +A hexadecimal string requires twice as much space as the source data +it represents (plus two bytes for the delimiter), simply storing each +byte as two hexadecimal digits, most significant digit first. The spec +allows for lower or upper case hex digits, but most PDF encoders seem +to use upper case. + +Special cases -- Legacy systems and readability +----------------------------------------------- + +It is possible to create a PDF document that uses 7 bit ASCII encoding, +and it is desirable in many cases to create PDFs that are reasonably +readable when opened in a text editor. For these reasons, the syntax +for both literal strings and hexadecimal strings is slightly more +complicated that the initial description above. In general, the additional +syntax allows the following features: + + - Making the delineation between characters, or between sections of + a string, apparent, and easy to see in an editor. + - Keeping output lines from getting too wide for some editors + - Keeping output lines from being so narrow that you can only see the + small fraction of a string at a time in an editor. + - Suppressing unprintable characters + - Restricting the output string to 7 bit ASCII + +Hexadecimal readability +~~~~~~~~~~~~~~~~~~~~~~~ + +For hexadecimal strings, only the first two bullets are relevant. The syntax +to accomplish this is simple, allowing any ASCII whitespace to be inserted +anywhere in the encoded hex string. + +Literal readability +~~~~~~~~~~~~~~~~~~~ + +For literal strings, all of the bullets except the first are relevant. +The syntax has two methods to help with these goals. The first method +is to overload the escape operator to be able to do different functions, +and the second method can reduce the number of escapes required for +parentheses in the normal case. + +The escape function works differently, depending on what byte follows +the backslash. In all cases, the escaping backslash is discarded, +and then the next character is examined: + + - For parentheses and backslashes (and, in fact, for all characters + not described otherwise in this list), the character after the + backslash is preserved in the output. + - A letter from the set of "nrtbf" following a backslash is interpreted as + a line feed, carriage return, tab, backspace, or form-feed, respectively. + - One to three octal digits following the backslash indicate the + numeric value of the encoded byte. + - A carriage return, carriage return/line feed, or line feed following + the backslash indicates a line break that was put in for readability, + and that is not part of the actual data, so this is discarded. + +The second method that can be used to improve readability (and reduce space) +in literal strings is to not escape parentheses. This only works, and is +only allowed, when the parentheses are properly balanced. For example, +"((Hello))" is a valid encoding for a literal string, but "((Hello)" is not; +the latter case should be encoded "(\(Hello)" + +Encoding text into strings +========================== + +Section 3.8.1 of the PDF specification describes text strings. + +The individual characters of a text string can all be considered to +be Unicode; Adobe specifies two different ways to encode these characters +into a string of bytes before further encoding the byte string as a +literal string or a hexadecimal string. + +The first way to encode these strings is called PDFDocEncoding. This +is mostly a one-for-one mapping of bytes into single bytes, similar to +Latin-1. The representable character set is limited to the number of +characters that can fit in a byte, and this encoding cannot be used +with Unicode strings that start with the two characters making up the +UTF-16-BE BOM. + +The second way to encode these strings is with UTF-16-BE. Text strings +encoded with this method must start with the BOM, and although the spec +does not appear to mandate that the resultant bytes be encoded into a +hexadecimal string, that seems to be the canonical way to do it. + +When encoding a string into UTF-16-BE, this module always adds the BOM, +and when decoding a string from UTF-16-BE, this module always strips +the BOM. If a source string contains a BOM, that will remain in the +final string after a round-trip through the encoder and decoder, as +the goal of the encoding/decoding process is transparency. + + +PDF string handling in pdfrw +============================= + +Responsibility for handling PDF strings in the pdfrw library is shared +between this module, the tokenizer, and the pdfwriter. + +tokenizer string handling +-------------------------- + +As far as the tokenizer and its clients such as the pdfreader are concerned, +the PdfString class must simply be something that it can instantiate by +passing a string, that doesn't compare equal (or throw an exception when +compared) to other possible token strings. The tokenizer must understand +enough about the syntax of the string to successfully find its beginning +and end in a stream of tokens, but doesn't otherwise know or care about +the data represented by the string. + +pdfwriter string handling +-------------------------- + +The pdfwriter knows and cares about two attributes of PdfString instances: + + - First, PdfString objects have an 'indirect' attribute, which pdfwriter + uses as an indication that the object knows how to represent itself + correctly when output to a new PDF. (In the case of a PdfString object, + no work is really required, because it is already a string.) + - Second, the PdfString.encode() method is used as a convenience to + automatically convert any user-supplied strings (that didn't come + from PDFs) when a PDF is written out to a file. + +pdfstring handling +------------------- + +The code in this module is designed to support those uses by the +tokenizer and the pdfwriter, and to additionally support encoding +and decoding of PdfString objects as a convenience for the user. + +Most users of the pdfrw library never encode or decode a PdfString, +so it is imperative that (a) merely importing this module does not +take a significant amount of CPU time; and (b) it is cheap for the +tokenizer to produce a PdfString, and cheap for the pdfwriter to +consume a PdfString -- if the tokenizer finds a string that conforms +to the PDF specification, it will be wrapped in a PdfString object, +and if the pdfwriter finds an object with an indirect attribute, it +simply calls str() to ask it to format itself. + +Encoding and decoding are not actually performed very often at all, +compared to how often tokenization and then subsequent concatenation +by the pdfwriter are performed. In fact, versions of pdfrw prior to +0.4 did not even support Unicode for this function. Encoding and +decoding can also easily be performed by the user, outside of the +library, and this might still be recommended, at least for encoding, +if the visual appeal of encodings generated by this module is found +lacking. + + +Decoding strings +~~~~~~~~~~~~~~~~~~~ + +Decoding strings can be tricky, but is a bounded process. Each +properly-encoded encoded string represents exactly one output string, +with the caveat that is up to the caller of the function to know whether +he expects a Unicode string, or just bytes. + +The caller can call PdfString.to_bytes() to get a byte string (which may +or may not represent encoded Unicode), or may call PdfString.to_unicode() +to get a Unicode string. Byte strings will be regular strings in Python 2, +and b'' bytes in Python 3; Unicode strings will be regular strings in +Python 3, and u'' unicode strings in Python 2. + +To maintain application compatibility with earlier versions of pdfrw, +PdfString.decode() is an alias for PdfString.to_unicode(). + +Encoding strings +~~~~~~~~~~~~~~~~~~ + +PdfString has three factory functions that will encode strings into +PdfString objects: + + - PdfString.from_bytes() accepts a byte string (regular string in Python 2 + or b'' bytes string in Python 3) and returns a PdfString object. + - PdfString.from_unicode() accepts a Unicode string (u'' Unicode string in + Python 2 or regular string in Python 3) and returns a PdfString object. + - PdfString.encode() examines the type of object passed, and either + calls from_bytes() or from_unicode() to do the real work. + +Unlike decoding(), encoding is not (mathematically) a function. +There are (literally) an infinite number of ways to encode any given +source string. (Of course, most of them would be stupid, unless +the intent is some sort of denial-of-service attack.) + +So encoding strings is either simpler than decoding, or can be made to +be an open-ended science fair project (to create the best looking +encoded strings). + +There are parameters to the encoding functions that allow control over +the final encoded string, but the intention is to make the default values +produce a reasonable encoding. + +As mentioned previously, if encoding does not do what a particular +user needs, that user is free to write his own encoder, and then +simply instantiate a PdfString object by passing a string to the +default constructor, the same way that the tokenizer does it. + +However, if desirable, encoding may gradually become more capable +over time, adding the ability to generate more aesthetically pleasing +encoded strings. + +PDFDocString encoding and decoding +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To handle this encoding in a fairly standard way, this module registers +an encoder and decoder for PDFDocEncoding with the codecs module. + +""" + +import re +import codecs +import binascii +import itertools +from ..py23_diffs import convert_load, convert_store + +def find_pdfdocencoding(encoding): + """ This function conforms to the codec module registration + protocol. It defers calculating data structures until + a pdfdocencoding encode or decode is required. + + PDFDocEncoding is described in the PDF 1.7 reference manual. + """ + + if encoding != 'pdfdocencoding': + return + + # Create the decoding map based on the table in section D.2 of the + # PDF 1.7 manual + + # Start off with the characters with 1:1 correspondence + decoding_map = set(range(0x20, 0x7F)) | set(range(0xA1, 0x100)) + decoding_map.update((0x09, 0x0A, 0x0D)) + decoding_map.remove(0xAD) + decoding_map = dict((x, x) for x in decoding_map) + + # Add in the special Unicode characters + decoding_map.update(zip(range(0x18, 0x20), ( + 0x02D8, 0x02C7, 0x02C6, 0x02D9, 0x02DD, 0x02DB, 0x02DA, 0x02DC))) + decoding_map.update(zip(range(0x80, 0x9F), ( + 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x0192, 0x2044, + 0x2039, 0x203A, 0x2212, 0x2030, 0x201E, 0x201C, 0x201D, 0x2018, + 0x2019, 0x201A, 0x2122, 0xFB01, 0xFB02, 0x0141, 0x0152, 0x0160, + 0x0178, 0x017D, 0x0131, 0x0142, 0x0153, 0x0161, 0x017E))) + decoding_map[0xA0] = 0x20AC + + # Make the encoding map from the decoding map + encoding_map = codecs.make_encoding_map(decoding_map) + + # Not every PDF producer follows the spec, so conform to Postel's law + # and interpret encoded strings if at all possible. In particular, they + # might have nulls and form-feeds, judging by random code snippets + # floating around the internet. + decoding_map.update(((x, x) for x in range(0x18))) + + def encode(input, errors='strict'): + return codecs.charmap_encode(input, errors, encoding_map) + + def decode(input, errors='strict'): + return codecs.charmap_decode(input, errors, decoding_map) + + return codecs.CodecInfo(encode, decode, name='pdfdocencoding') + +codecs.register(find_pdfdocencoding) + +class PdfString(str): + """ A PdfString is an encoded string. It has a decode + method to get the actual string data out, and there + is an encode class method to create such a string. + Like any PDF object, it could be indirect, but it + defaults to being a direct object. + """ + indirect = False + + + # The byte order mark, and unicode that could be + # wrongly encoded into the byte order mark by the + # pdfdocencoding codec. + + bytes_bom = codecs.BOM_UTF16_BE + bad_pdfdoc_prefix = bytes_bom.decode('latin-1') + + # Used by decode_literal; filled in on first use + + unescape_dict = None + unescape_func = None + + @classmethod + def init_unescapes(cls): + """ Sets up the unescape attributes for decode_literal + """ + unescape_pattern = r'\\([0-7]{1,3}|\r\n|.)' + unescape_func = re.compile(unescape_pattern, re.DOTALL).split + cls.unescape_func = unescape_func + + unescape_dict = dict(((chr(x), chr(x)) for x in range(0x100))) + unescape_dict.update(zip('nrtbf', '\n\r\t\b\f')) + unescape_dict['\r'] = '' + unescape_dict['\n'] = '' + unescape_dict['\r\n'] = '' + for i in range(0o10): + unescape_dict['%01o' % i] = chr(i) + for i in range(0o100): + unescape_dict['%02o' % i] = chr(i) + for i in range(0o400): + unescape_dict['%03o' % i] = chr(i) + cls.unescape_dict = unescape_dict + return unescape_func + + def decode_literal(self): + """ Decode a PDF literal string, which is enclosed in parentheses () + + Many pdfrw users never decode strings, so defer creating + data structures to do so until the first string is decoded. + + Possible string escapes from the spec: + (PDF 1.7 Reference, section 3.2.3, page 53) + + 1. \[nrtbf\()]: simple escapes + 2. \\d{1,3}: octal. Must be zero-padded to 3 digits + if followed by digit + 3. \<end of line>: line continuation. We don't know the EOL + marker used in the PDF, so accept \r, \n, and \r\n. + 4. Any other character following \ escape -- the backslash + is swallowed. + """ + result = (self.unescape_func or self.init_unescapes())(self[1:-1]) + if len(result) == 1: + return convert_store(result[0]) + unescape_dict = self.unescape_dict + result[1::2] = [unescape_dict[x] for x in result[1::2]] + return convert_store(''.join(result)) + + + def decode_hex(self): + """ Decode a PDF hexadecimal-encoded string, which is enclosed + in angle brackets <>. + """ + hexstr = convert_store(''.join(self[1:-1].split())) + if len(hexstr) % 1: # odd number of chars indicates a truncated 0 + hexstr += '0' + return binascii.unhexlify(hexstr) + + + def to_bytes(self): + """ Decode a PDF string to bytes. This is a convenience function + for user code, in that (as of pdfrw 0.3) it is never + actually used inside pdfrw. + """ + if self.startswith('(') and self.endswith(')'): + return self.decode_literal() + + elif self.startswith('<') and self.endswith('>'): + return self.decode_hex() + + else: + raise ValueError('Invalid PDF string "%s"' % repr(self)) + + def to_unicode(self): + """ Decode a PDF string to a unicode string. This is a + convenience function for user code, in that (as of + pdfrw 0.3) it is never actually used inside pdfrw. + + There are two Unicode storage methods used -- either + UTF16_BE, or something called PDFDocEncoding, which + is defined in the PDF spec. The determination of + which decoding method to use is done by examining the + first two bytes for the byte order marker. + """ + raw = self.to_bytes() + + if raw[:2] == self.bytes_bom: + return raw[2:].decode('utf-16-be') + else: + return raw.decode('pdfdocencoding') + + # Legacy-compatible interface + decode = to_unicode + + # Internal value used by encoding + + escape_splitter = None # Calculated on first use + + @classmethod + def init_escapes(cls): + """ Initialize the escape_splitter for the encode method + """ + cls.escape_splitter = re.compile(br'(\(|\\|\))').split + return cls.escape_splitter + + @classmethod + def from_bytes(cls, raw, bytes_encoding='auto'): + """ The from_bytes() constructor is called to encode a source raw + byte string into a PdfString that is suitable for inclusion + in a PDF. + + NOTE: There is no magic in the encoding process. A user + can certainly do his own encoding, and simply initialize a + PdfString() instance with his encoded string. That may be + useful, for example, to add line breaks to make it easier + to load PDFs into editors, or to not bother to escape balanced + parentheses, or to escape additional characters to make a PDF + more readable in a file editor. Those are features not + currently supported by this method. + + from_bytes() can use a heuristic to figure out the best + encoding for the string, or the user can control the process + by changing the bytes_encoding parameter to 'literal' or 'hex' + to force a particular conversion method. + """ + + # If hexadecimal is not being forced, then figure out how long + # the escaped literal string will be, and fall back to hex if + # it is too long. + + force_hex = bytes_encoding == 'hex' + if not force_hex: + if bytes_encoding not in ('literal', 'auto'): + raise ValueError('Invalid bytes_encoding value: %s' + % bytes_encoding) + splitlist = (cls.escape_splitter or cls.init_escapes())(raw) + if bytes_encoding == 'auto' and len(splitlist) // 2 >= len(raw): + force_hex = True + + if force_hex: + # The spec does not mandate uppercase, + # but it seems to be the convention. + fmt = '<%s>' + result = binascii.hexlify(raw).upper() + else: + fmt = '(%s)' + splitlist[1::2] = [(b'\\' + x) for x in splitlist[1::2]] + result = b''.join(splitlist) + + return cls(fmt % convert_load(result)) + + @classmethod + def from_unicode(cls, source, text_encoding='auto', + bytes_encoding='auto'): + """ The from_unicode() constructor is called to encode a source + string into a PdfString that is suitable for inclusion in a PDF. + + NOTE: There is no magic in the encoding process. A user + can certainly do his own encoding, and simply initialize a + PdfString() instance with his encoded string. That may be + useful, for example, to add line breaks to make it easier + to load PDFs into editors, or to not bother to escape balanced + parentheses, or to escape additional characters to make a PDF + more readable in a file editor. Those are features not + supported by this method. + + from_unicode() can use a heuristic to figure out the best + encoding for the string, or the user can control the process + by changing the text_encoding parameter to 'pdfdocencoding' + or 'utf16', and/or by changing the bytes_encoding parameter + to 'literal' or 'hex' to force particular conversion methods. + + The function will raise an exception if it cannot perform + the conversion as requested by the user. + """ + + # Give preference to pdfdocencoding, since it only + # requires one raw byte per character, rather than two. + if text_encoding != 'utf16': + force_pdfdoc = text_encoding == 'pdfdocencoding' + if text_encoding != 'auto' and not force_pdfdoc: + raise ValueError('Invalid text_encoding value: %s' + % text_encoding) + + if source.startswith(cls.bad_pdfdoc_prefix): + if force_pdfdoc: + raise UnicodeError('Prefix of string %r cannot be encoded ' + 'in pdfdocencoding' % source[:20]) + else: + try: + raw = source.encode('pdfdocencoding') + except UnicodeError: + if force_pdfdoc: + raise + else: + return cls.from_bytes(raw, bytes_encoding) + + # If the user is not forcing literal strings, + # it makes much more sense to use hexadecimal with 2-byte chars + raw = cls.bytes_bom + source.encode('utf-16-be') + encoding = 'hex' if bytes_encoding == 'auto' else bytes_encoding + return cls.from_bytes(raw, encoding) + + @classmethod + def encode(cls, source, uni_type = type(u''), isinstance=isinstance): + """ The encode() constructor is a legacy function that is + also a convenience for the PdfWriter. + """ + if isinstance(source, uni_type): + return cls.from_unicode(source) + else: + return cls.from_bytes(source) diff --git a/plugin.program.AML/pdfrw/pdfrw/pagemerge.py b/plugin.program.AML/pdfrw/pdfrw/pagemerge.py new file mode 100644 index 0000000000..455511077a --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/pagemerge.py @@ -0,0 +1,250 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' +This module contains code to edit pages. Sort of a canvas, I +suppose, but I wouldn't want to call it that and get people all +excited or anything. + +No, this is just for doing basic things like merging/splitting +apart pages, watermarking, etc. All it does is allow converting +pages (or parts of pages) into Form XObject rectangles, and then +plopping those down on new or pre-existing pages. +''' + +from .objects import PdfDict, PdfArray, PdfName +from .buildxobj import pagexobj, ViewInfo + +NullInfo = ViewInfo() + + +class RectXObj(PdfDict): + ''' This class facilitates doing positioning (moving and scaling) + of Form XObjects within their containing page, by modifying + the Form XObject's transformation matrix. + + By default, this class keeps the aspect ratio locked. For + example, if your object is foo, you can write 'foo.w = 200', + and it will scale in both the x and y directions. + + To unlock the aspect ration, you have to do a tiny bit of math + and call the scale function. + ''' + def __init__(self, page, viewinfo=NullInfo, **kw): + ''' The page is a page returned by PdfReader. It will be + turned into a cached Form XObject (so that multiple + rectangles can be extracted from it if desired), and then + another Form XObject will be built using it and the viewinfo + (which should be a ViewInfo class). The viewinfo includes + source coordinates (from the top/left) and rotation information. + + Once the object has been built, its destination coordinates + may be examined and manipulated by using x, y, w, h, and + scale. The destination coordinates are in the normal + PDF programmatic system (starting at bottom left). + ''' + if kw: + if viewinfo is not NullInfo: + raise ValueError("Cannot modify preexisting ViewInfo") + viewinfo = ViewInfo(**kw) + viewinfo.cacheable = False + base = pagexobj(page, viewinfo) + self.update(base) + self.indirect = True + self.stream = base.stream + private = self.private + private._rect = [base.x, base.y, base.w, base.h] + matrix = self.Matrix + if matrix is None: + matrix = self.Matrix = PdfArray((1, 0, 0, 1, 0, 0)) + private._matrix = matrix # Lookup optimization + # Default to lower-left corner + self.x = 0 + self.y = 0 + + @property + def x(self): + ''' X location (from left) of object in points + ''' + return self._rect[0] + + @property + def y(self): + ''' Y location (from bottom) of object in points + ''' + return self._rect[1] + + @property + def w(self): + ''' Width of object in points + ''' + return self._rect[2] + + @property + def h(self): + ''' Height of object in points + ''' + return self._rect[3] + + def __setattr__(self, name, value, next=PdfDict.__setattr__, + mine=set('x y w h'.split())): + ''' The underlying __setitem__ won't let us use a property + setter, so we have to fake one. + ''' + if name not in mine: + return next(self, name, value) + if name in 'xy': + r_index, m_index = (0, 4) if name == 'x' else (1, 5) + self._rect[r_index], old = value, self._rect[r_index] + self._matrix[m_index] += value - old + else: + index = 2 + (value == 'h') + self.scale(value / self._rect[index]) + + def scale(self, x_scale, y_scale=None): + ''' Current scaling deals properly with things that + have been rotated in 90 degree increments + (via the ViewMerge object given when instantiating). + ''' + if y_scale is None: + y_scale = x_scale + x, y, w, h = rect = self._rect + ao, bo, co, do, eo, fo = matrix = self._matrix + an = ao * x_scale + bn = bo * y_scale + cn = co * x_scale + dn = do * y_scale + en = x + (eo - x) * 1.0 * (an + cn) / (ao + co) + fn = y + (fo - y) * 1.0 * (bn + dn) / (bo + do) + matrix[:] = an, bn, cn, dn, en, fn + rect[:] = x, y, w * x_scale, h * y_scale + + @property + def box(self): + ''' Return the bounding box for the object + ''' + x, y, w, h = self._rect + return PdfArray([x, y, x + w, y + h]) + + +class PageMerge(list): + ''' A PageMerge object can have 0 or 1 underlying pages + (that get edited with the results of the merge) + and 0-n RectXObjs that can be applied before or + after the underlying page. + ''' + page = None + mbox = None + cbox = None + resources = None + rotate = None + contents = None + + def __init__(self, page=None): + if page is not None: + self.setpage(page) + + def setpage(self, page): + if page.Type != PdfName.Page: + raise TypeError("Expected page") + self.append(None) # Placeholder + self.page = page + inheritable = page.inheritable + self.mbox = inheritable.MediaBox + self.cbox = inheritable.CropBox + self.resources = inheritable.Resources + self.rotate = inheritable.Rotate + self.contents = page.Contents + + def __add__(self, other): + if isinstance(other, dict): + other = [other] + for other in other: + self.add(other) + return self + + def add(self, obj, prepend=False, **kw): + if kw: + obj = RectXObj(obj, **kw) + elif obj.Type == PdfName.Page: + obj = RectXObj(obj) + if prepend: + self.insert(0, obj) + else: + self.append(obj) + return self + + def render(self): + def do_xobjs(xobj_list, restore_first=False): + content = ['Q'] if restore_first else [] + for obj in xobj_list: + index = PdfName('pdfrw_%d' % (key_offset + len(xobjs))) + if xobjs.setdefault(index, obj) is not obj: + raise KeyError("XObj key %s already in use" % index) + content.append('%s Do' % index) + return PdfDict(indirect=True, stream='\n'.join(content)) + + mbox = self.mbox + cbox = self.cbox + page = self.page + old_contents = self.contents + resources = self.resources or PdfDict() + + key_offset = 0 + xobjs = resources.XObject + if xobjs is None: + xobjs = resources.XObject = PdfDict() + else: + allkeys = xobjs.keys() + if allkeys: + keys = (x for x in allkeys if x.startswith('/pdfrw_')) + keys = (x for x in keys if x[7:].isdigit()) + keys = sorted(keys, key=lambda x: int(x[7:])) + key_offset = (int(keys[-1][7:]) + 1) if keys else 0 + key_offset -= len(allkeys) + + if old_contents is None: + new_contents = do_xobjs(self) + else: + isdict = isinstance(old_contents, PdfDict) + old_contents = [old_contents] if isdict else old_contents + new_contents = PdfArray() + index = self.index(None) + if index: + new_contents.append(do_xobjs(self[:index])) + + index += 1 + if index < len(self): + # There are elements to add after the original page contents, + # so push the graphics state to the stack. Restored below. + new_contents.append(PdfDict(indirect=True, stream='q')) + + new_contents.extend(old_contents) + + if index < len(self): + # Restore graphics state and add other elements. + new_contents.append(do_xobjs(self[index:], restore_first=True)) + + if mbox is None: + cbox = None + mbox = self.xobj_box + mbox[0] = min(0, mbox[0]) + mbox[1] = min(0, mbox[1]) + + page = PdfDict(indirect=True) if page is None else page + page.Type = PdfName.Page + page.Resources = resources + page.MediaBox = mbox + page.CropBox = cbox + page.Rotate = self.rotate + page.Contents = new_contents + return page + + @property + def xobj_box(self): + ''' Return the smallest box that encloses every object + in the list. + ''' + a, b, c, d = zip(*(xobj.box for xobj in self)) + return PdfArray((min(a), min(b), max(c), max(d))) diff --git a/plugin.program.AML/pdfrw/pdfrw/pdfreader.py b/plugin.program.AML/pdfrw/pdfrw/pdfreader.py new file mode 100644 index 0000000000..c2ae030192 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/pdfreader.py @@ -0,0 +1,691 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# Copyright (C) 2012-2015 Nerijus Mika +# MIT license -- See LICENSE.txt for details + +''' +The PdfReader class reads an entire PDF file into memory and +parses the top-level container objects. (It does not parse +into streams.) The object subclasses PdfDict, and the +document pages are stored in a list in the pages attribute +of the object. +''' +import gc +import binascii +import collections +import itertools + +from .errors import PdfParseError, log +from .tokens import PdfTokens +from .objects import PdfDict, PdfArray, PdfName, PdfObject, PdfIndirect +from .uncompress import uncompress +from . import crypt +from .py23_diffs import convert_load, convert_store, iteritems + + +class PdfReader(PdfDict): + + def findindirect(self, objnum, gennum, PdfIndirect=PdfIndirect, int=int): + ''' Return a previously loaded indirect object, or create + a placeholder for it. + ''' + key = int(objnum), int(gennum) + result = self.indirect_objects.get(key) + if result is None: + self.indirect_objects[key] = result = PdfIndirect(key) + self.deferred_objects.add(key) + result._loader = self.loadindirect + return result + + def readarray(self, source, PdfArray=PdfArray): + ''' Found a [ token. Parse the tokens after that. + ''' + specialget = self.special.get + result = [] + pop = result.pop + append = result.append + + for value in source: + if value in ']R': + if value == ']': + break + generation = pop() + value = self.findindirect(pop(), generation) + else: + func = specialget(value) + if func is not None: + value = func(source) + append(value) + return PdfArray(result) + + def readdict(self, source, PdfDict=PdfDict): + ''' Found a << token. Parse the tokens after that. + ''' + specialget = self.special.get + result = PdfDict() + next = source.next + + tok = next() + while tok != '>>': + if not tok.startswith('/'): + source.error('Expected PDF /name object') + tok = next() + continue + key = tok + value = next() + func = specialget(value) + if func is not None: + value = func(source) + tok = next() + else: + tok = next() + if value.isdigit() and tok.isdigit(): + tok2 = next() + if tok2 != 'R': + source.error('Expected "R" following two integers') + tok = tok2 + continue + value = self.findindirect(value, tok) + tok = next() + result[key] = value + return result + + def empty_obj(self, source, PdfObject=PdfObject): + ''' Some silly git put an empty object in the + file. Back up so the caller sees the endobj. + ''' + source.floc = source.tokstart + + def badtoken(self, source): + ''' Didn't see that coming. + ''' + source.exception('Unexpected delimiter') + + def findstream(self, obj, tok, source, len=len): + ''' Figure out if there is a content stream + following an object, and return the start + pointer to the content stream if so. + + (We can't read it yet, because we might not + know how long it is, because Length might + be an indirect object.) + ''' + + fdata = source.fdata + startstream = source.tokstart + len(tok) + gotcr = fdata[startstream] == '\r' + startstream += gotcr + gotlf = fdata[startstream] == '\n' + startstream += gotlf + if not gotlf: + if not gotcr: + source.error(r'stream keyword not followed by \n') + else: + source.warning(r"stream keyword terminated " + r"by \r without \n") + return startstream + + def readstream(self, obj, startstream, source, exact_required=False, + streamending='endstream endobj'.split(), int=int): + fdata = source.fdata + length = int(obj.Length) + source.floc = target_endstream = startstream + length + endit = source.multiple(2) + obj._stream = fdata[startstream:target_endstream] + if endit == streamending: + return + + if exact_required: + source.exception('Expected endstream endobj') + + # The length attribute does not match the distance between the + # stream and endstream keywords. + + # TODO: Extract maxstream from dictionary of object offsets + # and use rfind instead of find. + maxstream = len(fdata) - 20 + endstream = fdata.find('endstream', startstream, maxstream) + source.floc = startstream + room = endstream - startstream + if endstream < 0: + source.error('Could not find endstream') + return + if (length == room + 1 and + fdata[startstream - 2:startstream] == '\r\n'): + source.warning(r"stream keyword terminated by \r without \n") + obj._stream = fdata[startstream - 1:target_endstream - 1] + return + source.floc = endstream + if length > room: + source.error('stream /Length attribute (%d) appears to ' + 'be too big (size %d) -- adjusting', + length, room) + obj.stream = fdata[startstream:endstream] + return + if fdata[target_endstream:endstream].rstrip(): + source.error('stream /Length attribute (%d) appears to ' + 'be too small (size %d) -- adjusting', + length, room) + obj.stream = fdata[startstream:endstream] + return + endobj = fdata.find('endobj', endstream, maxstream) + if endobj < 0: + source.error('Could not find endobj after endstream') + return + if fdata[endstream:endobj].rstrip() != 'endstream': + source.error('Unexpected data between endstream and endobj') + return + source.error('Illegal endstream/endobj combination') + + def loadindirect(self, key, PdfDict=PdfDict, + isinstance=isinstance): + result = self.indirect_objects.get(key) + if not isinstance(result, PdfIndirect): + return result + source = self.source + offset = int(self.source.obj_offsets.get(key, '0')) + if not offset: + source.warning("Did not find PDF object %s", key) + return None + + # Read the object header and validate it + objnum, gennum = key + source.floc = offset + objid = source.multiple(3) + ok = len(objid) == 3 + ok = ok and objid[0].isdigit() and int(objid[0]) == objnum + ok = ok and objid[1].isdigit() and int(objid[1]) == gennum + ok = ok and objid[2] == 'obj' + if not ok: + source.floc = offset + source.next() + objheader = '%d %d obj' % (objnum, gennum) + fdata = source.fdata + offset2 = (fdata.find('\n' + objheader) + 1 or + fdata.find('\r' + objheader) + 1) + if (not offset2 or + fdata.find(fdata[offset2 - 1] + objheader, offset2) > 0): + source.warning("Expected indirect object '%s'", objheader) + return None + source.warning("Indirect object %s found at incorrect " + "offset %d (expected offset %d)", + objheader, offset2, offset) + source.floc = offset2 + len(objheader) + + # Read the object, and call special code if it starts + # an array or dictionary + obj = source.next() + func = self.special.get(obj) + if func is not None: + obj = func(source) + + self.indirect_objects[key] = obj + self.deferred_objects.remove(key) + + # Mark the object as indirect, and + # just return it if it is a simple object. + obj.indirect = key + tok = source.next() + if tok == 'endobj': + return obj + + # Should be a stream. Either that or it's broken. + isdict = isinstance(obj, PdfDict) + if isdict and tok == 'stream': + self.readstream(obj, self.findstream(obj, tok, source), source) + return obj + + # Houston, we have a problem, but let's see if it + # is easily fixable. Leaving out a space before endobj + # is apparently an easy mistake to make on generation + # (Because it won't be noticed unless you are specifically + # generating an indirect object that doesn't end with any + # sort of delimiter.) It is so common that things like + # okular just handle it. + + if isinstance(obj, PdfObject) and obj.endswith('endobj'): + source.error('No space or delimiter before endobj') + obj = PdfObject(obj[:-6]) + else: + source.error("Expected 'endobj'%s token", + isdict and " or 'stream'" or '') + obj = PdfObject('') + + obj.indirect = key + self.indirect_objects[key] = obj + return obj + + def read_all(self): + deferred = self.deferred_objects + prev = set() + while 1: + new = deferred - prev + if not new: + break + prev |= deferred + for key in new: + self.loadindirect(key) + + def decrypt_all(self): + self.read_all() + + if self.crypt_filters is not None: + crypt.decrypt_objects( + self.indirect_objects.values(), self.stream_crypt_filter, + self.crypt_filters) + + def uncompress(self): + self.read_all() + + uncompress(self.indirect_objects.values()) + + def load_stream_objects(self, object_streams): + # read object streams + objs = [] + for num in object_streams: + obj = self.findindirect(num, 0).real_value() + assert obj.Type == '/ObjStm' + objs.append(obj) + + # read objects from stream + if objs: + # Decrypt + if self.crypt_filters is not None: + crypt.decrypt_objects( + objs, self.stream_crypt_filter, self.crypt_filters) + + # Decompress + uncompress(objs) + + for obj in objs: + objsource = PdfTokens(obj.stream, 0, False) + next = objsource.next + offsets = [] + firstoffset = int(obj.First) + while objsource.floc < firstoffset: + offsets.append((int(next()), firstoffset + int(next()))) + for num, offset in offsets: + # Read the object, and call special code if it starts + # an array or dictionary + objsource.floc = offset + sobj = next() + func = self.special.get(sobj) + if func is not None: + sobj = func(objsource) + + key = (num, 0) + self.indirect_objects[key] = sobj + if key in self.deferred_objects: + self.deferred_objects.remove(key) + + # Mark the object as indirect, and + # add it to the list of streams if it starts a stream + sobj.indirect = key + + def findxref(self, fdata): + ''' Find the cross reference section at the end of a file + ''' + startloc = fdata.rfind('startxref') + if startloc < 0: + raise PdfParseError('Did not find "startxref" at end of file') + source = PdfTokens(fdata, startloc, False, self.verbose) + tok = source.next() + assert tok == 'startxref' # (We just checked this...) + tableloc = source.next_default() + if not tableloc.isdigit(): + source.exception('Expected table location') + if source.next_default().rstrip().lstrip('%') != 'EOF': + source.exception('Expected %%EOF') + return startloc, PdfTokens(fdata, int(tableloc), True, self.verbose) + + def parse_xref_stream(self, source, int=int, range=range, + enumerate=enumerate, islice=itertools.islice, + defaultdict=collections.defaultdict, + hexlify=binascii.hexlify): + ''' Parse (one of) the cross-reference file section(s) + ''' + + def readint(s, lengths): + offset = 0 + for length in itertools.cycle(lengths): + next = offset + length + yield int(hexlify(s[offset:next]), 16) if length else None + offset = next + + setdefault = source.obj_offsets.setdefault + next = source.next + # check for xref stream object + objid = source.multiple(3) + ok = len(objid) == 3 + ok = ok and objid[0].isdigit() + ok = ok and objid[1] == 'obj' + ok = ok and objid[2] == '<<' + if not ok: + source.exception('Expected xref stream start') + obj = self.readdict(source) + if obj.Type != PdfName.XRef: + source.exception('Expected dict type of /XRef') + tok = next() + self.readstream(obj, self.findstream(obj, tok, source), source, True) + old_strm = obj.stream + if not uncompress([obj], True): + source.exception('Could not decompress Xref stream') + stream = obj.stream + # Fix for issue #76 -- goofy compressed xref stream + # that is NOT ACTUALLY COMPRESSED + stream = stream if stream is not old_strm else convert_store(old_strm) + num_pairs = obj.Index or PdfArray(['0', obj.Size]) + num_pairs = [int(x) for x in num_pairs] + num_pairs = zip(num_pairs[0::2], num_pairs[1::2]) + entry_sizes = [int(x) for x in obj.W] + if len(entry_sizes) != 3: + source.exception('Invalid entry size') + object_streams = defaultdict(list) + get = readint(stream, entry_sizes) + for objnum, size in num_pairs: + for cnt in range(size): + xtype, p1, p2 = islice(get, 3) + if xtype in (1, None): + if p1: + setdefault((objnum, p2 or 0), p1) + elif xtype == 2: + object_streams[p1].append((objnum, p2)) + objnum += 1 + + obj.private.object_streams = object_streams + return obj + + def parse_xref_table(self, source, int=int, range=range): + ''' Parse (one of) the cross-reference file section(s) + ''' + setdefault = source.obj_offsets.setdefault + next = source.next + # plain xref table + start = source.floc + try: + while 1: + tok = next() + if tok == 'trailer': + return + startobj = int(tok) + for objnum in range(startobj, startobj + int(next())): + offset = int(next()) + generation = int(next()) + inuse = next() + if inuse == 'n': + if offset != 0: + setdefault((objnum, generation), offset) + elif inuse != 'f': + raise ValueError + except: + pass + try: + # Table formatted incorrectly. + # See if we can figure it out anyway. + end = source.fdata.rindex('trailer', start) + table = source.fdata[start:end].splitlines() + for line in table: + tokens = line.split() + if len(tokens) == 2: + objnum = int(tokens[0]) + elif len(tokens) == 3: + offset, generation, inuse = (int(tokens[0]), + int(tokens[1]), tokens[2]) + if offset != 0 and inuse == 'n': + setdefault((objnum, generation), offset) + objnum += 1 + elif tokens: + log.error('Invalid line in xref table: %s' % + repr(line)) + raise ValueError + log.warning('Badly formatted xref table') + source.floc = end + next() + except: + source.floc = start + source.exception('Invalid table format') + + def parsexref(self, source): + ''' Parse (one of) the cross-reference file section(s) + ''' + next = source.next + try: + tok = next() + except StopIteration: + tok = '' + if tok.isdigit(): + return self.parse_xref_stream(source), True + elif tok == 'xref': + self.parse_xref_table(source) + tok = next() + if tok != '<<': + source.exception('Expected "<<" starting catalog') + return self.readdict(source), False + else: + source.exception('Expected "xref" keyword or xref stream object') + + def readpages(self, node): + pagename = PdfName.Page + pagesname = PdfName.Pages + catalogname = PdfName.Catalog + typename = PdfName.Type + kidname = PdfName.Kids + + try: + result = [] + stack = [node] + append = result.append + pop = stack.pop + while stack: + node = pop() + nodetype = node[typename] + if nodetype == pagename: + append(node) + elif nodetype == pagesname: + stack.extend(reversed(node[kidname])) + elif nodetype == catalogname: + stack.append(node[pagesname]) + else: + log.error('Expected /Page or /Pages dictionary, got %s' % + repr(node)) + return result + except (AttributeError, TypeError) as s: + log.error('Invalid page tree: %s' % s) + return [] + + def _parse_encrypt_info(self, source, password, trailer): + """Check password and initialize crypt filters.""" + # Create and check password key + key = crypt.create_key(password, trailer) + + if not crypt.check_user_password(key, trailer): + source.warning('User password does not validate') + + # Create default crypt filters + private = self.private + crypt_filters = self.crypt_filters + version = int(trailer.Encrypt.V or 0) + if version in (1, 2): + crypt_filter = crypt.RC4CryptFilter(key) + private.stream_crypt_filter = crypt_filter + private.string_crypt_filter = crypt_filter + elif version == 4: + if PdfName.CF in trailer.Encrypt: + for name, params in iteritems(trailer.Encrypt.CF): + if name == PdfName.Identity: + continue + + cfm = params.CFM + if cfm == PdfName.AESV2: + crypt_filters[name] = crypt.AESCryptFilter(key) + elif cfm == PdfName.V2: + crypt_filters[name] = crypt.RC4CryptFilter(key) + else: + source.warning( + 'Unsupported crypt filter: {}, {}'.format( + name, cfm)) + + # Read default stream filter + if PdfName.StmF in trailer.Encrypt: + name = trailer.Encrypt.StmF + if name in crypt_filters: + private.stream_crypt_filter = crypt_filters[name] + else: + source.warning( + 'Invalid crypt filter name in /StmF:' + ' {}'.format(name)) + + # Read default string filter + if PdfName.StrF in trailer.Encrypt: + name = trailer.Encrypt.StrF + if name in crypt_filters: + private.string_crypt_filter = crypt_filters[name] + else: + source.warning( + 'Invalid crypt filter name in /StrF:' + ' {}'.format(name)) + else: + source.warning( + 'Unsupported Encrypt version: {}'.format(version)) + + def __init__(self, fname=None, fdata=None, decompress=False, + decrypt=False, password='', disable_gc=True, verbose=True): + self.private.verbose = verbose + + # Runs a lot faster with GC off. + disable_gc = disable_gc and gc.isenabled() + if disable_gc: + gc.disable() + + try: + if fname is not None: + assert fdata is None + # Allow reading preexisting streams like pyPdf + if hasattr(fname, 'read'): + fdata = fname.read() + else: + try: + f = open(fname, 'rb') + fdata = f.read() + f.close() + except IOError: + raise PdfParseError('Could not read PDF file %s' % + fname) + + assert fdata is not None + fdata = convert_load(fdata) + + if not fdata.startswith('%PDF-'): + startloc = fdata.find('%PDF-') + if startloc >= 0: + log.warning('PDF header not at beginning of file') + else: + lines = fdata.lstrip().splitlines() + if not lines: + raise PdfParseError('Empty PDF file!') + raise PdfParseError('Invalid PDF header: %s' % + repr(lines[0])) + + self.private.version = fdata[5:8] + + endloc = fdata.rfind('%EOF') + if endloc < 0: + raise PdfParseError('EOF mark not found: %s' % + repr(fdata[-20:])) + endloc += 6 + junk = fdata[endloc:] + fdata = fdata[:endloc] + if junk.rstrip('\00').strip(): + log.warning('Extra data at end of file') + + private = self.private + private.indirect_objects = {} + private.deferred_objects = set() + private.special = {'<<': self.readdict, + '[': self.readarray, + 'endobj': self.empty_obj, + } + for tok in r'\ ( ) < > { } ] >> %'.split(): + self.special[tok] = self.badtoken + + startloc, source = self.findxref(fdata) + private.source = source + + # Find all the xref tables/streams, and + # then deal with them backwards. + xref_list = [] + while 1: + source.obj_offsets = {} + trailer, is_stream = self.parsexref(source) + prev = trailer.Prev + if prev is None: + token = source.next() + if token != 'startxref' and not xref_list: + source.warning('Expected "startxref" ' + 'at end of xref table') + break + xref_list.append((source.obj_offsets, trailer, is_stream)) + source.floc = int(prev) + + # Handle document encryption + private.crypt_filters = None + if decrypt and PdfName.Encrypt in trailer: + identity_filter = crypt.IdentityCryptFilter() + crypt_filters = { + PdfName.Identity: identity_filter + } + private.crypt_filters = crypt_filters + private.stream_crypt_filter = identity_filter + private.string_crypt_filter = identity_filter + + if not crypt.HAS_CRYPTO: + raise PdfParseError( + 'Install PyCrypto to enable encryption support') + + self._parse_encrypt_info(source, password, trailer) + + if is_stream: + self.load_stream_objects(trailer.object_streams) + + while xref_list: + later_offsets, later_trailer, is_stream = xref_list.pop() + source.obj_offsets.update(later_offsets) + if is_stream: + trailer.update(later_trailer) + self.load_stream_objects(later_trailer.object_streams) + else: + trailer = later_trailer + + trailer.Prev = None + + if (trailer.Version and + float(trailer.Version) > float(self.version)): + self.private.version = trailer.Version + + if decrypt: + self.decrypt_all() + trailer.Encrypt = None + + if is_stream: + self.Root = trailer.Root + self.Info = trailer.Info + self.ID = trailer.ID + self.Size = trailer.Size + self.Encrypt = trailer.Encrypt + else: + self.update(trailer) + + # self.read_all_indirect(source) + private.pages = self.readpages(self.Root) + if decompress: + self.uncompress() + + # For compatibility with pyPdf + private.numPages = len(self.pages) + finally: + if disable_gc: + gc.enable() + + # For compatibility with pyPdf + def getPage(self, pagenum): + return self.pages[pagenum] diff --git a/plugin.program.AML/pdfrw/pdfrw/pdfwriter.py b/plugin.program.AML/pdfrw/pdfrw/pdfwriter.py new file mode 100644 index 0000000000..3c887baa3f --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/pdfwriter.py @@ -0,0 +1,385 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' +The PdfWriter class writes an entire PDF file out to disk. + +The writing process is not at all optimized or organized. + +An instance of the PdfWriter class has two methods: + addpage(page) +and + write(fname) + +addpage() assumes that the pages are part of a valid +tree/forest of PDF objects. +''' +import gc + +from .objects import (PdfName, PdfArray, PdfDict, IndirectPdfDict, + PdfObject, PdfString) +from .compress import compress as do_compress +from .errors import PdfOutputError, log +from .py23_diffs import iteritems, convert_store + +NullObject = PdfObject('null') +NullObject.indirect = True +NullObject.Type = 'Null object' + + +def user_fmt(obj, isinstance=isinstance, float=float, str=str, + basestring=(type(u''), type(b'')), encode=PdfString.encode): + ''' This function may be replaced by the user for + specialized formatting requirements. + ''' + + if isinstance(obj, basestring): + return encode(obj) + + # PDFs don't handle exponent notation + if isinstance(obj, float): + return ('%.9f' % obj).rstrip('0').rstrip('.') + + return str(obj) + + +def FormatObjects(f, trailer, version='1.3', compress=True, killobj=(), + user_fmt=user_fmt, do_compress=do_compress, + convert_store=convert_store, iteritems=iteritems, + id=id, isinstance=isinstance, getattr=getattr, len=len, + sum=sum, set=set, str=str, hasattr=hasattr, repr=repr, + enumerate=enumerate, list=list, dict=dict, tuple=tuple, + PdfArray=PdfArray, PdfDict=PdfDict, PdfObject=PdfObject): + ''' FormatObjects performs the actual formatting and disk write. + Should be a class, was a class, turned into nested functions + for performace (to reduce attribute lookups). + ''' + + def f_write(s): + f.write(convert_store(s)) + + def add(obj): + ''' Add an object to our list, if it's an indirect + object. Just format it if not. + ''' + # Can't hash dicts, so just hash the object ID + objid = id(obj) + + # Automatically set stream objects to indirect + if isinstance(obj, PdfDict): + indirect = obj.indirect or (obj.stream is not None) + else: + indirect = getattr(obj, 'indirect', False) + + if not indirect: + if objid in visited: + log.warning('Replicating direct %s object, ' + 'should be indirect for optimal file size' % + type(obj)) + obj = type(obj)(obj) + objid = id(obj) + visiting(objid) + result = format_obj(obj) + leaving(objid) + return result + + objnum = indirect_dict_get(objid) + + # If we haven't seen the object yet, we need to + # add it to the indirect object list. + if objnum is None: + swapped = swapobj(objid) + if swapped is not None: + old_id = objid + obj = swapped + objid = id(obj) + objnum = indirect_dict_get(objid) + if objnum is not None: + indirect_dict[old_id] = objnum + return '%s 0 R' % objnum + objnum = len(objlist) + 1 + objlist_append(None) + indirect_dict[objid] = objnum + deferred.append((objnum - 1, obj)) + return '%s 0 R' % objnum + + def format_array(myarray, formatter): + # Format array data into semi-readable ASCII + if sum([len(x) for x in myarray]) <= 70: + return formatter % space_join(myarray) + return format_big(myarray, formatter) + + def format_big(myarray, formatter): + bigarray = [] + count = 1000000 + for x in myarray: + lenx = len(x) + 1 + count += lenx + if count > 71: + subarray = [] + bigarray.append(subarray) + count = lenx + subarray.append(x) + return formatter % lf_join([space_join(x) for x in bigarray]) + + def format_obj(obj): + ''' format PDF object data into semi-readable ASCII. + May mutually recurse with add() -- add() will + return references for indirect objects, and add + the indirect object to the list. + ''' + while 1: + if isinstance(obj, (list, dict, tuple)): + if isinstance(obj, PdfArray): + myarray = [add(x) for x in obj] + return format_array(myarray, '[%s]') + elif isinstance(obj, PdfDict): + if compress and obj.stream: + do_compress([obj]) + pairs = sorted((getattr(x, 'encoded', None) or x, y) + for (x, y) in obj.iteritems()) + myarray = [] + for key, value in pairs: + myarray.append(key) + myarray.append(add(value)) + result = format_array(myarray, '<<%s>>') + stream = obj.stream + if stream is not None: + result = ('%s\nstream\n%s\nendstream' % + (result, stream)) + return result + obj = (PdfArray, PdfDict)[isinstance(obj, dict)](obj) + continue + + # We assume that an object with an indirect + # attribute knows how to represent itself to us. + if hasattr(obj, 'indirect'): + return str(getattr(obj, 'encoded', None) or obj) + return user_fmt(obj) + + def format_deferred(): + while deferred: + index, obj = deferred.pop() + objlist[index] = format_obj(obj) + + indirect_dict = {} + indirect_dict_get = indirect_dict.get + objlist = [] + objlist_append = objlist.append + visited = set() + visiting = visited.add + leaving = visited.remove + space_join = ' '.join + lf_join = '\n '.join + + deferred = [] + + # Don't reference old catalog or pages objects -- + # swap references to new ones. + type_remap = {PdfName.Catalog: trailer.Root, + PdfName.Pages: trailer.Root.Pages, None: trailer}.get + swapobj = [(objid, type_remap(obj.Type) if new_obj is None else new_obj) + for objid, (obj, new_obj) in iteritems(killobj)] + swapobj = dict((objid, obj is None and NullObject or obj) + for objid, obj in swapobj).get + + for objid in killobj: + assert swapobj(objid) is not None + + # The first format of trailer gets all the information, + # but we throw away the actual trailer formatting. + format_obj(trailer) + # Keep formatting until we're done. + # (Used to recurse inside format_obj for this, but + # hit system limit.) + format_deferred() + # Now we know the size, so we update the trailer dict + # and get the formatted data. + trailer.Size = PdfObject(len(objlist) + 1) + trailer = format_obj(trailer) + + # Now we have all the pieces to write out to the file. + # Keep careful track of the counts while we do it so + # we can correctly build the cross-reference. + + header = '%%PDF-%s\n%%\xe2\xe3\xcf\xd3\n' % version + f_write(header) + offset = len(header) + offsets = [(0, 65535, 'f')] + offsets_append = offsets.append + + for i, x in enumerate(objlist): + objstr = '%s 0 obj\n%s\nendobj\n' % (i + 1, x) + offsets_append((offset, 0, 'n')) + offset += len(objstr) + f_write(objstr) + + f_write('xref\n0 %s\n' % len(offsets)) + for x in offsets: + f_write('%010d %05d %s\r\n' % x) + f_write('trailer\n\n%s\nstartxref\n%s\n%%%%EOF\n' % (trailer, offset)) + + +class PdfWriter(object): + + _trailer = None + canonicalize = False + fname = None + + def __init__(self, fname=None, version='1.3', compress=False, **kwargs): + """ + Parameters: + fname -- Output file name, or file-like binary object + with a write method + version -- PDF version to target. Currently only 1.3 + supported. + compress -- True to do compression on output. Currently + compresses stream objects. + """ + + # Legacy support: fname is new, was added in front + if fname is not None: + try: + float(fname) + except (ValueError, TypeError): + pass + else: + if version != '1.3': + assert compress == False + compress = version + version = fname + fname = None + + self.fname = fname + self.version = version + self.compress = compress + + if kwargs: + for name, value in iteritems(kwargs): + if name not in self.replaceable: + raise ValueError("Cannot set attribute %s " + "on PdfWriter instance" % name) + setattr(self, name, value) + + self.pagearray = PdfArray() + self.killobj = {} + + def addpage(self, page): + self._trailer = None + if page.Type != PdfName.Page: + raise PdfOutputError('Bad /Type: Expected %s, found %s' + % (PdfName.Page, page.Type)) + inheritable = page.inheritable # searches for resources + self.pagearray.append( + IndirectPdfDict( + page, + Resources=inheritable.Resources, + MediaBox=inheritable.MediaBox, + CropBox=inheritable.CropBox, + Rotate=inheritable.Rotate, + ) + ) + + # Add parents in the hierarchy to objects we + # don't want to output + killobj = self.killobj + obj, new_obj = page, self.pagearray[-1] + while obj is not None: + objid = id(obj) + if objid in killobj: + break + killobj[objid] = obj, new_obj + obj = obj.Parent + new_obj = None + return self + + addPage = addpage # for compatibility with pyPdf + + def addpages(self, pagelist): + for page in pagelist: + self.addpage(page) + return self + + def _get_trailer(self): + trailer = self._trailer + if trailer is not None: + return trailer + + if self.canonicalize: + self.make_canonical() + + # Create the basic object structure of the PDF file + trailer = PdfDict( + Root=IndirectPdfDict( + Type=PdfName.Catalog, + Pages=IndirectPdfDict( + Type=PdfName.Pages, + Count=PdfObject(len(self.pagearray)), + Kids=self.pagearray + ) + ) + ) + # Make all the pages point back to the page dictionary and + # ensure they are indirect references + pagedict = trailer.Root.Pages + for page in pagedict.Kids: + page.Parent = pagedict + page.indirect = True + self._trailer = trailer + return trailer + + def _set_trailer(self, trailer): + self._trailer = trailer + + trailer = property(_get_trailer, _set_trailer) + + def write(self, fname=None, trailer=None, user_fmt=user_fmt, + disable_gc=True): + + trailer = trailer or self.trailer + + # Support fname for legacy applications + if (fname is not None) == (self.fname is not None): + raise PdfOutputError( + "PdfWriter fname must be specified exactly once") + + fname = fname or self.fname + + # Dump the data. We either have a filename or a preexisting + # file object. + preexisting = hasattr(fname, 'write') + f = preexisting and fname or open(fname, 'wb') + if disable_gc: + gc.disable() + + try: + FormatObjects(f, trailer, self.version, self.compress, + self.killobj, user_fmt=user_fmt) + finally: + if not preexisting: + f.close() + if disable_gc: + gc.enable() + + def make_canonical(self): + ''' Canonicalizes a PDF. Assumes everything + is a Pdf object already. + ''' + visited = set() + workitems = list(self.pagearray) + while workitems: + obj = workitems.pop() + objid = id(obj) + if objid in visited: + continue + visited.add(objid) + obj.indirect = False + if isinstance(obj, (PdfArray, PdfDict)): + obj.indirect = True + if isinstance(obj, PdfArray): + workitems += obj + else: + workitems += obj.values() + + replaceable = set(vars()) \ No newline at end of file diff --git a/plugin.program.AML/pdfrw/pdfrw/py23_diffs.py b/plugin.program.AML/pdfrw/pdfrw/py23_diffs.py new file mode 100644 index 0000000000..b3509d0204 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/py23_diffs.py @@ -0,0 +1,53 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +# Deal with Python2/3 differences + +try: + import zlib +except ImportError: + zlib = None + +try: + unicode = unicode +except NameError: + + def convert_load(s): + if isinstance(s, bytes): + return s.decode('Latin-1') + return s + + def convert_store(s): + return s.encode('Latin-1') + + def from_array(a): + return a.tobytes() + +else: + + def convert_load(s): + return s + + def convert_store(s): + return s + + def from_array(a): + return a.tostring() + +nextattr, = (x for x in dir(iter([])) if 'next' in x) + +try: + iteritems = dict.iteritems +except AttributeError: + iteritems = dict.items + +try: + xrange = xrange +except NameError: + xrange = range + +try: + intern = intern +except NameError: + from sys import intern diff --git a/plugin.program.AML/pdfrw/pdfrw/tokens.py b/plugin.program.AML/pdfrw/pdfrw/tokens.py new file mode 100644 index 0000000000..2b69e02c94 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/tokens.py @@ -0,0 +1,229 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' +A tokenizer for PDF streams. + +In general, documentation used was "PDF reference", +sixth edition, for PDF version 1.7, dated November 2006. + +''' + +import re +import itertools +from .objects import PdfString, PdfObject +from .objects.pdfname import BasePdfName +from .errors import log, PdfParseError +from .py23_diffs import nextattr, intern + + +def linepos(fdata, loc): + line = fdata.count('\n', 0, loc) + 1 + line += fdata.count('\r', 0, loc) - fdata.count('\r\n', 0, loc) + col = loc - max(fdata.rfind('\n', 0, loc), fdata.rfind('\r', 0, loc)) + return line, col + + +class PdfTokens(object): + + # Table 3.1, page 50 of reference, defines whitespace + eol = '\n\r' + whitespace = '\x00 \t\f' + eol + + # Text on page 50 defines delimiter characters + # Escape the ] + delimiters = r'()<>{}[\]/%' + + # "normal" stuff is all but delimiters or whitespace. + + p_normal = r'(?:[^\\%s%s]+|\\[^%s])+' % (whitespace, delimiters, + whitespace) + + p_comment = r'\%%[^%s]*' % eol + + # This will get the bulk of literal strings. + p_literal_string = r'\((?:[^\\()]+|\\.)*[()]?' + + # This will get more pieces of literal strings + # (Don't ask me why, but it hangs without the trailing ?.) + p_literal_string_extend = r'(?:[^\\()]+|\\.)*[()]?' + + # A hex string. This one's easy. + p_hex_string = r'\<[%s0-9A-Fa-f]*\>' % whitespace + + p_dictdelim = r'\<\<|\>\>' + p_name = r'/[^%s%s]*' % (delimiters, whitespace) + + p_catchall = '[^%s]' % whitespace + + pattern = '|'.join([p_normal, p_name, p_hex_string, p_dictdelim, + p_literal_string, p_comment, p_catchall]) + findtok = re.compile('(%s)[%s]*' % (pattern, whitespace), + re.DOTALL).finditer + findparen = re.compile('(%s)[%s]*' % (p_literal_string_extend, + whitespace), re.DOTALL).finditer + + def _gettoks(self, startloc, intern=intern, + delimiters=delimiters, findtok=findtok, + findparen=findparen, PdfString=PdfString, + PdfObject=PdfObject, BasePdfName=BasePdfName): + ''' Given a source data string and a location inside it, + gettoks generates tokens. Each token is a tuple of the form: + <starting file loc>, <ending file loc>, <token string> + The ending file loc is past any trailing whitespace. + + The main complication here is the literal strings, which + can contain nested parentheses. In order to cope with these + we can discard the current iterator and loop back to the + top to get a fresh one. + + We could use re.search instead of re.finditer, but that's slower. + ''' + fdata = self.fdata + current = self.current = [(startloc, startloc)] + cache = {} + get_cache = cache.get + while 1: + for match in findtok(fdata, current[0][1]): + current[0] = tokspan = match.span() + token = match.group(1) + firstch = token[0] + toktype = intern + if firstch not in delimiters: + toktype = PdfObject + elif firstch in '/<(%': + if firstch == '/': + # PDF Name + toktype = BasePdfName + elif firstch == '<': + # << dict delim, or < hex string > + if token[1:2] != '<': + toktype = PdfString + elif firstch == '(': + # Literal string + # It's probably simple, but maybe not + # Nested parentheses are a bear, and if + # they are present, we exit the for loop + # and get back in with a new starting location. + ends = None # For broken strings + if fdata[match.end(1) - 1] != ')': + nest = 2 + m_start, loc = tokspan + for match in findparen(fdata, loc): + loc = match.end(1) + ending = fdata[loc - 1] == ')' + nest += 1 - ending * 2 + if not nest: + break + if ending and ends is None: + ends = loc, match.end(), nest + token = fdata[m_start:loc] + current[0] = m_start, match.end() + if nest: + # There is one possible recoverable error + # seen in the wild -- some stupid generators + # don't escape (. If this happens, just + # terminate on first unescaped ). The string + # won't be quite right, but that's a science + # fair project for another time. + (self.error, self.exception)[not ends]( + 'Unterminated literal string') + loc, ends, nest = ends + token = fdata[m_start:loc] + ')' * nest + current[0] = m_start, ends + toktype = PdfString + elif firstch == '%': + # Comment + if self.strip_comments: + continue + else: + self.exception(('Tokenizer logic incorrect -- ' + 'should never get here')) + + newtok = get_cache(token) + if newtok is None: + newtok = cache[token] = toktype(token) + yield newtok + if current[0] is not tokspan: + break + else: + if self.strip_comments: + break + raise StopIteration + + def __init__(self, fdata, startloc=0, strip_comments=True, verbose=True): + self.fdata = fdata + self.strip_comments = strip_comments + self.iterator = iterator = self._gettoks(startloc) + self.msgs_dumped = None if verbose else set() + self.next = getattr(iterator, nextattr) + self.current = [(startloc, startloc)] + + def setstart(self, startloc): + ''' Change the starting location. + ''' + current = self.current + if startloc != current[0][1]: + current[0] = startloc, startloc + + def floc(self): + ''' Return the current file position + (where the next token will be retrieved) + ''' + return self.current[0][1] + floc = property(floc, setstart) + + def tokstart(self): + ''' Return the file position of the most + recently retrieved token. + ''' + return self.current[0][0] + tokstart = property(tokstart, setstart) + + def __iter__(self): + return self.iterator + + def multiple(self, count, islice=itertools.islice, list=list): + ''' Retrieve multiple tokens + ''' + return list(islice(self, count)) + + def next_default(self, default='nope'): + for result in self: + return result + return default + + def msg(self, msg, *arg): + dumped = self.msgs_dumped + if dumped is not None: + if msg in dumped: + return + dumped.add(msg) + if arg: + msg %= arg + fdata = self.fdata + begin, end = self.current[0] + if begin >= len(fdata): + return '%s (filepos %s past EOF %s)' % (msg, begin, len(fdata)) + line, col = linepos(fdata, begin) + if end > begin: + tok = fdata[begin:end].rstrip() + if len(tok) > 30: + tok = tok[:26] + ' ...' + return ('%s (line=%d, col=%d, token=%s)' % + (msg, line, col, repr(tok))) + return '%s (line=%d, col=%d)' % (msg, line, col) + + def warning(self, *arg): + s = self.msg(*arg) + if s: + log.warning(s) + + def error(self, *arg): + s = self.msg(*arg) + if s: + log.error(s) + + def exception(self, *arg): + raise PdfParseError(self.msg(*arg)) diff --git a/plugin.program.AML/pdfrw/pdfrw/toreportlab.py b/plugin.program.AML/pdfrw/pdfrw/toreportlab.py new file mode 100644 index 0000000000..3434fbf2c1 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/toreportlab.py @@ -0,0 +1,146 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# MIT license -- See LICENSE.txt for details + +''' +Converts pdfrw objects into reportlab objects. + +Designed for and tested with rl 2.3. + +Knows too much about reportlab internals. +What can you do? + +The interface to this function is through the makerl() function. + +Parameters: + canv - a reportlab "canvas" (also accepts a "document") + pdfobj - a pdfrw PDF object + +Returns: + A corresponding reportlab object, or if the + object is a PDF Form XObject, the name to + use with reportlab for the object. + + Will recursively convert all necessary objects. + Be careful when converting a page -- if /Parent is set, + will recursively convert all pages! + +Notes: + 1) Original objects are annotated with a + derived_rl_obj attribute which points to the + reportlab object. This keeps multiple reportlab + objects from being generated for the same pdfobj + via repeated calls to makerl. This is great for + not putting too many objects into the + new PDF, but not so good if you are modifying + objects for different pages. Then you + need to do your own deep copying (of circular + structures). You're on your own. + + 2) ReportLab seems weird about FormXObjects. + They pass around a partial name instead of the + object or a reference to it. So we have to + reach into reportlab and get a number for + a unique name. I guess this is to make it + where you can combine page streams with + impunity, but that's just a guess. + + 3) Updated 1/23/2010 to handle multipass documents + (e.g. with a table of contents). These have + a different doc object on every pass. + +''' + +from reportlab.pdfbase import pdfdoc as rldocmodule +from .objects import PdfDict, PdfArray, PdfName +from .py23_diffs import convert_store + +RLStream = rldocmodule.PDFStream +RLDict = rldocmodule.PDFDictionary +RLArray = rldocmodule.PDFArray + + +def _makedict(rldoc, pdfobj): + rlobj = rldict = RLDict() + if pdfobj.indirect: + rlobj.__RefOnly__ = 1 + rlobj = rldoc.Reference(rlobj) + pdfobj.derived_rl_obj[rldoc] = rlobj, None + + for key, value in pdfobj.iteritems(): + rldict[key[1:]] = makerl_recurse(rldoc, value) + + return rlobj + + +def _makestream(rldoc, pdfobj, xobjtype=PdfName.XObject): + rldict = RLDict() + rlobj = RLStream(rldict, convert_store(pdfobj.stream)) + + if pdfobj.Type == xobjtype: + shortname = 'pdfrw_%s' % (rldoc.objectcounter + 1) + fullname = rldoc.getXObjectName(shortname) + else: + shortname = fullname = None + result = rldoc.Reference(rlobj, fullname) + pdfobj.derived_rl_obj[rldoc] = result, shortname + + for key, value in pdfobj.iteritems(): + rldict[key[1:]] = makerl_recurse(rldoc, value) + + return result + + +def _makearray(rldoc, pdfobj): + rlobj = rlarray = RLArray([]) + if pdfobj.indirect: + rlobj.__RefOnly__ = 1 + rlobj = rldoc.Reference(rlobj) + pdfobj.derived_rl_obj[rldoc] = rlobj, None + + mylist = rlarray.sequence + for value in pdfobj: + mylist.append(makerl_recurse(rldoc, value)) + + return rlobj + + +def _makestr(rldoc, pdfobj): + assert isinstance(pdfobj, (float, int, str)), repr(pdfobj) + # TODO: Add fix for float like in pdfwriter + return str(getattr(pdfobj, 'encoded', None) or pdfobj) + + +def makerl_recurse(rldoc, pdfobj): + docdict = getattr(pdfobj, 'derived_rl_obj', None) + if docdict is not None: + value = docdict.get(rldoc) + if value is not None: + return value[0] + if isinstance(pdfobj, PdfDict): + if pdfobj.stream is not None: + func = _makestream + else: + func = _makedict + if docdict is None: + pdfobj.private.derived_rl_obj = {} + elif isinstance(pdfobj, PdfArray): + func = _makearray + if docdict is None: + pdfobj.derived_rl_obj = {} + else: + func = _makestr + return func(rldoc, pdfobj) + + +def makerl(canv, pdfobj): + try: + rldoc = canv._doc + except AttributeError: + rldoc = canv + rlobj = makerl_recurse(rldoc, pdfobj) + try: + name = pdfobj.derived_rl_obj[rldoc][1] + except AttributeError: + name = None + return name or rlobj diff --git a/plugin.program.AML/pdfrw/pdfrw/uncompress.py b/plugin.program.AML/pdfrw/pdfrw/uncompress.py new file mode 100644 index 0000000000..1921817e35 --- /dev/null +++ b/plugin.program.AML/pdfrw/pdfrw/uncompress.py @@ -0,0 +1,194 @@ +# A part of pdfrw (https://github.com/pmaupin/pdfrw) +# Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas +# Copyright (C) 2012-2015 Nerijus Mika +# MIT license -- See LICENSE.txt for details +# Copyright (c) 2006, Mathieu Fenniak +# BSD license -- see LICENSE.txt for details +''' +A small subset of decompression filters. Should add more later. + +I believe, after looking at the code, that portions of the flate +PNG predictor were originally transcribed from PyPDF2, which is +probably an excellent source of additional filters. +''' +import array +from .objects import PdfDict, PdfName, PdfArray +from .errors import log +from .py23_diffs import zlib, xrange, from_array, convert_load, convert_store +import math + +def streamobjects(mylist, isinstance=isinstance, PdfDict=PdfDict): + for obj in mylist: + if isinstance(obj, PdfDict) and obj.stream is not None: + yield obj + +# Hack so we can import if zlib not available +decompressobj = zlib if zlib is None else zlib.decompressobj + + +def uncompress(mylist, leave_raw=False, warnings=set(), + flate=PdfName.FlateDecode, decompress=decompressobj, + isinstance=isinstance, list=list, len=len): + ok = True + for obj in streamobjects(mylist): + ftype = obj.Filter + if ftype is None: + continue + if isinstance(ftype, list) and len(ftype) == 1: + # todo: multiple filters + ftype = ftype[0] + parms = obj.DecodeParms or obj.DP + if ftype != flate: + msg = ('Not decompressing: cannot use filter %s' + ' with parameters %s') % (repr(ftype), repr(parms)) + if msg not in warnings: + warnings.add(msg) + log.warning(msg) + ok = False + else: + dco = decompress() + try: + data = dco.decompress(convert_store(obj.stream)) + except Exception as s: + error = str(s) + else: + error = None + if isinstance(parms, PdfArray): + oldparms = parms + parms = PdfDict() + for x in oldparms: + parms.update(x) + if parms: + predictor = int(parms.Predictor or 1) + columns = int(parms.Columns or 1) + colors = int(parms.Colors or 1) + bpc = int(parms.BitsPerComponent or 8) + if 10 <= predictor <= 15: + data, error = flate_png(data, predictor, columns, colors, bpc) + elif predictor != 1: + error = ('Unsupported flatedecode predictor %s' % + repr(predictor)) + if error is None: + assert not dco.unconsumed_tail + if dco.unused_data.strip(): + error = ('Unconsumed compression data: %s' % + repr(dco.unused_data[:20])) + if error is None: + obj.Filter = None + obj.stream = data if leave_raw else convert_load(data) + else: + log.error('%s %s' % (error, repr(obj.indirect))) + ok = False + return ok + +def flate_png_impl(data, predictor=1, columns=1, colors=1, bpc=8): + + # http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html + # https://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters + # Reconstruction functions + # x: the byte being filtered; + # a: the byte corresponding to x in the pixel immediately before the pixel containing x (or the byte immediately before x, when the bit depth is less than 8); + # b: the byte corresponding to x in the previous scanline; + # c: the byte corresponding to b in the pixel immediately before the pixel containing b (or the byte immediately before b, when the bit depth is less than 8). + + def subfilter(data, prior_row_data, start, length, pixel_size): + # filter type 1: Sub + # Recon(x) = Filt(x) + Recon(a) + for i in xrange(pixel_size, length): + left = data[start + i - pixel_size] + data[start + i] = (data[start + i] + left) % 256 + + def upfilter(data, prior_row_data, start, length, pixel_size): + # filter type 2: Up + # Recon(x) = Filt(x) + Recon(b) + for i in xrange(length): + up = prior_row_data[i] + data[start + i] = (data[start + i] + up) % 256 + + def avgfilter(data, prior_row_data, start, length, pixel_size): + # filter type 3: Avg + # Recon(x) = Filt(x) + floor((Recon(a) + Recon(b)) / 2) + for i in xrange(length): + left = data[start + i - pixel_size] if i >= pixel_size else 0 + up = prior_row_data[i] + floor = math.floor((left + up) / 2) + data[start + i] = (data[start + i] + int(floor)) % 256 + + def paethfilter(data, prior_row_data, start, length, pixel_size): + # filter type 4: Paeth + # Recon(x) = Filt(x) + PaethPredictor(Recon(a), Recon(b), Recon(c)) + def paeth_predictor(a, b, c): + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + return a + elif pb <= pc: + return b + else: + return c + for i in xrange(length): + left = data[start + i - pixel_size] if i >= pixel_size else 0 + up = prior_row_data[i] + up_left = prior_row_data[i - pixel_size] if i >= pixel_size else 0 + data[start + i] = (data[start + i] + paeth_predictor(left, up, up_left)) % 256 + + columnbytes = ((columns * colors * bpc) + 7) // 8 + pixel_size = (colors * bpc + 7) // 8 + data = array.array('B', data) + rowlen = columnbytes + 1 + if predictor == 15: + padding = (rowlen - len(data)) % rowlen + data.extend([0] * padding) + assert len(data) % rowlen == 0 + + rows = xrange(0, len(data), rowlen) + prior_row_data = [ 0 for i in xrange(columnbytes) ] + for row_index in rows: + + filter_type = data[row_index] + + if filter_type == 0: # None filter + pass + + elif filter_type == 1: # Sub filter + subfilter(data, prior_row_data, row_index + 1, columnbytes, pixel_size) + + elif filter_type == 2: # Up filter + upfilter(data, prior_row_data, row_index + 1, columnbytes, pixel_size) + + elif filter_type == 3: # Average filter + avgfilter(data, prior_row_data, row_index + 1, columnbytes, pixel_size) + + elif filter_type == 4: # Paeth filter + paethfilter(data, prior_row_data, row_index + 1, columnbytes, pixel_size) + + else: + return None, 'Unsupported PNG filter %d' % filter_type + + prior_row_data = data[row_index + 1 : row_index + 1 + columnbytes] # without filter_type + + for row_index in reversed(rows): + data.pop(row_index) + + return data, None + +def flate_png(data, predictor=1, columns=1, colors=1, bpc=8): + ''' PNG prediction is used to make certain kinds of data + more compressible. Before the compression, each data + byte is either left the same, or is set to be a delta + from the previous byte, or is set to be a delta from + the previous row. This selection is done on a per-row + basis, and is indicated by a compression type byte + prepended to each row of data. + + Within more recent PDF files, it is normal to use + this technique for Xref stream objects, which are + quite regular. + ''' + d, e = flate_png_impl(data, predictor, columns, colors, bpc) + if d is not None: + d = from_array(d) + return d, e + diff --git a/plugin.program.AML/resources/__init__.py b/plugin.program.AML/resources/__init__.py new file mode 100644 index 0000000000..a9e5f9b83e --- /dev/null +++ b/plugin.program.AML/resources/__init__.py @@ -0,0 +1 @@ +# Dummy file to make directory a Python package diff --git a/plugin.program.AML/resources/assets.py b/plugin.program.AML/resources/assets.py new file mode 100644 index 0000000000..ee0dd7aea1 --- /dev/null +++ b/plugin.program.AML/resources/assets.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher asset (artwork) related stuff. + +# ------------------------------------------------------------------------------------------------- +# Asset functions +# ------------------------------------------------------------------------------------------------- +# +# Make sure this match the contents of settings.xml! +# values="Title|Snap|Flyer|Cabinet|PCB" +# +def assets_get_asset_key_MAME_icon(asset_index): + asset_key = 'title' # Default value + + if asset_index == 0: asset_key = 'title' + elif asset_index == 1: asset_key = 'snap' + elif asset_index == 2: asset_key = 'flyer' + elif asset_index == 3: asset_key = 'cabinet' + elif asset_index == 4: asset_key = 'PCB' + + return asset_key + +# +# values="Fanart|Snap|Title|Flyer|CPanel" +# +def assets_get_asset_key_MAME_fanart(asset_index): + asset_key = 'fanart' # Default value + + if asset_index == 0: asset_key = 'fanart' + elif asset_index == 1: asset_key = 'snap' + elif asset_index == 2: asset_key = 'title' + elif asset_index == 3: asset_key = 'flyer' + elif asset_index == 4: asset_key = 'cpanel' + + return asset_key + +# +# values="Boxfront|Title|Snap" +# +def assets_get_asset_key_SL_icon(asset_index): + asset_key = 'boxfront' # Default value + + if asset_index == 0: asset_key = 'boxfront' + elif asset_index == 1: asset_key = 'title' + elif asset_index == 2: asset_key = 'snap' + + return asset_key + +# +# values="Fanart|Snap|Title" +# +def assets_get_asset_key_SL_fanart(asset_index): + asset_key = 'fanart' # Default value + + if asset_index == 0: asset_key = 'fanart' + elif asset_index == 1: asset_key = 'snap' + elif asset_index == 2: asset_key = 'title' + + return asset_key diff --git a/plugin.program.AML/resources/constants.py b/plugin.program.AML/resources/constants.py new file mode 100644 index 0000000000..104fe802ac --- /dev/null +++ b/plugin.program.AML/resources/constants.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2021 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced Emulator/MAME Launcher constants and globals. +# This module has no external dependencies. + +# Transitional code from Python 2 to Python 3 (https://github.com/benjaminp/six/blob/master/six.py) +import sys +ADDON_RUNNING_PYTHON_2 = sys.version_info[0] == 2 +ADDON_RUNNING_PYTHON_3 = sys.version_info[0] == 3 +if ADDON_RUNNING_PYTHON_3: + text_type = str + binary_type = bytes +elif ADDON_RUNNING_PYTHON_2: + text_type = unicode + binary_type = str +else: + raise TypeError('Unknown Python runtime version') + +# ------------------------------------------------------------------------------------------------- +# Addon options and tuneables. +# ------------------------------------------------------------------------------------------------- +# Compact, smaller size, non-human readable JSON. False forces human-readable JSON for development. +# In AEL speed is not as critical so False. In AML this must be True when releasing. +OPTION_COMPACT_JSON = False + +# Use less memory when writing big JSON files, but writing is slower. +# In AEL this can be False when releasing. In AML it must be True. +OPTION_LOWMEM_WRITE_JSON = False + +# The addon name in the GUI. Title of Kodi dialogs (yesno, progress, etc.) and used also in log functions. +ADDON_LONG_NAME = 'Advanced MAME Launcher' +ADDON_SHORT_NAME = 'AML' + +# These parameters are used in utils_write_JSON_file() when pprint is True or +# OPTION_COMPACT_JSON is False. Otherwise non-human readable, compact JSON is written. +# pprint = True function parameter overrides option OPTION_COMPACT_JSON. +# More compact JSON files (less blanks) load faster because file size is smaller. +JSON_INDENT = 1 +JSON_SEP = (', ', ': ') + +# ------------------------------------------------------------------------------------------------- +# DEBUG/TEST settings +# ------------------------------------------------------------------------------------------------- +# If True MAME is not launched. Useful to test the Recently Played and Most Played code. +# This setting must be False when releasing. +DISABLE_MAME_LAUNCHING = False + +# ------------------------------------------------------------------------------------------------- +# This is to ease printing colors in Kodi. +# ------------------------------------------------------------------------------------------------- +KC_RED = '[COLOR red]' +KC_ORANGE = '[COLOR orange]' +KC_GREEN = '[COLOR green]' +KC_YELLOW = '[COLOR yellow]' +KC_VIOLET = '[COLOR violet]' +KC_BLUEVIOLET = '[COLOR blueviolet]' +KC_END = '[/COLOR]' + +# ------------------------------------------------------------------------------------------------- +# Image file constants. +# ------------------------------------------------------------------------------------------------- +# Supported image files in: +# 1. misc_identify_image_id_by_contents() +# 2. misc_identify_image_id_by_ext() +IMAGE_PNG_ID = 'PNG' +IMAGE_JPEG_ID = 'JPEG' +IMAGE_GIF_ID = 'GIF' +IMAGE_BMP_ID = 'BMP' +IMAGE_TIFF_ID = 'TIFF' +IMAGE_UKNOWN_ID = 'Image unknown' +IMAGE_CORRUPT_ID = 'Image corrupt' + +IMAGE_IDS = [ + IMAGE_PNG_ID, + IMAGE_JPEG_ID, + IMAGE_GIF_ID, + IMAGE_BMP_ID, + IMAGE_TIFF_ID, +] + +IMAGE_EXTENSIONS = { + IMAGE_PNG_ID : ['png'], + IMAGE_JPEG_ID : ['jpg', 'jpeg'], + IMAGE_GIF_ID : ['gif'], + IMAGE_BMP_ID : ['bmp'], + IMAGE_TIFF_ID : ['tif', 'tiff'], +} + +# Image file magic numbers. All at file offset 0. +# See https://en.wikipedia.org/wiki/List_of_file_signatures +# b prefix is a byte string in both Python 2 and 3. +IMAGE_MAGIC_DIC = { + IMAGE_PNG_ID : [ b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' ], + IMAGE_JPEG_ID : [ + b'\xFF\xD8\xFF\xDB', + b'\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00\x01', + b'\xFF\xD8\xFF\xEE', + b'\xFF\xD8\xFF\xE1', + ], + IMAGE_GIF_ID : [ + b'\x47\x49\x46\x38\x37\x61', + b'\x47\x49\x46\x38\x39\x61', + ], + IMAGE_BMP_ID : [ b'\x42\x4D' ], + IMAGE_TIFF_ID : [ + b'\x49\x49\x2A\x00', + b'\x4D\x4D\x00\x2A', + ] +} + +# ------------------------------------------------------------------------------------------------- +# Addon constants +# ------------------------------------------------------------------------------------------------- +# Operational modes +# This must match setting op_mode_raw in settings.xml or bad things will happen. +OP_MODE_VANILLA = 'Vanilla MAME' +OP_MODE_RETRO_MAME2003PLUS = 'Retroarch MAME 2003 Plus' +OP_MODE_RETRO_MAME2010 = 'Retroarch MAME 2010' +OP_MODE_RETRO_MAME2014 = 'Retroarch MAME 2014' +OP_MODE_LIST = [ + OP_MODE_VANILLA, + OP_MODE_RETRO_MAME2003PLUS, + OP_MODE_RETRO_MAME2010, + OP_MODE_RETRO_MAME2014, +] + +# In MAME 2003 Plus the MAME version is not found on the XML file. +MAME2003PLUS_VERSION_RAW = '0.78 (RA2003Plus)' + +# Make sure these strings are equal to the ones in settings.xml or bad things will happen. +VIEW_MODE_FLAT = 0 # 'Flat' +VIEW_MODE_PCLONE = 1 # 'Parent/Clone' +ROMSET_MAME_MERGED = 0 # 'Merged' +ROMSET_MAME_SPLIT = 1 # 'Split' +ROMSET_MAME_NONMERGED = 2 # 'Non-merged' +ROMSET_MAME_FULLYNONMERGED = 3 # 'Fully non-merged' +ROMSET_SL_MERGED = 0 # 'Merged' +ROMSET_SL_SPLIT = 1 # 'Split' + +ROMSET_NAME_LIST = ['Merged', 'Split', 'Non-merged', 'Fully non-merged'] +CHDSET_NAME_LIST = ['Merged', 'Split', 'Non-merged'] + +# ------------------------------------------------------------------------------------------------- +# Advanced MAME Launcher constants +# ------------------------------------------------------------------------------------------------- +# Database status. Status it determined with timestamps in control_dic +MAME_MAIN_DB_BUILT = 200 +MAME_AUDIT_DB_BUILT = 300 +MAME_CATALOG_BUILT = 400 +MAME_MACHINES_SCANNED = 500 +MAME_ASSETS_SCANNED = 600 +SL_MAIN_DB_BUILT = 700 +SL_ITEMS_SCANNED = 800 +SL_ASSETS_SCANNED = 900 + +# INI and DAT files default names. +ALLTIME_INI = 'Alltime.ini' +ARTWORK_INI = 'Artwork.ini' +BESTGAMES_INI = 'bestgames.ini' +CATEGORY_INI = 'Category.ini' +CATLIST_INI = 'catlist.ini' +CATVER_INI = 'catver.ini' +GENRE_INI = 'genre.ini' +MATURE_INI = 'mature.ini' +NPLAYERS_INI = 'nplayers.ini' +SERIES_INI = 'series.ini' +COMMAND_DAT = 'command.dat' +GAMEINIT_DAT = 'gameinit.dat' +HISTORY_XML = 'history.xml' +HISTORY_DAT = 'history.dat' +MAMEINFO_DAT = 'mameinfo.dat' + +# --- Used in the addon URLs so mark the location of machines/ROMs --- +LOCATION_STANDARD = 'STANDARD' +LOCATION_MAME_FAVS = 'MAME_FAVS' +LOCATION_MAME_MOST_PLAYED = 'MAME_MOST_PLAYED' +LOCATION_MAME_RECENT_PLAYED = 'MAME_RECENT_PLAYED' +LOCATION_SL_FAVS = 'SL_FAVS' +LOCATION_SL_MOST_PLAYED = 'SL_MOST_PLAYED' +LOCATION_SL_RECENT_PLAYED = 'SL_RECENT_PLAYED' + +# --- ROM flags used by skins to display status icons --- +AEL_INFAV_BOOL_LABEL = 'AEL_InFav' +AEL_PCLONE_STAT_LABEL = 'AEL_PClone_stat' + +AEL_INFAV_BOOL_VALUE_TRUE = 'InFav_True' +AEL_INFAV_BOOL_VALUE_FALSE = 'InFav_False' +AEL_PCLONE_STAT_VALUE_PARENT = 'PClone_Parent' +AEL_PCLONE_STAT_VALUE_CLONE = 'PClone_Clone' +AEL_PCLONE_STAT_VALUE_NONE = 'PClone_None' + +# --- SL ROM launching cases --- +SL_LAUNCH_CASE_A = 'Case A' +SL_LAUNCH_CASE_B = 'Case B' +SL_LAUNCH_CASE_C = 'Case C' +SL_LAUNCH_CASE_D = 'Case D' +SL_LAUNCH_CASE_ERROR = 'Case ERROR!' + +# --- ROM types --- +ROM_TYPE_ROM = 'ROM' # Normal ROM (no merged, no BIOS) +ROM_TYPE_BROM = 'BROM' # BIOS merged ROM +ROM_TYPE_XROM = 'XROM' # BIOS non-merged ROM +ROM_TYPE_MROM = 'MROM' # non-BIOS merged ROM +ROM_TYPE_DROM = 'DROM' # Device ROM +ROM_TYPE_DISK = 'DISK' +ROM_TYPE_SAMPLE = 'SAM' +ROM_TYPE_ERROR = 'ERR' + +# --- ROM audit status --- +AUDIT_STATUS_OK = 'OK' +AUDIT_STATUS_OK_INVALID_ROM = 'OK (invalid ROM)' +AUDIT_STATUS_OK_INVALID_CHD = 'OK (invalid CHD)' +AUDIT_STATUS_OK_WRONG_NAME_ROM = 'OK (wrong named ROM)' +AUDIT_STATUS_ZIP_NO_FOUND = 'ZIP not found' +AUDIT_STATUS_CHD_NO_FOUND = 'CHD not found' +AUDIT_STATUS_BAD_ZIP_FILE = 'Bad ZIP file' +AUDIT_STATUS_BAD_CHD_FILE = 'Bad CHD file' +AUDIT_STATUS_ROM_NOT_IN_ZIP = 'ROM not in ZIP' +AUDIT_STATUS_ROM_BAD_CRC = 'ROM bad CRC' +AUDIT_STATUS_ROM_BAD_SIZE = 'ROM bad size' +AUDIT_STATUS_CHD_BAD_VERSION = 'CHD bad version' +AUDIT_STATUS_CHD_BAD_SHA1 = 'CHD bad SHA1' +AUDIT_STATUS_SAMPLE_NOT_IN_ZIP = 'SAMPLE not in ZIP' + +# --- File name extensions --- +MAME_ROM_EXTS = ['zip'] +MAME_CHD_EXTS = ['chd'] +MAME_SAMPLE_EXTS = ['zip'] +SL_ROM_EXTS = ['zip'] +SL_CHD_EXTS = ['chd'] +ASSET_ARTWORK_EXTS = ['zip'] +ASSET_MANUAL_EXTS = ['pdf', 'cbz', 'cbr'] +ASSET_TRAILER_EXTS = ['mp4'] +ASSET_IMAGE_EXTS = ['png'] + +# Colors for filters and items in the root main menu. +COLOR_FILTER_MAIN = '[COLOR thistle]' +COLOR_FILTER_BINARY = '[COLOR lightblue]' +COLOR_FILTER_CATALOG_DAT = '[COLOR violet]' +COLOR_FILTER_CATALOG_NODAT = '[COLOR sandybrown]' +COLOR_MAME_DAT_BROWSER = '[COLOR lightgreen]' +COLOR_SOFTWARE_LISTS = '[COLOR goldenrod]' +COLOR_MAME_CUSTOM_FILTERS = '[COLOR darkgray]' +COLOR_AEL_ROLS = '[COLOR blue]' +COLOR_MAME_SPECIAL = '[COLOR silver]' +COLOR_SL_SPECIAL = '[COLOR gold]' +COLOR_UTILITIES = '[COLOR limegreen]' +COLOR_GLOBAL_REPORTS = '[COLOR darkorange]' +COLOR_DEFAULT = '[COLOR white]' +COLOR_END = '[/COLOR]' diff --git a/plugin.program.AML/resources/db.py b/plugin.program.AML/resources/db.py new file mode 100644 index 0000000000..da05e5927b --- /dev/null +++ b/plugin.program.AML/resources/db.py @@ -0,0 +1,1222 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher high-level filesystem I/O functions and database model. +# +# In the future this module must strictly use the FileName class for all IO operations and +# not the Python runtime. + +# --- AEL packages --- +from .constants import * +from .utils import * + +# --- Python standard library --- +import copy +import hashlib +import io +import re +import subprocess +import threading +import time +import xml.etree.ElementTree as ET + +# ------------------------------------------------------------------------------------------------- +# Advanced MAME Launcher data model +# ------------------------------------------------------------------------------------------------- +# http://xmlwriter.net/xml_guide/attlist_declaration.shtml#CdataEx +# #REQUIRED The attribute must always be included +# #IMPLIED The attribute does not have to be included. +# +# Example from MAME 0.190: +# <!ELEMENT device (instance*, extension*)> +# <!ATTLIST device type CDATA #REQUIRED> +# <!ATTLIST device tag CDATA #IMPLIED> +# <!ATTLIST device fixed_image CDATA #IMPLIED> +# <!ATTLIST device mandatory CDATA #IMPLIED> +# <!ATTLIST device interface CDATA #IMPLIED> +# <!ELEMENT instance EMPTY> +# <!ATTLIST instance name CDATA #REQUIRED> +# <!ATTLIST instance briefname CDATA #REQUIRED> +# <!ELEMENT extension EMPTY> +# <!ATTLIST extension name CDATA #REQUIRED> +# +# <device> tags. Example of machine aes (Neo Geo AES) +# <device type="memcard" tag="memcard"> +# <instance name="memcard" briefname="memc"/> +# <extension name="neo"/> +# </device> +# <device type="cartridge" tag="cslot1" interface="neo_cart"> +# <instance name="cartridge" briefname="cart"/> +# <extension name="bin"/> +# </device> +# +# This is how it is stored: +# devices = [ +# { +# 'att_interface' : string, +# 'att_mandatory' : bool, +# 'att_tag' : string, +# 'att_type' : string, +# 'ext_names' : [string1, string2], +# 'instance' : {'name' : string, 'briefname' : string} +# }, ... +# ] +# +# Rendering on AML Machine Information text window. +# devices[0]: +# att_interface: text_type +# att_mandatory: text_type(bool) +# att_tag: text_type +# att_type: text_type +# ext_names: text_type(string list), +# instance: text_type(dictionary), +# devices[1]: +# ... +# +def db_new_machine_dic(): + return { + # --- <machine> tag attributes --- + 'isMechanical' : False, + 'romof' : '', + 'sampleof' : '', + 'sourcefile' : '', + # --- Other tags inside <machine> from MAME XML --- + # <!ATTLIST chip type (cpu|audio) #REQUIRED> + # <!ATTLIST chip name CDATA #REQUIRED> + # Name of the chip when type == 'cpu' + # Example <chip type="cpu" tag="maincpu" name="Zilog Z80" /> + 'chip_cpu_name' : [], + 'devices' : [], # List of dictionaries. See comments above. + 'display_height' : [], # <!ATTLIST display height CDATA #IMPLIED> + 'display_refresh' : [], # <!ATTLIST display refresh CDATA #REQUIRED> + 'display_rotate' : [], # <!ATTLIST display rotate (0|90|180|270) #IMPLIED> + 'display_type' : [], # <!ATTLIST display type (raster|vector|lcd|svg|unknown) #REQUIRED> + 'display_width' : [], # <!ATTLIST display width CDATA #IMPLIED> + 'input' : {}, + 'softwarelists' : [], + # --- Custom AML data (from INI files or generated) --- + 'alltime' : '', # MASH Alltime.ini + 'artwork' : [], # MASH Artwork.ini + 'bestgames' : '', # betsgames.ini + 'category' : [], # MASH category.ini + 'catlist' : '', # catlist.ini + 'catver' : '', # catver.ini + 'genre' : '', # genre.ini + 'series' : [], # series.ini + 'veradded' : '', # catver.ini + # --- AML generated field --- + 'isDead' : False, + } + +# +# Object used in MAME_render_db.json +# +def db_new_machine_render_dic(): + return { + # --- <machine> attributes --- + 'cloneof' : '', # Must be in the render DB to generate the PClone flag + 'isBIOS' : False, + 'isDevice' : False, + # --- Other tags inside <machine> from MAME XML --- + 'description' : '', + 'year' : '', + 'manufacturer' : '', + 'driver_status' : '', + # --- Custom AML data (from INI files or generated) --- + 'isMature' : False, # Taken from mature.ini + 'nplayers' : '', # Taken from NPlayers.ini + # Genre used in AML for the skin + # Taken from Genre.ini or Catver.ini or Catlist.ini + 'genre' : '', + } + +# +# Object used in MAME_DB_roms.json +# machine_roms = { +# 'machine_name' : { +# 'bios' : [ db_new_bios_dic(), ... ], +# 'disks' : [ db_new_disk_dic(), ... ], +# 'roms' : [ db_new_rom_dic(), ... ], +# } +# } +# +def db_new_roms_object(): + return { + 'bios' : [], + 'roms' : [], + 'disks' : [], + 'samples' : [], + } + +def db_new_bios_dic(): + return { + 'name' : '', + 'description' : '', + } + +def db_new_disk_dic(): + return { + 'name' : '', + 'merge' : '', + 'sha1' : '', # sha1 allows to know if CHD is valid or not. CHDs don't have crc + } + +def db_new_rom_dic(): + return { + 'name' : '', + 'merge' : '', + 'bios' : '', + 'size' : 0, + 'crc' : '', # crc allows to know if ROM is valid or not + } + +def db_new_audit_dic(): + return { + 'machine_has_ROMs_or_CHDs' : False, + 'machine_has_ROMs' : False, + 'machine_has_CHDs' : False, + 'machine_is_OK' : True, + 'machine_ROMs_are_OK' : True, + 'machine_CHDs_are_OK' : True, + } + +# +# First element is the database dictionary key of the asset, second element is the subdirectory name. +# List used in mame_scan_MAME_assets() +# +ASSET_MAME_T_LIST = [ + ('3dbox', '3dboxes'), + ('artpreview', 'artpreviews'), + ('artwork', 'artwork'), + ('cabinet', 'cabinets'), + ('clearlogo', 'clearlogos'), + ('cpanel', 'cpanels'), + ('fanart', 'fanarts'), # Created by AML automatically when building Fanarts. + ('flyer', 'flyers'), + ('manual', 'manuals'), + ('marquee', 'marquees'), + ('PCB', 'PCBs'), + ('snap', 'snaps'), + ('title', 'titles'), + ('trailer', 'videosnaps'), +] + +# +# flags -> ROM, CHD, Samples, SoftwareLists, Pluggable Devices +# +# Status flags meaning: +# - Machine doesn't have ROMs | Machine doesn't have Software Lists +# ? Machine has own ROMs and ROMs not been scanned +# r Machine has own ROMs and ROMs doesn't exist +# R Machine has own ROMs and ROMs exists | Machine has Software Lists +# +# Status device flag: +# - Machine has no devices +# d Machine has device/s but are not mandatory (can be booted without the device). +# D Machine has device/s and must be plugged in order to boot. +# +def db_new_MAME_asset(): + return { + '3dbox' : '', + 'artpreview' : '', + 'artwork' : '', + 'cabinet' : '', + 'clearlogo' : '', + 'cpanel' : '', + 'fanart' : '', + 'flags' : '-----', + 'flyer' : '', + 'history' : '', + 'manual' : '', + 'marquee' : '', + 'PCB' : '', + 'plot' : '', + 'snap' : '', + 'title' : '', + 'trailer' : '', + } + +# Status flags meaning: +# ? SL ROM not scanned +# r Missing ROM +# R Have ROM +def db_new_SL_ROM_part(): + return { + 'name' : '', + 'interface' : '' + } + +def db_new_SL_ROM(): + return { + 'description' : '', + 'year' : '', + 'publisher' : '', + 'plot' : '', # Generated from other fields + 'cloneof' : '', + 'parts' : [], + 'hasROMs' : False, + 'hasCHDs' : False, + 'status_ROM' : '-', + 'status_CHD' : '-', + } + +def db_new_SL_ROM_audit_dic(): + return { + 'type' : '', + 'name' : '', + 'size' : '', + 'crc' : '', + 'location' : '', + } + +def db_new_SL_DISK_audit_dic(): + return { + 'type' : '', + 'name' : '', + 'sha1' : '', + 'location' : '', + } + +ASSET_SL_T_LIST = [ + ('3dbox', '3dboxes_SL'), + ('title', 'titles_SL'), + ('snap', 'snaps_SL'), + ('boxfront', 'covers_SL'), + ('fanart', 'fanarts_SL'), + ('trailer', 'videosnaps_SL'), + ('manual', 'manuals_SL'), +] + +def db_new_SL_asset(): + return { + '3dbox' : '', + 'title' : '', + 'snap' : '', + 'boxfront' : '', + 'fanart' : '', + 'trailer' : '', + 'manual' : '', + } + +# Some fields are used in all working modes. +# Some fields are used in Vanilla MAME mode. +# Some fields are used in MAME 2003 Plus mode. +def db_new_MAME_XML_control_dic(): + return { + 't_XML_extraction' : 0, # Result of time.time() [float] + 't_XML_preprocessing' : 0, # Result of time.time() [float] + 'total_machines' : 0, # [integer] + 'st_size' : 0, # Bytes [integer] + 'st_mtime' : 0.0, # seconds [float] + 'ver_mame_int' : 0, # Allows version comparisons [integer] + 'ver_mame_str' : 'undefined', # [Unicode string] + } + +def db_new_control_dic(): + return { + # --- Filed in when extracting/preprocessing MAME XML --- + # Operation mode when the database is created. If the OP mode is changed database + # must be rebuilt. + 'op_mode_raw' : 0, + 'op_mode' : '', + 'stats_total_machines' : 0, + + # --- Timestamps --- + # MAME + 't_MAME_DB_build' : 0.0, + 't_MAME_Audit_DB_build' : 0.0, + 't_MAME_Catalog_build' : 0.0, + 't_MAME_ROMs_scan' : 0.0, + 't_MAME_assets_scan' : 0.0, + 't_MAME_plots_build' : 0.0, + 't_MAME_fanart_build' : 0.0, + 't_MAME_3dbox_build' : 0.0, + 't_MAME_machine_hash' : 0.0, + 't_MAME_asset_hash' : 0.0, + 't_MAME_render_cache_build' : 0.0, + 't_MAME_asset_cache_build' : 0.0, + # Software Lists + 't_SL_DB_build' : 0.0, + 't_SL_ROMs_scan' : 0.0, + 't_SL_assets_scan' : 0.0, + 't_SL_plots_build' : 0.0, + 't_SL_fanart_build' : 0.0, + 't_SL_3dbox_build' : 0.0, + # Misc + 't_Custom_Filter_build' : 0.0, + 't_MAME_audit' : 0.0, + 't_SL_audit' : 0.0, + + # --- Filed in when building main MAME database --- + 'ver_AML_int' : 0, + 'ver_AML_str' : 'Undefined', + # Numerical MAME version. Allows for comparisons like ver_mame >= MAME_VERSION_0190 + # MAME string version, as reported by the executable stdout. Example: '0.194 (mame0194)' + 'ver_mame_int' : 0, + 'ver_mame_str' : 'Undefined', + # INI files + 'ver_alltime' : 'MAME database not built', + 'ver_artwork' : 'MAME database not built', + 'ver_bestgames' : 'MAME database not built', + 'ver_category' : 'MAME database not built', + 'ver_catlist' : 'MAME database not built', + 'ver_catver' : 'MAME database not built', + 'ver_genre' : 'MAME database not built', + 'ver_mature' : 'MAME database not built', + 'ver_nplayers' : 'MAME database not built', + 'ver_series' : 'MAME database not built', + + # DAT files + 'ver_command' : 'MAME database not built', + 'ver_gameinit' : 'MAME database not built', + 'ver_history' : 'MAME database not built', + 'ver_mameinfo' : 'MAME database not built', + + # Basic stats + 'stats_processed_machines' : 0, + 'stats_parents' : 0, + 'stats_clones' : 0, + # Excluding devices machines (devices are not runnable) + 'stats_runnable' : 0, + 'stats_runnable_parents' : 0, + 'stats_runnable_clones' : 0, + # Main filters + 'stats_coin' : 0, + 'stats_coin_parents' : 0, + 'stats_coin_clones' : 0, + 'stats_nocoin' : 0, + 'stats_nocoin_parents' : 0, + 'stats_nocoin_clones' : 0, + 'stats_mechanical' : 0, + 'stats_mechanical_parents' : 0, + 'stats_mechanical_clones' : 0, + 'stats_dead' : 0, + 'stats_dead_parents' : 0, + 'stats_dead_clones' : 0, + 'stats_devices' : 0, + 'stats_devices_parents' : 0, + 'stats_devices_clones' : 0, + # Binary filters + 'stats_BIOS' : 0, + 'stats_BIOS_parents' : 0, + 'stats_BIOS_clones' : 0, + 'stats_samples' : 0, + 'stats_samples_parents' : 0, + 'stats_samples_clones' : 0, + + # --- Main filter statistics --- + # Filed in when building the MAME catalogs in mame_build_MAME_catalogs() + # driver_status for device machines is always the empty string '' + 'stats_MF_Normal_Total' : 0, 'stats_MF_Normal_Total_parents' : 0, + 'stats_MF_Normal_Good' : 0, 'stats_MF_Normal_Good_parents' : 0, + 'stats_MF_Normal_Imperfect' : 0, 'stats_MF_Normal_Imperfect_parents' : 0, + 'stats_MF_Normal_Nonworking' : 0, 'stats_MF_Normal_Nonworking_parents' : 0, + 'stats_MF_Unusual_Total' : 0, 'stats_MF_Unusual_Total_parents' : 0, + 'stats_MF_Unusual_Good' : 0, 'stats_MF_Unusual_Good_parents' : 0, + 'stats_MF_Unusual_Imperfect' : 0, 'stats_MF_Unusual_Imperfect_parents' : 0, + 'stats_MF_Unusual_Nonworking' : 0, 'stats_MF_Unusual_Nonworking_parents' : 0, + 'stats_MF_Nocoin_Total' : 0, 'stats_MF_Nocoin_Total_parents' : 0, + 'stats_MF_Nocoin_Good' : 0, 'stats_MF_Nocoin_Good_parents' : 0, + 'stats_MF_Nocoin_Imperfect' : 0, 'stats_MF_Nocoin_Imperfect_parents' : 0, + 'stats_MF_Nocoin_Nonworking' : 0, 'stats_MF_Nocoin_Nonworking_parents' : 0, + 'stats_MF_Mechanical_Total' : 0, 'stats_MF_Mechanical_Total_parents' : 0, + 'stats_MF_Mechanical_Good' : 0, 'stats_MF_Mechanical_Good_parents' : 0, + 'stats_MF_Mechanical_Imperfect' : 0, 'stats_MF_Mechanical_Imperfect_parents' : 0, + 'stats_MF_Mechanical_Nonworking' : 0, 'stats_MF_Mechanical_Nonworking_parents' : 0, + 'stats_MF_Dead_Total' : 0, 'stats_MF_Dead_Total_parents' : 0, + 'stats_MF_Dead_Good' : 0, 'stats_MF_Dead_Good_parents' : 0, + 'stats_MF_Dead_Imperfect' : 0, 'stats_MF_Dead_Imperfect_parents' : 0, + 'stats_MF_Dead_Nonworking' : 0, 'stats_MF_Dead_Nonworking_parents' : 0, + + # --- Filed in when building the ROM audit databases --- + 'stats_audit_MAME_machines_runnable' : 0, + # Number of ROM ZIP files in the Merged, Split or Non-merged sets. + 'stats_audit_MAME_ROM_ZIP_files' : 0, + # Number of Sample ZIP files. + 'stats_audit_MAME_Sample_ZIP_files' : 0, + # Number of CHD files in the Merged, Split or Non-merged sets. + 'stats_audit_MAME_CHD_files' : 0, + + # Number of machines that require one or more ROM ZIP archives to run + 'stats_audit_machine_archives_ROM' : 0, + 'stats_audit_machine_archives_ROM_parents' : 0, + 'stats_audit_machine_archives_ROM_clones' : 0, + # Number of machines that require one or more CHDs to run + 'stats_audit_machine_archives_CHD' : 0, + 'stats_audit_machine_archives_CHD_parents' : 0, + 'stats_audit_machine_archives_CHD_clones' : 0, + # Number of machines that require Sample ZIPs + 'stats_audit_machine_archives_Samples' : 0, + 'stats_audit_machine_archives_Samples_parents' : 0, + 'stats_audit_machine_archives_Samples_clones' : 0, + # ROM less machines do not need any ZIP archive or CHD to run + 'stats_audit_archive_less' : 0, + 'stats_audit_archive_less_parents' : 0, + 'stats_audit_archive_less_clones' : 0, + + # ROM statistics (not implemented yet) + 'stats_audit_ROMs_total' : 0, + 'stats_audit_ROMs_valid' : 0, + 'stats_audit_ROMs_invalid' : 0, + 'stats_audit_ROMs_unique' : 0, # Not implemented + 'stats_audit_ROMs_SHA_merged' : 0, # Not implemented + 'stats_audit_CHDs_total' : 0, + 'stats_audit_CHDs_valid' : 0, + 'stats_audit_CHDs_invalid' : 0, + + # --- Filed in when auditing the MAME machines --- + # >> Machines with ROMs/CHDs archives that are OK or not + 'audit_MAME_machines_with_arch' : 0, + 'audit_MAME_machines_with_arch_OK' : 0, + 'audit_MAME_machines_with_arch_BAD' : 0, + 'audit_MAME_machines_without' : 0, + # >> Machines with ROM archives that are OK or not + 'audit_MAME_machines_with_ROMs' : 0, + 'audit_MAME_machines_with_ROMs_OK' : 0, + 'audit_MAME_machines_with_ROMs_BAD' : 0, + 'audit_MAME_machines_without_ROMs' : 0, + # >> Machines with Samples archives that are OK or not + 'audit_MAME_machines_with_SAMPLES' : 0, + 'audit_MAME_machines_with_SAMPLES_OK' : 0, + 'audit_MAME_machines_with_SAMPLES_BAD' : 0, + 'audit_MAME_machines_without_SAMPLES' : 0, + # >> Machines with CHDs that are OK or not + 'audit_MAME_machines_with_CHDs' : 0, + 'audit_MAME_machines_with_CHDs_OK' : 0, + 'audit_MAME_machines_with_CHDs_BAD' : 0, + 'audit_MAME_machines_without_CHDs' : 0, + + # --- Filed in when building the SL item databases --- + # Number of SL databases (equal to the number of XML files). + 'stats_SL_XML_files' : 0, + 'stats_SL_software_items' : 0, + # Number of SL items that require one or more ROM ZIP archives to run + 'stats_SL_items_with_ROMs' : 0, + # Number of SL items that require one or more CHDs to run + 'stats_SL_items_with_CHDs' : 0, + + # --- Filed in when building the SL audit databases --- + 'stats_audit_SL_items_runnable' : 0, + 'stats_audit_SL_items_with_arch' : 0, # ROM ZIP or CHD or both + 'stats_audit_SL_items_with_arch_ROM' : 0, # At least ROM ZIP (and maybe CHD) + 'stats_audit_SL_items_with_CHD' : 0, # At least CHD (and maybe ROM ZIP) + + # --- Filed in when auditing the SL items --- + 'audit_SL_items_runnable' : 0, + 'audit_SL_items_with_arch' : 0, + 'audit_SL_items_with_arch_OK' : 0, + 'audit_SL_items_with_arch_BAD' : 0, + 'audit_SL_items_without_arch' : 0, + 'audit_SL_items_with_arch_ROM' : 0, + 'audit_SL_items_with_arch_ROM_OK' : 0, + 'audit_SL_items_with_arch_ROM_BAD' : 0, + 'audit_SL_items_without_arch_ROM' : 0, + 'audit_SL_items_with_CHD' : 0, + 'audit_SL_items_with_CHD_OK' : 0, + 'audit_SL_items_with_CHD_BAD' : 0, + 'audit_SL_items_without_CHD' : 0, + + # --- Filed in by the MAME ROM/CHD/Samples scanner --- + # ROM_Set_ROM_list.json database + # Number of ROM ZIP files, including device ROMs. + 'scan_ROM_ZIP_files_total' : 0, + 'scan_ROM_ZIP_files_have' : 0, + 'scan_ROM_ZIP_files_missing' : 0, + + # ROM_Set_Sample_list.json + # Number of Samples ZIP files. + 'scan_Samples_ZIP_total' : 0, + 'scan_Samples_ZIP_have' : 0, + 'scan_Samples_ZIP_missing' : 0, + + # ROM_Set_CHD_list.json database + # Number of CHD files. + 'scan_CHD_files_total' : 0, + 'scan_CHD_files_have' : 0, + 'scan_CHD_files_missing' : 0, + + # ROM_Set_machine_files.json database + # Number of runnable machines that need one or more ROM ZIP file to run (excluding devices). + # Number of machines you can run, excluding devices. + # Number of machines you cannot run, excluding devices. + 'scan_machine_archives_ROM_total' : 0, + 'scan_machine_archives_ROM_have' : 0, + 'scan_machine_archives_ROM_missing' : 0, + + # Sames with Samples + 'scan_machine_archives_Samples_total' : 0, + 'scan_machine_archives_Samples_have' : 0, + 'scan_machine_archives_Samples_missing' : 0, + + # Number of machines that need one or more CHDs to run. + # Number of machines with CHDs you can run. + # Number of machines with CHDs you cannot run. + 'scan_machine_archives_CHD_total' : 0, + 'scan_machine_archives_CHD_have' : 0, + 'scan_machine_archives_CHD_missing' : 0, + + # --- Filed in by the SL ROM/CHD scanner --- + 'scan_SL_archives_ROM_total' : 0, + 'scan_SL_archives_ROM_have' : 0, + 'scan_SL_archives_ROM_missing' : 0, + 'scan_SL_archives_CHD_total' : 0, + 'scan_SL_archives_CHD_have' : 0, + 'scan_SL_archives_CHD_missing' : 0, + + # --- Filed in by the MAME asset scanner --- + 'assets_num_MAME_machines' : 0, + 'assets_3dbox_have' : 0, + 'assets_3dbox_missing' : 0, + 'assets_3dbox_alternate' : 0, + 'assets_artpreview_have' : 0, + 'assets_artpreview_missing' : 0, + 'assets_artpreview_alternate' : 0, + 'assets_artwork_have' : 0, + 'assets_artwork_missing' : 0, + 'assets_artwork_alternate' : 0, + 'assets_cabinets_have' : 0, + 'assets_cabinets_missing' : 0, + 'assets_cabinets_alternate' : 0, + 'assets_clearlogos_have' : 0, + 'assets_clearlogos_missing' : 0, + 'assets_clearlogos_alternate' : 0, + 'assets_cpanels_have' : 0, + 'assets_cpanels_missing' : 0, + 'assets_cpanels_alternate' : 0, + 'assets_fanarts_have' : 0, + 'assets_fanarts_missing' : 0, + 'assets_fanarts_alternate' : 0, + 'assets_flyers_have' : 0, + 'assets_flyers_missing' : 0, + 'assets_flyers_alternate' : 0, + 'assets_manuals_have' : 0, + 'assets_manuals_missing' : 0, + 'assets_manuals_alternate' : 0, + 'assets_marquees_have' : 0, + 'assets_marquees_missing' : 0, + 'assets_marquees_alternate' : 0, + 'assets_PCBs_have' : 0, + 'assets_PCBs_missing' : 0, + 'assets_PCBs_alternate' : 0, + 'assets_snaps_have' : 0, + 'assets_snaps_missing' : 0, + 'assets_snaps_alternate' : 0, + 'assets_titles_have' : 0, + 'assets_titles_missing' : 0, + 'assets_titles_alternate' : 0, + 'assets_trailers_have' : 0, + 'assets_trailers_missing' : 0, + 'assets_trailers_alternate' : 0, + + # --- Filed in by the SL asset scanner --- + 'assets_SL_num_items' : 0, + 'assets_SL_3dbox_have' : 0, + 'assets_SL_3dbox_missing' : 0, + 'assets_SL_3dbox_alternate' : 0, + 'assets_SL_titles_have' : 0, + 'assets_SL_titles_missing' : 0, + 'assets_SL_titles_alternate' : 0, + 'assets_SL_snaps_have' : 0, + 'assets_SL_snaps_missing' : 0, + 'assets_SL_snaps_alternate' : 0, + 'assets_SL_boxfronts_have' : 0, + 'assets_SL_boxfronts_missing' : 0, + 'assets_SL_boxfronts_alternate' : 0, + 'assets_SL_fanarts_have' : 0, + 'assets_SL_fanarts_missing' : 0, + 'assets_SL_fanarts_alternate' : 0, + 'assets_SL_trailers_have' : 0, + 'assets_SL_trailers_missing' : 0, + 'assets_SL_trailers_alternate' : 0, + 'assets_SL_manuals_have' : 0, + 'assets_SL_manuals_missing' : 0, + 'assets_SL_manuals_alternate' : 0, + } + +# Safe way of change a dictionary without adding new fields. +def db_safe_edit(my_dic, field, value): + if field in my_dic: + my_dic[field] = value + else: + raise TypeError('Field {} not in dictionary'.format(field)) + +# +# Favourite MAME object creation. +# Simple means the main data and assets are used to created the Favourite. +# Full means that the main data, the render data and the assets are used to create the Favourite. +# +# Both functioncs create a complete Favourite. When simple() is used the machine is taken from +# the hashed database, which includes both the main and render machine data. +# +# Changes introduced in 0.9.6 +# 1) fav_machine['name'] = machine_name +# +def db_get_MAME_Favourite_simple(machine_name, machine, assets, control_dic): + fav_machine = {} + + fav_machine = copy.deepcopy(machine) + fav_machine['name'] = machine_name + fav_machine['ver_mame_int'] = control_dic['ver_mame_int'] + fav_machine['ver_mame_str'] = control_dic['ver_mame_str'] + fav_machine['assets'] = copy.deepcopy(assets) + + return fav_machine + +def db_get_MAME_Favourite_full(machine_name, machine, machine_render, assets, control_dic): + fav_machine = {} + + fav_machine = copy.deepcopy(machine) + fav_machine.update(machine_render) + fav_machine['name'] = machine_name + fav_machine['ver_mame_int'] = control_dic['ver_mame_int'] + fav_machine['ver_mame_str'] = control_dic['ver_mame_str'] + fav_machine['assets'] = copy.deepcopy(assets) + + return fav_machine + +def db_get_SL_Favourite(SL_name, ROM_name, ROM, assets, control_dic): + fav_SL_item = {} + + SL_DB_key = SL_name + '-' + ROM_name + fav_SL_item = copy.deepcopy(ROM) + fav_SL_item['SL_name'] = SL_name + fav_SL_item['SL_ROM_name'] = ROM_name + fav_SL_item['SL_DB_key'] = SL_DB_key + fav_SL_item['ver_mame_int'] = control_dic['ver_mame_int'] + fav_SL_item['ver_mame_str'] = control_dic['ver_mame_str'] + fav_SL_item['launch_machine'] = '' + fav_SL_item['assets'] = copy.deepcopy(assets) + + return fav_SL_item + +# Get Catalog databases +def db_get_cataloged_dic_parents(cfg, catalog_name): + if catalog_name == 'Main': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_MAIN_PARENT_PATH.getPath()) + elif catalog_name == 'Binary': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_BINARY_PARENT_PATH.getPath()) + elif catalog_name == 'Catver': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CATVER_PARENT_PATH.getPath()) + elif catalog_name == 'Catlist': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CATLIST_PARENT_PATH.getPath()) + elif catalog_name == 'Genre': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_GENRE_PARENT_PATH.getPath()) + elif catalog_name == 'Category': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CATEGORY_PARENT_PATH.getPath()) + elif catalog_name == 'NPlayers': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_NPLAYERS_PARENT_PATH.getPath()) + elif catalog_name == 'Bestgames': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_BESTGAMES_PARENT_PATH.getPath()) + elif catalog_name == 'Series': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_SERIES_PARENT_PATH.getPath()) + elif catalog_name == 'Alltime': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_ALLTIME_PARENT_PATH.getPath()) + elif catalog_name == 'Artwork': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_ARTWORK_PARENT_PATH.getPath()) + elif catalog_name == 'Version': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_VERADDED_PARENT_PATH.getPath()) + elif catalog_name == 'Controls_Expanded': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CONTROL_EXPANDED_PARENT_PATH.getPath()) + elif catalog_name == 'Controls_Compact': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CONTROL_COMPACT_PARENT_PATH.getPath()) + elif catalog_name == 'Devices_Expanded': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DEVICE_EXPANDED_PARENT_PATH.getPath()) + elif catalog_name == 'Devices_Compact': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DEVICE_COMPACT_PARENT_PATH.getPath()) + elif catalog_name == 'Display_Type': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DISPLAY_TYPE_PARENT_PATH.getPath()) + elif catalog_name == 'Display_VSync': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DISPLAY_VSYNC_PARENT_PATH.getPath()) + elif catalog_name == 'Display_Resolution': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DISPLAY_RES_PARENT_PATH.getPath()) + elif catalog_name == 'CPU': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CPU_PARENT_PATH.getPath()) + elif catalog_name == 'Driver': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DRIVER_PARENT_PATH.getPath()) + elif catalog_name == 'Manufacturer': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_MANUFACTURER_PARENT_PATH.getPath()) + elif catalog_name == 'ShortName': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_SHORTNAME_PARENT_PATH.getPath()) + elif catalog_name == 'LongName': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_LONGNAME_PARENT_PATH.getPath()) + elif catalog_name == 'BySL': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_SL_PARENT_PATH.getPath()) + elif catalog_name == 'Year': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_YEAR_PARENT_PATH.getPath()) + else: + log_error('db_get_cataloged_dic_parents() Unknown catalog_name = "{}"'.format(catalog_name)) + + return catalog_dic + +def db_get_cataloged_dic_all(cfg, catalog_name): + if catalog_name == 'Main': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_MAIN_ALL_PATH.getPath()) + elif catalog_name == 'Binary': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_BINARY_ALL_PATH.getPath()) + elif catalog_name == 'Catver': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CATVER_ALL_PATH.getPath()) + elif catalog_name == 'Catlist': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CATLIST_ALL_PATH.getPath()) + elif catalog_name == 'Genre': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_GENRE_ALL_PATH.getPath()) + elif catalog_name == 'Category': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CATEGORY_ALL_PATH.getPath()) + elif catalog_name == 'NPlayers': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_NPLAYERS_ALL_PATH.getPath()) + elif catalog_name == 'Bestgames': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_BESTGAMES_ALL_PATH.getPath()) + elif catalog_name == 'Series': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_SERIES_ALL_PATH.getPath()) + elif catalog_name == 'Alltime': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_ALLTIME_ALL_PATH.getPath()) + elif catalog_name == 'Artwork': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_ARTWORK_ALL_PATH.getPath()) + elif catalog_name == 'Version': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_VERADDED_ALL_PATH.getPath()) + elif catalog_name == 'Controls_Expanded': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CONTROL_EXPANDED_ALL_PATH.getPath()) + elif catalog_name == 'Controls_Compact': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CONTROL_COMPACT_ALL_PATH.getPath()) + elif catalog_name == 'Devices_Expanded': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DEVICE_EXPANDED_ALL_PATH.getPath()) + elif catalog_name == 'Devices_Compact': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DEVICE_COMPACT_ALL_PATH.getPath()) + elif catalog_name == 'Display_Type': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DISPLAY_TYPE_ALL_PATH.getPath()) + elif catalog_name == 'Display_VSync': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DISPLAY_VSYNC_ALL_PATH.getPath()) + elif catalog_name == 'Display_Resolution': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DISPLAY_RES_ALL_PATH.getPath()) + elif catalog_name == 'CPU': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_CPU_ALL_PATH.getPath()) + elif catalog_name == 'Driver': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_DRIVER_ALL_PATH.getPath()) + elif catalog_name == 'Manufacturer': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_MANUFACTURER_ALL_PATH.getPath()) + elif catalog_name == 'ShortName': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_SHORTNAME_ALL_PATH.getPath()) + elif catalog_name == 'LongName': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_LONGNAME_ALL_PATH.getPath()) + elif catalog_name == 'BySL': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_SL_ALL_PATH.getPath()) + elif catalog_name == 'Year': + catalog_dic = utils_load_JSON_file(cfg.CATALOG_YEAR_ALL_PATH.getPath()) + else: + log_error('db_get_cataloged_dic_all() Unknown catalog_name = "{}"'.format(catalog_name)) + + return catalog_dic + +# +# Locates object index in a list of dictionaries by 'name' field. +# Returns -1 if object cannot be found. Uses a linear search (slow!). +# +def db_locate_idx_by_MAME_name(object_list, object_name): + object_index = -1 + for i, machine in enumerate(object_list): + if object_name == machine['name']: + object_index = i + break + + return object_index + +# +# Same as previous function but on a list of Software List items +# +def db_locate_idx_by_SL_item_name(object_list, SL_name, SL_ROM_name): + SL_fav_DB_key = SL_name + '-' + SL_ROM_name + object_index = -1 + for i, machine in enumerate(object_list): + if SL_fav_DB_key == machine['SL_DB_key']: + object_index = i + break + + return object_index + +# Valid ROM: ROM has CRC hash +# Valid CHD: CHD has SHA1 hash +def db_initial_flags(machine, machine_render, m_roms): + # Machine has own ROMs (at least one ROM is valid and has empty 'merge' attribute) + has_own_ROMs = False + for rom in m_roms['roms']: + if not rom['merge'] and rom['crc']: + has_own_ROMs = True + break + flag_ROM = '?' if has_own_ROMs else '-' + + # Machine has own CHDs + has_own_CHDs = False + for rom in m_roms['disks']: + if not rom['merge'] and rom['sha1']: + has_own_CHDs = True + break + flag_CHD = '?' if has_own_CHDs else '-' + + # Samples flag + flag_Samples = '?' if machine['sampleof'] else '-' + + # Software List flag + flag_SL = 'L' if machine['softwarelists'] else '-' + + # Pluggable Devices flag + if machine['devices']: + num_dev_mandatory = 0 + for device in machine['devices']: + if device['att_mandatory']: + flag_Devices = 'D' + num_dev_mandatory += 1 + else: + flag_Devices = 'd' + if num_dev_mandatory > 2: + message = 'Machine {} has {} mandatory devices'.format(machine_name, num_dev_mandatory) + raise CriticalError(message) + else: + flag_Devices = '-' + + return '{}{}{}{}{}'.format(flag_ROM, flag_CHD, flag_Samples, flag_SL, flag_Devices) + +# +# Update m_dic using Python pass by assignment. +# Remember that strings are inmutable! +# +def db_set_ROM_flag(m_dic, new_ROM_flag): + flag_ROM = m_dic['flags'][0] + flag_CHD = m_dic['flags'][1] + flag_Samples = m_dic['flags'][2] + flag_SL = m_dic['flags'][3] + flag_Devices = m_dic['flags'][4] + flag_ROM = new_ROM_flag + m_dic['flags'] = '{}{}{}{}{}'.format(flag_ROM, flag_CHD, flag_Samples, flag_SL, flag_Devices) + +def db_set_CHD_flag(m_dic, new_CHD_flag): + flag_ROM = m_dic['flags'][0] + flag_CHD = m_dic['flags'][1] + flag_Samples = m_dic['flags'][2] + flag_SL = m_dic['flags'][3] + flag_Devices = m_dic['flags'][4] + flag_CHD = new_CHD_flag + m_dic['flags'] = '{}{}{}{}{}'.format(flag_ROM, flag_CHD, flag_Samples, flag_SL, flag_Devices) + +def db_set_Sample_flag(m_dic, new_Sample_flag): + flag_ROM = m_dic['flags'][0] + flag_CHD = m_dic['flags'][1] + flag_Samples = m_dic['flags'][2] + flag_SL = m_dic['flags'][3] + flag_Devices = m_dic['flags'][4] + flag_Samples = new_Sample_flag + m_dic['flags'] = '{}{}{}{}{}'.format(flag_ROM, flag_CHD, flag_Samples, flag_SL, flag_Devices) + +# ------------------------------------------------------------------------------------------------- +# MAME hashed databases. Useful when only one item in a big dictionary is required. +# ------------------------------------------------------------------------------------------------- +# Hash database with 256 elements (2 hex digits) +def db_build_main_hashed_db(cfg, control_dic, machines, machines_render): + log_info('db_build_main_hashed_db() Building main hashed database...') + + # machine_name -> MD5 -> take two letters -> aa.json, ab.json, ... + # A) First create an index + # db_main_hash_idx = { 'machine_name' : 'aa', ... } + # B) Then traverse a list [0, 1, ..., f] and write the machines in that sub database section. + pDialog = KodiProgressDialog() + pDialog.startProgress('Building main hashed database...', len(machines)) + db_main_hash_idx = {} + for key in machines: + pDialog.updateProgressInc() + md5_str = hashlib.md5(key.encode('utf-8')).hexdigest() + db_name = md5_str[0:2] # WARNING Python slicing does not work like in C/C++! + db_main_hash_idx[key] = db_name + # log_debug('Machine {:20s} / hash {} / db file {}'.format(key, md5_str, db_name)) + pDialog.endProgress() + + hex_digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] + distributed_db_files = [] + for u in range(len(hex_digits)): + for v in range(len(hex_digits)): + distributed_db_files.append('{}{}'.format(hex_digits[u], hex_digits[v])) + pDialog.startProgress('Building main hashed database JSON files...', len(distributed_db_files)) + for db_prefix in distributed_db_files: + pDialog.updateProgressInc() + # log_debug('db prefix {}'.format(db_prefix)) + # --- Generate dictionary in this JSON file --- + hashed_db_dic = {} + for key in db_main_hash_idx: + if db_main_hash_idx[key] == db_prefix: + machine_dic = machines[key].copy() + # >> returns None because it mutates machine_dic + machine_dic.update(machines_render[key]) + hashed_db_dic[key] = machine_dic + # --- Save JSON file --- + hash_DB_FN = cfg.MAIN_DB_HASH_DIR.pjoin(db_prefix + '_machines.json') + utils_write_JSON_file(hash_DB_FN.getPath(), hashed_db_dic, verbose = False) + pDialog.endProgress() + + # Update timestamp in control_dic. + db_safe_edit(control_dic, 't_MAME_machine_hash', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + +# +# Retrieves machine from distributed database. +# This is very quick for retrieving individual machines, very slow for multiple machines. +# +def db_get_machine_main_hashed_db(cfg, machine_name): + log_debug('db_get_machine_main_hashed_db() machine {}'.format(machine_name)) + md5_str = hashlib.md5(machine_name.encode('utf-8')).hexdigest() + # WARNING Python slicing does not work like in C/C++! + hash_DB_FN = cfg.MAIN_DB_HASH_DIR.pjoin(md5_str[0:2] + '_machines.json') + hashed_db_dic = utils_load_JSON_file(hash_DB_FN.getPath()) + + return hashed_db_dic[machine_name] + +# MAME hash database with 256 elements (2 hex digits) +def db_build_asset_hashed_db(cfg, control_dic, assets_dic): + log_info('db_build_asset_hashed_db() Building assets hashed database ...') + + # machine_name -> MD5 -> take two letters -> aa.json, ab.json, ... + pDialog = KodiProgressDialog() + pDialog.startProgress('Building asset hashed database...', len(assets_dic)) + db_main_hash_idx = {} + for key in assets_dic: + pDialog.updateProgressInc() + md5_str = hashlib.md5(key.encode('utf-8')).hexdigest() + db_name = md5_str[0:2] # WARNING Python slicing does not work like in C/C++! + db_main_hash_idx[key] = db_name + # log_debug('Machine {:20s} / hash {} / db file {}'.format(key, md5_str, db_name)) + pDialog.endProgress() + + hex_digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] + distributed_db_files = [] + for u in range(len(hex_digits)): + for v in range(len(hex_digits)): + distributed_db_files.append('{}{}'.format(hex_digits[u], hex_digits[v])) + pDialog.startProgress('Building asset hashed database JSON files...', len(distributed_db_files)) + for db_prefix in distributed_db_files: + pDialog.updateProgressInc() + hashed_db_dic = {} + for key in db_main_hash_idx: + if db_main_hash_idx[key] == db_prefix: + hashed_db_dic[key] = assets_dic[key] + hash_DB_FN = cfg.MAIN_DB_HASH_DIR.pjoin(db_prefix + '_assets.json') + utils_write_JSON_file(hash_DB_FN.getPath(), hashed_db_dic, verbose = False) + pDialog.endProgress() + + # --- Timestamp --- + db_safe_edit(control_dic, 't_MAME_asset_hash', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + +# +# Retrieves machine from distributed hashed database. +# This is very quick for retrieving individual machines, slow for multiple machines. +# +def db_get_machine_assets_hashed_db(cfg, machine_name): + log_debug('db_get_machine_assets_hashed_db() machine {}'.format(machine_name)) + md5_str = hashlib.md5(machine_name.encode('utf-8')).hexdigest() + hash_DB_FN = cfg.MAIN_DB_HASH_DIR.pjoin(md5_str[0:2] + '_assets.json') + hashed_db_dic = utils_load_JSON_file(hash_DB_FN.getPath()) + + return hashed_db_dic[machine_name] + +# ------------------------------------------------------------------------------------------------- +# MAME machine render cache +# Creates a separate MAME render and assets databases for each catalog to speed up +# access of ListItems when rendering machine lists. +# ------------------------------------------------------------------------------------------------- +def db_cache_get_key(catalog_name, category_name): + return hashlib.md5('{} - {}'.format(catalog_name, category_name).encode('utf-8')).hexdigest() + +def db_build_render_cache(cfg, control_dic, cache_index_dic, machines_render, force_build = False): + log_info('db_build_render_cache() Initialising...') + log_debug('debug_enable_MAME_render_cache is {}'.format(cfg.settings['debug_enable_MAME_render_cache'])) + log_debug('force_build is {}'.format(force_build)) + if not cfg.settings['debug_enable_MAME_render_cache'] and not force_build: + log_info('db_build_render_cache() Render cache disabled.') + return + # Notify user this is a forced build. + if not cfg.settings['debug_enable_MAME_render_cache'] and force_build: + t = 'MAME render cache disabled but forcing rebuilding.' + log_info(t) + kodi_dialog_OK(t) + + # --- Clean 'cache' directory JSON ROM files --- + log_info('Cleaning dir "{}"'.format(cfg.CACHE_DIR.getPath())) + pDialog = KodiProgressDialog() + pDialog.startProgress('Listing render cache JSON files...') + file_list = os.listdir(cfg.CACHE_DIR.getPath()) + log_info('Found {} files'.format(len(file_list))) + deleted_items = 0 + pDialog.resetProgress('Cleaning render cache JSON files...', len(file_list)) + for file in file_list: + pDialog.updateProgressInc() + if not file.endswith('_render.json'): continue + full_path = os.path.join(cfg.CACHE_DIR.getPath(), file) + # log_debug('UNLINK "{}"'.format(full_path)) + os.unlink(full_path) + deleted_items += 1 + pDialog.endProgress() + log_info('Deleted {} files'.format(deleted_items)) + + # --- Build ROM cache --- + num_catalogs = len(cache_index_dic) + catalog_count = 1 + pDialog.startProgress('Building MAME render cache') + for catalog_name in sorted(cache_index_dic): + catalog_index_dic = cache_index_dic[catalog_name] + catalog_all = db_get_cataloged_dic_all(cfg, catalog_name) + diag_t = 'Building MAME [COLOR orange]{}[/COLOR] render cache ({} of {})...'.format( + catalog_name, catalog_count, num_catalogs) + pDialog.resetProgress(diag_t, len(catalog_index_dic)) + for catalog_key in catalog_index_dic: + pDialog.updateProgressInc() + hash_str = catalog_index_dic[catalog_key]['hash'] + # log_debug('db_build_ROM_cache() Catalog "{}" --- Key "{}"'.format(catalog_name, catalog_key)) + # log_debug('db_build_ROM_cache() hash {}'.format(hash_str)) + + # Build all machines cache + m_render_all_dic = {} + for machine_name in catalog_all[catalog_key]: + m_render_all_dic[machine_name] = machines_render[machine_name] + ROMs_all_FN = cfg.CACHE_DIR.pjoin(hash_str + '_render.json') + utils_write_JSON_file(ROMs_all_FN.getPath(), m_render_all_dic, verbose = False) + catalog_count += 1 + pDialog.endProgress() + + # --- Timestamp --- + db_safe_edit(control_dic, 't_MAME_render_cache_build', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + +def db_get_render_cache_row(cfg, cache_index_dic, catalog_name, category_name): + hash_str = cache_index_dic[catalog_name][category_name]['hash'] + ROMs_all_FN = cfg.CACHE_DIR.pjoin(hash_str + '_render.json') + + return utils_load_JSON_file(ROMs_all_FN.getPath()) + +# ------------------------------------------------------------------------------------------------- +# MAME asset cache +# ------------------------------------------------------------------------------------------------- +def db_build_asset_cache(cfg, control_dic, cache_index_dic, assets_dic, force_build = False): + log_info('db_build_asset_cache() Initialising...') + log_debug('debug_enable_MAME_asset_cache is {}'.format(cfg.settings['debug_enable_MAME_asset_cache'])) + log_debug('force_build is {}'.format(force_build)) + if not cfg.settings['debug_enable_MAME_asset_cache'] and not force_build: + log_info('db_build_asset_cache() Asset cache disabled.') + return + # Notify user this is a forced build. + if not cfg.settings['debug_enable_MAME_render_cache'] and force_build: + t = 'MAME asset cache disabled but forcing rebuilding.' + log_info(t) + kodi_dialog_OK(t) + + # --- Clean 'cache' directory JSON Asset files --- + log_info('Cleaning dir "{}"'.format(cfg.CACHE_DIR.getPath())) + pDialog = KodiProgressDialog() + pDialog.startProgress('Listing asset cache JSON files...') + file_list = os.listdir(cfg.CACHE_DIR.getPath()) + log_info('Found {} files'.format(len(file_list))) + deleted_items = 0 + pDialog.resetProgress('Cleaning asset cache JSON files...', len(file_list)) + for file in file_list: + pDialog.updateProgressInc() + if not file.endswith('_assets.json'): continue + full_path = os.path.join(cfg.CACHE_DIR.getPath(), file) + # log_debug('UNLINK "{}"'.format(full_path)) + os.unlink(full_path) + deleted_items += 1 + pDialog.endProgress() + log_info('Deleted {} files'.format(deleted_items)) + + # --- Build MAME asset cache --- + num_catalogs = len(cache_index_dic) + catalog_count = 1 + pDialog.startProgress('Building MAME asset cache') + for catalog_name in sorted(cache_index_dic): + catalog_index_dic = cache_index_dic[catalog_name] + catalog_all = db_get_cataloged_dic_all(cfg, catalog_name) + diag_t = 'Building MAME [COLOR orange]{}[/COLOR] asset cache ({} of {})...'.format( + catalog_name, catalog_count, num_catalogs) + pDialog.resetProgress(diag_t, len(catalog_index_dic)) + for catalog_key in catalog_index_dic: + pDialog.updateProgressInc() + hash_str = catalog_index_dic[catalog_key]['hash'] + # log_debug('db_build_asset_cache() Catalog "{}" --- Key "{}"'.format(catalog_name, catalog_key)) + # log_debug('db_build_asset_cache() hash {}'.format(hash_str)) + + # Build all machines cache + m_assets_all_dic = {} + for machine_name in catalog_all[catalog_key]: + m_assets_all_dic[machine_name] = assets_dic[machine_name] + ROMs_all_FN = cfg.CACHE_DIR.pjoin(hash_str + '_assets.json') + utils_write_JSON_file(ROMs_all_FN.getPath(), m_assets_all_dic, verbose = False) + catalog_count += 1 + pDialog.endProgress() + + # Update timestamp and save control_dic. + db_safe_edit(control_dic, 't_MAME_asset_cache_build', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + +def db_get_asset_cache_row(cfg, cache_index_dic, catalog_name, category_name): + hash_str = cache_index_dic[catalog_name][category_name]['hash'] + ROMs_all_FN = cfg.CACHE_DIR.pjoin(hash_str + '_assets.json') + + return utils_load_JSON_file(ROMs_all_FN.getPath()) + +# ------------------------------------------------------------------------------------------------- +# Load and save a bunch of JSON files +# ------------------------------------------------------------------------------------------------- +# +# Accepts a list of JSON files to be loaded. Displays a progress dialog. +# Returns a dictionary with the context of the loaded files. +# +def db_load_files(db_files): + log_debug('db_load_files() Loading {} JSON database files...'.format(len(db_files))) + db_dic = {} + d_text = 'Loading databases...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(db_files)) + for f_item in db_files: + dict_key, db_name, db_path = f_item + pDialog.updateProgressInc('{}\nDatabase [COLOR orange]{}[/COLOR]'.format(d_text, db_name)) + db_dic[dict_key] = utils_load_JSON_file(db_path) + pDialog.endProgress() + + return db_dic + +def db_save_files(db_files, json_write_func = utils_write_JSON_file): + log_debug('db_save_files() Saving {} JSON database files...'.format(len(db_files))) + d_text = 'Saving databases...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(db_files)) + for f_item in db_files: + dict_data, db_name, db_path = f_item + pDialog.updateProgressInc('{}\nDatabase [COLOR orange]{}[/COLOR]'.format(d_text, db_name)) + json_write_func(db_path, dict_data) + pDialog.endProgress() + +# ------------------------------------------------------------------------------------------------- +# Export stuff +# ------------------------------------------------------------------------------------------------- +def db_export_Read_Only_Launcher(export_FN, catalog_dic, machines, machines_render, assets_dic): + log_debug('db_export_Read_Only_Launcher() File "{}"'.format(export_FN.getPath())) + + # Create list of strings. + sl = [] + sl.append('<?xml version="1.0" encoding="utf-8" standalone="yes"?>') + sl.append('<!-- Exported by AML on {} -->'.format(time.strftime("%Y-%m-%d %H:%M:%S"))) + sl.append('<advanced_MAME_launcher_virtual_launcher>') + for m_name, r_name in catalog_dic.items(): + sl.append('<machine>') + sl.append(XML_text('name', m_name)) + sl.append(XML_text('description', machines_render[m_name]['description'])) + sl.append(XML_text('genre', machines_render[m_name]['genre'])) + sl.append(XML_text('year', machines_render[m_name]['year'])) + sl.append(XML_text('cabinet', assets_dic[m_name]['cabinet'])) + sl.append('</machine>') + sl.append('</advanced_MAME_launcher_virtual_launcher>') + utils_write_str_list_to_file(sl, export_FN) diff --git a/plugin.program.AML/resources/filters.py b/plugin.program.AML/resources/filters.py new file mode 100644 index 0000000000..30f698bc93 --- /dev/null +++ b/plugin.program.AML/resources/filters.py @@ -0,0 +1,1604 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher MAME filter engine. + +# --- Modules/packages in this plugin --- +from .constants import * +from .utils import * +from .misc import * +from .db import * +from .mame_misc import * + +# --- Python standard library --- +import xml.etree.ElementTree + +# ------------------------------------------------------------------------------------------------- +# Constants +# ------------------------------------------------------------------------------------------------- +OPTIONS_KEYWORK_LIST = [ + 'NoClones', + 'NoCoin', + 'NoCoinLess', + 'NoROMs', + 'NoCHDs', + 'NoSamples', + 'NoMature', + 'NoBIOS', + 'NoMechanical', + 'NoImperfect', + 'NoNonworking', + 'NoVertical', + 'NoHorizontal', + 'NoMissingROMs', + 'NoMissingCHDs', + 'NoMissingSamples', +] + +# ------------------------------------------------------------------------------------------------- +# Parse filter XML definition +# ------------------------------------------------------------------------------------------------- +# +# Strips a list of strings. +# +def _strip_str_list(t_list): + for i, s_t in enumerate(t_list): + t_list[i] = s_t.strip() + + return t_list + +# +# Returns a comma-separated string of values as a list of strings. +# +def _get_comma_separated_list(text_t): + if not text_t: + return [] + else: + return _strip_str_list(text_t.split(',')) + +# +# Parse a string 'XXXXXX with YYYYYY' and return a tuple. +# +def _get_change_tuple(text_t): + if not text_t: return () + # Returns a list of strings or list of tuples. + tuple_list = re.findall('(\w+) with (\w+)', text_t) + if tuple_list: + return tuple_list[0] + else: + log_error('_get_change_tuple() text_t = "{}"'.format(text_t)) + m = '(Exception) Cannot parse <Change> "{}"'.format(text_t) + log_error(m) + raise Addon_Error(m) + +# ------------------------------------------------------------------------------------------------- +# String Parser (SP) engine. Grammar token objects. +# Parser inspired by http://effbot.org/zone/simple-top-down-parsing.htm +# +# SP operators: and, or, not, has, lacks, literal. +# ------------------------------------------------------------------------------------------------- +debug_SP_parser = False +debug_SP_parse_exec = False + +class SP_literal_token: + def __init__(self, value): self.value = value + def nud(self): + if debug_SP_parser: log_debug('Call LITERAL token nud()') + return self + def exec_token(self): + if debug_SP_parser: log_debug('Executing LITERAL token value "{}"'.format(self.value)) + ret = self.value + if debug_SP_parser: log_debug('Token LITERAL returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return '<LITERAL "{}">'.format(self.value) + +class SP_operator_has_token: + lbp = 50 + def __init__(self): pass + def nud(self): + if debug_SP_parser: log_debug('Call HAS token nud()') + self.first = SP_expression(50) + return self + def exec_token(self): + if debug_SP_parser: log_debug('Executing HAS token') + literal_str = self.first.exec_token() + if type(literal_str) is not text_type: + raise SyntaxError("HAS token exec; expected string, got {}".format(type(literal_str))) + ret = True if SP_parser_search_string.find(literal_str) >= 0 else False + if debug_SP_parser: log_debug('Token HAS returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP has>" + +class SP_operator_lacks_token: + lbp = 50 + def __init__(self): pass + def nud(self): + if debug_SP_parser: log_debug('Call LACKS token nud()') + self.first = SP_expression(50) + return self + def exec_token(self): + if debug_SP_parser: log_debug('Executing LACKS token') + literal_str = self.first.exec_token() + if type(literal_str) is not text_type: + raise SyntaxError("LACKS token exec; expected string, got {}".format(type(literal_str))) + ret = False if SP_parser_search_string.find(literal_str) >= 0 else True + if debug_SP_parser: log_debug('Token LACKS returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP lacks>" + +class SP_operator_not_token: + lbp = 50 + def __init__(self): pass + def nud(self): + if debug_SP_parser: log_debug('Call NOT token nud()') + self.first = SP_expression(50) + return self + def exec_token(self): + if debug_SP_parser: log_debug('Executing NOT token') + exp_bool = self.first.exec_token() + if type(exp_bool) is not bool: + raise SyntaxError("NOT token exec; expected string, got {}".format(type(exp_bool))) + ret = not exp_bool + if debug_SP_parser: log_debug('Token NOT returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP not>" + +class SP_operator_and_token: + lbp = 10 + def __init__(self): pass + def led(self, left): + if debug_SP_parser: log_debug('Call AND token led()') + self.first = left + self.second = SP_expression(10) + return self + def exec_token(self): + if debug_SP_parser: log_debug('Executing AND token') + ret = self.first.exec_token() and self.second.exec_token() + if debug_SP_parser: log_debug('Token AND returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP and>" + +class SP_operator_or_token: + lbp = 10 + def __init__(self): pass + def led(self, left): + if debug_SP_parser: log_debug('Call OR token led()') + self.first = left + self.second = SP_expression(10) + return self + def exec_token(self): + if debug_SP_parser: log_debug('Executing OR token') + ret = self.first.exec_token() or self.second.exec_token() + if debug_SP_parser: log_debug('Token OR returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP or>" + +class SP_end_token: + lbp = 0 + def __init__(self): pass + def __repr__(self): return "<END token>" + +# ------------------------------------------------------------------------------------------------- +# String Parser (SP) Tokenizer +# See http://jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/ +# ------------------------------------------------------------------------------------------------- +SP_token_pat = re.compile("\s*(?:(and|or|not|has|lacks)|(\"[ \.\w_\-\&\/]+\")|([\.\w_\-\&]+))") + +def SP_tokenize(program): + # \s* -> Matches any number of blanks [ \t\n\r\f\v]. + # (?:...) -> A non-capturing version of regular parentheses. + # \w -> Matches [a-zA-Z0-9_] + for operator, q_string, string in SP_token_pat.findall(program): + if string: + yield SP_literal_token(string) + elif q_string: + if q_string[0] == '"': q_string = q_string[1:] + if q_string[-1] == '"': q_string = q_string[:-1] + yield SP_literal_token(q_string) + elif operator == "and": + yield SP_operator_and_token() + elif operator == "or": + yield SP_operator_or_token() + elif operator == "not": + yield SP_operator_not_token() + elif operator == "has": + yield SP_operator_has_token() + elif operator == "lacks": + yield SP_operator_lacks_token() + else: + raise SyntaxError("Unknown operator: '{}'".format(operator)) + yield SP_end_token() + +# ------------------------------------------------------------------------------------------------- +# String Parser (SP) inspired by http://effbot.org/zone/simple-top-down-parsing.htm +# ------------------------------------------------------------------------------------------------- +def SP_expression(rbp = 0): + global SP_token + + t = SP_token + SP_token = SP_next() + left = t.nud() + while rbp < SP_token.lbp: + t = SP_token + SP_token = SP_next() + left = t.led(left) + + return left + +def SP_parse_exec(program, search_string): + global SP_token, SP_next, SP_parser_search_string + + if debug_SP_parse_exec: + log_debug('SP_parse_exec() Initialising program execution') + log_debug('SP_parse_exec() Search string "{}"'.format(search_string)) + log_debug('SP_parse_exec() Program "{}"'.format(program)) + SP_parser_search_string = search_string + if ADDON_RUNNING_PYTHON_2: + SP_next = SP_tokenize(program).next + elif ADDON_RUNNING_PYTHON_3: + SP_next = SP_tokenize(program).__next__ + else: + raise TypeError('Undefined Python runtime version.') + SP_token = SP_next() + + # Old function parse_exec() + rbp = 0 + t = SP_token + SP_token = SP_next() + left = t.nud() + while rbp < SP_token.lbp: + t = SP_token + SP_token = SP_next() + left = t.led(left) + if debug_SP_parse_exec: + log_debug('SP_parse_exec() Init exec program in token {}'.format(left)) + + return left.exec_token() + +# ------------------------------------------------------------------------------------------------- +# List of String Parser (LSP) engine. Grammar token objects. +# Parser inspired by http://effbot.org/zone/simple-top-down-parsing.htm +# Also see test_parser_SP.py for more documentation. +# +# LSP operators: and, or, not, has, lacks, '(', ')', literal. +# ------------------------------------------------------------------------------------------------- +debug_LSP_parser = False +debug_LSP_parse_exec = False + +class LSP_literal_token: + def __init__(self, value): self.value = value + def nud(self): + if debug_LSP_parser: log_debug('Call LITERAL token nud()') + return self + def exec_token(self): + if debug_LSP_parser: log_debug('Executing LITERAL token value "{}"'.format(self.value)) + ret = self.value + if debug_LSP_parser: log_debug('Token LITERAL returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return '<LITERAL "{}">'.format(self.value) + +# id is a type object as return by type() +def LSP_advance(id = None): + global LSP_token + + if id and type(LSP_token) != id: + raise SyntaxError("Expected {}".format(type(LSP_token))) + LSP_token = LSP_next() + +class LSP_operator_open_par_token: + lbp = 0 + def __init__(self): pass + def nud(self): + if debug_LSP_parser: log_debug('Call ( token nud()') + expr = LSP_expression() + LSP_advance(LSP_operator_close_par_token) + return expr + def __repr__(self): return "<OP (>" + +class LSP_operator_close_par_token: + lbp = 0 + def __init__(self): pass + def __repr__(self): return "<OP )>" + +class LSP_operator_has_token: + lbp = 50 + def __init__(self): pass + def nud(self): + if debug_LSP_parser: log_debug('Call HAS token nud()') + self.first = LSP_expression(50) + return self + def exec_token(self): + if debug_LSP_parser: log_debug('Executing HAS token') + literal_str = self.first.exec_token() + if type(literal_str) is not text_type: + raise SyntaxError("HAS token exec; expected string, got {}".format(type(literal_str))) + ret = literal_str in LSP_parser_search_list + if debug_LSP_parser: log_debug('Token HAS returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP has>" + +class LSP_operator_lacks_token: + lbp = 50 + def __init__(self): pass + def nud(self): + if debug_LSP_parser: log_debug('Call LACKS token nud()') + self.first = LSP_expression(50) + return self + def exec_token(self): + if debug_LSP_parser: log_debug('Executing LACKS token') + literal_str = self.first.exec_token() + if type(literal_str) is not text_type: + raise SyntaxError("LACKS token exec; expected string, got {}".format(type(literal_str))) + ret = literal_str not in LSP_parser_search_list + if debug_LSP_parser: log_debug('Token LACKS returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP lacks>" + +class LSP_operator_not_token: + lbp = 50 + def __init__(self): pass + def nud(self): + if debug_LSP_parser: log_debug('Call NOT token nud()') + self.first = LSP_expression(50) + return self + def exec_token(self): + if debug_LSP_parser: log_debug('Executing NOT token') + exp_bool = self.first.exec_token() + if type(exp_bool) is not bool: + raise SyntaxError("NOT token exec; expected string, got {}".format(type(exp_bool))) + ret = not exp_bool + if debug_LSP_parser: log_debug('Token NOT returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP not>" + +class LSP_operator_and_token: + lbp = 10 + def __init__(self): pass + def led(self, left): + if debug_LSP_parser: log_debug('Call AND token led()') + self.first = left + self.second = LSP_expression(10) + return self + def exec_token(self): + if debug_LSP_parser: log_debug('Executing AND token') + ret = self.first.exec_token() and self.second.exec_token() + if debug_LSP_parser: log_debug('Token AND returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP and>" + +class LSP_operator_or_token: + lbp = 10 + def __init__(self): pass + def led(self, left): + if debug_LSP_parser: log_debug('Call OR token led()') + self.first = left + self.second = LSP_expression(10) + return self + def exec_token(self): + if debug_LSP_parser: log_debug('Executing OR token') + ret = self.first.exec_token() or self.second.exec_token() + if debug_LSP_parser: log_debug('Token OR returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "<OP or>" + +class LSP_end_token: + lbp = 0 + def __init__(self): pass + def __repr__(self): return "<END token>" + +# ------------------------------------------------------------------------------------------------- +# List of String Parser (LSP) Tokenizer +# See http://jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/ +# ------------------------------------------------------------------------------------------------- +LSP_token_pat = re.compile("\s*(?:(and|or|not|has|lacks|\(|\))|(\"[ \.\w_\-\&\/]+\")|([\.\w_\-\&]+))") + +def LSP_tokenize(program): + for operator, q_string, string in LSP_token_pat.findall(program): + if string: + yield LSP_literal_token(string) + elif q_string: + if q_string[0] == '"': q_string = q_string[1:] + if q_string[-1] == '"': q_string = q_string[:-1] + yield LSP_literal_token(q_string) + elif operator == "and": + yield LSP_operator_and_token() + elif operator == "or": + yield LSP_operator_or_token() + elif operator == "not": + yield LSP_operator_not_token() + elif operator == "has": + yield LSP_operator_has_token() + elif operator == "lacks": + yield LSP_operator_lacks_token() + elif operator == "(": + yield LSP_operator_open_par_token() + elif operator == ")": + yield LSP_operator_close_par_token() + else: + raise SyntaxError("Unknown operator: '{}'".format(operator)) + yield LSP_end_token() + +# ------------------------------------------------------------------------------------------------- +# List of String Parser (LSP) inspired by http://effbot.org/zone/simple-top-down-parsing.htm +# ------------------------------------------------------------------------------------------------- +def LSP_expression(rbp = 0): + global LSP_token + + t = LSP_token + LSP_token = LSP_next() + left = t.nud() + while rbp < LSP_token.lbp: + t = LSP_token + LSP_token = LSP_next() + left = t.led(left) + return left + +def LSP_parse_exec(program, search_list): + global LSP_token, LSP_next, LSP_parser_search_list + + if debug_LSP_parse_exec: + log_debug('LSP_parse_exec() Initialising program execution') + log_debug('LSP_parse_exec() Search "{}"'.format(text_type(search_list))) + log_debug('LSP_parse_exec() Program "{}"'.format(program)) + LSP_parser_search_list = search_list + if ADDON_RUNNING_PYTHON_2: + LSP_next = LSP_tokenize(program).next + elif ADDON_RUNNING_PYTHON_3: + LSP_next = LSP_tokenize(program).__next__ + else: + raise TypeError('Undefined Python runtime version.') + LSP_token = LSP_next() + + # Old function parse_exec(). + rbp = 0 + t = LSP_token + LSP_token = LSP_next() + left = t.nud() + while rbp < LSP_token.lbp: + t = LSP_token + LSP_token = LSP_next() + left = t.led(left) + if debug_LSP_parse_exec: + log_debug('LSP_parse_exec() Init exec program in token {}'.format(left)) + + return left.exec_token() + +# ------------------------------------------------------------------------------------------------- +# Year Parser (YP) engine. Grammar token objects. +# Parser inspired by http://effbot.org/zone/simple-top-down-parsing.htm +# See also test_parser_SP.py for more documentation. +# +# YP operators: ==, !=, >, <, >=, <=, and, or, not, '(', ')', literal. +# literal may be the special variable 'year' or a MAME number. +# ------------------------------------------------------------------------------------------------- +debug_YP_parser = False +debug_YP_parse_exec = False + +class YP_literal_token: + def __init__(self, value): self.value = value + def nud(self): return self + def exec_token(self): + if self.value == 'year': + ret = YP_year + else: + ret = int(self.value) + if debug_YP_parser: log_debug('Token LITERAL returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return '[LITERAL "{}"]'.format(self.value) + +def YP_advance(id = None): + global YP_token + + if id and type(YP_token) != id: + raise SyntaxError("Expected {}".format(type(YP_token))) + YP_token = YP_next() + +class YP_operator_open_par_token: + lbp = 0 + def __init__(self): pass + def nud(self): + expr = YP_expression() + YP_advance(YP_operator_close_par_token) + return expr + def __repr__(self): return "[OP (]" + +class YP_operator_close_par_token: + lbp = 0 + def __init__(self): pass + def __repr__(self): return "[OP )]" + +class YP_operator_not_token: + lbp = 60 + def __init__(self): pass + def nud(self): + self.first = YP_expression(50) + return self + def exec_token(self): + ret = not self.first.exec_token() + if debug_YP_parser: log_debug('Token NOT returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP not]" + +class YP_operator_and_token: + lbp = 10 + def __init__(self): pass + def led(self, left): + self.first = left + self.second = YP_expression(10) + return self + def exec_token(self): + ret = self.first.exec_token() and self.second.exec_token() + if debug_YP_parser: log_debug('Token AND returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP and]" + +class YP_operator_or_token: + lbp = 10 + def __init__(self): pass + def led(self, left): + self.first = left + self.second = YP_expression(10) + return self + def exec_token(self): + ret = self.first.exec_token() or self.second.exec_token() + if debug_YP_parser: log_debug('Token OR returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP or]" + +class YP_operator_equal_token: + lbp = 50 + def __init__(self): pass + def led(self, left): + self.first = left + self.second = YP_expression(10) + return self + def exec_token(self): + ret = self.first.exec_token() == self.second.exec_token() + if debug_YP_parser: log_debug('Token == returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP ==]" + +class YP_operator_not_equal_token: + lbp = 50 + def __init__(self): pass + def led(self, left): + self.first = left + self.second = YP_expression(10) + return self + def exec_token(self): + ret = self.first.exec_token() != self.second.exec_token() + if debug_YP_parser: log_debug('Token != returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP !=]" + +class YP_operator_great_than_token: + lbp = 50 + def __init__(self): pass + def led(self, left): + self.first = left + self.second = YP_expression(10) + return self + def exec_token(self): + ret = self.first.exec_token() > self.second.exec_token() + if debug_YP_parser: log_debug('Token > returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP >]" + +class YP_operator_less_than_token: + lbp = 50 + def __init__(self): pass + def led(self, left): + self.first = left + self.second = YP_expression(10) + return self + def exec_token(self): + ret = self.first.exec_token() < self.second.exec_token() + if debug_YP_parser: log_debug('Token < returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP <]" + +class YP_operator_great_or_equal_than_token: + lbp = 50 + def __init__(self): pass + def led(self, left): + self.first = left + self.second = YP_expression(10) + return self + def exec_token(self): + ret = self.first.exec_token() >= self.second.exec_token() + if debug_YP_parser: log_debug('Token >= returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP >=]" + +class YP_operator_less_or_equal_than_token: + lbp = 50 + def __init__(self): pass + def led(self, left): + self.first = left + self.second = YP_expression(10) + return self + def exec_token(self): + ret = self.first.exec_token() <= self.second.exec_token() + if debug_YP_parser: log_debug('Token <= returns {} "{}"'.format(type(ret), text_type(ret))) + return ret + def __repr__(self): return "[OP <=]" + +class YP_end_token: + lbp = 0 + def __init__(self): pass + def __repr__(self): return "[END token]" + +# ------------------------------------------------------------------------------------------------- +# Year Parser Tokenizer +# See http://jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/ +# ------------------------------------------------------------------------------------------------- +YP_token_pat = re.compile("\s*(?:(==|!=|>=|<=|>|<|and|or|not|\(|\))|([\w]+))") + +def YP_tokenize(program): + for operator, n_string in YP_token_pat.findall(program): + if n_string: yield YP_literal_token(n_string) + elif operator == "==": yield YP_operator_equal_token() + elif operator == "!=": yield YP_operator_not_equal_token() + elif operator == ">": yield YP_operator_great_than_token() + elif operator == "<": yield YP_operator_less_than_token() + elif operator == ">=": yield YP_operator_great_or_equal_than_token() + elif operator == "<=": yield YP_operator_less_or_equal_than_token() + elif operator == "and": yield YP_operator_and_token() + elif operator == "or": yield YP_operator_or_token() + elif operator == "not": yield YP_operator_not_token() + elif operator == "(": yield YP_operator_open_par_token() + elif operator == ")": yield YP_operator_close_par_token() + else: raise SyntaxError("Unknown operator: '{}'".format(operator)) + yield YP_end_token() + +# ------------------------------------------------------------------------------------------------- +# Year Parser (YP) inspired by http://effbot.org/zone/simple-top-down-parsing.htm +# ------------------------------------------------------------------------------------------------- +def YP_expression(rbp = 0): + global YP_token + + t = YP_token + YP_token = YP_next() + left = t.nud() + while rbp < YP_token.lbp: + t = YP_token + YP_token = YP_next() + left = t.led(left) + return left + +def YP_parse_exec(program, year_str): + global YP_token, YP_next, YP_year + + # --- Transform year_str to an integer. year_str may be ill formed --- + if re.findall(r'^[0-9]{4}$', year_str): + year = int(year_str) + elif re.findall(r'^[0-9]{4}\?$', year_str): + year = int(year_str[0:4]) + else: + year = 0 + + if debug_YP_parse_exec: + log_debug('YP_parse_exec() Initialising program execution') + log_debug('YP_parse_exec() year "{}"'.format(year)) + log_debug('YP_parse_exec() Program "{}"'.format(program)) + YP_year = year + if ADDON_RUNNING_PYTHON_2: + YP_next = YP_tokenize(program).next + elif ADDON_RUNNING_PYTHON_3: + YP_next = YP_tokenize(program).__next__ + else: + raise TypeError('Undefined Python runtime version.') + YP_token = YP_next() + + # Old function parse_exec(). + rbp = 0 + t = YP_token + YP_token = YP_next() + left = t.nud() + while rbp < YP_token.lbp: + t = YP_token + YP_token = YP_next() + left = t.led(left) + if debug_YP_parse_exec: + log_debug('YP_parse_exec() Init exec program in token {}'.format(left)) + + return left.exec_token() + +# ------------------------------------------------------------------------------------------------- +# MAME machine filters +# ------------------------------------------------------------------------------------------------- +# +# Default filter removes device machines. +# +def filter_mame_Default(mame_xml_dic): + log_debug('filter_mame_Default() Starting ...') + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = {} + for m_name in sorted(mame_xml_dic): + if mame_xml_dic[m_name]['isDevice']: + filtered_out_games += 1 + else: + machines_filtered_dic[m_name] = mame_xml_dic[m_name] + log_debug('filter_mame_Default() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Options_tag(mame_xml_dic, f_definition): + # log_debug('filter_mame_Options_tag() Starting ...') + options_list = f_definition['options'] + + if not options_list: + log_debug('filter_mame_Options_tag() Option list is empty.') + return mame_xml_dic + log_debug('Option list "{}"'.format(options_list)) + + # --- Compute bool variables --- + # This must match OPTIONS_KEYWORK_LIST + NoClones_bool = True if 'NoClones' in options_list else False + NoCoin_bool = True if 'NoCoin' in options_list else False + NoCoinLess_bool = True if 'NoCoinLess' in options_list else False + NoROMs_bool = True if 'NoROMs' in options_list else False + NoCHDs_bool = True if 'NoCHDs' in options_list else False + NoSamples_bool = True if 'NoSamples' in options_list else False + NoMature_bool = True if 'NoMature' in options_list else False + NoBIOS_bool = True if 'NoBIOS' in options_list else False + NoMechanical_bool = True if 'NoMechanical' in options_list else False + NoImperfect_bool = True if 'NoImperfect' in options_list else False + NoNonWorking_bool = True if 'NoNonworking' in options_list else False + NoVertical_bool = True if 'NoVertical' in options_list else False + NoHorizontal_bool = True if 'NoHorizontal' in options_list else False + # ROM scanner boolean filters. + NoMissingROMs_bool = True if 'NoMissingROMs' in options_list else False + NoMissingCHDs_bool = True if 'NoMissingCHDs' in options_list else False + NoMissingSamples_bool = True if 'NoMissingSamples' in options_list else False + + log_debug('NoClones_bool {}'.format(NoClones_bool)) + log_debug('NoCoin_bool {}'.format(NoCoin_bool)) + log_debug('NoCoinLess_bool {}'.format(NoCoinLess_bool)) + log_debug('NoROMs_bool {}'.format(NoROMs_bool)) + log_debug('NoCHDs_bool {}'.format(NoCHDs_bool)) + log_debug('NoSamples_bool {}'.format(NoSamples_bool)) + log_debug('NoMature_bool {}'.format(NoMature_bool)) + log_debug('NoBIOS_bool {}'.format(NoBIOS_bool)) + log_debug('NoMechanical_bool {}'.format(NoMechanical_bool)) + log_debug('NoImperfect_bool {}'.format(NoImperfect_bool)) + log_debug('NoNonWorking_bool {}'.format(NoNonWorking_bool)) + log_debug('NoVertical_bool {}'.format(NoVertical_bool)) + log_debug('NoHorizontal_bool {}'.format(NoHorizontal_bool)) + log_debug('NoMissingROMs_bool {}'.format(NoMissingROMs_bool)) + log_debug('NoMissingCHDs_bool {}'.format(NoMissingCHDs_bool)) + log_debug('NoMissingSamples_bool {}'.format(NoMissingSamples_bool)) + + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = {} + for m_name in sorted(mame_xml_dic): + # Remove Clone machines + if NoClones_bool and mame_xml_dic[m_name]['isClone']: + filtered_out_games += 1 + continue + # Remove Coin machines + if NoCoin_bool and mame_xml_dic[m_name]['coins'] > 0: + filtered_out_games += 1 + continue + # Remove CoinLess machines + if NoCoinLess_bool and mame_xml_dic[m_name]['coins'] == 0: + filtered_out_games += 1 + continue + # Remove ROM machines + if NoROMs_bool and mame_xml_dic[m_name]['hasROMs']: + filtered_out_games += 1 + continue + # Remove CHD machines + if NoCHDs_bool and mame_xml_dic[m_name]['hasCHDs']: + filtered_out_games += 1 + continue + # Remove Samples machines + if NoSamples_bool and mame_xml_dic[m_name]['hasSamples']: + filtered_out_games += 1 + continue + # Remove Mature machines + if NoMature_bool and mame_xml_dic[m_name]['isMature']: + filtered_out_games += 1 + continue + # Remove BIOS machines + if NoBIOS_bool and mame_xml_dic[m_name]['isBIOS']: + filtered_out_games += 1 + continue + # Remove Mechanical machines + if NoMechanical_bool and mame_xml_dic[m_name]['isMechanical']: + filtered_out_games += 1 + continue + # Remove Imperfect machines + if NoImperfect_bool and mame_xml_dic[m_name]['isImperfect']: + filtered_out_games += 1 + continue + # Remove NonWorking machines + if NoNonWorking_bool and mame_xml_dic[m_name]['isNonWorking']: + filtered_out_games += 1 + continue + # Remove Vertical machines + if NoVertical_bool and mame_xml_dic[m_name]['isVertical']: + filtered_out_games += 1 + continue + # Remove Horizontal machines + if NoHorizontal_bool and mame_xml_dic[m_name]['isHorizontal']: + filtered_out_games += 1 + continue + # Remove machines with Missing ROMs + if NoMissingROMs_bool and mame_xml_dic[m_name]['missingROMs']: + filtered_out_games += 1 + continue + # Remove machines with Missing CHDs + if NoMissingCHDs_bool and mame_xml_dic[m_name]['missingCHDs']: + filtered_out_games += 1 + continue + # Remove machines with Missing Samples + if NoMissingSamples_bool and mame_xml_dic[m_name]['missingSamples']: + filtered_out_games += 1 + continue + # If machine was not removed then add it + machines_filtered_dic[m_name] = mame_xml_dic[m_name] + log_debug( + 'filter_mame_Options_tag() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Driver_tag(mame_xml_dic, f_definition): + # log_debug('filter_mame_Driver_tag() Starting ...') + filter_expression = f_definition['driver'] + + if not filter_expression: + log_debug('filter_mame_Driver_tag() User wants all drivers') + return mame_xml_dic + log_debug('Expression "{}"'.format(filter_expression)) + + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = {} + for m_name in sorted(mame_xml_dic): + search_list = [ mame_xml_dic[m_name]['driver'] ] + bool_result = LSP_parse_exec(filter_expression, search_list) + if not bool_result: + filtered_out_games += 1 + else: + machines_filtered_dic[m_name] = mame_xml_dic[m_name] + log_debug('filter_mame_Driver_tag() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Manufacturer_tag(mame_xml_dic, f_definition): + # log_debug('filter_mame_Manufacturer_tag() Starting ...') + filter_expression = f_definition['manufacturer'] + + if not filter_expression: + log_debug('filter_mame_Manufacturer_tag() User wants all manufacturers') + return mame_xml_dic + log_debug('Expression "{}"'.format(filter_expression)) + + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = {} + for m_name in sorted(mame_xml_dic): + bool_result = SP_parse_exec(filter_expression, mame_xml_dic[m_name]['manufacturer']) + if not bool_result: + filtered_out_games += 1 + else: + machines_filtered_dic[m_name] = mame_xml_dic[m_name] + log_debug('filter_mame_Manufacturer_tag() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Genre_tag(mame_xml_dic, f_definition): + # log_debug('filter_mame_Genre_tag() Starting ...') + filter_expression = f_definition['genre'] + + if not filter_expression: + log_debug('filter_mame_Genre_tag() User wants all genres') + return mame_xml_dic + log_debug('Expression "{}"'.format(filter_expression)) + + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = {} + for m_name in sorted(mame_xml_dic): + search_list = [ mame_xml_dic[m_name]['genre'] ] + bool_result = LSP_parse_exec(filter_expression, search_list) + if not bool_result: + filtered_out_games += 1 + else: + machines_filtered_dic[m_name] = mame_xml_dic[m_name] + log_debug('filter_mame_Genre_tag() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Controls_tag(mame_xml_dic, f_definition): + # log_debug('filter_mame_Controls_tag() Starting ...') + filter_expression = f_definition['controls'] + + if not filter_expression: + log_debug('filter_mame_Controls_tag() User wants all genres') + return mame_xml_dic + log_debug('Expression "{}"'.format(filter_expression)) + + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = {} + for m_name in sorted(mame_xml_dic): + bool_result = LSP_parse_exec(filter_expression, mame_xml_dic[m_name]['control_list']) + if not bool_result: + filtered_out_games += 1 + else: + machines_filtered_dic[m_name] = mame_xml_dic[m_name] + log_debug('filter_mame_Controls_tag() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_PluggableDevices_tag(mame_xml_dic, f_definition): + # log_debug('filter_mame_PluggableDevices_tag() Starting ...') + filter_expression = f_definition['pluggabledevices'] + + if not filter_expression: + log_debug('filter_mame_PluggableDevices_tag() User wants all genres') + return mame_xml_dic + log_debug('Expression "{}"'.format(filter_expression)) + + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = {} + for m_name in sorted(mame_xml_dic): + # --- Update search list variable and call parser to evaluate expression --- + bool_result = LSP_parse_exec(filter_expression, mame_xml_dic[m_name]['pluggable_device_list']) + if not bool_result: + filtered_out_games += 1 + else: + machines_filtered_dic[m_name] = mame_xml_dic[m_name] + log_debug( + 'filter_mame_PluggableDevices_tag() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Year_tag(mame_xml_dic, f_definition): + # log_debug('filter_mame_Year_tag() Starting ...') + filter_expression = f_definition['year'] + + if not filter_expression: + log_debug('filter_mame_Year_tag() User wants all genres') + return mame_xml_dic + log_debug('Expression "{}"'.format(filter_expression)) + + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = {} + for m_name in sorted(mame_xml_dic): + # --- Update search int variable and call parser to evaluate expression --- + bool_result = YP_parse_exec(filter_expression, mame_xml_dic[m_name]['year']) + if not bool_result: + filtered_out_games += 1 + else: + machines_filtered_dic[m_name] = mame_xml_dic[m_name] + log_debug('filter_mame_Year_tag() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Include_tag(mame_xml_dic, f_definition, machines_dic): + # log_debug('filter_mame_Include_tag() Starting ...') + log_debug('filter_mame_Include_tag() Include machines {}'.format(text_type(f_definition['include']))) + added_machines = 0 + machines_filtered_dic = mame_xml_dic.copy() + # If no machines to include then skip processing + if not f_definition['include']: + log_debug('filter_mame_Include_tag() No machines to include. Exiting.') + return machines_filtered_dic + # First traverse all MAME machines, then traverse list of strings to include. + for m_name in sorted(machines_dic): + for f_name in f_definition['include']: + if f_name == m_name: + log_debug('filter_mame_Include_tag() Matched machine {}'.format(f_name)) + if f_name in machines_filtered_dic: + log_debug('filter_mame_Include_tag() Machine {} already in filtered list'.format(f_name)) + else: + log_debug('filter_mame_Include_tag() Adding machine {}'.format(f_name)) + machines_filtered_dic[m_name] = machines_dic[m_name] + added_machines += 1 + log_debug('filter_mame_Include_tag() Initial {} | '.format(len(mame_xml_dic)) + \ + 'Added {} | '.format(added_machines) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Exclude_tag(mame_xml_dic, f_definition): + # log_debug('filter_mame_Exclude_tag() Starting ...') + log_debug('filter_mame_Exclude_tag() Exclude machines {}'.format(text_type(f_definition['exclude']))) + initial_num_games = len(mame_xml_dic) + filtered_out_games = 0 + machines_filtered_dic = mame_xml_dic.copy() + # If no machines to exclude then skip processing + if not f_definition['exclude']: + log_debug('filter_mame_Exclude_tag() No machines to exclude. Exiting.') + return machines_filtered_dic + # First traverse current set of machines, then traverse list of strings to include. + for m_name in sorted(mame_xml_dic): + for f_name in f_definition['exclude']: + if f_name == m_name: + log_debug('filter_mame_Exclude_tag() Matched machine {}'.format(f_name)) + log_debug('filter_mame_Exclude_tag() Deleting machine {}'.format(f_name)) + del machines_filtered_dic[f_name] + filtered_out_games += 1 + log_debug('filter_mame_Exclude_tag() Initial {} | '.format(initial_num_games) + \ + 'Removed {} | '.format(filtered_out_games) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +def filter_mame_Change_tag(mame_xml_dic, f_definition, machines_dic): + # log_debug('filter_mame_Change_tag() Starting ...') + log_debug('filter_mame_Change_tag() Change machines {}'.format(text_type(f_definition['change']))) + initial_num_games = len(mame_xml_dic) + changed_machines = 0 + machines_filtered_dic = mame_xml_dic.copy() + # If no machines to change then skip processing + if not f_definition['change']: + log_debug('filter_mame_Change_tag() No machines to swap. Exiting.') + return machines_filtered_dic + # First traverse current set of machines, then traverse list of strings to include. + for m_name in sorted(mame_xml_dic): + for (f_name, new_name) in f_definition['change']: + if f_name == m_name: + log_debug('filter_mame_Change_tag() Matched machine {}'.format(f_name)) + if new_name in machines_dic: + log_debug('filter_mame_Change_tag() Changing machine {} with {}'.format(f_name, new_name)) + del machines_filtered_dic[f_name] + machines_filtered_dic[new_name] = machines_dic[new_name] + changed_machines += 1 + else: + log_warning('filter_mame_Change_tag() New machine {} not found on MAME machines.'.format(new_name)) + log_debug('filter_mame_Change_tag() Initial {} | '.format(initial_num_games) + \ + 'Changed {} | '.format(changed_machines) + \ + 'Remaining {}'.format(len(machines_filtered_dic))) + + return machines_filtered_dic + +# ------------------------------------------------------------------------------------------------- +# Build MAME custom filters +# ------------------------------------------------------------------------------------------------- +# +# Returns a list of dictionaries, each dictionary has the filter definition. +# +def filter_parse_XML(fname_str): + __debug_xml_parser = False + + # If XML has errors (invalid characters, etc.) this will rais exception 'err' + XML_FN = FileName(fname_str) + if not XML_FN.exists(): + kodi_dialog_OK('Custom filter XML file not found.') + return + log_debug('filter_parse_XML() Reading XML OP "{}"'.format(XML_FN.getOriginalPath())) + log_debug('filter_parse_XML() Reading XML P "{}"'.format(XML_FN.getPath())) + try: + xml_tree = ET.parse(XML_FN.getPath()) + except IOError as ex: + log_error('(Exception) {}'.format(ex)) + log_error('(Exception) Syntax error in the XML file definition') + raise Addon_Error('(Exception) ET.parse(XML_FN.getPath()) failed.') + xml_root = xml_tree.getroot() + define_dic = {} + filters_list = [] + for root_element in xml_root: + if __debug_xml_parser: log_debug('Root child {}'.format(root_element.tag)) + + if root_element.tag == 'DEFINE': + name_str = root_element.attrib['name'] + define_str = root_element.text if root_element.text else '' + log_debug('DEFINE "{}" := "{}"'.format(name_str, define_str)) + define_dic[name_str] = define_str + elif root_element.tag == 'MAMEFilter': + this_filter_dic = { + 'name' : '', + 'plot' : '', + 'options' : [], # List of strings + 'driver' : '', + 'manufacturer' : '', + 'genre' : '', + 'controls' : '', + 'pluggabledevices' : '', + 'year' : '', + 'include' : [], # List of strings + 'exclude' : [], # List of strings + 'change' : [], # List of tuples (change_orig string, change_dest string) + } + for filter_element in root_element: + if ADDON_RUNNING_PYTHON_2: + # In Python 2 filter_element.text has type str and not Unicode. + text_t = text_type(filter_element.text if filter_element.text else '') + elif ADDON_RUNNING_PYTHON_3: + text_t = filter_element.text if filter_element.text else '' + else: + raise TypeError('Undefined Python runtime version.') + # log_debug('text_t "{}" type "{}"'.format(text_t, type(text_t))) + if filter_element.tag == 'Name': + this_filter_dic['name'] = text_t + elif filter_element.tag == 'Plot': + this_filter_dic['plot'] = text_t + elif filter_element.tag == 'Options': + t_list = _get_comma_separated_list(text_t) + if t_list: + this_filter_dic['options'].extend(t_list) + elif filter_element.tag == 'Driver': + this_filter_dic['driver'] = text_t + elif filter_element.tag == 'Manufacturer': + this_filter_dic['manufacturer'] = text_t + elif filter_element.tag == 'Genre': + this_filter_dic['genre'] = text_t + elif filter_element.tag == 'Controls': + this_filter_dic['controls'] = text_t + elif filter_element.tag == 'PluggableDevices': + this_filter_dic['pluggabledevices'] = text_t + elif filter_element.tag == 'Year': + this_filter_dic['year'] = text_t + elif filter_element.tag == 'Include': + t_list = _get_comma_separated_list(text_t) + if t_list: this_filter_dic['include'].extend(t_list) + elif filter_element.tag == 'Exclude': + t_list = _get_comma_separated_list(text_t) + if t_list: this_filter_dic['exclude'].extend(t_list) + elif filter_element.tag == 'Change': + tuple_t = _get_change_tuple(text_t) + if tuple_t: this_filter_dic['change'].append(tuple_t) + else: + m = '(Exception) Unrecognised tag <{}>'.format(filter_element.tag) + log_debug(m) + raise Addon_Error(m) + log_debug('Adding filter "{}"'.format(this_filter_dic['name'])) + filters_list.append(this_filter_dic) + + # Resolve DEFINE tags (substitute by the defined value) + for f_definition in filters_list: + for initial_str in define_dic: + final_str = define_dic[initial_str] + f_definition['driver'] = f_definition['driver'].replace(initial_str, final_str) + f_definition['manufacturer'] = f_definition['manufacturer'].replace(initial_str, final_str) + f_definition['genre'] = f_definition['genre'].replace(initial_str, final_str) + f_definition['controls'] = f_definition['controls'].replace(initial_str, final_str) + f_definition['pluggabledevices'] = f_definition['pluggabledevices'].replace(initial_str, final_str) + # Replace strings in list of strings. + for i, s_t in enumerate(f_definition['include']): + f_definition['include'][i] = s_t.replace(initial_str, final_str) + for i, s_t in enumerate(f_definition['exclude']): + f_definition['exclude'][i] = s_t.replace(initial_str, final_str) + # for i, s_t in enumerate(f_definition['change']): + # f_definition['change'][i] = s_t.replace(initial_str, final_str) + + return filters_list + +# Makes a list of all machines and add flags for easy filtering. +# Returns a dictionary of dictionaries, indexed by the machine name. +# This includes all MAME machines, including parents and clones. +def filter_get_filter_DB(cfg, db_dic_in): + machine_main_dic = db_dic_in['machines'] + renderdb_dic = db_dic_in['renderdb'] + assetdb_dic = db_dic_in['assetdb'] + machine_archives_dic = db_dic_in['machine_archives'] + + main_filter_dic = {} + # Sets are used to check the integrity of the filters defined in the XML. + drivers_set = set() + genres_set = set() + controls_set = set() + pdevices_set = set() + # Histograms + # The driver histogram is too big and unuseful. + genres_drivers_dic = {} + controls_drivers_dic = {} + pdevices_drivers_dic = {} + pDialog = KodiProgressDialog() + pDialog.startProgress('Building filter database...', len(machine_main_dic)) + for m_name in machine_main_dic: + pDialog.updateProgressInc() + if 'att_coins' in machine_main_dic[m_name]['input']: + coins = machine_main_dic[m_name]['input']['att_coins'] + else: + coins = 0 + if m_name in machine_archives_dic: + hasROMs = True if machine_archives_dic[m_name]['ROMs'] else False + else: + hasROMs = False + if m_name in machine_archives_dic: + hasCHDs = True if machine_archives_dic[m_name]['CHDs'] else False + else: + hasCHDs = False + if m_name in machine_archives_dic: + hasSamples = True if machine_archives_dic[m_name]['Samples'] else False + else: + hasSamples = False + # If the machine has no displays then both isVertical and isHorizontal are False. + isVertical, isHorizontal = False, False + for drotate in machine_main_dic[m_name]['display_rotate']: + if drotate == '0' or drotate == '180': + isHorizontal = True + elif drotate == '90' or drotate == '270': + isVertical = True + + # ROM/CHD/Sample scanner flags. See funcion db_initial_flags() + missingROMs = True if assetdb_dic[m_name]['flags'][0] == 'r' else False + missingCHDs = True if assetdb_dic[m_name]['flags'][1] == 'c' else False + missingSamples = True if assetdb_dic[m_name]['flags'][2] == 's' else False + + # Fix controls to match "Machines by Controls (Compact)" filter + if machine_main_dic[m_name]['input']: + raw_control_list = [ + ctrl_dic['type'] for ctrl_dic in machine_main_dic[m_name]['input']['control_list'] + ] + else: + raw_control_list = [] + pretty_control_type_list = misc_improve_mame_control_type_list(raw_control_list) + control_list = misc_compress_mame_item_list_compact(pretty_control_type_list) + if not control_list: control_list = [ 'None' ] + + # Fix this to match "Machines by Pluggable Devices (Compact)" filter + raw_device_list = [ device['att_type'] for device in machine_main_dic[m_name]['devices'] ] + pretty_device_list = misc_improve_mame_device_list(raw_device_list) + pluggable_device_list = misc_compress_mame_item_list_compact(pretty_device_list) + if not pluggable_device_list: pluggable_device_list = [ 'None' ] + + # --- Build filtering dictionary --- + main_filter_dic[m_name] = { + # --- Default filters --- + 'isDevice' : renderdb_dic[m_name]['isDevice'], + # --- <Option> filters --- + 'isClone' : True if renderdb_dic[m_name]['cloneof'] else False, + 'coins' : coins, + 'hasROMs' : hasROMs, + 'hasCHDs' : hasCHDs, + 'hasSamples' : hasSamples, + 'isMature' : renderdb_dic[m_name]['isMature'], + 'isBIOS' : renderdb_dic[m_name]['isBIOS'], + 'isMechanical' : machine_main_dic[m_name]['isMechanical'], + 'isImperfect' : True if renderdb_dic[m_name]['driver_status'] == 'imperfect' else False, + 'isNonWorking' : True if renderdb_dic[m_name]['driver_status'] == 'preliminary' else False, + 'isHorizontal' : isHorizontal, + 'isVertical' : isVertical, + # --- <Option> scanner-related filters --- + 'missingROMs' : missingROMs, + 'missingCHDs' : missingCHDs, + 'missingSamples' : missingSamples, + # --- Other filters --- + 'driver' : machine_main_dic[m_name]['sourcefile'], + 'manufacturer' : renderdb_dic[m_name]['manufacturer'], + 'genre' : renderdb_dic[m_name]['genre'], + 'control_list' : control_list, + 'pluggable_device_list' : pluggable_device_list, + 'year' : renderdb_dic[m_name]['year'], + } + + # --- Make sets of drivers, genres, controls, and pluggable devices --- + mdict = main_filter_dic[m_name] + drivers_set.add(mdict['driver']) + genres_set.add(mdict['genre']) + for control in mdict['control_list']: controls_set.add(control) + for device in mdict['pluggable_device_list']: pdevices_set.add(device) + # --- Histograms --- + if mdict['genre'] in genres_drivers_dic: + genres_drivers_dic[mdict['genre']] += 1 + else: + genres_drivers_dic[mdict['genre']] = 1 + for control in mdict['control_list']: + if control in controls_drivers_dic: + controls_drivers_dic[control] += 1 + else: + controls_drivers_dic[control] = 1 + for device in mdict['pluggable_device_list']: + if device in pdevices_drivers_dic: + pdevices_drivers_dic[device] += 1 + else: + pdevices_drivers_dic[device] = 1 + pDialog.endProgress() + + # --- Write statistics report --- + log_info('Writing report "{}"'.format(cfg.REPORT_CF_HISTOGRAMS_PATH.getPath())) + rslist = [ + '*** Advanced MAME Launcher MAME histogram report ***', + '', + ] + table_str = [ + ['right', 'right'], + ['Genre', 'Number of machines'], + ] + for dname, dnumber in sorted(genres_drivers_dic.items(), key = lambda x: x[1], reverse = True): + table_str.append(['{}'.format(dname), '{}'.format(dnumber)]) + rslist.extend(text_render_table(table_str)) + rslist.append('') + + table_str = [ + ['right', 'right'], + ['Control', 'Number of machines'], + ] + for dname, dnumber in sorted(controls_drivers_dic.items(), key = lambda x: x[1], reverse = True): + table_str.append(['{}'.format(dname), '{}'.format(dnumber)]) + rslist.extend(text_render_table(table_str)) + rslist.append('') + + table_str = [ + ['right', 'right'], + ['Device', 'Number of machines'], + ] + for dname, dnumber in sorted(pdevices_drivers_dic.items(), key = lambda x: x[1], reverse = True): + table_str.append(['{}'.format(dname), '{}'.format(dnumber)]) + rslist.extend(text_render_table(table_str)) + rslist.append('') + utils_write_slist_to_file(cfg.REPORT_CF_HISTOGRAMS_PATH.getPath(), rslist) + + sets_dic = { + 'drivers_set' : drivers_set, + 'genres_set' : genres_set, + 'controls_set' : controls_set, + 'pdevices_set' : pdevices_set, + } + + return (main_filter_dic, sets_dic) + +# +# Returns a tuple (filter_list, options_dic). +# +def filter_custom_filters_load_XML(cfg, db_dic_in, main_filter_dic, sets_dic): + control_dic = db_dic_in['control_dic'] + filter_list = [] + options_dic = { + # No errors by default until an error is found. + 'XML_errors' : False, + } + + # --- Open custom filter XML and parse it --- + cf_XML_path_str = cfg.settings['filter_XML'] + log_debug('cf_XML_path_str = "{}"'.format(cf_XML_path_str)) + if not cf_XML_path_str: + log_debug('Using default XML custom filter.') + XML_FN = cfg.CUSTOM_FILTER_PATH + else: + log_debug('Using user-defined in addon settings XML custom filter.') + XML_FN = FileName(cf_XML_path_str) + log_debug('filter_custom_filters_load_XML() Reading XML OP "{}"'.format(XML_FN.getOriginalPath())) + log_debug('filter_custom_filters_load_XML() Reading XML P "{}"'.format(XML_FN.getPath())) + try: + filter_list = filter_parse_XML(XML_FN.getPath()) + except Addon_Error as ex: + kodi_notify_warn('{}'.format(ex)) + return (filter_list, options_dic) + else: + log_debug('Filter XML read succesfully.') + + # --- Check XML for errors and write report --- + # Filters sorted as defined in the XML. + OPTIONS_KEYWORK_SET = set(OPTIONS_KEYWORK_LIST) + r_full = [] + for filter_dic in filter_list: + c_list = [] + + # Check 1) Keywords in <Options> are correct. + for option_keyword in filter_dic['options']: + if option_keyword not in OPTIONS_KEYWORK_SET: + c_list.append('<Options> keywork "{}" unrecognised.'.format(option_keyword)) + + # Check 2) Drivers in <Driver> exist. <Driver> uses the LSP parser. + keyword_list = [] + for token in SP_tokenize(filter_dic['driver']): + if isinstance(token, SP_literal_token): + keyword_list.append(token.value) + for dname in keyword_list: + if dname not in sets_dic['drivers_set']: + c_list.append('<Driver> "{}" not found.'.format(dname)) + + # Check 3) Genres in <Genre> exist. <Genre> uses the LSP parser. + keyword_list = [] + for token in LSP_tokenize(filter_dic['genre']): + if isinstance(token, SP_literal_token): + keyword_list.append(token.value) + for dname in keyword_list: + if dname not in sets_dic['genres_set']: + c_list.append('<Genre> "{}" not found.'.format(dname)) + + # Check 4) Controls in <Controls> exist. <Controls> uses the LSP parser. + keyword_list = [] + for token in SP_tokenize(filter_dic['controls']): + if isinstance(token, SP_literal_token): + keyword_list.append(token.value) + for dname in keyword_list: + if dname not in sets_dic['controls_set']: + c_list.append('<Controls> "{}" not found.'.format(dname)) + + # Check 5) Plugabble devices in <PluggableDevices> exist. + # <PluggableDevices> uses the LSP parser. + keyword_list = [] + for token in SP_tokenize(filter_dic['pluggabledevices']): + if isinstance(token, SP_literal_token): + keyword_list.append(token.value) + for dname in keyword_list: + if dname not in sets_dic['pdevices_set']: + c_list.append('<PluggableDevices> "{}" not found.'.format(dname)) + + # Check 6) Machines in <Include> exist. + for m_name in filter_dic['include']: + if m_name not in main_filter_dic: + c_list.append('<Include> machine "{}" not found.'.format(m_name)) + + # Check 7) Machines in <Exclude> exist. + for m_name in filter_dic['exclude']: + if m_name not in main_filter_dic: + c_list.append('<Exclude> machine "{}" not found.'.format(m_name)) + + # Check 8) Machines in <Change> exist. + for change_tuple in filter_dic['change']: + if change_tuple[0] not in main_filter_dic: + c_list.append('<Change> machine "{}" not found.'.format(change_tuple[0])) + if change_tuple[1] not in main_filter_dic: + c_list.append('<Change> machine "{}" not found.'.format(change_tuple[1])) + + # Build report + r_full.append('Filter "{}"'.format(filter_dic['name'])) + if not c_list: + r_full.append('No issues found.') + else: + r_full.extend(c_list) + options_dic['XML_errors'] = True # Error found, set the flag. + r_full.append('') + + # --- Write MAME scanner reports --- + log_info('Writing report "{}"'.format(cfg.REPORT_CF_XML_SYNTAX_PATH.getPath())) + report_slist = [ + '*** Advanced MAME Launcher MAME custom filter XML syntax report ***', + 'There are {} custom filters defined.'.format(len(filter_list)), + 'XML "{}"'.format(XML_FN.getOriginalPath()), + '', + ] + report_slist.extend(r_full) + utils_write_slist_to_file(cfg.REPORT_CF_XML_SYNTAX_PATH.getPath(), report_slist) + + return (filter_list, options_dic) + +# +# filter_index_dic = { +# 'name' : { +# 'display_name' : Unicode, +# 'num_machines' : int, +# 'num_parents' : int, +# 'order' : int, +# 'plot' : Unicode, +# 'rom_DB_noext' : Unicode, +# } +# } +# AML_DATA_DIR/filters/'rom_DB_noext'_render.json -> machine_render = {} +# AML_DATA_DIR/filters/'rom_DB_noext'_assets.json -> asset_dic = {} +# +def filter_build_custom_filters(cfg, db_dic_in, filter_list, main_filter_dic): + control_dic = db_dic_in['control_dic'] + machines_dic = db_dic_in['machines'] + renderdb_dic = db_dic_in['renderdb'] + assetdb_dic = db_dic_in['assetdb'] + + # --- Clean 'filters' directory JSON files --- + log_info('filter_build_custom_filters() Cleaning dir "{}"'.format(cfg.FILTERS_DB_DIR.getPath())) + pDialog = KodiProgressDialog() + pDialog.startProgress('Listing filter JSON files...') + file_list = os.listdir(cfg.FILTERS_DB_DIR.getPath()) + num_files = len(file_list) + if num_files > 1: + log_info('Found {} files'.format(num_files)) + processed_items = 0 + pDialog.resetProgress('Cleaning filter JSON files...', num_files) + for file in file_list: + pDialog.updateProgress(processed_items) + if file.endswith('.json'): + full_path = os.path.join(cfg.FILTERS_DB_DIR.getPath(), file) + # log_debug('UNLINK "{}"'.format(full_path)) + os.unlink(full_path) + processed_items += 1 + pDialog.endProgress() + + # --- Report header --- + r_full = [ + 'Number of machines {}'.format(len(main_filter_dic)), + 'Number of filters {}'.format(len(filter_list)), + '', + ] + + # --- Traverse list of filters, build filter index and compute filter list --- + Filters_index_dic = {} + processed_items = 0 + diag_t = 'Building custom MAME filters...' + pDialog.startProgress(diag_t, len(filter_list)) + for f_definition in filter_list: + # --- Initialise --- + f_name = f_definition['name'] + log_debug('filter_build_custom_filters() Processing filter "{}"'.format(f_name)) + # log_debug('f_definition = {}'.format(text_type(f_definition))) + pDialog.updateProgressInc('{}\nFilter "{}"'.format(diag_t, f_name)) + + # --- Do filtering --- + filtered_machine_dic = filter_mame_Default(main_filter_dic) + filtered_machine_dic = filter_mame_Options_tag(filtered_machine_dic, f_definition) + filtered_machine_dic = filter_mame_Driver_tag(filtered_machine_dic, f_definition) + filtered_machine_dic = filter_mame_Manufacturer_tag(filtered_machine_dic, f_definition) + filtered_machine_dic = filter_mame_Genre_tag(filtered_machine_dic, f_definition) + filtered_machine_dic = filter_mame_Controls_tag(filtered_machine_dic, f_definition) + filtered_machine_dic = filter_mame_PluggableDevices_tag(filtered_machine_dic, f_definition) + filtered_machine_dic = filter_mame_Year_tag(filtered_machine_dic, f_definition) + filtered_machine_dic = filter_mame_Include_tag(filtered_machine_dic, f_definition, machines_dic) + filtered_machine_dic = filter_mame_Exclude_tag(filtered_machine_dic, f_definition) + filtered_machine_dic = filter_mame_Change_tag(filtered_machine_dic, f_definition, machines_dic) + + # --- Make indexed catalog --- + filtered_render_dic = {} + filtered_assets_dic = {} + for m_name in filtered_machine_dic: + filtered_render_dic[m_name] = renderdb_dic[m_name] + filtered_assets_dic[m_name] = assetdb_dic[m_name] + rom_DB_noext = hashlib.md5(f_name.encode('utf-8')).hexdigest() + this_filter_idx_dic = { + 'display_name' : f_definition['name'], + 'num_machines' : len(filtered_render_dic), + 'order' : processed_items, + 'plot' : f_definition['plot'], + 'rom_DB_noext' : rom_DB_noext + } + Filters_index_dic[f_name] = this_filter_idx_dic + processed_items += 1 + + # --- Save filter database --- + writing_ticks_start = time.time() + output_FN = cfg.FILTERS_DB_DIR.pjoin(rom_DB_noext + '_render.json') + utils_write_JSON_file(output_FN.getPath(), filtered_render_dic, verbose = False) + output_FN = cfg.FILTERS_DB_DIR.pjoin(rom_DB_noext + '_assets.json') + utils_write_JSON_file(output_FN.getPath(), filtered_assets_dic, verbose = False) + writing_ticks_end = time.time() + writing_time = writing_ticks_end - writing_ticks_start + log_debug('JSON writing time {:.4f} s'.format(writing_time)) + + # --- Report --- + r_full.append('Filter "{}"'.format(f_name)) + r_full.append('{} machines'.format(len(filtered_machine_dic))) + r_full.append('') + + # --- Save custom filter index --- + utils_write_JSON_file(cfg.FILTERS_INDEX_PATH.getPath(), Filters_index_dic) + pDialog.endProgress() + + # --- Update timestamp --- + db_safe_edit(control_dic, 't_Custom_Filter_build', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + + # --- Write MAME scanner reports --- + log_info('Writing report "{}"'.format(cfg.REPORT_CF_DB_BUILD_PATH.getPath())) + report_slist = [ + '*** Advanced MAME Launcher MAME custom filter XML syntax report ***', + 'File "{}"'.format(cfg.REPORT_CF_DB_BUILD_PATH.getPath()), + '', + ] + report_slist.extend(r_full) + utils_write_slist_to_file(cfg.REPORT_CF_DB_BUILD_PATH.getPath(), report_slist) diff --git a/plugin.program.AML/resources/graphics.py b/plugin.program.AML/resources/graphics.py new file mode 100644 index 0000000000..ad5b6a0dec --- /dev/null +++ b/plugin.program.AML/resources/graphics.py @@ -0,0 +1,1115 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher graphics plotting functions. + +# --- Modules/packages in this addon --- +from .constants import * +from .utils import * +from .db import * + +# --- Python standard library --- +import collections +import time +import xml.etree.ElementTree as ET +try: + from PIL import Image + from PIL import ImageDraw + from PIL import ImageFont + PILLOW_AVAILABLE = True +except: + PILLOW_AVAILABLE = False + +# ------------------------------------------------------------------------------------------------ +# ETA +# ------------------------------------------------------------------------------------------------ +# Global variables to keep ETA status. +ETA_total_items = 0 +ETA_actual_processed_items = 0 +ETA_total_build_time = 0.0 +ETA_average_build_time = 0.0 + +# +# Returns initial ETA_str +# +def ETA_reset(total_items): + global ETA_total_items + global ETA_actual_processed_items + global ETA_total_build_time + global ETA_average_build_time + + ETA_total_items = total_items + ETA_actual_processed_items = 0 + ETA_total_build_time = 0.0 + ETA_average_build_time = 0.0 + + return 'calculating' + +# +# BUILD_SUCCESS True if image was generated correctly (time is accurate) +# +def ETA_update(build_OK_flag, total_processed_items, build_time): + global ETA_actual_processed_items + global ETA_total_build_time + global ETA_average_build_time + + if build_OK_flag: + ETA_actual_processed_items += 1 + ETA_total_build_time += build_time + ETA_average_build_time = ETA_total_build_time / ETA_actual_processed_items + remaining_items = ETA_total_items - total_processed_items + # log_debug('build_time {}'.format(build_time)) + # log_debug('ETA_average_build_time {}'.format(ETA_average_build_time)) + # log_debug('ETA_actual_processed_items {}'.format(ETA_actual_processed_items)) + # log_debug('total_processed_items {}'.format(total_processed_items)) + # log_debug('remaining items {}'.format(remaining_items)) + if ETA_average_build_time > 0: + ETA_s = remaining_items * ETA_average_build_time + hours, minutes, seconds = int(ETA_s // 3600), int((ETA_s % 3600) // 60), int(ETA_s % 60) + ETA_str = '{0:02d}:{1:02d}:{2:02d}'.format(hours, minutes, seconds) + else: + ETA_str = 'calculating' + + return ETA_str + +# ------------------------------------------------------------------------------------------------ +# Math functions +# ------------------------------------------------------------------------------------------------ +# Here is a more elegant and scalable solution, imo. It'll work for any nxn matrix and +# you may find use for the other methods. Note that getMatrixInverse(m) takes in an +# array of arrays as input. +def math_MatrixTranspose(X): + # return map(list, zip(*X)) + return [[X[j][i] for j in range(len(X))] for i in range(len(X[0]))] + +def math_MatrixMinor(m, i, j): + return [row[:j] + row[j+1:] for row in (m[:i]+m[i+1:])] + +def math_MatrixDeterminant(m): + # Base case for 2x2 matrix + if len(m) == 2: + return m[0][0]*m[1][1]-m[0][1]*m[1][0] + + determinant = 0 + for c in range(len(m)): + determinant += ((-1)**c)*m[0][c]*math_MatrixDeterminant(math_MatrixMinor(m,0,c)) + + return determinant + +def math_MatrixInverse(m): + determinant = math_MatrixDeterminant(m) + + # Special case for 2x2 matrix: + if len(m) == 2: + return [ + [m[1][1]/determinant, -1*m[0][1]/determinant], + [-1*m[1][0]/determinant, m[0][0]/determinant], + ] + + # Find matrix of cofactors + cofactors = [] + for r in range(len(m)): + cofactorRow = [] + for c in range(len(m)): + minor = math_MatrixMinor(m,r,c) + cofactorRow.append(((-1)**(r+c)) * math_MatrixDeterminant(minor)) + cofactors.append(cofactorRow) + cofactors = math_MatrixTranspose(cofactors) + for r in range(len(cofactors)): + for c in range(len(cofactors)): + cofactors[r][c] = cofactors[r][c]/determinant + + return cofactors + +# Both A and B have sizes NxM where N, M >= 2 (list of lists of floats). +def math_MatrixProduct(A, B): + return [[sum(a*b for a,b in zip(A_row, B_col)) for B_col in zip(*B)] for A_row in A] + +# A is a MxN matrix, B is a Nx1 matrix, result is a Mx1 matrix given as a list. +# Returns a list with the result. Note that this list corresponds to a column matrix. +def math_MatrixProduct_Column(A, B): + return [sum(a*b for a,b in zip(A_row, B)) for A_row in A] + +# ------------------------------------------------------------------------------------------------ +# Auxiliar functions +# ------------------------------------------------------------------------------------------------ +# +# Scales and centers img into a box of size (box_x_size, box_y_size). +# Scaling keeps original img aspect ratio. +# Returns an image of size (box_x_size, box_y_size) +# +def resize_proportional(img, layout, dic_key, CANVAS_COLOR = (0, 0, 0)): + box_x_size = layout[dic_key]['width'] + box_y_size = layout[dic_key]['height'] + # log_debug('resize_proportional() Initialising ...') + # log_debug('img X_size = {} | Y_size = {}'.format(img.size[0], img.size[1])) + # log_debug('box X_size = {} | Y_size = {}'.format(box_x_size, box_y_size)) + + # --- First try to fit X dimension --- + # log_debug('resize_proportional() Fitting X dimension') + wpercent = (box_x_size / float(img.size[0])) + hsize = int((float(img.size[1]) * float(wpercent))) + r_x_size = box_x_size + r_y_size = hsize + x_offset = 0 + y_offset = int((box_y_size - r_y_size) / 2) + # log_debug('resize X_size = {} | Y_size = {}'.format(r_x_size, r_y_size)) + # log_debug('resize x_offset = {} | y_offset = {}'.format(x_offset, y_offset)) + + # --- Second try to fit Y dimension --- + if y_offset < 0: + # log_debug('Fitting Y dimension') + hpercent = (box_y_size / float(img.size[1])) + wsize = int((float(img.size[0]) * float(hpercent))) + r_x_size = wsize + r_y_size = box_y_size + x_offset = int((box_x_size - r_x_size) / 2) + y_offset = 0 + # log_debug('resize X_size = {} | Y_size = {}'.format(r_x_size, r_y_size)) + # log_debug('resize x_offset = {} | y_offset = {}'.format(x_offset, y_offset)) + + # >> Create a new image and paste original image centered. + canvas_img = Image.new('RGB', (box_x_size, box_y_size), CANVAS_COLOR) + # >> Resize and paste + img = img.resize((r_x_size, r_y_size), Image.ANTIALIAS) + canvas_img.paste(img, (x_offset, y_offset, x_offset + r_x_size, y_offset + r_y_size)) + + return canvas_img + +def paste_image(img, img_title, layout, dic_key): + box = ( + layout[dic_key]['left'], + layout[dic_key]['top'], + layout[dic_key]['left'] + layout[dic_key]['width'], + layout[dic_key]['top'] + layout[dic_key]['height'] + ) + img.paste(img_title, box) + + return img + +# source_coords is the four vertices in the current plane and target_coords contains +# four vertices in the resulting plane. +# coords is a list of tuples (x, y) +# +def perspective_coeffs(source_coords, target_coords): + A = [] + for s, t in zip(source_coords, target_coords): + s = [float(i) for i in s] + t = [float(i) for i in t] + A.append([t[0], t[1], 1, 0, 0, 0, -s[0]*t[0], -s[0]*t[1]]) + A.append([0, 0, 0, t[0], t[1], 1, -s[1]*t[0], -s[1]*t[1]]) + # print('A =\n{}'.format(pprint.pformat(A))) + + B = [float(item) for sublist in source_coords for item in sublist] + # print('B =\n{}'.format(pprint.pformat(B))) + + A_T = math_MatrixTranspose(A) + A_T_A = math_MatrixProduct(A_T, A) + A_T_A_inv = math_MatrixInverse(A_T_A) + A_T_A_inv_A_T = math_MatrixProduct(A_T_A_inv, A_T) + res = math_MatrixProduct_Column(A_T_A_inv_A_T, B) + # print('res =\n{}'.format(pprint.pformat(res))) + + return res + +def project_texture(img_boxfront, coordinates, CANVAS_SIZE, rotate = False): + # print('project_texture() BEGIN ...') + + # --- Rotate 90 degress clockwise --- + if rotate: + # print('Rotating image 90 degress clockwise') + img_boxfront = img_boxfront.rotate(-90, expand = True) + # img_boxfront.save('rotated.png') + + # --- Info --- + width, height = img_boxfront.size + # print('Image width {}, height {}'.format(width, height)) + + # --- Transform --- + # Conver list of lists to list of tuples + n_coords = [(int(c[0]), int(c[1])) for c in coordinates] + # top/left, top/right, bottom/right, bottom/left + coeffs = perspective_coeffs([(0, 0), (width, 0), (width, height), (0, height)], n_coords) + # print(coeffs) + img_t = img_boxfront.transform(CANVAS_SIZE, Image.PERSPECTIVE, coeffs, Image.BICUBIC) + + # --- Add polygon with alpha channel for blending --- + # In the alpha channel 0 means transparent and 255 opaque. + mask = Image.new('L', CANVAS_SIZE, color = 0) + draw = ImageDraw.Draw(mask) + # print(n_coords) + draw.polygon(n_coords, fill = 255) + img_t.putalpha(mask) + + return img_t + +# ------------------------------------------------------------------------------------------------ +# Default templates and cached data +# ------------------------------------------------------------------------------------------------ +# Cache font objects in global variables. +# Used in mame.py, mame_build_fanart() and mame_build_SL_fanart() +font_mono = None +font_mono_SL = None +font_mono_item = None +font_mono_debug = None + +# --- Fanart layout --- +MAME_layout_example = { + 'Title' : {'width' : 450, 'height' : 450, 'left' : 50, 'top' : 50}, + 'Snap' : {'width' : 450, 'height' : 450, 'left' : 50, 'top' : 550}, + 'Flyer' : {'width' : 450, 'height' : 450, 'left' : 1420, 'top' : 50}, + 'Cabinet' : {'width' : 300, 'height' : 425, 'left' : 1050, 'top' : 625}, + 'Artpreview' : {'width' : 450, 'height' : 550, 'left' : 550, 'top' : 500}, + 'PCB' : {'width' : 300, 'height' : 300, 'left' : 1500, 'top' : 525}, + 'Clearlogo' : {'width' : 450, 'height' : 200, 'left' : 1400, 'top' : 850}, + 'CPanel' : {'width' : 300, 'height' : 100, 'left' : 1050, 'top' : 500}, + 'Marquee' : {'width' : 800, 'height' : 275, 'left' : 550, 'top' : 200}, + 'MachineName' : {'left' : 550, 'top' : 50, 'fontsize' : 72}, +} + +MAME_layout_assets = { + 'Title' : 'title', + 'Snap' : 'snap', + 'Flyer' : 'flyer', + 'Cabinet' : 'cabinet', + 'Artpreview' : 'artpreview', + 'PCB' : 'PCB', + 'Clearlogo' : 'clearlogo', + 'CPanel' : 'cpanel', + 'Marquee' : 'marquee', +} + +SL_layout_example = { + 'Title' : {'width' : 600, 'height' : 600, 'left' : 690, 'top' : 430}, + 'Snap' : {'width' : 600, 'height' : 600, 'left' : 1300, 'top' : 430}, + 'BoxFront' : {'width' : 650, 'height' : 980, 'left' : 30, 'top' : 50}, + 'SLName' : {'left' : 730, 'top' : 90, 'fontsize' : 76}, + 'ItemName' : {'left' : 730, 'top' : 180, 'fontsize' : 76}, +} + +SL_layout_assets = { + 'Title' : 'title', + 'Snap' : 'snap', + 'BoxFront' : 'boxfront', +} + +# ------------------------------------------------------------------------------------------------ +# Graphics high level interface functions +# ------------------------------------------------------------------------------------------------ +# +# Rebuild Fanart for a given MAME machine. +# Returns True if the Fanart was built succesfully, False if error. +# +def graphs_build_MAME_Fanart(cfg, layout, m_name, assets_dic, Fanart_FN, + CANVAS_COLOR = (0, 0, 0), test_flag = False): + global font_mono + global font_mono_debug + canvas_size = (1920, 1080) + canvas_bg_color = (0, 0, 0) + color_white = (255, 255, 255) + t_color_fg = (255, 255, 0) + t_color_bg = (102, 102, 0) + + # Quickly check if machine has valid assets, and skip fanart generation if not. + # log_debug('graphs_build_MAME_Fanart() Building fanart for machine {}'.format(m_name)) + machine_has_valid_assets = False + for asset_key in MAME_layout_assets: + asset_db_name = MAME_layout_assets[asset_key] + m_assets = assets_dic[m_name] + if m_assets[asset_db_name]: + machine_has_valid_assets = True + break + if not machine_has_valid_assets: return False + + # --- If font object does not exists open font an cache it. --- + if not font_mono: + log_debug('graphs_build_MAME_Fanart() Creating font_mono object') + log_debug('graphs_build_MAME_Fanart() Loading "{}"'.format(cfg.MONO_FONT_PATH.getPath())) + font_mono = ImageFont.truetype(cfg.MONO_FONT_PATH.getPath(), layout['MachineName']['fontsize']) + if not font_mono_debug: + log_debug('graphs_build_MAME_Fanart() Creating font_mono_debug object') + log_debug('graphs_build_MAME_Fanart() Loading "{}"'.format(cfg.MONO_FONT_PATH.getPath())) + font_mono_debug = ImageFont.truetype(cfg.MONO_FONT_PATH.getPath(), 44) + + # --- Create fanart canvas --- + fanart_img = Image.new('RGB', canvas_size, canvas_bg_color) + draw = ImageDraw.Draw(fanart_img) + + # --- Draw assets according to layout --- + # layout is an ordered dictionary, so the assets are draw in the order they appear + # in the XML file. + img_index = 1 + # log_debug(text_type(layout)) + for asset_key in layout: + # log_debug('{0:<11} initialising'.format(asset_key)) + m_assets = assets_dic[m_name] + if asset_key == 'MachineName': + t_left = layout['MachineName']['left'] + t_top = layout['MachineName']['top'] + draw.text((t_left, t_top), m_name, color_white, font_mono) + else: + asset_db_name = MAME_layout_assets[asset_key] + if not m_assets[asset_db_name]: + # log_debug('{0:<10} DB empty'.format(asset_db_name)) + continue + Asset_FN = FileName(m_assets[asset_db_name]) + if not Asset_FN.exists(): + # log_debug('{0:<10} file not found'.format(asset_db_name)) + continue + # log_debug('{0:<10} found'.format(asset_db_name)) + # Sometimes PIL_resize_proportional() fails. + # File "~/plugin.program.AML.dev/resources/mame.py", line 3017, in PIL_resize_proportional + # img = img.resize((r_x_size, r_y_size), Image.ANTIALIAS) + # File "/usr/lib/python2.7/dist-packages/PIL/Image.py", line 1804, in resize + # self.load() + # File "/usr/lib/python2.7/dist-packages/PIL/ImageFile.py", line 252, in load + # self.load_end() + # File "/usr/lib/python2.7/dist-packages/PIL/PngImagePlugin.py", line 680, in load_end + # self.png.call(cid, pos, length) + # File "/usr/lib/python2.7/dist-packages/PIL/PngImagePlugin.py", line 140, in call + # return getattr(self, "chunk_" + cid.decode('ascii'))(pos, length) + # AttributeError: 'PngStream' object has no attribute 'chunk_tIME' + # If so, report the machine that produces the fail and do not generate the + # Fanart. + try: + img_asset = Image.open(Asset_FN.getPath()) + img_asset = resize_proportional(img_asset, layout, asset_key, CANVAS_COLOR) + except AttributeError: + a = 'graphs_build_MAME_Fanart() Exception AttributeError' + b = 'in m_name {}, asset_key {}'.format(m_name, asset_key) + log_error(a) + log_error(b) + else: + fanart_img = paste_image(fanart_img, img_asset, layout, asset_key) + # In debug mode print asset name and draw order. + if test_flag: + t_off = 15 + bg_off = 2 + t_bg_coord = ( + layout[asset_key]['left'] + t_off + bg_off, + layout[asset_key]['top'] + t_off + bg_off) + t_coord = (layout[asset_key]['left'] + t_off, layout[asset_key]['top'] + t_off) + debug_text = '{} {}'.format(img_index, asset_key) + # Draw text background first, then front text to create a nice effect. + draw.text(t_bg_coord, debug_text, t_color_bg, font_mono_debug) + draw.text(t_coord, debug_text, t_color_fg, font_mono_debug) + img_index += 1 + + # --- Save fanart and update database --- + # log_debug('graphs_build_MAME_Fanart() Saving Fanart "{}"'.format(Fanart_FN.getPath())) + fanart_img.save(Fanart_FN.getPath()) + assets_dic[m_name]['fanart'] = Fanart_FN.getPath() + + # Fanart succesfully built. + return True + +# +# Rebuild Fanart for a given SL item +# Returns True if the Fanart was built succesfully, False if error. +# +def graphs_build_SL_Fanart(cfg, layout, SL_name, m_name, assets_dic, Fanart_FN, + CANVAS_COLOR = (0, 0, 0), test_flag = False): + global font_mono_SL + global font_mono_item + global font_mono_debug + canvas_size = (1920, 1080) + canvas_bg_color = (0, 0, 0) + color_white = (255, 255, 255) + t_color_fg = (255, 255, 0) + t_color_bg = (102, 102, 0) + + # Quickly check if machine has valid assets, and skip fanart generation if not. + # log_debug('graphs_build_SL_Fanart() Building fanart for SL {} item {}'.format(SL_name, m_name)) + machine_has_valid_assets = False + for asset_key in SL_layout_assets: + asset_db_name = SL_layout_assets[asset_key] + m_assets = assets_dic[m_name] + if m_assets[asset_db_name]: + machine_has_valid_assets = True + break + if not machine_has_valid_assets: return False + + # If font object does not exists open font an cache it. + if not font_mono_SL: + log_debug('graphs_build_SL_Fanart() Creating font_mono_SL object') + log_debug('graphs_build_SL_Fanart() Loading "{}"'.format(cfg.MONO_FONT_PATH.getPath())) + font_mono_SL = ImageFont.truetype(cfg.MONO_FONT_PATH.getPath(), layout['SLName']['fontsize']) + if not font_mono_item: + log_debug('graphs_build_SL_Fanart() Creating font_mono_item object') + log_debug('graphs_build_SL_Fanart() Loading "{}"'.format(cfg.MONO_FONT_PATH.getPath())) + font_mono_item = ImageFont.truetype(cfg.MONO_FONT_PATH.getPath(), layout['ItemName']['fontsize']) + if not font_mono_debug: + log_debug('graphs_build_SL_Fanart() Creating font_mono_debug object') + log_debug('graphs_build_SL_Fanart() Loading "{}"'.format(cfg.MONO_FONT_PATH.getPath())) + font_mono_debug = ImageFont.truetype(cfg.MONO_FONT_PATH.getPath(), 44) + + # --- Create fanart canvas --- + fanart_img = Image.new('RGB', canvas_size, canvas_bg_color) + draw = ImageDraw.Draw(fanart_img) + + # --- Draw assets according to layout --- + # layout is an ordered dictionary, so the assets are draw in the order they appear + # in the XML file. + img_index = 1 + for asset_key in layout: + # log_debug('{0:<10} initialising'.format(asset_key)) + m_assets = assets_dic[m_name] + if asset_key == 'SLName' or asset_key == 'ItemName': + t_left = layout[asset_key]['left'] + t_top = layout[asset_key]['top'] + if asset_key == 'SLName': name = SL_name + elif asset_key == 'ItemName': name = m_name + else: raise TypeError + draw.text((t_left, t_top), name, color_white, font_mono_SL) + else: + asset_db_name = SL_layout_assets[asset_key] + if not m_assets[asset_db_name]: + # log_debug('{0:<10} DB empty'.format(asset_db_name)) + continue + Asset_FN = FileName(m_assets[asset_db_name]) + if not Asset_FN.exists(): + # log_debug('{0:<10} file not found'.format(asset_db_name)) + continue + # log_debug('{0:<10} found'.format(asset_db_name)) + img_asset = Image.open(Asset_FN.getPath()) + img_asset = resize_proportional(img_asset, layout, asset_key, CANVAS_COLOR) + fanart_img = paste_image(fanart_img, img_asset, layout, asset_key) + # In debug mode print asset name and draw order. + if test_flag: + t_off = 15 + bg_off = 2 + t_bg_coord = ( + layout[asset_key]['left'] + t_off + bg_off, + layout[asset_key]['top'] + t_off + bg_off) + t_coord = (layout[asset_key]['left'] + t_off, layout[asset_key]['top'] + t_off) + debug_text = '{} {}'.format(img_index, asset_key) + # Draw text background first, then front text to create a nice effect. + draw.text(t_bg_coord, debug_text, t_color_bg, font_mono_debug) + draw.text(t_coord, debug_text, t_color_fg, font_mono_debug) + img_index += 1 + # --- Save fanart and update database --- + # log_debug('graphs_build_SL_Fanart() Saving Fanart "{}"'.format(Fanart_FN.getPath())) + fanart_img.save(Fanart_FN.getPath()) + assets_dic[m_name]['fanart'] = Fanart_FN.getPath() + + # Fanart succesfully built. + return True + +# +# Builds a MAME or SL 3D Box. +# +def graphs_build_MAME_3DBox(cfg, coord_dic, SL_name, m_name, assets_dic, image_FN, + CANVAS_COLOR = (0, 0, 0), test_flag = False): + global font_mono + global font_mono_debug + FONT_SIZE = 90 + CANVAS_SIZE = (1000, 1500) + # CANVAS_BG_COLOR = (50, 50, 75) if test_flag else (0, 0, 0) + CANVAS_BG_COLOR = (0, 0, 0) + FRONTBOX_BG_COLOR = (200, 100, 100) + SPINE_BG_COLOR = (100, 200, 100) + MAME_logo_FN = cfg.ADDON_CODE_DIR.pjoin('media/MAME_clearlogo.png') + + # --- If font object does not exists open font an cache it. --- + if not font_mono: + log_debug('graphs_build_MAME_3DBox() Creating font_mono object') + log_debug('graphs_build_MAME_3DBox() Loading "{}"'.format(cfg.MONO_FONT_PATH.getPath())) + font_mono = ImageFont.truetype(cfg.MONO_FONT_PATH.getPath(), 90) + if test_flag and not font_mono_debug: + log_debug('graphs_build_MAME_3DBox() Creating font_mono_debug object') + log_debug('graphs_build_MAME_3DBox() Loading "{}"'.format(cfg.MONO_FONT_PATH.getPath())) + font_mono_debug = ImageFont.truetype(cfg.MONO_FONT_PATH.getPath(), 40) + + # --- Open assets --- + # MAME 3D Box requires Flyer and (Clearlogo or Marquee) + # SL 3D Box requires Boxfront (not clearlogos available for SLs). + if SL_name == 'MAME': + # Check Flyer exists. + # Check Clearlogo or Marquee exists. + if not assets_dic[m_name]['flyer']: return False + if not assets_dic[m_name]['clearlogo'] and not assets_dic[m_name]['marquee']: + return False + # Try to open the Flyer. + try: + img_flyer = Image.open(assets_dic[m_name]['flyer']) + except: + return False + # Try to open the Clearlogo or Marquee if Clearlogo not available. + try: + img_clearlogo = Image.open(assets_dic[m_name]['clearlogo']) + except: + try: + img_clearlogo = Image.open(assets_dic[m_name]['marquee']) + except: + return False + else: + # Check Boxfront exists. + if not assets_dic[m_name]['boxfront']: return False + # Try to open the Boxfront as flyer. + try: + img_flyer = Image.open(assets_dic[m_name]['boxfront']) + except: + return False + + # --- Create 3dbox canvas --- + # Create RGB image with alpha channel. + # Canvas size of destination transformation must have the same size as the final canvas. + canvas = Image.new('RGBA', CANVAS_SIZE, CANVAS_BG_COLOR) + + # --- Frontbox --- + img_front = Image.new('RGBA', CANVAS_SIZE, FRONTBOX_BG_COLOR) + img_t = project_texture(img_front, coord_dic['Frontbox'], CANVAS_SIZE) + canvas.paste(img_t, mask = img_t) + + # --- Spine --- + img_spine = Image.new('RGBA', CANVAS_SIZE, SPINE_BG_COLOR) + img_t = project_texture(img_spine, coord_dic['Spine'], CANVAS_SIZE) + canvas.paste(img_t, mask = img_t) + + # --- Flyer image --- + # At this point img_flyer is present and opened. + img_t = project_texture(img_flyer, coord_dic['Flyer'], CANVAS_SIZE) + try: + canvas.paste(img_t, mask = img_t) + except ValueError: + log_error('graphs_build_MAME_3DBox() Exception ValueError in Front Flyer') + log_error('SL_name = {}, m_name = {}'.format(SL_name, m_name)) + + # --- Spine game clearlogo --- + # Skip Spine Clearlogo in SLs 3D Boxes. + if SL_name == 'MAME': + img_t = project_texture(img_clearlogo, coord_dic['Clearlogo'], CANVAS_SIZE, rotate = True) + try: + canvas.paste(img_t, mask = img_t) + except ValueError: + log_error('graphs_build_MAME_3DBox() Exception ValueError in Spine Clearlogo') + log_error('SL_name = {}, m_name = {}'.format(SL_name, m_name)) + + # --- MAME background --- + img_mame = Image.open(MAME_logo_FN.getPath()) + img_t = project_texture(img_mame, coord_dic['Clearlogo_MAME'], CANVAS_SIZE, rotate = True) + canvas.paste(img_t, mask = img_t) + + # --- Machine name --- + img_name = Image.new('RGBA', (1000, 100), (0, 0, 0)) + draw = ImageDraw.Draw(img_name) + draw.text((5, 0), '{} {}'.format(SL_name, m_name), (255, 255, 255), font = font_mono) + img_t = project_texture(img_name, coord_dic['Front_Title'], CANVAS_SIZE) + canvas.paste(img_t, mask = img_t) + + # --- Model data in debug mode --- + if test_flag: + data = coord_dic['data'] + C_WHITE = (255, 255, 255) + C_BLACK = (0, 0, 0) + BOX_SIZE = (300, 200) + PASTE_POINT = (680, 1280) + img_name = Image.new('RGBA', BOX_SIZE, C_BLACK) + draw = ImageDraw.Draw(img_name) + draw.text((10, 0), 'angleX {}'.format(data['angleX']), C_WHITE, font = font_mono_debug) + draw.text((10, 35), 'angleY {}'.format(data['angleY']), C_WHITE, font = font_mono_debug) + draw.text((10, 70), 'angleZ {}'.format(data['angleZ']), C_WHITE, font = font_mono_debug) + draw.text((10, 105), 'FOV {}'.format(data['fov']), C_WHITE, font = font_mono_debug) + draw.text((10, 140), 'd {}'.format(data['viewer_distance']), C_WHITE, font = font_mono_debug) + box = (PASTE_POINT[0], PASTE_POINT[1], PASTE_POINT[0]+BOX_SIZE[0], PASTE_POINT[1]+BOX_SIZE[1]) + canvas.paste(img_name, box, mask = img_name) + + # --- Save fanart and update database --- + # log_debug('graphs_build_MAME_3DBox() Saving Fanart "{}"'.format(image_FN.getPath())) + canvas.save(image_FN.getPath()) + assets_dic[m_name]['3dbox'] = image_FN.getPath() + + # 3D Box was sucessfully generated. Return true to estimate ETA. + return True + +# +# Returns an Ordered dictionary with the layout of the fanart. +# The Ordered dictionary is to keep the order of the tags in the XML +# +def graphs_load_MAME_Fanart_template(Template_FN): + # Load XML file. + layout = collections.OrderedDict() + if not os.path.isfile(Template_FN.getPath()): return None + log_debug('graphs_load_MAME_Fanart_template() Loading XML "{}"'.format(Template_FN.getPath())) + try: + xml_tree = ET.parse(Template_FN.getPath()) + except IOError as E: + return None + xml_root = xml_tree.getroot() + + # --- Parse XML file --- + art_list = ['Title', 'Snap', 'Flyer', 'Cabinet', 'Artpreview', 'PCB', 'Clearlogo', 'CPanel', 'Marquee'] + art_tag_list = ['width', 'height', 'left', 'top'] + text_list = ['MachineName'] + test_tag_list = ['left', 'top', 'fontsize'] + for root_element in xml_root: + # log_debug('Root child {}'.format(root_element.tag)) + if root_element.tag in art_list: + art_dic = {key : 0 for key in art_tag_list} + for art_child in root_element: + if art_child.tag in art_tag_list: + art_dic[art_child.tag] = int(art_child.text) + else: + log_error('Inside root tag <{}>'.format(root_element.tag)) + log_error('Unknown tag <{}>'.format(art_child.tag)) + return None + layout[root_element.tag] = art_dic + elif root_element.tag in text_list: + text_dic = {key : 0 for key in test_tag_list} + for art_child in root_element: + if art_child.tag in test_tag_list: + text_dic[art_child.tag] = int(art_child.text) + else: + log_error('Inside root tag <{}>'.format(root_element.tag)) + log_error('Unknown tag <{}>'.format(art_child.tag)) + return None + layout[root_element.tag] = text_dic + else: + log_error('Unknown root tag <{}>'.format(root_element.tag)) + return None + + return layout + +# Returns a dictionary with all the data necessary to build the fanarts. +def graphs_load_MAME_Fanart_stuff(cfg, st_dic, BUILD_MISSING): + data_dic = { + 'BUILD_MISSING' : BUILD_MISSING, + } + + # --- If artwork directory not configured abort --- + if not cfg.settings['assets_path']: + kodi_set_error_status(st_dic, 'Asset directory not configured. Aborting Fanart generation.') + return + + # --- If Fanart directory doesn't exist create it --- + Asset_path_FN = FileName(cfg.settings['assets_path']) + Fanart_path_FN = Asset_path_FN.pjoin('fanarts') + if not Fanart_path_FN.isdir(): + log_info('Creating MAME Fanart dir "{}"'.format(Fanart_path_FN.getPath())) + Fanart_path_FN.makedirs() + data_dic['Fanart_path_FN'] = Fanart_path_FN + + # --- Load Fanart template from XML file --- + Template_FN = cfg.ADDON_CODE_DIR.pjoin('templates/AML-MAME-Fanart-template.xml') + layout = graphs_load_MAME_Fanart_template(Template_FN) + # log_debug(text_type(layout)) + if not layout: + kodi_set_error_status(st_dic, 'Error loading XML MAME Fanart layout.') + return + data_dic['layout'] = layout + + # --- Load Assets DB --- + pDialog = KodiProgressDialog() + pDialog.startProgress('Loading MAME asset database...') + assetdb_dic = utils_load_JSON_file(cfg.ASSET_DB_PATH.getPath()) + pDialog.endProgress() + data_dic['assetdb'] = assetdb_dic + + return data_dic + +# Builds or rebuilds missing MAME Fanarts. +# Caller code is responsible for updating caches. +def graphs_build_MAME_Fanart_all(cfg, st_dic, data_dic): + # Traverse all machines and build fanart from other pieces of artwork + pDialog_canceled = False + pDialog = KodiProgressDialog() + total_machines, processed_machines = len(data_dic['assetdb']), 0 + ETA_str = ETA_reset(total_machines) + diag_t = 'Building MAME machine Fanarts...' + pDialog.startProgress(diag_t, total_machines) + for m_name in sorted(data_dic['assetdb']): + build_time_start = time.time() + pDialog.updateProgress(processed_machines, '{}\nETA {} machine {}'.format(diag_t, ETA_str, m_name)) + if pDialog.isCanceled(): + pDialog_canceled = True + # kodi_dialog_OK('Fanart generation was canceled by the user.') + break + # If build missing Fanarts was chosen only build fanart if file cannot be found. + Fanart_FN = data_dic['Fanart_path_FN'].pjoin('{}.png'.format(m_name)) + if data_dic['BUILD_MISSING']: + if Fanart_FN.exists(): + data_dic['assetdb'][m_name]['fanart'] = Fanart_FN.getPath() + build_OK_flag = False + else: + build_OK_flag = graphs_build_MAME_Fanart(cfg, + data_dic['layout'], m_name, data_dic['assetdb'], Fanart_FN) + else: + build_OK_flag = graphs_build_MAME_Fanart(cfg, + data_dic['layout'], m_name, data_dic['assetdb'], Fanart_FN) + processed_machines += 1 + build_time_end = time.time() + build_time = build_time_end - build_time_start + # Only update ETA if Fanart was successfully build. + ETA_str = ETA_update(build_OK_flag, processed_machines, build_time) + pDialog.endProgress() + + # Save MAME assets DB + pDialog.startProgress('Saving MAME asset database...') + utils_write_JSON_file(cfg.ASSET_DB_PATH.getPath(), data_dic['assetdb']) + pDialog.endProgress() + + # Update MAME Fanart build timestamp + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + db_safe_edit(control_dic, 't_MAME_fanart_build', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + + # Inform user. + if pDialog_canceled: + kodi_notify('MAME Fanart building stopped. Partial progress saved.') + else: + kodi_notify('MAME Fanart building finished') + +# +# Returns an Ordered dictionary with the layout of the fanart. +# The Ordered dictionary is to keep the order of the tags in the XML +# +def graphs_load_SL_Fanart_template(Template_FN): + # Load XML file. + layout = collections.OrderedDict() + if not os.path.isfile(Template_FN.getPath()): return None + log_debug('mame_load_SL_Fanart_template() Loading XML "{}"'.format(Template_FN.getPath())) + try: + xml_tree = ET.parse(Template_FN.getPath()) + except IOError as E: + return None + xml_root = xml_tree.getroot() + + # --- Parse file --- + art_list = ['Title', 'Snap', 'BoxFront'] + art_tag_list = ['width', 'height', 'left', 'top'] + text_list = ['SLName', 'ItemName'] + test_tag_list = ['left', 'top', 'fontsize'] + for root_element in xml_root: + # log_debug('Root child {}'.format(root_element.tag)) + if root_element.tag in art_list: + # Default size tags to 0 + art_dic = {key : 0 for key in art_tag_list} + for art_child in root_element: + if art_child.tag in art_tag_list: + art_dic[art_child.tag] = int(art_child.text) + else: + log_error('Inside root tag <{}>'.format(root_element.tag)) + log_error('Unknown tag <{}>'.format(art_child.tag)) + return None + layout[root_element.tag] = art_dic + elif root_element.tag in text_list: + text_dic = {key : 0 for key in test_tag_list} + for art_child in root_element: + if art_child.tag in test_tag_list: + text_dic[art_child.tag] = int(art_child.text) + else: + log_error('Inside root tag <{}>'.format(root_element.tag)) + log_error('Unknown tag <{}>'.format(art_child.tag)) + return None + layout[root_element.tag] = text_dic + else: + log_error('Unknown root tag <{}>'.format(root_element.tag)) + return None + + return layout + +# Returns a dictionary with all the data necessary to build the fanarts. +# The dictionary has the 'abort' field if an error was detected. +def graphs_load_SL_Fanart_stuff(cfg, st_dic, BUILD_MISSING): + data_dic = { + 'BUILD_MISSING' : BUILD_MISSING, + } + + # --- If artwork directory not configured abort --- + if not cfg.settings['assets_path']: + kodi_set_error_status(st_dic, 'Asset directory not configured. Aborting SL Fanart generation.') + return + + # --- Load Fanart template from XML file --- + # SL Fanart directories are created later in graphs_build_SL_Fanart_all() + Template_FN = cfg.ADDON_CODE_DIR.pjoin('templates/AML-SL-Fanart-template.xml') + layout = graphs_load_SL_Fanart_template(Template_FN) + # log_debug(text_type(layout)) + if not layout: + kodi_set_error_status(st_dic, 'Error loading XML Software List Fanart layout.') + return + data_dic['layout'] = layout + + # --- Load SL index --- + SL_index = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + data_dic['SL_index'] = SL_index + + return data_dic + +# Builds or rebuilds missing SL Fanarts. +def graphs_build_SL_Fanart_all(cfg, st_dic, data_dic): + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + + # Traverse all SL and on each SL every item + pDialog_canceled = False + pDialog = KodiProgressDialog() + SL_number, SL_count = len(data_dic['SL_index']), 1 + total_SL_items, total_processed_SL_items = control_dic['stats_SL_software_items'], 0 + ETA_str = ETA_reset(total_SL_items) + log_debug('graphs_build_SL_Fanart_all() total_SL_items = {}'.format(total_SL_items)) + pDialog.startProgress('Building Software List Fanarts...') + for SL_name in sorted(data_dic['SL_index']): + # Update progres dialog + dtext = 'Processing SL {} ({} of {})...'.format(SL_name, SL_count, SL_number) + pDialog.resetProgress(dtext) + + # If fanart directory doesn't exist create it. + Asset_path_FN = FileName(cfg.settings['assets_path']) + Fanart_path_FN = Asset_path_FN.pjoin('fanarts_SL/{}'.format(SL_name)) + if not Fanart_path_FN.isdir(): + log_info('Creating SL Fanart dir "{}"'.format(Fanart_path_FN.getPath())) + Fanart_path_FN.makedirs() + + # Load Assets DB + pDialog.resetProgress('{}\n{}'.format(dtext, 'Loading SL asset database')) + assets_file_name = data_dic['SL_index'][SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_assets_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + + # Traverse all SL items and build fanart from other pieces of artwork + # Last slot of the progress bar is to save the JSON database. + total_SL_items, processed_SL_items = len(SL_assets_dic) + 1, 0 + pDialog.resetProgress(dtext, total_SL_items) + for m_name in sorted(SL_assets_dic): + build_time_start = time.time() + pDialog.updateProgress(processed_SL_items, '{}\nETA {} SL item {}'.format(dtext, ETA_str, m_name)) + if pDialog.isCanceled(): + pDialog_canceled = True + # kodi_dialog_OK('SL Fanart generation was cancelled by the user.') + break + # If build missing Fanarts was chosen only build fanart if file cannot be found. + Fanart_FN = Fanart_path_FN.pjoin('{}.png'.format(m_name)) + if data_dic['BUILD_MISSING']: + if Fanart_FN.exists(): + SL_assets_dic[m_name]['fanart'] = Fanart_FN.getPath() + build_OK_flag = False + else: + build_OK_flag = graphs_build_SL_Fanart(cfg, + data_dic['layout'], SL_name, m_name, SL_assets_dic, Fanart_FN) + else: + build_OK_flag = graphs_build_SL_Fanart(cfg, + data_dic['layout'], SL_name, m_name, SL_assets_dic, Fanart_FN) + processed_SL_items += 1 + total_processed_SL_items += 1 # For total ETA calculation + build_time_end = time.time() + build_time = build_time_end - build_time_start + # Only update ETA if 3DBox was sucesfully build. + ETA_str = ETA_update(build_OK_flag, total_processed_SL_items, build_time) + # Save SL assets DB. + pDialog.updateProgress(processed_SL_items, '{}\nSaving SL {} asset database'.format(dtext, SL_name)) + utils_write_JSON_file(SL_asset_DB_FN.getPath(), SL_assets_dic) + # Update progress. + SL_count += 1 + if pDialog_canceled: break + pDialog.endProgress() + + # Update SL Fanart build timestamp + db_safe_edit(control_dic, 't_SL_fanart_build', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + + # Inform user. + if pDialog_canceled: + kodi_notify('SL Fanart building stopped. Partial progress saved.') + else: + kodi_notify('SL Fanart building finished') + +# Returns a dictionary with all the data necessary to build the fanarts. +# The dictionary has the 'abort' field if an error was detected. +def graphs_load_MAME_3DBox_stuff(cfg, st_dic, BUILD_MISSING): + data_dic = { + 'BUILD_MISSING' : BUILD_MISSING, + } + + # --- If artwork directory not configured abort --- + if not cfg.settings['assets_path']: + kodi_set_error_status(st_dic, 'Asset directory not configured. Aborting MAME 3D box generation.') + return + + # --- If 3DBox directory doesn't exist create it --- + Asset_path_FN = FileName(cfg.settings['assets_path']) + Boxes_path_FN = Asset_path_FN.pjoin('3dboxes') + if not Boxes_path_FN.isdir(): + log_info('Creating 3DBox dir "{}"'.format(Boxes_path_FN.getPath())) + Boxes_path_FN.makedirs() + data_dic['Boxes_path_FN'] = Boxes_path_FN + + # --- Load 3DBox template from XML file --- + # TProjection_FN = cfg.ADDON_CODE_DIR.pjoin('templates/3dbox_angleY_56.json') + TProjection_FN = cfg.ADDON_CODE_DIR.pjoin('templates/3dbox_angleY_60.json') + t_projection = utils_load_JSON_file(TProjection_FN.getPath()) + if not t_projection: + kodi_set_error_status(st_dic, 'Error loading JSON 3dbox projection data.') + return + data_dic['t_projection'] = t_projection + + # --- Load Assets DB --- + pDialog = KodiProgressDialog() + pDialog.startProgress('Loading MAME asset database...') + assetdb_dic = utils_load_JSON_file(cfg.ASSET_DB_PATH.getPath()) + pDialog.endProgress() + data_dic['assetdb'] = assetdb_dic + + return data_dic + +# Builds or rebuilds missing MAME Fanarts. +def graphs_build_MAME_3DBox_all(cfg, st_dic, data_dic): + # Traverse all machines and build 3D boxes from other pieces of artwork + SL_name = 'MAME' + total_machines, processed_machines = len(data_dic['assetdb']), 0 + ETA_str = ETA_reset(total_machines) + pDialog_canceled = False + pDialog = KodiProgressDialog() + d_text = 'Building MAME machine 3D Boxes...' + pDialog.startProgress(d_text, total_machines) + for m_name in sorted(data_dic['assetdb']): + build_time_start = time.time() + d_str = '{}\nETA {} machine {}'.format(d_text, ETA_str, m_name) + pDialog.updateProgress(processed_machines, d_str) + if pDialog.isCanceled(): + pDialog_canceled = True + break + Image_FN = data_dic['Boxes_path_FN'].pjoin('{}.png'.format(m_name)) + if data_dic['BUILD_MISSING']: + if Image_FN.exists(): + data_dic['assetdb'][m_name]['3dbox'] = Image_FN.getPath() + build_OK_flag = False + else: + build_OK_flag = graphs_build_MAME_3DBox(cfg, + data_dic['t_projection'], SL_name, m_name, data_dic['assetdb'], Image_FN) + else: + build_OK_flag = graphs_build_MAME_3DBox(cfg, + data_dic['t_projection'], SL_name, m_name, data_dic['assetdb'], Image_FN) + processed_machines += 1 + build_time_end = time.time() + build_time = build_time_end - build_time_start + # Only update ETA if 3DBox was successfully build. + ETA_str = ETA_update(build_OK_flag, processed_machines, build_time) + pDialog.endProgress() + + # --- Save assets DB --- + pDialog.startProgress('Saving MAME asset database...') + utils_write_JSON_file(cfg.ASSET_DB_PATH.getPath(), data_dic['assetdb']) + pDialog.endProgress() + + # --- MAME Fanart build timestamp --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + db_safe_edit(control_dic, 't_MAME_3dbox_build', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + + # --- Inform user --- + if pDialog_canceled: + kodi_notify('MAME 3D Boxes building stopped. Partial progress saved.') + else: + kodi_notify('MAME 3D Boxes building finished') + +# Called before building all SL 3D Boxes. +def graphs_load_SL_3DBox_stuff(cfg, st_dic, BUILD_MISSING): + data_dic = { + 'BUILD_MISSING' : BUILD_MISSING, + } + + # --- If artwork directory not configured abort --- + # SL 3dbox directories are created later in graphs_build_SL_3DBox_all() + if not cfg.settings['assets_path']: + kodi_set_error_status(st_dic, 'Asset directory not configured. Aborting SL 3DBox generation.') + return + + # --- Load 3D projection template from XML file --- + # TProjection_FN = cfg.ADDON_CODE_DIR.pjoin('templates/3dbox_angleY_56.json') + TProjection_FN = cfg.ADDON_CODE_DIR.pjoin('templates/3dbox_angleY_60.json') + t_projection = utils_load_JSON_file(TProjection_FN.getPath()) + if not t_projection: + kodi_set_error_status(st_dic, 'Error loading JSON SL 3dbox projection data.') + return + data_dic['t_projection'] = t_projection + + # --- Load SL index --- + SL_index = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + data_dic['SL_index'] = SL_index + + return data_dic + +# Builds or rebuilds missing SL Fanarts. +def graphs_build_SL_3DBox_all(cfg, st_dic, data_dic): + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + + # Traverse all SL and on each SL every item + SL_number, SL_count = len(data_dic['SL_index']), 1 + total_SL_items, total_processed_SL_items = control_dic['stats_SL_software_items'], 0 + ETA_str = ETA_reset(total_SL_items) + log_debug('graphs_build_SL_3DBox_all() total_SL_items = {}'.format(total_SL_items)) + pDialog_canceled = False + pDialog = KodiProgressDialog() + pDialog.startProgress('Advanced MAME Launcher') + for SL_name in sorted(data_dic['SL_index']): + d_text = 'Processing SL {} ({} of {})...'.format(SL_name, SL_count, SL_number) + + # If fanart directory doesn't exist create it. + pDialog.resetProgress(d_text + '\n' + 'Creating SL Fanart directory') + Asset_path_FN = FileName(cfg.settings['assets_path']) + Boxes_path_FN = Asset_path_FN.pjoin('3dboxes_SL/{}'.format(SL_name)) + if not Boxes_path_FN.isdir(): + log_info('Creating SL 3D Box dir "{}"'.format(Boxes_path_FN.getPath())) + Boxes_path_FN.makedirs() + + # Load Assets DB + pDialog.resetProgress(d_text + '\n' + 'Loading SL asset database') + assets_file_name = data_dic['SL_index'][SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_assets_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + + # Traverse all SL items and build fanart from other pieces of artwork + # Last slot of the progress bar is to save the JSON database. + processed_SL_items = 0 + pDialog.resetProgress(d_text, len(SL_assets_dic)) + for m_name in sorted(SL_assets_dic): + build_time_start = time.time() + d_str = d_text + '\n' + 'ETA {} SL item {}'.format(ETA_str, m_name) + pDialog.updateProgress(processed_SL_items, d_str) + if pDialog.isCanceled(): + pDialog_canceled = True + break + Image_FN = Boxes_path_FN.pjoin('{}.png'.format(m_name)) + if data_dic['BUILD_MISSING']: + if Image_FN.exists(): + SL_assets_dic[m_name]['3dbox'] = Image_FN.getPath() + build_OK_flag = False + else: + build_OK_flag = graphs_build_MAME_3DBox(cfg, + data_dic['t_projection'], SL_name, m_name, SL_assets_dic, Image_FN) + else: + build_OK_flag = graphs_build_MAME_3DBox(cfg, + data_dic['t_projection'], SL_name, m_name, SL_assets_dic, Image_FN) + processed_SL_items += 1 # For current list progress dialog + total_processed_SL_items += 1 # For total ETA calculation + build_time_end = time.time() + build_time = build_time_end - build_time_start + # Only update ETA if 3DBox was sucesfully build. + ETA_str = ETA_update(build_OK_flag, total_processed_SL_items, build_time) + # Save SL assets DB. + pDialog.updateMessage(d_text + '\n' + 'Saving SL {} asset database'.format(SL_name)) + utils_write_JSON_file(SL_asset_DB_FN.getPath(), SL_assets_dic) + # Update progress. + SL_count += 1 + if pDialog_canceled: break + pDialog.endProgress() + + # --- SL Fanart build timestamp --- + db_safe_edit(control_dic, 't_SL_3dbox_build', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + + # --- Inform user --- + if pDialog_canceled: + kodi_notify('SL 3D Boxes building stopped. Partial progress saved.') + else: + kodi_notify('SL 3D Boxes building finished') diff --git a/plugin.program.AML/resources/main.py b/plugin.program.AML/resources/main.py new file mode 100644 index 0000000000..780cc2848d --- /dev/null +++ b/plugin.program.AML/resources/main.py @@ -0,0 +1,8029 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher main script file. + +# First include modules in this package. +# Then include Kodi modules. +# Finally include standard library modules. + +# --- Modules/packages in this plugin --- +# Addon module dependencies: +# main <-- mame <-- disk_IO <-- assets, misc, utils, constants +# mame <-- filters <-- misc, utils, constants +# manuals <- misc, utils, constants +# graphics <- misc, utils, constants +from .constants import * +from .assets import * +from .utils import * +from .db import * +from .filters import * +from .mame import * +from .manuals import * +from .graphics import * + +# --- Kodi stuff --- +import xbmc +import xbmcgui +import xbmcplugin +import xbmcaddon + +# --- Python standard library --- +import copy +import datetime +import os +import subprocess +if ADDON_RUNNING_PYTHON_2: + import urlparse +elif ADDON_RUNNING_PYTHON_3: + import urllib.parse +else: + raise TypeError('Undefined Python runtime version.') + +# --- Plugin database indices --- +# _PATH is a filename | _DIR is a directory +class Configuration: + def __init__(self): + # --- Kodi-related variables and data --- + # TODO Use this instead of the bottom deprecated code. + # Change self.addon.info_id with self.addon.info_id + self.addon = kodi_addon_obj() + + # Former global variables + self.settings = {} + self.base_url = '' + self.addon_handle = 0 + self.content_type = '' + + # Map of AEL artwork types to Kodi standard types, + self.mame_icon = '' + self.mame_fanart = '' + self.SL_icon = '' + self.SL_fanart = '' + + # --- File and directory names --- + self.HOME_DIR = FileName('special://home') + self.PROFILE_DIR = FileName('special://profile') + self.ADDONS_DATA_DIR = FileName('special://profile/addon_data') + self.ADDON_DATA_DIR = self.ADDONS_DATA_DIR.pjoin(self.addon.info_id) + self.ADDONS_CODE_DIR = self.HOME_DIR.pjoin('addons') + self.ADDON_CODE_DIR = self.ADDONS_CODE_DIR.pjoin(self.addon.info_id) + self.ICON_FILE_PATH = self.ADDON_CODE_DIR.pjoin('media/icon.png') + self.FANART_FILE_PATH = self.ADDON_CODE_DIR.pjoin('media/fanart.jpg') + + # MAME stdout/strderr files. + self.MAME_STDOUT_PATH = self.ADDON_DATA_DIR.pjoin('log_stdout.log') + self.MAME_STDERR_PATH = self.ADDON_DATA_DIR.pjoin('log_stderr.log') + self.MAME_STDOUT_VER_PATH = self.ADDON_DATA_DIR.pjoin('log_version_stdout.log') + self.MAME_STDERR_VER_PATH = self.ADDON_DATA_DIR.pjoin('log_version_stderr.log') + self.MAME_OUTPUT_PATH = self.ADDON_DATA_DIR.pjoin('log_output.log') + self.MONO_FONT_PATH = self.ADDON_CODE_DIR.pjoin('fonts/Inconsolata.otf') + self.CUSTOM_FILTER_PATH = self.ADDON_CODE_DIR.pjoin('filters/AML-MAME-filters.xml') + + # Addon control databases. + self.MAME_XML_PATH = self.ADDON_DATA_DIR.pjoin('MAME.xml') + self.MAME_XML_CONTROL_PATH = self.ADDON_DATA_DIR.pjoin('XML_control_MAME.json') + self.MAME_2003_PLUS_XML_CONTROL_PATH = self.ADDON_DATA_DIR.pjoin('XML_control_MAME_2003_plus.json') + self.MAIN_CONTROL_PATH = self.ADDON_DATA_DIR.pjoin('MAME_control_dic.json') + # Main MAME databases. + self.MAIN_DB_PATH = self.ADDON_DATA_DIR.pjoin('MAME_DB_main.json') + self.ROMS_DB_PATH = self.ADDON_DATA_DIR.pjoin('MAME_DB_roms.json') + self.DEVICES_DB_PATH = self.ADDON_DATA_DIR.pjoin('MAME_DB_devices.json') + self.SHA1_HASH_DB_PATH = self.ADDON_DATA_DIR.pjoin('MAME_DB_SHA1_hashes.json') + self.MAIN_PCLONE_DB_PATH = self.ADDON_DATA_DIR.pjoin('MAME_DB_pclone_dic.json') + # Databases used for rendering. + self.RENDER_DB_PATH = self.ADDON_DATA_DIR.pjoin('MAME_renderdb.json') + self.ASSET_DB_PATH = self.ADDON_DATA_DIR.pjoin('MAME_assetdb.json') + + # Audit and ROM Set databases. + self.ROM_AUDIT_DB_PATH = self.ADDON_DATA_DIR.pjoin('ROM_Audit_DB.json') + self.ROM_SET_MACHINE_FILES_DB_PATH = self.ADDON_DATA_DIR.pjoin('ROM_Set_machine_files.json') + + # DAT indices and databases. + self.HISTORY_IDX_PATH = self.ADDON_DATA_DIR.pjoin('DAT_History_index.json') + self.HISTORY_DB_PATH = self.ADDON_DATA_DIR.pjoin('DAT_History_DB.json') + self.MAMEINFO_IDX_PATH = self.ADDON_DATA_DIR.pjoin('DAT_MAMEInfo_index.json') + self.MAMEINFO_DB_PATH = self.ADDON_DATA_DIR.pjoin('DAT_MAMEInfo_DB.json') + self.GAMEINIT_IDX_PATH = self.ADDON_DATA_DIR.pjoin('DAT_GameInit_index.json') + self.GAMEINIT_DB_PATH = self.ADDON_DATA_DIR.pjoin('DAT_GameInit_DB.json') + self.COMMAND_IDX_PATH = self.ADDON_DATA_DIR.pjoin('DAT_Command_index.json') + self.COMMAND_DB_PATH = self.ADDON_DATA_DIR.pjoin('DAT_Command_DB.json') + + # Most played and Recently played + self.MAME_MOST_PLAYED_FILE_PATH = self.ADDON_DATA_DIR.pjoin('most_played_MAME.json') + self.MAME_RECENT_PLAYED_FILE_PATH = self.ADDON_DATA_DIR.pjoin('recently_played_MAME.json') + self.SL_MOST_PLAYED_FILE_PATH = self.ADDON_DATA_DIR.pjoin('most_played_SL.json') + self.SL_RECENT_PLAYED_FILE_PATH = self.ADDON_DATA_DIR.pjoin('recently_played_SL.json') + + # Disabled. Now there are global properties for this. + # self.MAIN_PROPERTIES_PATH = self.ADDON_DATA_DIR.pjoin('MAME_properties.json') + + # ROM cache. + self.CACHE_DIR = self.ADDON_DATA_DIR.pjoin('cache') + self.CACHE_INDEX_PATH = self.ADDON_DATA_DIR.pjoin('MAME_cache_index.json') + + # Catalogs. + self.CATALOG_DIR = self.ADDON_DATA_DIR.pjoin('catalogs') + self.CATALOG_MAIN_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_main_parents.json') + self.CATALOG_MAIN_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_main_all.json') + self.CATALOG_BINARY_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_binary_parents.json') + self.CATALOG_BINARY_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_binary_all.json') + self.CATALOG_CATVER_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_catver_parents.json') + self.CATALOG_CATVER_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_catver_all.json') + self.CATALOG_CATLIST_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_catlist_parents.json') + self.CATALOG_CATLIST_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_catlist_all.json') + self.CATALOG_GENRE_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_genre_parents.json') + self.CATALOG_GENRE_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_genre_all.json') + self.CATALOG_CATEGORY_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_category_parents.json') + self.CATALOG_CATEGORY_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_category_all.json') + self.CATALOG_NPLAYERS_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_nplayers_parents.json') + self.CATALOG_NPLAYERS_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_nplayers_all.json') + self.CATALOG_BESTGAMES_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_bestgames_parents.json') + self.CATALOG_BESTGAMES_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_bestgames_all.json') + self.CATALOG_SERIES_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_series_parents.json') + self.CATALOG_SERIES_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_series_all.json') + self.CATALOG_ALLTIME_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_alltime_parents.json') + self.CATALOG_ALLTIME_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_alltime_all.json') + self.CATALOG_ARTWORK_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_artwork_parents.json') + self.CATALOG_ARTWORK_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_artwork_all.json') + self.CATALOG_VERADDED_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_version_parents.json') + self.CATALOG_VERADDED_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_version_all.json') + + self.CATALOG_CONTROL_EXPANDED_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_control_expanded_parents.json') + self.CATALOG_CONTROL_EXPANDED_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_control_expanded_all.json') + self.CATALOG_CONTROL_COMPACT_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_control_compact_parents.json') + self.CATALOG_CONTROL_COMPACT_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_control_compact_all.json') + self.CATALOG_DEVICE_EXPANDED_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_device_expanded_parents.json') + self.CATALOG_DEVICE_EXPANDED_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_device_expanded_all.json') + self.CATALOG_DEVICE_COMPACT_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_device_compact_parents.json') + self.CATALOG_DEVICE_COMPACT_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_device_compact_all.json') + self.CATALOG_DISPLAY_TYPE_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_display_type_parents.json') + self.CATALOG_DISPLAY_TYPE_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_display_type_all.json') + self.CATALOG_DISPLAY_VSYNC_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_display_vsync_parents.json') + self.CATALOG_DISPLAY_VSYNC_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_display_vsync_all.json') + self.CATALOG_DISPLAY_RES_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_display_resolution_parents.json') + self.CATALOG_DISPLAY_RES_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_display_resolution_all.json') + self.CATALOG_CPU_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_CPU_parents.json') + self.CATALOG_CPU_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_CPU_all.json') + self.CATALOG_DRIVER_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_driver_parents.json') + self.CATALOG_DRIVER_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_driver_all.json') + self.CATALOG_MANUFACTURER_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_manufacturer_parents.json') + self.CATALOG_MANUFACTURER_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_manufacturer_all.json') + self.CATALOG_SHORTNAME_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_shortname_parents.json') + self.CATALOG_SHORTNAME_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_shortname_all.json') + self.CATALOG_LONGNAME_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_longname_parents.json') + self.CATALOG_LONGNAME_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_longname_all.json') + self.CATALOG_SL_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_SL_parents.json') + self.CATALOG_SL_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_SL_all.json') + self.CATALOG_YEAR_PARENT_PATH = self.CATALOG_DIR.pjoin('catalog_year_parents.json') + self.CATALOG_YEAR_ALL_PATH = self.CATALOG_DIR.pjoin('catalog_year_all.json') + + # Distributed hashed database. + self.MAIN_DB_HASH_DIR = self.ADDON_DATA_DIR.pjoin('hash') + self.ROMS_DB_HASH_DIR = self.ADDON_DATA_DIR.pjoin('hash_ROM') + self.ROM_AUDIT_DB_HASH_DIR = self.ADDON_DATA_DIR.pjoin('hash_ROM_Audit') + + # MAME custom filters. + self.FILTERS_DB_DIR = self.ADDON_DATA_DIR.pjoin('filters') + self.FILTERS_INDEX_PATH = self.ADDON_DATA_DIR.pjoin('Filter_index.json') + + # Software Lists. + self.SL_DB_DIR = self.ADDON_DATA_DIR.pjoin('SoftwareLists') + self.SL_NAMES_PATH = self.ADDON_DATA_DIR.pjoin('SoftwareLists_names.json') + self.SL_INDEX_PATH = self.ADDON_DATA_DIR.pjoin('SoftwareLists_index.json') + self.SL_MACHINES_PATH = self.ADDON_DATA_DIR.pjoin('SoftwareLists_machines.json') + self.SL_PCLONE_DIC_PATH = self.ADDON_DATA_DIR.pjoin('SoftwareLists_pclone_dic.json') + # Disabled. Not used at the moment. + # self.SL_MACHINES_PROP_PATH = self.ADDON_DATA_DIR.pjoin('SoftwareLists_properties.json') + + # Favourites. + self.FAV_MACHINES_PATH = self.ADDON_DATA_DIR.pjoin('Favourite_Machines.json') + self.FAV_SL_ROMS_PATH = self.ADDON_DATA_DIR.pjoin('Favourite_SL_ROMs.json') + + # ROM/CHD scanner reports. These reports show missing ROM/CHDs only. + self.REPORTS_DIR = self.ADDON_DATA_DIR.pjoin('reports') + self.REPORT_MAME_SCAN_MACHINE_ARCH_FULL_PATH = self.REPORTS_DIR.pjoin('Scanner_MAME_machine_archives_full.txt') + self.REPORT_MAME_SCAN_MACHINE_ARCH_HAVE_PATH = self.REPORTS_DIR.pjoin('Scanner_MAME_machine_archives_have.txt') + self.REPORT_MAME_SCAN_MACHINE_ARCH_MISS_PATH = self.REPORTS_DIR.pjoin('Scanner_MAME_machine_archives_miss.txt') + self.REPORT_MAME_SCAN_ROM_LIST_MISS_PATH = self.REPORTS_DIR.pjoin('Scanner_MAME_ROM_list_miss.txt') + self.REPORT_MAME_SCAN_SAM_LIST_MISS_PATH = self.REPORTS_DIR.pjoin('Scanner_MAME_SAM_list_miss.txt') + self.REPORT_MAME_SCAN_CHD_LIST_MISS_PATH = self.REPORTS_DIR.pjoin('Scanner_MAME_CHD_list_miss.txt') + + self.REPORT_SL_SCAN_MACHINE_ARCH_FULL_PATH = self.REPORTS_DIR.pjoin('Scanner_SL_item_archives_full.txt') + self.REPORT_SL_SCAN_MACHINE_ARCH_HAVE_PATH = self.REPORTS_DIR.pjoin('Scanner_SL_item_archives_have.txt') + self.REPORT_SL_SCAN_MACHINE_ARCH_MISS_PATH = self.REPORTS_DIR.pjoin('Scanner_SL_item_archives_miss.txt') + + # Asset scanner reports. These reports show have and missing assets. + self.REPORT_MAME_ASSETS_PATH = self.REPORTS_DIR.pjoin('Assets_MAME.txt') + self.REPORT_SL_ASSETS_PATH = self.REPORTS_DIR.pjoin('Assets_SL.txt') + + # Statistics report. + self.REPORT_STATS_PATH = self.REPORTS_DIR.pjoin('Statistics.txt') + + # Audit report. + self.REPORT_MAME_AUDIT_FULL_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_full.txt') + self.REPORT_MAME_AUDIT_GOOD_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_good.txt') + self.REPORT_MAME_AUDIT_ERRORS_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_errors.txt') + self.REPORT_MAME_AUDIT_ROM_GOOD_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_ROMs_good.txt') + self.REPORT_MAME_AUDIT_ROM_ERRORS_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_ROMs_errors.txt') + self.REPORT_MAME_AUDIT_SAMPLES_GOOD_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_SAMPLES_good.txt') + self.REPORT_MAME_AUDIT_SAMPLES_ERRORS_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_SAMPLES_errors.txt') + self.REPORT_MAME_AUDIT_CHD_GOOD_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_CHDs_good.txt') + self.REPORT_MAME_AUDIT_CHD_ERRORS_PATH = self.REPORTS_DIR.pjoin('Audit_MAME_CHDs_errors.txt') + + self.REPORT_SL_AUDIT_FULL_PATH = self.REPORTS_DIR.pjoin('Audit_SL_full.txt') + self.REPORT_SL_AUDIT_GOOD_PATH = self.REPORTS_DIR.pjoin('Audit_SL_good.txt') + self.REPORT_SL_AUDIT_ERRORS_PATH = self.REPORTS_DIR.pjoin('Audit_SL_errors.txt') + self.REPORT_SL_AUDIT_ROMS_GOOD_PATH = self.REPORTS_DIR.pjoin('Audit_SL_ROMs_good.txt') + self.REPORT_SL_AUDIT_ROMS_ERRORS_PATH = self.REPORTS_DIR.pjoin('Audit_SL_ROMs_errors.txt') + self.REPORT_SL_AUDIT_CHDS_GOOD_PATH = self.REPORTS_DIR.pjoin('Audit_SL_CHDs_good.txt') + self.REPORT_SL_AUDIT_CHDS_ERRORS_PATH = self.REPORTS_DIR.pjoin('Audit_SL_CHDs_errors.txt') + + # Custom filters report. + self.REPORT_CF_XML_SYNTAX_PATH = self.REPORTS_DIR.pjoin('Custom_filter_XML_check.txt') + self.REPORT_CF_DB_BUILD_PATH = self.REPORTS_DIR.pjoin('Custom_filter_database_report.txt') + self.REPORT_CF_HISTOGRAMS_PATH = self.REPORTS_DIR.pjoin('Custom_filter_histogram.txt') + + # DEBUG data + self.REPORT_DEBUG_MAME_MACHINE_DATA_PATH = self.REPORTS_DIR.pjoin('debug_MAME_machine_data.txt') + self.REPORT_DEBUG_MAME_MACHINE_ROM_DATA_PATH = self.REPORTS_DIR.pjoin('debug_MAME_machine_ROM_DB_data.txt') + self.REPORT_DEBUG_MAME_MACHINE_AUDIT_DATA_PATH = self.REPORTS_DIR.pjoin('debug_MAME_machine_Audit_DB_data.txt') + self.REPORT_DEBUG_SL_ITEM_DATA_PATH = self.REPORTS_DIR.pjoin('debug_SL_item_data.txt') + self.REPORT_DEBUG_SL_ITEM_ROM_DATA_PATH = self.REPORTS_DIR.pjoin('debug_SL_item_ROM_DB_data.txt') + self.REPORT_DEBUG_SL_ITEM_AUDIT_DATA_PATH = self.REPORTS_DIR.pjoin('debug_SL_item_Audit_DB_data.txt') + self.REPORT_DEBUG_MAME_COLLISIONS_PATH = self.REPORTS_DIR.pjoin('debug_MAME_collisions.txt') + self.REPORT_DEBUG_SL_COLLISIONS_PATH = self.REPORTS_DIR.pjoin('debug_SL_collisions.txt') + +# --- Global variables --- +# Use functional programming as much as possible and avoid global variables. +# g_base_url must be a global variable because it is used in the misc_url_*() functions +# for speed reasons. +g_base_url = '' + +# Module loading time. This variable is read only (only modified here). +g_time_str = text_type(datetime.datetime.now()) + +# Do not change context menus with listitem.addContextMenuItems() in Kiosk mode. +# In other words, change the CM if Kiosk mode is disabled. +# By default kiosk mode is disabled. +g_kiosk_mode_disabled = True + +# --------------------------------------------------------------------------------------------- +# This is the plugin entry point. +# --------------------------------------------------------------------------------------------- +def run_plugin(addon_argv): + global g_base_url + + # Unify all global variables into an object to simplify function calling. + # Keep compatibility with legacy code until all addon has been refactored. + # Instead of using a global variable create an instance of the cfg object here + # and pass as first argument of all functions. Long live to functional programming! + cfg = Configuration() + + # --- Initialize log system --- + # Force DEBUG log level for development. + # Place it before setting loading so settings can be dumped during debugging. + # set_log_level(LOG_DEBUG) + + # --- Fill in settings dictionary using addon_obj.getSetting() --- + get_settings(cfg) + set_log_level(cfg.settings['log_level']) + + # --- Some debug stuff for development --- + log_debug('-------------------- Called AML run_plugin() --------------------') + log_debug('sys.platform "{}"'.format(sys.platform)) + # log_debug('WindowId "{}"'.format(xbmcgui.getCurrentWindowId())) + # log_debug('WindowName "{}"'.format(xbmc.getInfoLabel('Window.Property(xmlfile)'))) + log_debug('Python version "' + sys.version.replace('\n', '') + '"') + # log_debug('addon_name "{}"'.format(cfg.addon.info_name)) + log_debug('addon_id "{}"'.format(cfg.addon.info_id)) + log_debug('addon_version "{}"'.format(cfg.addon.info_version)) + # log_debug('addon_author "{}"'.format(cfg.addon.info_author)) + # log_debug('addon_profile "{}"'.format(cfg.addon.info_profile)) + # log_debug('addon_type "{}"'.format(cfg.addon.info_type)) + for i in range(len(addon_argv)): log_debug('addon_argv[{}] "{}"'.format(i, addon_argv[i])) + # log_debug('PLUGIN_DATA_DIR OP "{}"'.format(cfg.PLUGIN_DATA_DIR.getOriginalPath())) + # log_debug('PLUGIN_DATA_DIR P "{}"'.format(cfg.PLUGIN_DATA_DIR.getPath())) + # log_debug('ADDON_CODE_DIR OP "{}"'.format(cfg.ADDON_CODE_DIR.getOriginalPath())) + # log_debug('ADDON_CODE_DIR P "{}"'.format(cfg.ADDON_CODE_DIR.getPath())) + + # Print Python module path. + # for i in range(len(sys.path)): log_debug('sys.path[{}] "{}"'.format(i, text_type(sys.path[i]))) + + # --- Secondary setting processing --- + get_settings_log_enabled(cfg) + log_debug('Operation mode "{}"'.format(cfg.settings['op_mode'])) + log_debug('SL global enable is {}'.format(cfg.settings['global_enable_SL'])) + + # --- Playground and testing code --- + # kodi_get_screensaver_mode() + + # Kiosk mode for skins. + g_kiosk_mode_disabled = xbmc.getCondVisibility('!Skin.HasSetting(KioskMode.Enabled)') + + # --- Addon data paths creation --- + if not cfg.ADDON_DATA_DIR.exists(): cfg.ADDON_DATA_DIR.makedirs() + if not cfg.CACHE_DIR.exists(): cfg.CACHE_DIR.makedirs() + if not cfg.CATALOG_DIR.exists(): cfg.CATALOG_DIR.makedirs() + if not cfg.MAIN_DB_HASH_DIR.exists(): cfg.MAIN_DB_HASH_DIR.makedirs() + if not cfg.FILTERS_DB_DIR.exists(): cfg.FILTERS_DB_DIR.makedirs() + if not cfg.SL_DB_DIR.exists(): cfg.SL_DB_DIR.makedirs() + if not cfg.REPORTS_DIR.exists(): cfg.REPORTS_DIR.makedirs() + + # --- Process URL --- + cfg.base_url = addon_argv[0] + g_base_url = cfg.base_url + cfg.addon_handle = int(addon_argv[1]) + if ADDON_RUNNING_PYTHON_2: + args = urlparse.parse_qs(addon_argv[2][1:]) + elif ADDON_RUNNING_PYTHON_3: + args = urllib.parse.parse_qs(addon_argv[2][1:]) + else: + raise TypeError('Undefined Python runtime version.') + # log_debug('args = {}'.format(args)) + # Interestingly, if plugin is called as type executable then args is empty. + # However, if plugin is called as type game then Kodi adds the following + # even for the first call: 'content_type': ['game'] + cfg.content_type = args['content_type'] if 'content_type' in args else None + log_debug('content_type = {}'.format(cfg.content_type)) + + # --- URL routing ------------------------------------------------------------------------- + # Show addon root window. + args_size = len(args) + if not 'catalog' in args and not 'command' in args: + render_root_list(cfg) + log_debug('Advanced MAME Launcher exit (addon root)') + return + + # Render a list of something. + elif 'catalog' in args and not 'command' in args: + catalog_name = args['catalog'][0] + # --- Software list is a special case --- + if catalog_name == 'SL' or catalog_name == 'SL_ROM' or \ + catalog_name == 'SL_CHD' or catalog_name == 'SL_ROM_CHD' or \ + catalog_name == 'SL_empty': + SL_name = args['category'][0] if 'category' in args else '' + parent_name = args['parent'][0] if 'parent' in args else '' + if SL_name and parent_name: + render_SL_pclone_set(cfg, SL_name, parent_name) + elif SL_name and not parent_name: + render_SL_ROMs(cfg, SL_name) + else: + render_SL_list(cfg, catalog_name) + # --- Custom filters --- + elif catalog_name == 'Custom': + render_custom_filter_machines(cfg, args['category'][0]) + # --- DAT browsing --- + elif catalog_name == 'History' or catalog_name == 'MAMEINFO' or \ + catalog_name == 'Gameinit' or catalog_name == 'Command': + category_name = args['category'][0] if 'category' in args else '' + machine_name = args['machine'][0] if 'machine' in args else '' + if category_name and machine_name: + render_DAT_machine_info(cfg, catalog_name, category_name, machine_name) + elif category_name and not machine_name: + render_DAT_category(cfg, catalog_name, category_name) + else: + render_DAT_list(cfg, catalog_name) + else: + category_name = args['category'][0] if 'category' in args else '' + parent_name = args['parent'][0] if 'parent' in args else '' + if category_name and parent_name: + render_catalog_clone_list(cfg, catalog_name, category_name, parent_name) + elif category_name and not parent_name: + render_catalog_parent_list(cfg, catalog_name, category_name) + else: + render_catalog_list(cfg, catalog_name) + + # Execute a command. + elif 'command' in args: + command = args['command'][0] + + # Commands used by skins to render items of the addon root menu. + if command == 'SKIN_SHOW_FAV_SLOTS': render_skin_fav_slots(cfg) + elif command == 'SKIN_SHOW_MAIN_FILTERS': render_skin_main_filters(cfg) + elif command == 'SKIN_SHOW_BINARY_FILTERS': render_skin_binary_filters(cfg) + elif command == 'SKIN_SHOW_CATALOG_FILTERS': render_skin_catalog_filters(cfg) + elif command == 'SKIN_SHOW_DAT_SLOTS': render_skin_dat_slots(cfg) + elif command == 'SKIN_SHOW_SL_FILTERS': render_skin_SL_filters(cfg) + + # Auxiliar commands from parent machine context menu + # Not sure if this will cause problems with the concurrent protected code once it's implemented. + elif command == 'EXEC_SHOW_MAME_CLONES': + catalog_name = args['catalog'][0] if 'catalog' in args else '' + category_name = args['category'][0] if 'category' in args else '' + machine_name = args['parent'][0] if 'parent' in args else '' + url = misc_url_3_arg('catalog', catalog_name, 'category', category_name, 'parent', machine_name) + xbmc.executebuiltin('Container.Update({})'.format(url)) + + elif command == 'EXEC_SHOW_SL_CLONES': + catalog_name = args['catalog'][0] if 'catalog' in args else '' + category_name = args['category'][0] if 'category' in args else '' + machine_name = args['parent'][0] if 'parent' in args else '' + url = misc_url_3_arg('catalog', 'SL', 'category', category_name, 'parent', machine_name) + xbmc.executebuiltin('Container.Update({})'.format(url)) + + # If location is not present in the URL default to standard. + elif command == 'LAUNCH': + machine = args['machine'][0] + location = args['location'][0] if 'location' in args else LOCATION_STANDARD + log_info('Launching MAME machine "{}" in "{}"'.format(machine, location)) + run_machine(cfg, machine, location) + elif command == 'LAUNCH_SL': + SL_name = args['SL'][0] + ROM_name = args['ROM'][0] + location = args['location'][0] if 'location' in args else LOCATION_STANDARD + log_info('Launching SL machine "{}" (ROM "{}")'.format(SL_name, ROM_name)) + run_SL_machine(cfg, SL_name, ROM_name, location) + + elif command == 'SETUP_PLUGIN': + command_context_setup_plugin(cfg) + + # Not used at the moment. + # Instead of per-catalog display mode settings there are global settings. + elif command == 'DISPLAY_SETTINGS_MAME': + catalog_name = args['catalog'][0] + category_name = args['category'][0] if 'category' in args else '' + command_context_display_settings(cfg, catalog_name, category_name) + elif command == 'DISPLAY_SETTINGS_SL': + command_context_display_settings_SL(cfg, args['category'][0]) + elif command == 'VIEW_DAT': + machine = args['machine'][0] if 'machine' in args else '' + SL = args['SL'][0] if 'SL' in args else '' + ROM = args['ROM'][0] if 'ROM' in args else '' + location = args['location'][0] if 'location' in args else LOCATION_STANDARD + command_context_info_utils(cfg, machine, SL, ROM, location) + elif command == 'VIEW': + machine = args['machine'][0] if 'machine' in args else '' + SL = args['SL'][0] if 'SL' in args else '' + ROM = args['ROM'][0] if 'ROM' in args else '' + location = args['location'][0] if 'location' in args else LOCATION_STANDARD + command_context_view_audit(cfg, machine, SL, ROM, location) + elif command == 'UTILITIES': + catalog_name = args['catalog'][0] if 'catalog' in args else '' + category_name = args['category'][0] if 'category' in args else '' + command_context_utilities(cfg, catalog_name, category_name) + + # MAME Favourites + elif command == 'ADD_MAME_FAV': + command_context_add_mame_fav(cfg, args['machine'][0]) + elif command == 'MANAGE_MAME_FAV': + # If called from the root menu machine is empty. + machine = args['machine'][0] if 'machine' in args else '' + command_context_manage_mame_fav(cfg, machine) + elif command == 'SHOW_MAME_FAVS': + command_show_mame_fav(cfg) + + # Most and Recently played + elif command == 'SHOW_MAME_MOST_PLAYED': + command_show_mame_most_played(cfg) + elif command == 'MANAGE_MAME_MOST_PLAYED': + m_name = args['machine'][0] if 'machine' in args else '' + command_context_manage_mame_most_played(cfg, m_name) + + elif command == 'SHOW_MAME_RECENTLY_PLAYED': + command_show_mame_recently_played(cfg) + elif command == 'MANAGE_MAME_RECENT_PLAYED': + m_name = args['machine'][0] if 'machine' in args else '' + command_context_manage_mame_recent_played(cfg, m_name) + + # SL Favourites + elif command == 'ADD_SL_FAV': + command_context_add_sl_fav(cfg, args['SL'][0], args['ROM'][0]) + elif command == 'MANAGE_SL_FAV': + SL_name = args['SL'][0] if 'SL' in args else '' + ROM_name = args['ROM'][0] if 'ROM' in args else '' + command_context_manage_sl_fav(cfg, SL_name, ROM_name) + elif command == 'SHOW_SL_FAVS': + command_show_sl_fav(cfg) + + elif command == 'SHOW_SL_MOST_PLAYED': + command_show_SL_most_played(cfg) + elif command == 'MANAGE_SL_MOST_PLAYED': + SL_name = args['SL'][0] if 'SL' in args else '' + ROM_name = args['ROM'][0] if 'ROM' in args else '' + command_context_manage_SL_most_played(cfg, SL_name, ROM_name) + + elif command == 'SHOW_SL_RECENTLY_PLAYED': + command_show_SL_recently_played(cfg) + elif command == 'MANAGE_SL_RECENT_PLAYED': + SL_name = args['SL'][0] if 'SL' in args else '' + ROM_name = args['ROM'][0] if 'ROM' in args else '' + command_context_manage_SL_recent_played(cfg, SL_name, ROM_name) + + elif command == 'SHOW_CUSTOM_FILTERS': + command_show_custom_filters(cfg) + elif command == 'SETUP_CUSTOM_FILTERS': + command_context_setup_custom_filters(cfg) + + elif command == 'SHOW_UTILITIES_VLAUNCHERS': + render_Utilities_vlaunchers(cfg) + elif command == 'SHOW_GLOBALREPORTS_VLAUNCHERS': + render_GlobalReports_vlaunchers(cfg) + + elif command == 'EXECUTE_UTILITY': + which_utility = args['which'][0] + command_exec_utility(cfg, which_utility) + + elif command == 'EXECUTE_REPORT': + which_report = args['which'][0] + command_exec_report(cfg, which_report) + + else: + u = 'Unknown command "{}"'.format(command) + log_error(u) + kodi_dialog_OK(u) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + else: + u = 'Error in URL routing' + log_error(u) + kodi_dialog_OK(u) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + + # --- So Long, and Thanks for All the Fish --- + log_debug('Advanced MAME Launcher exit') + +# Get Addon Settings. log_*() functions cannot be used here during normal operation. +def get_settings(cfg): + settings = cfg.settings + + # --- Main operation --- + settings['op_mode_raw'] = kodi_get_int_setting(cfg, 'op_mode_raw') + # Vanilla MAME settings. + settings['rom_path_vanilla'] = kodi_get_str_setting(cfg, 'rom_path_vanilla') + settings['enable_SL'] = kodi_get_bool_setting(cfg, 'enable_SL') + settings['mame_prog'] = kodi_get_str_setting(cfg, 'mame_prog') + settings['SL_hash_path'] = kodi_get_str_setting(cfg, 'SL_hash_path') + # MAME 2003 Plus settings. + settings['rom_path_2003_plus'] = kodi_get_str_setting(cfg, 'rom_path_2003_plus') + settings['retroarch_prog'] = kodi_get_str_setting(cfg, 'retroarch_prog') + settings['libretro_dir'] = kodi_get_str_setting(cfg, 'libretro_dir') + settings['xml_2003_path'] = kodi_get_str_setting(cfg, 'xml_2003_path') + + # --- Optional paths --- + settings['assets_path'] = kodi_get_str_setting(cfg, 'assets_path') + settings['dats_path'] = kodi_get_str_setting(cfg, 'dats_path') + settings['chd_path'] = kodi_get_str_setting(cfg, 'chd_path') + settings['samples_path'] = kodi_get_str_setting(cfg, 'samples_path') + settings['SL_rom_path'] = kodi_get_str_setting(cfg, 'SL_rom_path') + settings['SL_chd_path'] = kodi_get_str_setting(cfg, 'SL_chd_path') + + # --- ROM sets --- + settings['mame_rom_set'] = kodi_get_int_setting(cfg, 'mame_rom_set') + settings['mame_chd_set'] = kodi_get_int_setting(cfg, 'mame_chd_set') + settings['SL_rom_set'] = kodi_get_int_setting(cfg, 'SL_rom_set') + settings['SL_chd_set'] = kodi_get_int_setting(cfg, 'SL_chd_set') + + # Misc separator + settings['filter_XML'] = kodi_get_str_setting(cfg, 'filter_XML') + settings['generate_history_infolabel'] = kodi_get_bool_setting(cfg, 'generate_history_infolabel') + + # --- Display I --- + settings['display_launcher_notify'] = kodi_get_bool_setting(cfg, 'display_launcher_notify') + settings['mame_view_mode'] = kodi_get_int_setting(cfg, 'mame_view_mode') + settings['sl_view_mode'] = kodi_get_int_setting(cfg, 'sl_view_mode') + settings['display_hide_Mature'] = kodi_get_bool_setting(cfg, 'display_hide_Mature') + settings['display_hide_BIOS'] = kodi_get_bool_setting(cfg, 'display_hide_BIOS') + settings['display_hide_imperfect'] = kodi_get_bool_setting(cfg, 'display_hide_imperfect') + settings['display_hide_nonworking'] = kodi_get_bool_setting(cfg, 'display_hide_nonworking') + settings['display_rom_available'] = kodi_get_bool_setting(cfg, 'display_rom_available') + settings['display_chd_available'] = kodi_get_bool_setting(cfg, 'display_chd_available') + settings['display_SL_items_available'] = kodi_get_bool_setting(cfg, 'display_SL_items_available') + settings['display_MAME_flags'] = kodi_get_bool_setting(cfg, 'display_MAME_flags') + settings['display_SL_flags'] = kodi_get_bool_setting(cfg, 'display_SL_flags') + + # --- Display II --- + settings['display_main_filters'] = kodi_get_bool_setting(cfg, 'display_main_filters') + settings['display_binary_filters'] = kodi_get_bool_setting(cfg, 'display_binary_filters') + settings['display_catalog_filters'] = kodi_get_bool_setting(cfg, 'display_catalog_filters') + settings['display_DAT_browser'] = kodi_get_bool_setting(cfg, 'display_DAT_browser') + settings['display_SL_browser'] = kodi_get_bool_setting(cfg, 'display_SL_browser') + settings['display_custom_filters'] = kodi_get_bool_setting(cfg, 'display_custom_filters') + settings['display_ROLs'] = kodi_get_bool_setting(cfg, 'display_ROLs') + settings['display_MAME_favs'] = kodi_get_bool_setting(cfg, 'display_MAME_favs') + settings['display_MAME_most'] = kodi_get_bool_setting(cfg, 'display_MAME_most') + settings['display_MAME_recent'] = kodi_get_bool_setting(cfg, 'display_MAME_recent') + settings['display_SL_favs'] = kodi_get_bool_setting(cfg, 'display_SL_favs') + settings['display_SL_most'] = kodi_get_bool_setting(cfg, 'display_SL_most') + settings['display_SL_recent'] = kodi_get_bool_setting(cfg, 'display_SL_recent') + settings['display_utilities'] = kodi_get_bool_setting(cfg, 'display_utilities') + settings['display_global_reports'] = kodi_get_bool_setting(cfg, 'display_global_reports') + + # --- Artwork / Assets --- + settings['display_hide_trailers'] = kodi_get_bool_setting(cfg, 'display_hide_trailers') + settings['artwork_mame_icon'] = kodi_get_int_setting(cfg, 'artwork_mame_icon') + settings['artwork_mame_fanart'] = kodi_get_int_setting(cfg, 'artwork_mame_fanart') + settings['artwork_SL_icon'] = kodi_get_int_setting(cfg, 'artwork_SL_icon') + settings['artwork_SL_fanart'] = kodi_get_int_setting(cfg, 'artwork_SL_fanart') + + # --- Advanced --- + settings['media_state_action'] = kodi_get_int_setting(cfg, 'media_state_action') + if ADDON_RUNNING_PYTHON_2: + settings['delay_tempo'] = kodi_get_float_setting_as_int(cfg, 'delay_tempo') + elif ADDON_RUNNING_PYTHON_3: + settings['delay_tempo'] = kodi_get_int_setting(cfg, 'delay_tempo') + else: + raise TypeError('Undefined Python runtime version.') + settings['suspend_audio_engine'] = kodi_get_bool_setting(cfg, 'suspend_audio_engine') + settings['suspend_screensaver'] = kodi_get_bool_setting(cfg, 'suspend_screensaver') + settings['toggle_window'] = kodi_get_bool_setting(cfg, 'toggle_window') + settings['log_level'] = kodi_get_int_setting(cfg, 'log_level') + settings['debug_enable_MAME_render_cache'] = kodi_get_bool_setting(cfg, 'debug_enable_MAME_render_cache') + settings['debug_enable_MAME_asset_cache'] = kodi_get_bool_setting(cfg, 'debug_enable_MAME_asset_cache') + settings['debug_MAME_machine_data'] = kodi_get_bool_setting(cfg, 'debug_MAME_machine_data') + settings['debug_MAME_ROM_DB_data'] = kodi_get_bool_setting(cfg, 'debug_MAME_ROM_DB_data') + settings['debug_MAME_Audit_DB_data'] = kodi_get_bool_setting(cfg, 'debug_MAME_Audit_DB_data') + settings['debug_SL_item_data'] = kodi_get_bool_setting(cfg, 'debug_SL_item_data') + settings['debug_SL_ROM_DB_data'] = kodi_get_bool_setting(cfg, 'debug_SL_ROM_DB_data') + settings['debug_SL_Audit_DB_data'] = kodi_get_bool_setting(cfg, 'debug_SL_Audit_DB_data') + + # --- Dump settings for DEBUG --- + # log_debug('Settings dump BEGIN') + # for key in sorted(settings): + # log_debug('{} --> {:10s} {}'.format(key.rjust(21), text_type(settings[key]), type(settings[key]))) + # log_debug('Settings dump END') + +# +# Called after log is enabled. Process secondary settings. +# +def get_settings_log_enabled(cfg): + # Convenience data. + cfg.addon_version_int = misc_addon_version_str_to_int(cfg.addon.info_version) + + # Additional settings. + cfg.settings['op_mode'] = OP_MODE_LIST[cfg.settings['op_mode_raw']] + + # Map AML artwork to Kodi standard artwork. + cfg.mame_icon = assets_get_asset_key_MAME_icon(cfg.settings['artwork_mame_icon']) + cfg.mame_fanart = assets_get_asset_key_MAME_fanart(cfg.settings['artwork_mame_fanart']) + cfg.SL_icon = assets_get_asset_key_SL_icon(cfg.settings['artwork_SL_icon']) + cfg.SL_fanart = assets_get_asset_key_SL_fanart(cfg.settings['artwork_SL_fanart']) + + # Enable or disable Software List depending on settings. + if cfg.settings['op_mode'] == OP_MODE_VANILLA and cfg.settings['enable_SL'] == True: + cfg.settings['global_enable_SL'] = True + elif cfg.settings['op_mode'] == OP_MODE_VANILLA and cfg.settings['enable_SL'] == False: + cfg.settings['global_enable_SL'] = False + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + cfg.settings['global_enable_SL'] = False + else: + raise TypeError('Wrong cfg.settings["op_mode"] = {}'.format(cfg.settings['op_mode'])) + +# --------------------------------------------------------------------------------------------- +# URL building functions. +# g_base_url is plugin://plugin.program.AML/ +# A class URLs: plugin://plugin.program.AML/?command=xxxxx +# B class URLs: RunPlugin(plugin://plugin.program.AML/?command=xxxxx) +# A class URLs are used in xbmcplugin.addDirectoryItem() +# B class URLs are used in listitem.addContextMenuItems() +# '&' must be scaped to '%26' in all URLs +# --------------------------------------------------------------------------------------------- +def misc_url(command): + command_escaped = command.replace('&', '%26') + + return '{}?command={}'.format(g_base_url, command_escaped) + +def misc_url_1_arg(arg_name, arg_value): + arg_value_escaped = arg_value.replace('&', '%26') + + return '{}?{}={}'.format(g_base_url, arg_name, arg_value_escaped) + +def misc_url_2_arg(arg_name_1, arg_value_1, arg_name_2, arg_value_2): + arg_value_1_escaped = arg_value_1.replace('&', '%26') + arg_value_2_escaped = arg_value_2.replace('&', '%26') + + return '{}?{}={}&{}={}'.format(g_base_url, + arg_name_1, arg_value_1_escaped, arg_name_2, arg_value_2_escaped) + +def misc_url_3_arg(arg_name_1, arg_value_1, arg_name_2, arg_value_2, arg_name_3, arg_value_3): + arg_value_1_escaped = arg_value_1.replace('&', '%26') + arg_value_2_escaped = arg_value_2.replace('&', '%26') + arg_value_3_escaped = arg_value_3.replace('&', '%26') + + return '{}?{}={}&{}={}&{}={}'.format(g_base_url, + arg_name_1, arg_value_1_escaped, arg_name_2, arg_value_2_escaped, arg_name_3, arg_value_3_escaped) + +def misc_url_4_arg(arg_name_1, arg_value_1, arg_name_2, arg_value_2, arg_name_3, arg_value_3, arg_name_4, arg_value_4): + arg_value_1_escaped = arg_value_1.replace('&', '%26') + arg_value_2_escaped = arg_value_2.replace('&', '%26') + arg_value_3_escaped = arg_value_3.replace('&', '%26') + arg_value_4_escaped = arg_value_4.replace('&', '%26') + + return '{}?{}={}&{}={}&{}={}&{}={}'.format(g_base_url, + arg_name_1, arg_value_1_escaped, arg_name_2, arg_value_2_escaped, + arg_name_3, arg_value_3_escaped,arg_name_4, arg_value_4_escaped) + +# Kodi Matrix do not support XBMC.RunPlugin() anymore. +# Leia can run RunPlugin() commands w/o XBMC prefix. +# What about Krypton? +def misc_url_RunPlugin(command): + command_esc = command.replace('&', '%26') + + return 'RunPlugin({}?command={})'.format(g_base_url, command_esc) + +def misc_url_1_arg_RunPlugin(arg_n_1, arg_v_1): + arg_v_1_esc = arg_v_1.replace('&', '%26') + + return 'RunPlugin({}?{}={})'.format(g_base_url, arg_n_1, arg_v_1_esc) + +def misc_url_2_arg_RunPlugin(arg_n_1, arg_v_1, arg_n_2, arg_v_2): + arg_v_1_esc = arg_v_1.replace('&', '%26') + arg_v_2_esc = arg_v_2.replace('&', '%26') + + return 'RunPlugin({}?{}={}&{}={})'.format(g_base_url, + arg_n_1, arg_v_1_esc, arg_n_2, arg_v_2_esc) + +def misc_url_3_arg_RunPlugin(arg_n_1, arg_v_1, arg_n_2, arg_v_2, arg_n_3, arg_v_3): + arg_v_1_esc = arg_v_1.replace('&', '%26') + arg_v_2_esc = arg_v_2.replace('&', '%26') + arg_v_3_esc = arg_v_3.replace('&', '%26') + + return 'RunPlugin({}?{}={}&{}={}&{}={})'.format(g_base_url, + arg_n_1, arg_v_1_esc, arg_n_2, arg_v_2_esc, arg_n_3, arg_v_3_esc) + +def misc_url_4_arg_RunPlugin(arg_n_1, arg_v_1, arg_n_2, arg_v_2, arg_n_3, arg_v_3, arg_n_4, arg_v_4): + arg_v_1_esc = arg_v_1.replace('&', '%26') + arg_v_2_esc = arg_v_2.replace('&', '%26') + arg_v_3_esc = arg_v_3.replace('&', '%26') + arg_v_4_esc = arg_v_4.replace('&', '%26') + + return 'RunPlugin({}?{}={}&{}={}&{}={}&{}={})'.format(g_base_url, + arg_n_1, arg_v_1_esc, arg_n_2, arg_v_2_esc, arg_n_3, arg_v_3_esc, arg_n_4, arg_v_4_esc) + +# List of sorting methods here http://mirrors.xbmc.org/docs/python-docs/16.x-jarvis/xbmcplugin.html#-setSetting +def set_Kodi_unsorted_method(cfg): + if cfg.addon_handle < 0: return + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_UNSORTED) + +def set_Kodi_all_sorting_methods(cfg): + if cfg.addon_handle < 0: return + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_VIDEO_YEAR) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_STUDIO) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_GENRE) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_UNSORTED) + +def set_Kodi_all_sorting_methods_and_size(cfg): + if cfg.addon_handle < 0: return + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_VIDEO_YEAR) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_STUDIO) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_GENRE) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_SIZE) + xbmcplugin.addSortMethod(handle = cfg.addon_handle, sortMethod = xbmcplugin.SORT_METHOD_UNSORTED) + +# --------------------------------------------------------------------------------------------- +# Root menu rendering +# --------------------------------------------------------------------------------------------- +# Returns a dictionary rd (render data). +def set_render_root_data(): + # Tuple: catalog_name, catalog_key, title, plot, render colour. + root_Main = { + # Main filter Catalog + 'Main_Normal' : [ + 'Main', 'Normal', + 'Machines with coin slot (Normal)', + ('[COLOR orange]Main filter[/COLOR] of MAME machines [COLOR violet]with coin ' + 'slot[/COLOR] and normal controls. This list includes the machines you would ' + 'typically find in Europe and USA amusement arcades some decades ago.'), + COLOR_FILTER_MAIN, + ], + 'Main_Unusual' : [ + 'Main', 'Unusual', + 'Machines with coin slot (Unusual)', + ('[COLOR orange]Main filter[/COLOR] of MAME machines [COLOR violet]with coin ' + 'slot[/COLOR] and Only buttons, Gambling, Hanafuda and Mahjong controls. ' + 'This corresponds to slot, gambling and Japanese card and mahjong machines.'), + COLOR_FILTER_MAIN, + ], + 'Main_NoCoin' : [ + 'Main', 'NoCoin', + 'Machines with no coin slot', + ('[COLOR orange]Main filter[/COLOR] of MAME machines [COLOR violet]with no coin ' + 'slot[/COLOR]. Here you will find the good old MESS machines, including computers, ' + 'video game consoles, hand-held video game consoles, etc.'), + COLOR_FILTER_MAIN, + ], + 'Main_Mechanical' : [ + 'Main', 'Mechanical', + 'Mechanical machines', + ('[COLOR orange]Main filter[/COLOR] of [COLOR violet]mechanical[/COLOR] MAME machines. ' + 'These machines have mechanical parts, for example pinballs, and currently do not work with MAME. ' + 'They are here for preservation and historical reasons.'), + COLOR_FILTER_MAIN, + ], + 'Main_Dead' : [ + 'Main', 'Dead', + 'Dead machines', + ('[COLOR orange]Main filter[/COLOR] of [COLOR violet]dead[/COLOR] MAME machines. ' + 'Dead machines do not work and have no controls, so you cannot interact with them in any way.'), + COLOR_FILTER_MAIN, + ], + 'Main_Devices' : [ + 'Main', 'Devices', + 'Device machines', + ('[COLOR orange]Main filter[/COLOR] of [COLOR violet]device machines[/COLOR]. ' + 'Device machines, for example the Zilog Z80 CPU, are components used by other machines ' + 'and cannot be run on their own.'), + COLOR_FILTER_MAIN, + ], + } + + # Tuple: catalog_name, catalog_key, title, plot + root_Binary = { + # Binary filters Catalog + 'BIOS' : [ + 'Binary', 'BIOS', + 'Machines [BIOS]', + ('[COLOR orange]Binary filter[/COLOR] of [COLOR violet]BIOS[/COLOR] machines. Some BIOS ' + 'machines can be run and usually will display a message like "Game not found".'), + COLOR_FILTER_BINARY, + ], + 'CHD' : [ + 'Binary', 'CHD', + 'Machines [with CHDs]', + ('[COLOR orange]Binary filter[/COLOR] of machines that need one or more ' + '[COLOR violet]CHDs[/COLOR] to run. They may also need ROMs and/or BIOS or not.'), + COLOR_FILTER_BINARY, + ], + 'Samples' : [ + 'Binary', 'Samples', + 'Machines [with Samples]', + ('[COLOR orange]Binary filter[/COLOR] of machines that require ' + '[COLOR violet]samples[/COLOR]. Samples are optional and will increase the quality ' + 'of the emulated sound.'), + COLOR_FILTER_BINARY, + ], + 'SoftwareLists' : [ + 'Binary', 'SoftwareLists', + 'Machines [with Software Lists]', + ('[COLOR orange]Binary filter[/COLOR] of machines that have one or more ' + '[COLOR violet]Software Lists[/COLOR] associated.'), + COLOR_FILTER_BINARY, + ], + } + + # Tuple: title, plot, URL + root_categories = { + # Cataloged filters (optional DAT/INI files required) + 'Catver' : [ + 'Machines by Category (Catver)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by category. ' + 'This filter requires that you configure [COLOR violet]catver.ini[/COLOR].'), + misc_url_1_arg('catalog', 'Catver'), + COLOR_FILTER_CATALOG_DAT, + ], + 'Catlist' : [ + 'Machines by Category (Catlist)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by category. ' + 'This filter requires that you configure [COLOR violet]catlist.ini[/COLOR].'), + misc_url_1_arg('catalog', 'Catlist'), + COLOR_FILTER_CATALOG_DAT, + ], + 'Genre' : [ + 'Machines by Category (Genre)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by Genre. ' + 'This filter requires that you configure [COLOR violet]genre.ini[/COLOR].'), + misc_url_1_arg('catalog', 'Genre'), + COLOR_FILTER_CATALOG_DAT, + ], + 'Category' : [ + 'Machines by Category (MASH)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by Category. ' + 'This filter requires that you configure [COLOR violet]Category.ini[/COLOR] by MASH.'), + misc_url_1_arg('catalog', 'Category'), + COLOR_FILTER_CATALOG_DAT, + ], + 'NPlayers' : [ + 'Machines by Number of players', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by the number of ' + 'players that can play simultaneously or alternatively. This filter requires ' + 'that you configure [COLOR violet]nplayers.ini[/COLOR].'), + misc_url_1_arg('catalog', 'NPlayers'), + COLOR_FILTER_CATALOG_DAT, + ], + 'Bestgames' : [ + 'Machines by Rating', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by rating. The rating ' + 'is subjective but is a good indicator about the quality of the games. ' + 'This filter requires that you configure [COLOR violet]bestgames.ini[/COLOR].'), + misc_url_1_arg('catalog', 'Bestgames'), + COLOR_FILTER_CATALOG_DAT, + ], + 'Series' : [ + 'Machines by Series', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by series. ' + 'This filter requires that you configure [COLOR violet]series.ini[/COLOR].'), + misc_url_1_arg('catalog', 'Series'), + COLOR_FILTER_CATALOG_DAT, + ], + 'Alltime' : [ + 'Machines by Alltime (MASH)', + ('[COLOR orange]Catalog filter[/COLOR] of a best-quality machine selection ' + 'sorted by year. ' + 'This filter requires that you configure [COLOR violet]Alltime.ini[/COLOR] by MASH.'), + misc_url_1_arg('catalog', 'Alltime'), + COLOR_FILTER_CATALOG_DAT, + ], + 'Artwork' : [ + 'Machines by Artwork (MASH)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by Artwork. ' + 'This filter requires that you configure [COLOR violet]Artwork.ini[/COLOR] by MASH.'), + misc_url_1_arg('catalog', 'Artwork'), + COLOR_FILTER_CATALOG_DAT, + ], + 'Version' : [ + 'Machines by Version Added (Catver)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by Version Added. ' + 'This filter requires that you configure [COLOR violet]catver.ini[/COLOR].'), + misc_url_1_arg('catalog', 'Version'), + COLOR_FILTER_CATALOG_DAT, + ], + + # Cataloged filters (always there, extracted from MAME XML) + # NOTE: use the same names as MAME executable + # -listdevices list available devices XML tag <device_ref> + # -listslots list available slots and slot devices XML tag <slot> + # -listmedia list available media for the system XML tag <device> + 'Controls_Expanded' : [ + 'Machines by Controls (Expanded)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by control. ' + 'For each machine, all controls are included in the list.'), + misc_url_1_arg('catalog', 'Controls_Expanded'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Controls_Compact' : [ + 'Machines by Controls (Compact)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by control. ' + 'Machines may have additional controls.'), + misc_url_1_arg('catalog', 'Controls_Compact'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Devices_Expanded' : [ + 'Machines by Pluggable Devices (Expanded)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by pluggable devices. ' + 'For each machine, all pluggable devices are included in the list.'), + misc_url_1_arg('catalog', 'Devices_Expanded'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Devices_Compact' : [ + 'Machines by Pluggable Devices (Compact)', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by pluggable devices. ' + 'Machines may have additional pluggable devices.'), + misc_url_1_arg('catalog', 'Devices_Compact'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Display_Type' : [ + 'Machines by Display Type', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by display type ' + 'and rotation.'), + misc_url_1_arg('catalog', 'Display_Type'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Display_VSync' : [ + 'Machines by Display VSync freq', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by the display ' + 'vertical synchronisation (VSync) frequency, also known as the display refresh rate or ' + 'frames per second (FPS).'), + misc_url_1_arg('catalog', 'Display_VSync'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Display_Resolution' : [ + 'Machines by Display Resolution', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by display resolution.'), + misc_url_1_arg('catalog', 'Display_Resolution'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'CPU' : [ + 'Machines by CPU', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by the CPU used.'), + misc_url_1_arg('catalog', 'CPU'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Driver' : [ + 'Machines by Driver', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by driver. ' + 'Brother machines have the same driver.'), + misc_url_1_arg('catalog', 'Driver'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Manufacturer' : [ + 'Machines by Manufacturer', + ('[COLOR orange]Catalog filter[/COLOR] of MAME machines sorted by ' + 'manufacturer.'), + misc_url_1_arg('catalog', 'Manufacturer'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'ShortName' : [ + 'Machines by MAME short name', + ('[COLOR orange]Catalog filter[/COLOR] of MAME machines sorted alphabetically ' + 'by the MAME short name. The short name originated during the old MS-DOS days ' + 'where filenames were restricted to 8 ASCII characters.'), + misc_url_1_arg('catalog', 'ShortName'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'LongName' : [ + 'Machines by MAME long name', + ('[COLOR orange]Catalog filter[/COLOR] of MAME machines sorted alphabetically ' + 'by the machine description or long name.'), + misc_url_1_arg('catalog', 'LongName'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'BySL' : [ + 'Machines by Software List', + ('[COLOR orange]Catalog filter[/COLOR] of the Software Lists and the machines ' + 'that run items belonging to that Software List.'), + misc_url_1_arg('catalog', 'BySL'), + COLOR_FILTER_CATALOG_NODAT, + ], + 'Year' : [ + 'Machines by Year', + ('[COLOR orange]Catalog filter[/COLOR] of machines sorted by release year.'), + misc_url_1_arg('catalog', 'Year'), + COLOR_FILTER_CATALOG_NODAT, + ], + } + + # Tuple: title, plot, URL + root_special = { + # DAT browser: history.dat, mameinfo.dat, gameinit.dat, command.dat. + 'History' : [ + 'History DAT', + ('Browse the contents of [COLOR orange]history.dat[/COLOR]. Note that ' + 'history.dat is also available on the MAME machines and SL items context menu.'), + misc_url_1_arg('catalog', 'History'), + COLOR_MAME_DAT_BROWSER, + ], + 'MAMEINFO' : [ + 'MAMEINFO DAT', + ('Browse the contents of [COLOR orange]mameinfo.dat[/COLOR]. Note that ' + 'mameinfo.dat is also available on the MAME machines context menu.'), + misc_url_1_arg('catalog', 'MAMEINFO'), + COLOR_MAME_DAT_BROWSER, + ], + 'Gameinit' : [ + 'Gameinit DAT', + ('Browse the contents of [COLOR orange]gameinit.dat[/COLOR]. Note that ' + 'gameinit.dat is also available on the MAME machines context menu.'), + misc_url_1_arg('catalog', 'Gameinit'), + COLOR_MAME_DAT_BROWSER, + ], + 'Command' : [ + 'Command DAT', + ('Browse the contents of [COLOR orange]command.dat[/COLOR]. Note that ' + 'command.dat is also available on the MAME machines context menu.'), + misc_url_1_arg('catalog', 'Command'), + COLOR_MAME_DAT_BROWSER, + ], + } + + # Tuple: title, plot, URL + root_SL = { + 'SL' : [ + 'Software Lists (all)', + ('Display all [COLOR orange]Software Lists[/COLOR].'), + misc_url_1_arg('catalog', 'SL'), + COLOR_SOFTWARE_LISTS, + ], + 'SL_ROM' : [ + 'Software Lists (with ROMs)', + ('Display [COLOR orange]Software Lists[/COLOR] that have only ROMs and not CHDs (disks).'), + misc_url_1_arg('catalog', 'SL_ROM'), + COLOR_SOFTWARE_LISTS, + ], + 'SL_ROM_CHD' : [ + 'Software Lists (with ROMs and CHDs)', + ('Display [COLOR orange]Software Lists[/COLOR] that have both ROMs and CHDs.'), + misc_url_1_arg('catalog', 'SL_ROM_CHD'), + COLOR_SOFTWARE_LISTS, + ], + 'SL_CHD' : [ + 'Software Lists (with CHDs)', + ('Display [COLOR orange]Software Lists[/COLOR] that have only CHDs and not ROMs.'), + misc_url_1_arg('catalog', 'SL_CHD'), + COLOR_SOFTWARE_LISTS, + ], + 'SL_empty' : [ + 'Software Lists (no ROMs nor CHDs)', + ('Display [COLOR orange]Software Lists[/COLOR] with no ROMs nor CHDs.'), + misc_url_1_arg('catalog', 'SL_empty'), + COLOR_SOFTWARE_LISTS, + ], + } + + root_filters_CM = { + 'Custom_Filters' : [ + '[Custom MAME filters]', + ('[COLOR orange]Custom filters[/COLOR] allows to generate machine ' + 'listings perfectly tailored to your whises. For example, you can define a filter of all ' + 'the machines released in the 1980s that use a joystick. AML includes a fairly ' + 'complete default set of filters in XML format which can be edited.'), + misc_url_1_arg('command', 'SHOW_CUSTOM_FILTERS'), + [('Setup custom filters', misc_url_1_arg_RunPlugin('command', 'SETUP_CUSTOM_FILTERS'))], + COLOR_MAME_CUSTOM_FILTERS, + ], + } + + root_ROLs_CM = { + 'ROLs' : [ + '[AEL Read Only Launchers]', + ('[COLOR orange]AEL Read Only Launchers[/COLOR] are special launchers ' + 'exported to AEL. You can select your Favourite MAME machines or setup a custom ' + 'filter to enjoy your MAME games in AEL togheter with other emulators.'), + misc_url_1_arg('command', 'SHOW_AEL_ROLS'), + [('Setup ROLs', misc_url_1_arg_RunPlugin('command', 'SETUP_AEL_ROLS'))], + COLOR_AEL_ROLS, + ], + } + + # Tuple: title, plot, URL, context_menu_list + root_special_CM = { + 'MAME_Favs' : [ + '<Favourite MAME machines>', + ('Display your [COLOR orange]Favourite MAME machines[/COLOR]. ' + 'To add machines to the Favourite list use the context menu on any MAME machine list.'), + misc_url_1_arg('command', 'SHOW_MAME_FAVS'), + [('Manage Favourites', misc_url_1_arg_RunPlugin('command', 'MANAGE_MAME_FAV'))], + COLOR_MAME_SPECIAL, + ], + 'MAME_Most' : [ + '{Most Played MAME machines}', + ('Display the MAME machines that you play most, sorted by the number ' + 'of times you have launched them.'), + misc_url_1_arg('command', 'SHOW_MAME_MOST_PLAYED'), + [('Manage Most Played', misc_url_1_arg_RunPlugin('command', 'MANAGE_MAME_MOST_PLAYED'))], + COLOR_MAME_SPECIAL, + ], + 'MAME_Recent' : [ + '{Recently Played MAME machines}', + ('Display the MAME machines that you have launched recently.'), + misc_url_1_arg('command', 'SHOW_MAME_RECENTLY_PLAYED'), + [('Manage Recently Played', misc_url_1_arg_RunPlugin('command', 'MANAGE_MAME_RECENT_PLAYED'))], + COLOR_MAME_SPECIAL, + ], + 'SL_Favs' : [ + '<Favourite Software Lists ROMs>', + ('Display your [COLOR orange]Favourite Software List items[/COLOR]. ' + 'To add machines to the SL Favourite list use the context menu on any SL item list.'), + misc_url_1_arg('command', 'SHOW_SL_FAVS'), + [('Manage SL Favourites', misc_url_1_arg_RunPlugin('command', 'MANAGE_SL_FAV'))], + COLOR_SL_SPECIAL, + ], + 'SL_Most' : [ + '{Most Played SL ROMs}', + ('Display the Software List itmes that you play most, sorted by the number ' + 'of times you have launched them.'), + misc_url_1_arg('command', 'SHOW_SL_MOST_PLAYED'), + [('Manage SL Most Played', misc_url_1_arg_RunPlugin('command', 'MANAGE_SL_MOST_PLAYED'))], + COLOR_SL_SPECIAL, + ], + 'SL_Recent' : [ + '{Recently Played SL ROMs}', + 'Display the Software List items that you have launched recently.', + misc_url_1_arg('command', 'SHOW_SL_RECENTLY_PLAYED'), + [('Manage SL Recently Played', misc_url_1_arg_RunPlugin('command', 'MANAGE_SL_RECENT_PLAYED'))], + COLOR_SL_SPECIAL, + ], + } + + rd = { + 'root_Main' : root_Main, + 'root_Binary' : root_Binary, + 'root_categories' : root_categories, + 'root_special' : root_special, + 'root_SL' : root_SL, + 'root_filters_CM' : root_filters_CM, + 'root_ROLs_CM' : root_ROLs_CM, + 'root_special_CM' : root_special_CM, + } + + return rd + +def render_root_list(cfg): + mame_view_mode = cfg.settings['mame_view_mode'] + rd = set_render_root_data() + + # MAME machine count. + cache_index_dic = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + + # Do not crash if cache_index_dic is corrupted or has missing fields (may happen in + # upgrades). This function must never crash because the user must have always access to + # the setup menu. + try: + num_m_Main_Normal = cache_index_dic['Main']['Normal']['num_machines'] + num_m_Main_Unusual = cache_index_dic['Main']['Unusual']['num_machines'] + num_m_Main_NoCoin = cache_index_dic['Main']['NoCoin']['num_machines'] + num_m_Main_Mechanical = cache_index_dic['Main']['Mechanical']['num_machines'] + num_m_Main_Dead = cache_index_dic['Main']['Dead']['num_machines'] + num_m_Main_Devices = cache_index_dic['Main']['Devices']['num_machines'] + num_m_Binary_BIOS = cache_index_dic['Binary']['BIOS']['num_machines'] + num_m_Binary_CHD = cache_index_dic['Binary']['CHD']['num_machines'] + num_m_Binary_Samples = cache_index_dic['Binary']['Samples']['num_machines'] + num_m_Binary_SoftwareLists = cache_index_dic['Binary']['SoftwareLists']['num_machines'] + + num_p_Main_Normal = cache_index_dic['Main']['Normal']['num_parents'] + num_p_Main_Unusual = cache_index_dic['Main']['Unusual']['num_parents'] + num_p_Main_NoCoin = cache_index_dic['Main']['NoCoin']['num_parents'] + num_p_Main_Mechanical = cache_index_dic['Main']['Mechanical']['num_parents'] + num_p_Main_Dead = cache_index_dic['Main']['Dead']['num_parents'] + num_p_Main_Devices = cache_index_dic['Main']['Devices']['num_parents'] + num_p_Binary_BIOS = cache_index_dic['Binary']['BIOS']['num_parents'] + num_p_Binary_CHD = cache_index_dic['Binary']['CHD']['num_parents'] + num_p_Binary_Samples = cache_index_dic['Binary']['Samples']['num_parents'] + num_p_Binary_SoftwareLists = cache_index_dic['Binary']['SoftwareLists']['num_parents'] + + num_cat_Catver = len(cache_index_dic['Catver']) + num_cat_Catlist = len(cache_index_dic['Catlist']) + num_cat_Genre = len(cache_index_dic['Genre']) + num_cat_Category = len(cache_index_dic['Category']) + num_cat_NPlayers = len(cache_index_dic['NPlayers']) + num_cat_Bestgames = len(cache_index_dic['Bestgames']) + num_cat_Series = len(cache_index_dic['Series']) + num_cat_Alltime = len(cache_index_dic['Alltime']) + num_cat_Artwork = len(cache_index_dic['Artwork']) + num_cat_Version = len(cache_index_dic['Version']) + + num_cat_Controls_Expanded = len(cache_index_dic['Controls_Expanded']) + num_cat_Controls_Compact = len(cache_index_dic['Controls_Compact']) + num_cat_Devices_Expanded = len(cache_index_dic['Devices_Expanded']) + num_cat_Devices_Compact = len(cache_index_dic['Devices_Compact']) + num_cat_Display_Type = len(cache_index_dic['Display_Type']) + num_cat_Display_VSync = len(cache_index_dic['Display_VSync']) + num_cat_Display_Resolution = len(cache_index_dic['Display_Resolution']) + num_cat_CPU = len(cache_index_dic['CPU']) + num_cat_Driver = len(cache_index_dic['Driver']) + num_cat_Manufacturer = len(cache_index_dic['Manufacturer']) + num_cat_ShortName = len(cache_index_dic['ShortName']) + num_cat_LongName = len(cache_index_dic['LongName']) + num_cat_BySL = len(cache_index_dic['BySL']) + num_cat_Year = len(cache_index_dic['Year']) + + MAME_counters_available = True + except KeyError: + MAME_counters_available = False + log_debug('render_root_list() MAME_counters_available = {}'.format(MAME_counters_available)) + + # --- SL item count --- + if cfg.settings['global_enable_SL']: + SL_index_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + try: + num_SL_all = 0 + num_SL_ROMs = 0 + num_SL_CHDs = 0 + num_SL_mixed = 0 + num_SL_empty = 0 + for l_name, l_dic in SL_index_dic.items(): + num_SL_all += 1 + if l_dic['num_with_ROMs'] > 0 and l_dic['num_with_CHDs'] == 0: + num_SL_ROMs += 1 + elif l_dic['num_with_ROMs'] == 0 and l_dic['num_with_CHDs'] > 0: + num_SL_CHDs += 1 + elif l_dic['num_with_ROMs'] > 0 and l_dic['num_with_CHDs'] > 0: + num_SL_mixed += 1 + elif l_dic['num_with_ROMs'] == 0 and l_dic['num_with_CHDs'] == 0: + num_SL_empty += 1 + else: + log_error('Logical error in SL {}'.format(l_name)) + SL_counters_available = True + # log_debug('There are {} SL_all lists.'.format(num_SL_all)) + # log_debug('There are {} SL_ROMs lists.'.format(num_SL_ROMs)) + # log_debug('There are {} SL_mixed lists.'.format(num_SL_mixed)) + # log_debug('There are {} SL_CHDs lists.'.format(num_SL_CHDs)) + # log_debug('There are {} SL_empty lists.'.format(num_SL_empty)) + except KeyError as E: + SL_counters_available = False + # num_SL_empty always used to control visibility. If 0 then 'SL empty' is not visible. + num_SL_empty = 0 + else: + SL_counters_available = False + log_debug('render_root_list() SL_counters_available = {}'.format(SL_counters_available)) + + # --- Machine counters --- + if MAME_counters_available: + if mame_view_mode == VIEW_MODE_FLAT: + a = ' [COLOR orange]({} machines)[/COLOR]' + rd['root_Main']['Main_Normal'][2] += a.format(num_m_Main_Normal) + rd['root_Main']['Main_Unusual'][2] += a.format(num_m_Main_Unusual) + rd['root_Main']['Main_NoCoin'][2] += a.format(num_m_Main_NoCoin) + rd['root_Main']['Main_Mechanical'][2] += a.format(num_m_Main_Mechanical) + rd['root_Main']['Main_Dead'][2] += a.format(num_m_Main_Dead) + rd['root_Main']['Main_Devices'][2] += a.format(num_m_Main_Devices) + rd['root_Binary']['BIOS'][2] += a.format(num_m_Binary_BIOS) + rd['root_Binary']['CHD'][2] += a.format(num_m_Binary_CHD) + rd['root_Binary']['Samples'][2] += a.format(num_m_Binary_Samples) + rd['root_Binary']['SoftwareLists'][2] += a.format(num_m_Binary_SoftwareLists) + elif mame_view_mode == VIEW_MODE_PCLONE: + a = ' [COLOR orange]({} parents)[/COLOR]' + rd['root_Main']['Main_Normal'][2] += a.format(num_p_Main_Normal) + rd['root_Main']['Main_Unusual'][2] += a.format(num_p_Main_Unusual) + rd['root_Main']['Main_NoCoin'][2] += a.format(num_p_Main_NoCoin) + rd['root_Main']['Main_Mechanical'][2] += a.format(num_p_Main_Mechanical) + rd['root_Main']['Main_Dead'][2] += a.format(num_p_Main_Dead) + rd['root_Main']['Main_Devices'][2] += a.format(num_p_Main_Devices) + rd['root_Binary']['BIOS'][2] += a.format(num_p_Binary_BIOS) + rd['root_Binary']['CHD'][2] += a.format(num_p_Binary_CHD) + rd['root_Binary']['Samples'][2] += a.format(num_p_Binary_Samples) + rd['root_Binary']['SoftwareLists'][2] += a.format(num_p_Binary_SoftwareLists) + + a = ' [COLOR gold]({} items)[/COLOR]' + # Optional + rd['root_categories']['Catver'][0] += a.format(num_cat_Catver) + rd['root_categories']['Catlist'][0] += a.format(num_cat_Catlist) + rd['root_categories']['Genre'][0] += a.format(num_cat_Genre) + rd['root_categories']['Category'][0] += a.format(num_cat_Category) + rd['root_categories']['NPlayers'][0] += a.format(num_cat_NPlayers) + rd['root_categories']['Bestgames'][0] += a.format(num_cat_Bestgames) + rd['root_categories']['Series'][0] += a.format(num_cat_Series) + rd['root_categories']['Alltime'][0] += a.format(num_cat_Alltime) + rd['root_categories']['Artwork'][0] += a.format(num_cat_Artwork) + rd['root_categories']['Version'][0] += a.format(num_cat_Version) + # Always present + rd['root_categories']['Controls_Expanded'][0] += a.format(num_cat_Controls_Expanded) + rd['root_categories']['Controls_Compact'][0] += a.format(num_cat_Controls_Compact) + rd['root_categories']['Devices_Expanded'][0] += a.format(num_cat_Devices_Expanded) + rd['root_categories']['Devices_Compact'][0] += a.format(num_cat_Devices_Compact) + rd['root_categories']['Display_Type'][0] += a.format(num_cat_Display_Type) + rd['root_categories']['Display_VSync'][0] += a.format(num_cat_Display_VSync) + rd['root_categories']['Display_Resolution'][0] += a.format(num_cat_Display_Resolution) + rd['root_categories']['CPU'][0] += a.format(num_cat_CPU) + rd['root_categories']['Driver'][0] += a.format(num_cat_Driver) + rd['root_categories']['Manufacturer'][0] += a.format(num_cat_Manufacturer) + rd['root_categories']['ShortName'][0] += a.format(num_cat_ShortName) + rd['root_categories']['LongName'][0] += a.format(num_cat_LongName) + rd['root_categories']['BySL'][0] += a.format(num_cat_BySL) + rd['root_categories']['Year'][0] += a.format(num_cat_Year) + + if SL_counters_available: + a = ' [COLOR orange]({} lists)[/COLOR]' + rd['root_SL']['SL'][0] += a.format(num_SL_all) + rd['root_SL']['SL_ROM'][0] += a.format(num_SL_ROMs) + rd['root_SL']['SL_ROM_CHD'][0] += a.format(num_SL_mixed) + rd['root_SL']['SL_CHD'][0] += a.format(num_SL_CHDs) + rd['root_SL']['SL_empty'][0] += a.format(num_SL_empty) + + # If everything deactivated render the main filters so user has access to the context menu. + big_OR = cfg.settings['display_main_filters'] or cfg.settings['display_binary_filters'] or \ + cfg.settings['display_catalog_filters'] or cfg.settings['display_DAT_browser'] or \ + cfg.settings['display_SL_browser'] or cfg.settings['display_MAME_favs'] or \ + cfg.settings['display_SL_favs'] or cfg.settings['display_custom_filters'] + if not big_OR: + cfg.settings['display_main_filters'] = True + + # Main filters (Virtual catalog 'Main') + if cfg.settings['display_main_filters']: + render_root_catalog_row(cfg, *rd['root_Main']['Main_Normal']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_Unusual']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_NoCoin']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_Mechanical']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_Dead']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_Devices']) + + # Binary filters (Virtual catalog 'Binary') + if cfg.settings['display_binary_filters']: + render_root_catalog_row(cfg, *rd['root_Binary']['BIOS']) + render_root_catalog_row(cfg, *rd['root_Binary']['CHD']) + render_root_catalog_row(cfg, *rd['root_Binary']['Samples']) + if cfg.settings['global_enable_SL']: + render_root_catalog_row(cfg, *rd['root_Binary']['SoftwareLists']) + + if cfg.settings['display_catalog_filters']: + # Optional cataloged filters (depend on a INI file) + render_root_category_row(cfg, *rd['root_categories']['Catver']) + render_root_category_row(cfg, *rd['root_categories']['Catlist']) + render_root_category_row(cfg, *rd['root_categories']['Genre']) + render_root_category_row(cfg, *rd['root_categories']['Category']) + render_root_category_row(cfg, *rd['root_categories']['NPlayers']) + render_root_category_row(cfg, *rd['root_categories']['Bestgames']) + render_root_category_row(cfg, *rd['root_categories']['Series']) + render_root_category_row(cfg, *rd['root_categories']['Alltime']) + render_root_category_row(cfg, *rd['root_categories']['Artwork']) + render_root_category_row(cfg, *rd['root_categories']['Version']) + + # Cataloged filters (always there) + render_root_category_row(cfg, *rd['root_categories']['Controls_Expanded']) + render_root_category_row(cfg, *rd['root_categories']['Controls_Compact']) + render_root_category_row(cfg, *rd['root_categories']['Devices_Expanded']) + render_root_category_row(cfg, *rd['root_categories']['Devices_Compact']) + render_root_category_row(cfg, *rd['root_categories']['Display_Type']) + render_root_category_row(cfg, *rd['root_categories']['Display_VSync']) + render_root_category_row(cfg, *rd['root_categories']['Display_Resolution']) + render_root_category_row(cfg, *rd['root_categories']['CPU']) + render_root_category_row(cfg, *rd['root_categories']['Driver']) + render_root_category_row(cfg, *rd['root_categories']['Manufacturer']) + render_root_category_row(cfg, *rd['root_categories']['ShortName']) + render_root_category_row(cfg, *rd['root_categories']['LongName']) + if cfg.settings['global_enable_SL']: + render_root_category_row(cfg, *rd['root_categories']['BySL']) + render_root_category_row(cfg, *rd['root_categories']['Year']) + + # --- DAT browsers --- + if cfg.settings['display_DAT_browser']: + render_root_category_row(cfg, *rd['root_special']['History']) + render_root_category_row(cfg, *rd['root_special']['MAMEINFO']) + render_root_category_row(cfg, *rd['root_special']['Gameinit']) + render_root_category_row(cfg, *rd['root_special']['Command']) + + # --- Software lists --- + # If SL are globally disabled do not render SL browser. + # If SL are globally enabled, SL databases are built but the user may choose to not + # render the SL browser. + if cfg.settings['display_SL_browser'] and cfg.settings['global_enable_SL']: + render_root_category_row(cfg, *rd['root_SL']['SL']) + render_root_category_row(cfg, *rd['root_SL']['SL_ROM']) + render_root_category_row(cfg, *rd['root_SL']['SL_ROM_CHD']) + render_root_category_row(cfg, *rd['root_SL']['SL_CHD']) + if num_SL_empty > 0: + render_root_category_row(cfg, *rd['root_SL']['SL_empty']) + + # --- Special launchers --- + if cfg.settings['display_custom_filters']: + render_root_category_row_custom_CM(cfg, *rd['root_filters_CM']['Custom_Filters']) + + if cfg.settings['display_ROLs']: + render_root_category_row_custom_CM(cfg, *rd['root_ROLs_CM']['ROLs']) + + # --- MAME Favourite stuff --- + if cfg.settings['display_MAME_favs']: + render_root_category_row_custom_CM(cfg, *rd['root_special_CM']['MAME_Favs']) + if cfg.settings['display_MAME_most']: + render_root_category_row_custom_CM(cfg, *rd['root_special_CM']['MAME_Most']) + if cfg.settings['display_MAME_recent']: + render_root_category_row_custom_CM(cfg, *rd['root_special_CM']['MAME_Recent']) + + # --- SL Favourite stuff --- + if cfg.settings['display_SL_favs'] and cfg.settings['global_enable_SL']: + render_root_category_row_custom_CM(cfg, *rd['root_special_CM']['SL_Favs']) + if cfg.settings['display_SL_most'] and cfg.settings['global_enable_SL']: + render_root_category_row_custom_CM(cfg, *rd['root_special_CM']['SL_Most']) + if cfg.settings['display_SL_recent'] and cfg.settings['global_enable_SL']: + render_root_category_row_custom_CM(cfg, *rd['root_special_CM']['SL_Recent']) + + # Utilities and Reports special menus. + if cfg.settings['display_utilities']: + Utilities_plot = ('Execute several [COLOR orange]Utilities[/COLOR]. For example, to ' + 'check you AML configuration.') + URL = misc_url_1_arg('command', 'SHOW_UTILITIES_VLAUNCHERS') + render_root_category_row(cfg, 'Utilities', Utilities_plot, URL, COLOR_UTILITIES) + if cfg.settings['display_global_reports']: + Global_Reports_plot = ('View the [COLOR orange]Global Reports[/COLOR] and ' + 'machine and audit [COLOR orange]Statistics[/COLOR].') + URL = misc_url_1_arg('command', 'SHOW_GLOBALREPORTS_VLAUNCHERS') + render_root_category_row(cfg, 'Global Reports', Global_Reports_plot, URL, COLOR_GLOBAL_REPORTS) + + # End of directory. + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +# +# These _render_skin_* functions used by skins to display widgets. +# These functions must never fail and be silent in case of error. +# They are called by skin widgets. +# +def render_skin_fav_slots(cfg): + try: + rd = set_render_root_data() + # Remove special markers (first and last character) + rsCM = rd.copy() + for key in rsCM['root_special_CM']: + rsCM['root_special_CM'][key][0] = rsCM['root_special_CM'][key][0][1:-1] + render_root_category_row_custom_CM(cfg, *rsCM['root_special_CM']['MAME_Favs']) + render_root_category_row_custom_CM(cfg, *rsCM['root_special_CM']['MAME_Most']) + render_root_category_row_custom_CM(cfg, *rsCM['root_special_CM']['MAME_Recent']) + render_root_category_row_custom_CM(cfg, *rsCM['root_special_CM']['SL_Favs']) + render_root_category_row_custom_CM(cfg, *rsCM['root_special_CM']['SL_Most']) + render_root_category_row_custom_CM(cfg, *rsCM['root_special_CM']['SL_Recent']) + except: + log_error('Excepcion in render_skin_fav_slots()') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_skin_main_filters(cfg): + try: + rd = set_render_root_data() + render_root_catalog_row(cfg, *rd['root_Main']['Main_Normal']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_Unusual']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_NoCoin']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_Mechanical']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_Dead']) + render_root_catalog_row(cfg, *rd['root_Main']['Main_Devices']) + except: + log_error('Excepcion in render_skin_main_filters()') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_skin_binary_filters(cfg): + try: + rd = set_render_root_data() + render_root_catalog_row(cfg, *rd['root_Binary']['BIOS']) + render_root_catalog_row(cfg, *rd['root_Binary']['CHD']) + render_root_catalog_row(cfg, *rd['root_Binary']['Samples']) + render_root_catalog_row(cfg, *rd['root_Binary']['SoftwareLists']) + except: + log_error('Excepcion in render_skin_binary_filters()') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_skin_catalog_filters(cfg): + try: + # A mechanism to render only configured filters must be developed. + rd = set_render_root_data() + render_root_category_row(cfg, *rd['root_categories']['Catver']) + render_root_category_row(cfg, *rd['root_categories']['Catlist']) + render_root_category_row(cfg, *rd['root_categories']['Genre']) + render_root_category_row(cfg, *rd['root_categories']['Category']) + render_root_category_row(cfg, *rd['root_categories']['NPlayers']) + render_root_category_row(cfg, *rd['root_categories']['Bestgames']) + render_root_category_row(cfg, *rd['root_categories']['Series']) + render_root_category_row(cfg, *rd['root_categories']['Alltime']) + render_root_category_row(cfg, *rd['root_categories']['Artwork']) + render_root_category_row(cfg, *rd['root_categories']['Version']) + render_root_category_row(cfg, *rd['root_categories']['Controls_Expanded']) + render_root_category_row(cfg, *rd['root_categories']['Controls_Compact']) + render_root_category_row(cfg, *rd['root_categories']['Devices_Expanded']) + render_root_category_row(cfg, *rd['root_categories']['Devices_Compact']) + render_root_category_row(cfg, *rd['root_categories']['Display_Type']) + render_root_category_row(cfg, *rd['root_categories']['Display_VSync']) + render_root_category_row(cfg, *rd['root_categories']['Display_Resolution']) + render_root_category_row(cfg, *rd['root_categories']['CPU']) + render_root_category_row(cfg, *rd['root_categories']['Driver']) + render_root_category_row(cfg, *rd['root_categories']['Manufacturer']) + render_root_category_row(cfg, *rd['root_categories']['ShortName']) + render_root_category_row(cfg, *rd['root_categories']['LongName']) + render_root_category_row(cfg, *rd['root_categories']['BySL']) + render_root_category_row(cfg, *rd['root_categories']['Year']) + except: + log_error('Excepcion in render_skin_catalog_filters()') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_skin_dat_slots(cfg): + try: + rd = set_render_root_data() + render_root_category_row(cfg, *rd['root_special']['History']) + render_root_category_row(cfg, *rd['root_special']['MAMEINFO']) + render_root_category_row(cfg, *rd['root_special']['Gameinit']) + render_root_category_row(cfg, *rd['root_special']['Command']) + except: + log_error('Excepcion in render_skin_dat_slots()') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_skin_SL_filters(cfg): + if not cfg.settings['enable_SL']: + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + try: + rd = set_render_root_data() + render_root_category_row(cfg, *rd['root_SL']['SL']) + render_root_category_row(cfg, *rd['root_SL']['SL_ROM']) + render_root_category_row(cfg, *rd['root_SL']['SL_ROM_CHD']) + render_root_category_row(cfg, *rd['root_SL']['SL_CHD']) + render_root_category_row(cfg, *rd['root_SL']['SL_empty']) + except: + log_error('Excepcion in render_skin_SL_filters()') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +# +# A Catalog is equivalent to a Launcher in AEL. +# +def render_root_catalog_row(cfg, catalog_name, catalog_key, display_name, plot_str, color_str = COLOR_DEFAULT): + # --- Create listitem row --- + ICON_OVERLAY = 6 + listitem = xbmcgui.ListItem('{}{}{}'.format(color_str, display_name, COLOR_END)) + listitem.setInfo('video', {'title' : display_name, 'overlay' : ICON_OVERLAY, 'plot' : plot_str}) + + # --- Artwork --- + icon_path = cfg.ICON_FILE_PATH.getPath() + fanart_path = cfg.FANART_FILE_PATH.getPath() + listitem.setArt({'icon' : icon_path, 'fanart' : fanart_path}) + + # --- Create context menu --- + URL_utils = misc_url_3_arg_RunPlugin( + 'command', 'UTILITIES', 'catalog', catalog_name, 'category', catalog_key) + commands = [ + ('Setup addon', misc_url_1_arg_RunPlugin('command', 'SETUP_PLUGIN')), + ('Utilities', URL_utils), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + listitem.addContextMenuItems(commands) + URL = misc_url_2_arg('catalog', catalog_name, 'category', catalog_key) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = True) + +# +# A Category is equivalent to a Category in AEL. It contains a list of Launchers (catalogs). +# +def render_root_category_row(cfg, display_name, plot_str, root_URL, color_str = COLOR_DEFAULT): + # --- Create listitem row --- + ICON_OVERLAY = 6 + listitem = xbmcgui.ListItem('{}{}{}'.format(color_str, display_name, COLOR_END)) + listitem.setInfo('video', {'title' : display_name, 'overlay' : ICON_OVERLAY, 'plot' : plot_str}) + + # --- Artwork --- + icon_path = cfg.ICON_FILE_PATH.getPath() + fanart_path = cfg.FANART_FILE_PATH.getPath() + listitem.setArt({'icon' : icon_path, 'fanart' : fanart_path}) + + # --- Create context menu --- + commands = [ + ('Setup addon', misc_url_1_arg_RunPlugin('command', 'SETUP_PLUGIN')), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + listitem.addContextMenuItems(commands) + xbmcplugin.addDirectoryItem(cfg.addon_handle, root_URL, listitem, isFolder = True) + +def render_root_category_row_custom_CM(cfg, display_name, plot_str, root_URL, cmenu_list, color_str = COLOR_DEFAULT): + # --- Create listitem row --- + ICON_OVERLAY = 6 + listitem = xbmcgui.ListItem('{}{}{}'.format(color_str, display_name, COLOR_END)) + listitem.setInfo('video', {'title' : display_name, 'overlay' : ICON_OVERLAY, 'plot' : plot_str}) + + # --- Artwork --- + icon_path = cfg.ICON_FILE_PATH.getPath() + fanart_path = cfg.FANART_FILE_PATH.getPath() + listitem.setArt({'icon' : icon_path, 'fanart' : fanart_path}) + + # --- Create context menu --- + commands = [ + ('Setup addon', misc_url_1_arg_RunPlugin('command', 'SETUP_PLUGIN')), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + cmenu_list.extend(commands) + listitem.addContextMenuItems(cmenu_list) + xbmcplugin.addDirectoryItem(cfg.addon_handle, root_URL, listitem, isFolder = True) + +# ------------------------------------------------------------------------------------------------- +# Utilities and Global reports +# ------------------------------------------------------------------------------------------------- +def aux_get_generic_listitem(cfg, name, plot, commands): + vcategory_name = name + vcategory_plot = plot + vcategory_icon = cfg.ICON_FILE_PATH.getPath() + vcategory_fanart = cfg.FANART_FILE_PATH.getPath() + listitem = xbmcgui.ListItem(vcategory_name) + listitem.setInfo('video', {'title': vcategory_name, 'plot' : vcategory_plot, 'overlay' : 4}) + listitem.setArt({'icon' : vcategory_icon, 'fanart' : vcategory_fanart}) + listitem.addContextMenuItems(commands) + + return listitem + +def render_Utilities_vlaunchers(cfg): + # --- Common context menu for all VLaunchers --- + common_commands = [ + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + + # --- Check MAME version --- + t = 'Check MAME version' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'CHECK_MAME_VERSION') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Check AML configuration --- + t = 'Check AML configuration' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'CHECK_CONFIG') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Check/Update all Favourite objects --- + t = 'Check/Update all Favourite objects' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'CHECK_ALL_FAV_OBJECTS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Check MAME CRC hash collisions --- + t = 'Check MAME CRC hash collisions' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'CHECK_MAME_COLLISIONS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Check SL CRC hash collisions --- + t = 'Check SL CRC hash collisions' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'CHECK_SL_COLLISIONS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Check SL CRC hash collisions --- + t = 'Show machines with biggest ROMs' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'SHOW_BIGGEST_ROMS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Check SL CRC hash collisions --- + t = 'Show machines with smallest ROMs' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'SHOW_SMALLEST_ROMS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Export MAME ROMs DAT file --- + t = 'Export MAME info in Billyc999 XML format' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'EXPORT_MAME_INFO_BILLYC999_XML') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Export MAME ROMs DAT file --- + t = 'Export MAME ROMs Logiqx XML DAT file' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'EXPORT_MAME_ROM_DAT') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Export MAME CHDs DAT file --- + t = 'Export MAME CHDs Logiqx XML DAT file' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'EXPORT_MAME_CHD_DAT') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Export SL ROMs DAT file --- + # In AML 0.9.10 only export MAME XMLs and see how it goes. SL XMLs cause more trouble + # than MAME. + # listitem = aux_get_generic_listitem(cfg, + # 'Export SL ROMs Logiqx XML DAT file', 'Export SL ROMs Logiqx XML DAT file', commands) + # url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'EXPORT_SL_ROM_DAT') + # xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Export SL CHDs DAT file --- + # listitem = aux_get_generic_listitem(cfg, + # 'Export SL CHDs Logiqx XML DAT file', 'Export SL CHDs Logiqx XML DAT file', commands) + # url_str = misc_url_2_arg('command', 'EXECUTE_UTILITY', 'which', 'EXPORT_SL_CHD_DAT') + # xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- End of directory --- + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +# +# Kodi BUG: if size of text file to display is 0 then previous text in window is rendered. +# Solution: report files are never empty. Always print a text header in the report. +# +def render_GlobalReports_vlaunchers(cfg): + # --- Common context menu for all VLaunchers --- + common_commands = [ + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + + # --- View MAME last execution output -------------------------------------------------------- + if cfg.MAME_OUTPUT_PATH.exists(): + filesize = cfg.MAME_OUTPUT_PATH.fileSize() + STD_status = '{} bytes'.format(filesize) + else: + STD_status = 'not found' + listitem = aux_get_generic_listitem(cfg, + 'View MAME last execution output ({})'.format(STD_status), + 'View MAME last execution output', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_EXEC_OUTPUT') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- View statistics ------------------------------------------------------------------------ + # --- View main statistics --- + t = 'View main statistics' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_STATS_MAIN') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- View scanner statistics --- + t = 'View scanner statistics' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_STATS_SCANNER') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # View audit statistics. + t = 'View audit statistics' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_STATS_AUDIT') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # View timestamps and DAT/INI version. + t = 'View timestamps' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_STATS_TIMESTAMPS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- View all statistics --- + t = 'View all statistics' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_STATS_ALL') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Write all statistics to file --- + t = 'Write all statistics to file' + listitem = aux_get_generic_listitem(cfg, t, t, common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_STATS_WRITE_FILE') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- View ROM scanner reports --------------------------------------------------------------- + listitem = aux_get_generic_listitem(cfg, + 'View MAME scanner Full archives report', + ('Report of all MAME machines and the ROM ZIP files, CHDs and Sample ZIP files required ' + 'to run each machine.'), + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_MAME_ARCH_FULL') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME scanner Have archives report', + ('Report of all MAME machines where you have all the ROM ZIP files, CHDs and Sample ZIP ' + 'files necessary to run each machine.'), + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_MAME_ARCH_HAVE') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME scanner Missing archives report', + ('Report of all MAME machines where some of all ROM ZIP files, CHDs or Sample ZIP files ' + 'are missing.'), + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_MAME_ARCH_MISS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME scanner Missing ROM ZIP files', + 'Report a list of all Missing ROM ZIP files.', + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_MAME_ROM_LIST_MISS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME scanner Missing Sample ZIP files', + 'Report a list of all Missing Sample ZIP files.', + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_MAME_SAM_LIST_MISS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME scanner Missing CHD files', + 'List of all missing CHD files.', + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_MAME_CHD_LIST_MISS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- View Software Lists scanner reports ---------------------------------------------------- + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists scanner Full archives report', + 'View Full Software Lists item archives', + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_SL_FULL') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists scanner Have archives report', + 'View Have Software Lists item archives', + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_SL_HAVE') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists scanner Missing archives report', + 'View Missing Software Lists item archives', + common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_SL_MISS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- Asset scanner reports ------------------------------------------------------------------ + listitem = aux_get_generic_listitem(cfg, + 'View MAME asset scanner report', + 'View MAME asset scanner report', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_MAME_ASSETS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists asset scanner report', + 'View Software Lists asset scanner report', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_SCANNER_SL_ASSETS') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- View MAME Audit reports ---------------------------------------------------------------- + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit Machine Full report', + 'View MAME audit report (Full)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_FULL') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit Machine Good report', + 'View MAME audit report (Good)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_GOOD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit Machine Bad report', + 'View MAME audit report (Errors)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_BAD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit ROM Good report', + 'View MAME audit report (ROMs Good)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_ROM_GOOD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit ROM Bad report', + 'View MAME audit report (ROM Errors)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_ROM_BAD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit Samples Good report', + 'View MAME audit report (Samples Good)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_SAM_GOOD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit Sample Bad report', + 'View MAME audit report (Sample Errors)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_SAM_BAD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit CHD Good report', + 'View MAME audit report (CHDs Good)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_CHD_GOOD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View MAME audit CHD Bad report', + 'View MAME audit report (CHD Errors)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_MAME_CHD_BAD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- View SL Audit reports ------------------------------------------------------------------ + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists audit Full report', + 'View SL audit report (Full)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_SL_FULL') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists audit Good report', + 'View SL audit report (Good)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_SL_GOOD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists audit Bad report', + 'View SL audit report (Errors)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_SL_BAD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists audit ROM Good report', + 'View SL audit report (ROM Good)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_SL_ROM_GOOD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists audit ROM Errors report', + 'View SL audit report (ROM Errors)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_SL_ROM_BAD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists audit CHD Good report', + 'View SL audit report (CHD Good)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_SL_CHD_GOOD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + listitem = aux_get_generic_listitem(cfg, + 'View Software Lists audit CHD Errors report', + 'View SL audit report (CHD Errors)', common_commands) + url_str = misc_url_2_arg('command', 'EXECUTE_REPORT', 'which', 'VIEW_AUDIT_SL_CHD_BAD') + xbmcplugin.addDirectoryItem(cfg.addon_handle, url_str, listitem, isFolder = False) + + # --- End of directory --- + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +#---------------------------------------------------------------------------------------------- +# Cataloged machines +#---------------------------------------------------------------------------------------------- +# +# Renders the Launchers inside a Category for MAME. +# +def render_catalog_list(cfg, catalog_name): + log_debug('render_catalog_list() Starting...') + log_debug('render_catalog_list() catalog_name = "{}"'.format(catalog_name)) + + # --- General AML plugin check --- + # Check if databases have been built, print warning messages, etc. This function returns + # False if no issues, True if there is issues and a dialog has been printed. + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + check_MAME_DB_before_rendering_catalog(cfg, st_dic, control_dic) + if kodi_is_error_status(st_dic): + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + kodi_display_status_message(st_dic) + return + + # Render categories in catalog index + set_Kodi_all_sorting_methods_and_size(cfg) + mame_view_mode = cfg.settings['mame_view_mode'] + loading_ticks_start = time.time() + cache_index_dic = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + if mame_view_mode == VIEW_MODE_FLAT: + catalog_dic = db_get_cataloged_dic_all(cfg, catalog_name) + elif mame_view_mode == VIEW_MODE_PCLONE: + catalog_dic = db_get_cataloged_dic_parents(cfg, catalog_name) + if not catalog_dic: + kodi_dialog_OK('Catalog is empty. Rebuild the MAME databases.') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + loading_ticks_end = time.time() + rendering_ticks_start = time.time() + for catalog_key in sorted(catalog_dic): + if mame_view_mode == VIEW_MODE_FLAT: + num_machines = cache_index_dic[catalog_name][catalog_key]['num_machines'] + machine_str = 'machine' if num_machines == 1 else 'machines' + elif mame_view_mode == VIEW_MODE_PCLONE: + num_machines = cache_index_dic[catalog_name][catalog_key]['num_parents'] + machine_str = 'parent' if num_machines == 1 else 'parents' + render_catalog_list_row(cfg, catalog_name, catalog_key, num_machines, machine_str) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + rendering_ticks_end = time.time() + + # DEBUG Data loading/rendering statistics. + log_debug('Loading seconds {}'.format(loading_ticks_end - loading_ticks_start)) + log_debug('Rendering seconds {}'.format(rendering_ticks_end - rendering_ticks_start)) + +def render_catalog_list_row(cfg, catalog_name, catalog_key, num_machines, machine_str): + # --- Create listitem row --- + ICON_OVERLAY = 6 + title_str = '{} [COLOR orange]({} {})[/COLOR]'.format(catalog_key, num_machines, machine_str) + plot_str = 'Catalog {}\nCategory {}'.format(catalog_name, catalog_key) + listitem = xbmcgui.ListItem(title_str) + listitem.setInfo('video', { + 'title' : title_str, 'plot' : plot_str, + 'overlay' : ICON_OVERLAY, 'size' : num_machines + }) + + # --- Artwork --- + icon_path = cfg.ICON_FILE_PATH.getPath() + fanart_path = cfg.FANART_FILE_PATH.getPath() + listitem.setArt({'icon' : icon_path, 'fanart' : fanart_path}) + + # --- Create context menu --- + URL_utils = misc_url_3_arg_RunPlugin( + 'command', 'UTILITIES', 'catalog', catalog_name, 'category', catalog_key) + commands = [ + ('Utilities', URL_utils), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + listitem.addContextMenuItems(commands) + URL = misc_url_2_arg('catalog', catalog_name, 'category', catalog_key) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = True) + +# +# Renders a list of parent MAME machines knowing the catalog name and the category. +# Also renders machine lists in flat mode. +# Display mode: a) parents only b) all machines (flat) +# +def render_catalog_parent_list(cfg, catalog_name, category_name): + # When using threads the performance gain is small: from 0.76 to 0.71, just 20 ms. + # It's not worth it. + log_debug('render_catalog_parent_list() catalog_name = {}'.format(catalog_name)) + log_debug('render_catalog_parent_list() category_name = {}'.format(category_name)) + + # --- Load ListItem properties (Not used at the moment) --- + # prop_key = '{} - {}'.format(catalog_name, category_name) + # log_debug('render_catalog_parent_list() Loading props with key "{}"'.format(prop_key)) + # mame_properties_dic = utils_load_JSON_file(cfg.MAIN_PROPERTIES_PATH.getPath()) + # prop_dic = mame_properties_dic[prop_key] + # view_mode_property = prop_dic['vm'] + + # --- Global properties --- + view_mode_property = cfg.settings['mame_view_mode'] + log_debug('render_catalog_parent_list() view_mode_property = {}'.format(view_mode_property)) + + # --- General AML plugin check --- + # Check if databases have been built, print warning messages, etc. + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + check_MAME_DB_before_rendering_machines(cfg, st_dic, control_dic) + if kodi_is_error_status(st_dic): + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + kodi_display_status_message(st_dic) + return + + # --- Load main MAME info databases and catalog --- + l_cataloged_dic_start = time.time() + if view_mode_property == VIEW_MODE_PCLONE: + catalog_dic = db_get_cataloged_dic_parents(cfg, catalog_name) + elif view_mode_property == VIEW_MODE_FLAT: + catalog_dic = db_get_cataloged_dic_all(cfg, catalog_name) + else: + kodi_dialog_OK('Wrong view_mode_property = "{}". '.format(view_mode_property) + + 'This is a bug, please report it.') + return + l_cataloged_dic_end = time.time() + l_render_db_start = time.time() + if cfg.settings['debug_enable_MAME_render_cache']: + cache_index_dic = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + render_db_dic = db_get_render_cache_row(cfg, cache_index_dic, catalog_name, category_name) + else: + log_debug('MAME machine cache disabled.') + render_db_dic = utils_load_JSON_file(cfg.RENDER_DB_PATH.getPath()) + l_render_db_end = time.time() + l_assets_db_start = time.time() + if cfg.settings['debug_enable_MAME_asset_cache']: + if 'cache_index_dic' not in locals(): + cache_index_dic = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + assets_db_dic = db_get_asset_cache_row(cfg, cache_index_dic, catalog_name, category_name) + else: + log_debug('MAME asset cache disabled.') + assets_db_dic = utils_load_JSON_file(cfg.ASSET_DB_PATH.getPath()) + l_assets_db_end = time.time() + l_pclone_dic_start = time.time() + main_pclone_dic = utils_load_JSON_file(cfg.MAIN_PCLONE_DB_PATH.getPath()) + l_pclone_dic_end = time.time() + l_favs_start = time.time() + fav_machines = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + l_favs_end = time.time() + + # --- Compute loading times --- + catalog_t = l_cataloged_dic_end - l_cataloged_dic_start + render_t = l_render_db_end - l_render_db_start + assets_t = l_assets_db_end - l_assets_db_start + pclone_t = l_pclone_dic_end - l_pclone_dic_start + favs_t = l_favs_end - l_favs_start + loading_time = catalog_t + render_t + assets_t + pclone_t + favs_t + + # --- Check if catalog is empty --- + if not catalog_dic: + kodi_dialog_OK('Catalog is empty. Check out "Setup addon" in the context menu.') + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + # --- Process ROMs for rendering --- + processing_ticks_start = time.time() + r_list = render_process_machines(cfg, catalog_dic, catalog_name, category_name, + render_db_dic, assets_db_dic, fav_machines, True, main_pclone_dic, False) + processing_time = time.time() - processing_ticks_start + + # --- Commit ROMs --- + rendering_ticks_start = time.time() + set_Kodi_all_sorting_methods(cfg) + render_commit_machines(cfg, r_list) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + rendering_time = time.time() - rendering_ticks_start + + # --- DEBUG Data loading/rendering statistics --- + total_time = loading_time + processing_time + rendering_time + # log_debug('Loading catalog {0:.4f} s'.format(catalog_t)) + # log_debug('Loading render db {0:.4f} s'.format(render_t)) + # log_debug('Loading assets db {0:.4f} s'.format(assets_t)) + # log_debug('Loading pclone dic {0:.4f} s'.format(pclone_t)) + # log_debug('Loading MAME favs {0:.4f} s'.format(favs_t)) + log_debug('Loading time {0:.4f} s'.format(loading_time)) + log_debug('Processing time {0:.4f} s'.format(processing_time)) + log_debug('Rendering time {0:.4f} s'.format(rendering_time)) + log_debug('Total time {0:.4f} s'.format(total_time)) + +# +# Renders a list of MAME Clone machines (including parent). +# No need to check for DB existance here. If this function is called is because parents and +# hence all ROMs databases exist. +# +def render_catalog_clone_list(cfg, catalog_name, category_name, parent_name): + log_debug('render_catalog_clone_list() catalog_name = {}'.format(catalog_name)) + log_debug('render_catalog_clone_list() category_name = {}'.format(category_name)) + log_debug('render_catalog_clone_list() parent_name = {}'.format(parent_name)) + display_hide_Mature = cfg.settings['display_hide_Mature'] + display_hide_BIOS = cfg.settings['display_hide_BIOS'] + if catalog_name == 'None' and category_name == 'BIOS': display_hide_BIOS = False + display_hide_nonworking = cfg.settings['display_hide_nonworking'] + display_hide_imperfect = cfg.settings['display_hide_imperfect'] + view_mode_property = cfg.settings['mame_view_mode'] + log_debug('render_catalog_clone_list() view_mode_property = {}'.format(view_mode_property)) + + # --- Load main MAME info DB --- + loading_ticks_start = time.time() + catalog_dic = db_get_cataloged_dic_all(cfg, catalog_name) + if cfg.settings['debug_enable_MAME_render_cache']: + cache_index_dic = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + render_db_dic = db_get_render_cache_row(cfg, cache_index_dic, catalog_name, category_name) + else: + log_debug('MAME machine cache disabled.') + render_db_dic = utils_load_JSON_file(cfg.RENDER_DB_PATH.getPath()) + if cfg.settings['debug_enable_MAME_asset_cache']: + if 'cache_index_dic' not in locals(): + cache_index_dic = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + assets_db_dic = db_get_asset_cache_row(cfg, cache_index_dic, catalog_name, category_name) + else: + log_debug('MAME asset cache disabled.') + assets_db_dic = utils_load_JSON_file(cfg.ASSET_DB_PATH.getPath()) + main_pclone_dic = utils_load_JSON_file(cfg.MAIN_PCLONE_DB_PATH.getPath()) + fav_machines = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + loading_ticks_end = time.time() + loading_time = loading_ticks_end - loading_ticks_start + + # --- Process ROMs --- + processing_ticks_start = time.time() + machine_dic = catalog_dic[category_name] + t_catalog_dic = {} + t_render_dic = {} + t_assets_dic = {} + # Render parent first + t_catalog_dic[category_name] = {parent_name : machine_dic[parent_name]} + t_render_dic[parent_name] = render_db_dic[parent_name] + t_assets_dic[parent_name] = assets_db_dic[parent_name] + # Then clones + for clone_name in main_pclone_dic[parent_name]: + t_catalog_dic[category_name][clone_name] = machine_dic[clone_name] + t_render_dic[clone_name] = render_db_dic[clone_name] + t_assets_dic[clone_name] = assets_db_dic[clone_name] + r_list = render_process_machines(cfg, t_catalog_dic, catalog_name, category_name, + t_render_dic, t_assets_dic, fav_machines, False, main_pclone_dic, False) + processing_ticks_end = time.time() + processing_time = processing_ticks_end - processing_ticks_start + + # --- Commit ROMs --- + rendering_ticks_start = time.time() + set_Kodi_all_sorting_methods(cfg) + render_commit_machines(cfg, r_list) + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + rendering_ticks_end = time.time() + rendering_time = rendering_ticks_end - rendering_ticks_start + + # --- DEBUG Data loading/rendering statistics --- + total_time = loading_time + processing_time + rendering_time + log_debug('Loading {0:.4f} s'.format(loading_time)) + log_debug('Processing {0:.4f} s'.format(processing_time)) + log_debug('Rendering {0:.4f} s'.format(rendering_time)) + log_debug('Total {0:.4f} s'.format(total_time)) + +# +# First make this function work OK, then try to optimize it. +# "Premature optimization is the root of all evil." Donald Knuth +# Returns a list of dictionaries: +# r_list = [ +# { +# 'm_name' : text_type, 'render_name' : text_type, +# 'info' : {}, 'props' : {}, 'art' : {}, +# 'context' : [], 'URL' ; text_type +# }, ... +# ] +# +# By default renders a flat list, main_pclone_dic is not needed and filters are ignored. +# These settings are for rendering the custom MAME filters. +# +def render_process_machines(cfg, catalog_dic, catalog_name, category_name, + render_db_dic, assets_dic, fav_machines, + flag_parent_list = False, main_pclone_dic = None, flag_ignore_filters = True): + # --- Prepare for processing --- + display_hide_Mature = cfg.settings['display_hide_Mature'] + display_hide_BIOS = cfg.settings['display_hide_BIOS'] + if catalog_name == 'None' and category_name == 'BIOS': display_hide_BIOS = False + display_hide_nonworking = cfg.settings['display_hide_nonworking'] + display_hide_imperfect = cfg.settings['display_hide_imperfect'] + display_rom_available = cfg.settings['display_rom_available'] + display_chd_available = cfg.settings['display_chd_available'] + display_MAME_flags = cfg.settings['display_MAME_flags'] + + # --- Traverse machines --- + r_list = [] + for machine_name in catalog_dic[category_name]: + render_name = catalog_dic[category_name][machine_name] + machine = render_db_dic[machine_name] + m_assets = assets_dic[machine_name] + if not flag_ignore_filters: + if display_hide_Mature and machine['isMature']: continue + if display_hide_BIOS and machine['isBIOS']: continue + if display_hide_nonworking and machine['driver_status'] == 'preliminary': continue + if display_hide_imperfect and machine['driver_status'] == 'imperfect': continue + if display_rom_available and m_assets['flags'][0] == 'r': continue + if display_chd_available and m_assets['flags'][1] == 'c': continue + + # --- Add machine to list, set default values --- + r_dict = {} + r_dict['m_name'] = machine_name + AEL_InFav_bool_value = AEL_INFAV_BOOL_VALUE_FALSE + AEL_PClone_stat_value = AEL_PCLONE_STAT_VALUE_NONE + + # main_pclone_dic and num_clones only used when rendering parents. + if flag_parent_list: + num_clones = len(main_pclone_dic[machine_name]) if machine_name in main_pclone_dic else 0 + + # --- Render machine name string --- + display_name = render_name + if display_MAME_flags: + # Mark Flags, BIOS, Devices, BIOS, Parent/Clone and Driver status. + flags_str = ' [COLOR skyblue]{}[/COLOR]'.format(m_assets['flags']) + if machine['isBIOS']: flags_str += ' [COLOR cyan][BIOS][/COLOR]' + if machine['isDevice']: flags_str += ' [COLOR violet][Dev][/COLOR]' + if machine['driver_status'] == 'imperfect': + flags_str += ' [COLOR yellow][Imp][/COLOR]' + elif machine['driver_status'] == 'preliminary': + flags_str += ' [COLOR red][Pre][/COLOR]' + else: + flags_str = '' + if flag_parent_list and num_clones > 0: + # All machines here are parents. Mark number of clones. + display_name += ' [COLOR orange] ({} clones)[/COLOR]'.format(num_clones) + # Machine flags. + if flags_str: display_name += flags_str + # Skin flags. + if machine_name in fav_machines: + display_name += ' [COLOR violet][Fav][/COLOR]' + AEL_InFav_bool_value = AEL_INFAV_BOOL_VALUE_TRUE + AEL_PClone_stat_value = AEL_PCLONE_STAT_VALUE_PARENT + else: + if flags_str: display_name += flags_str + if machine_name in fav_machines: + display_name += ' [COLOR violet][Fav][/COLOR]' + AEL_InFav_bool_value = AEL_INFAV_BOOL_VALUE_TRUE + if machine['cloneof']: + display_name += ' [COLOR orange][Clo][/COLOR]' + AEL_PClone_stat_value = AEL_PCLONE_STAT_VALUE_CLONE + else: + AEL_PClone_stat_value = AEL_PCLONE_STAT_VALUE_PARENT + + # --- Assets/artwork --- + icon_path = m_assets[cfg.mame_icon] if m_assets[cfg.mame_icon] else 'DefaultProgram.png' + fanart_path = m_assets[cfg.mame_fanart] + banner_path = m_assets['marquee'] + clearlogo_path = m_assets['clearlogo'] + poster_path = m_assets['3dbox'] if m_assets['3dbox'] else m_assets['flyer'] + + # --- Create listitem row --- + # Make all the infolabels compatible with Advanced Emulator Launcher + ICON_OVERLAY = 6 + r_dict['render_name'] = display_name + if cfg.settings['display_hide_trailers']: + r_dict['info'] = { + 'title' : display_name, 'year' : machine['year'], + 'genre' : machine['genre'], 'studio' : machine['manufacturer'], + 'plot' : m_assets['plot'], 'overlay' : ICON_OVERLAY, + } + else: + r_dict['info'] = { + 'title' : display_name, 'year' : machine['year'], + 'genre' : machine['genre'], 'studio' : machine['manufacturer'], + 'plot' : m_assets['plot'], 'overlay' : ICON_OVERLAY, + 'trailer' : m_assets['trailer'], + } + r_dict['props'] = { + 'nplayers' : machine['nplayers'], + 'platform' : 'MAME', + 'history' : m_assets['history'], + AEL_PCLONE_STAT_LABEL : AEL_PClone_stat_value, + AEL_INFAV_BOOL_LABEL : AEL_InFav_bool_value, + } + + # --- Assets --- + r_dict['art'] = { + 'title' : m_assets['title'], 'snap' : m_assets['snap'], + 'boxfront' : m_assets['cabinet'], 'boxback' : m_assets['cpanel'], + 'cartridge' : m_assets['PCB'], 'flyer' : m_assets['flyer'], + '3dbox' : m_assets['3dbox'], + 'icon' : icon_path, 'fanart' : fanart_path, + 'banner' : banner_path, 'clearlogo' : clearlogo_path, + 'poster' : poster_path + } + + # --- Create context menu --- + URL_view_DAT = misc_url_2_arg_RunPlugin('command', 'VIEW_DAT', 'machine', machine_name) + URL_view = misc_url_2_arg_RunPlugin('command', 'VIEW', 'machine', machine_name) + URL_fav = misc_url_2_arg_RunPlugin('command', 'ADD_MAME_FAV', 'machine', machine_name) + if flag_parent_list and num_clones > 0: + URL_clones = misc_url_4_arg_RunPlugin('command', 'EXEC_SHOW_MAME_CLONES', + 'catalog', catalog_name, 'category', category_name, 'parent', machine_name) + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Show clones', URL_clones), + ('Add to MAME Favourites', URL_fav), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + else: + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Add to MAME Favourites', URL_fav), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + r_dict['context'] = commands + + # Add row to the list. + r_dict['URL'] = misc_url_2_arg('command', 'LAUNCH', 'machine', machine_name) + r_list.append(r_dict) + + return r_list + +# Renders a processed list of machines/ROMs. Basically, this function only calls the +# Kodi API with the precomputed values. +def render_commit_machines(cfg, r_list): + listitem_list = [] + + if kodi_running_version >= KODI_VERSION_LEIA: + # Kodi Leia and up. + log_debug('Rendering machine list in Kodi Leia and up.') + for r_dict in r_list: + # --- New offscreen parameter in Leia --- + # offscreen increases the performance a bit. For example, for a list with 4058 items: + # offscreent = True Rendering time 0.4620 s + # offscreent = True Rendering time 0.5780 s + # See https://forum.kodi.tv/showthread.php?tid=329315&pid=2711937#pid2711937 + # and https://forum.kodi.tv/showthread.php?tid=307394&pid=2531524 + listitem = xbmcgui.ListItem(r_dict['render_name'], offscreen = True) + listitem.setInfo('video', r_dict['info']) + listitem.setProperties(r_dict['props']) + listitem.setArt(r_dict['art']) + listitem.addContextMenuItems(r_dict['context']) + listitem_list.append((r_dict['URL'], listitem, False)) + else: + # Kodi Krypton and down. + log_debug('Rendering machine list in Kodi Krypton and down.') + for r_dict in r_list: + listitem = xbmcgui.ListItem(r_dict['render_name']) + listitem.setInfo('video', r_dict['info']) + for prop_name in r_dict['props']: + listitem.setProperty(prop_name, r_dict['props'][prop_name]) + listitem.setArt(r_dict['art']) + listitem.addContextMenuItems(r_dict['context']) + listitem_list.append((r_dict['URL'], listitem, False)) + + # Add all listitems in one go. + xbmcplugin.addDirectoryItems(cfg.addon_handle, listitem_list, len(listitem_list)) + +# Not used at the moment -> There are global display settings in addon settings for this. +def command_context_display_settings(cfg, catalog_name, category_name): + # Load ListItem properties. + log_debug('command_display_settings() catalog_name "{}"'.format(catalog_name)) + log_debug('command_display_settings() category_name "{}"'.format(category_name)) + prop_key = '{} - {}'.format(catalog_name, category_name) + log_debug('command_display_settings() Loading props with key "{}"'.format(prop_key)) + mame_properties_dic = utils_load_JSON_file(cfg.MAIN_PROPERTIES_PATH.getPath()) + prop_dic = mame_properties_dic[prop_key] + dmode_str = 'Parents only' if prop_dic['vm'] == VIEW_MODE_NORMAL else 'Parents and clones' + + # Select menu. + dialog = xbmcgui.Dialog() + menu_item = dialog.select('Display settings', [ + 'Display mode (currently {})'.format(dmode_str), + 'Default Icon', + 'Default Fanart', + 'Default Banner', + 'Default Poster', + 'Default Clearlogo' + ]) + if menu_item < 0: return + + # --- Display settings --- + if menu_item == 0: + # Krypton feature: preselect the current item. + # NOTE Preselect must be called with named parameter, otherwise it does not work well. + # See http://forum.kodi.tv/showthread.php?tid=250936&pid=2327011#pid2327011 + p_idx = 0 if prop_dic['vm'] == VIEW_MODE_NORMAL else 1 + log_debug('command_display_settings() p_idx = "{}"'.format(p_idx)) + idx = dialog.select('Display mode', ['Parents only', 'Parents and clones'], preselect = p_idx) + log_debug('command_display_settings() idx = "{}"'.format(idx)) + if idx < 0: return + if idx == 0: prop_dic['vm'] = VIEW_MODE_NORMAL + elif idx == 1: prop_dic['vm'] = VIEW_MODE_ALL + + # --- Change default icon --- + elif menu_item == 1: + kodi_dialog_OK('Not coded yet. Sorry') + + # Changes made. Refresh container. + utils_write_JSON_file(cfg.MAIN_PROPERTIES_PATH.getPath(), mame_properties_dic) + kodi_refresh_container() + +#---------------------------------------------------------------------------------------------- +# Software Lists +#---------------------------------------------------------------------------------------------- +def render_SL_list(cfg, catalog_name): + log_debug('render_SL_list() catalog_name = {}\n'.format(catalog_name)) + + # --- General AML plugin check --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + check_SL_DB_before_rendering_catalog(cfg, st_dic, control_dic) + if kodi_is_error_status(st_dic): + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + kodi_display_status_message(st_dic) + return + + # --- Load Software List catalog and build render catalog --- + SL_main_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + SL_catalog_dic = {} + if catalog_name == 'SL': + for SL_name in SL_main_catalog_dic: + SL_catalog_dic[SL_name] = SL_main_catalog_dic[SL_name] + elif catalog_name == 'SL_ROM': + for SL_name in SL_main_catalog_dic: + SL_dic = SL_main_catalog_dic[SL_name] + if SL_dic['num_with_ROMs'] > 0 and SL_dic['num_with_CHDs'] == 0: + SL_catalog_dic[SL_name] = SL_dic + elif catalog_name == 'SL_CHD': + for SL_name in SL_main_catalog_dic: + SL_dic = SL_main_catalog_dic[SL_name] + if SL_dic['num_with_ROMs'] == 0 and SL_dic['num_with_CHDs'] > 0: + SL_catalog_dic[SL_name] = SL_dic + elif catalog_name == 'SL_ROM_CHD': + for SL_name in SL_main_catalog_dic: + SL_dic = SL_main_catalog_dic[SL_name] + if SL_dic['num_with_ROMs'] > 0 and SL_dic['num_with_CHDs'] > 0: + SL_catalog_dic[SL_name] = SL_dic + elif catalog_name == 'SL_empty': + for SL_name in SL_main_catalog_dic: + SL_dic = SL_main_catalog_dic[SL_name] + if SL_dic['num_with_ROMs'] == 0 and SL_dic['num_with_CHDs'] == 0: + SL_catalog_dic[SL_name] = SL_dic + else: + kodi_dialog_OK('Wrong catalog_name {}'.format(catalog_name)) + return + log_debug('render_SL_list() len(catalog_name) = {}\n'.format(len(SL_catalog_dic))) + + set_Kodi_all_sorting_methods(cfg) + for SL_name in SL_catalog_dic: + SL = SL_catalog_dic[SL_name] + render_SL_list_row(cfg, SL_name, SL) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_SL_ROMs(cfg, SL_name): + log_debug('render_SL_ROMs() SL_name "{}"'.format(SL_name)) + + # --- General AML plugin check --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + check_SL_DB_before_rendering_machines(cfg, st_dic, control_dic) + if kodi_is_error_status(st_dic): + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + kodi_display_status_message(st_dic) + return + + # Load ListItem properties (Not used at the moment) + # SL_properties_dic = utils_load_JSON_file(cfg.SL_MACHINES_PROP_PATH.getPath()) + # prop_dic = SL_properties_dic[SL_name] + # Global properties + view_mode_property = cfg.settings['sl_view_mode'] + log_debug('render_SL_ROMs() view_mode_property = {}'.format(view_mode_property)) + + # Load Software List ROMs + SL_PClone_dic = utils_load_JSON_file(cfg.SL_PCLONE_DIC_PATH.getPath()) + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_items.json' + SL_DB_FN = cfg.SL_DB_DIR.pjoin(file_name) + assets_file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath()) + SL_asset_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + + set_Kodi_all_sorting_methods(cfg) + SL_proper_name = SL_catalog_dic[SL_name]['display_name'] + if view_mode_property == VIEW_MODE_PCLONE: + # Get list of parents + log_debug('render_SL_ROMs() Rendering Parent/Clone launcher') + parent_list = [] + for parent_name in sorted(SL_PClone_dic[SL_name]): parent_list.append(parent_name) + for parent_name in parent_list: + ROM = SL_roms[parent_name] + assets = SL_asset_dic[parent_name] if parent_name in SL_asset_dic else db_new_SL_asset() + num_clones = len(SL_PClone_dic[SL_name][parent_name]) + ROM['genre'] = SL_proper_name # Add the SL name as 'genre' + render_SL_ROM_row(cfg, SL_name, parent_name, ROM, assets, True, num_clones) + elif view_mode_property == VIEW_MODE_FLAT: + log_debug('render_SL_ROMs() Rendering Flat launcher') + for rom_name in SL_roms: + ROM = SL_roms[rom_name] + assets = SL_asset_dic[rom_name] if rom_name in SL_asset_dic else db_new_SL_asset() + ROM['genre'] = SL_proper_name # Add the SL name as 'genre' + render_SL_ROM_row(cfg, SL_name, rom_name, ROM, assets) + else: + kodi_dialog_OK('Wrong vm = "{}". This is a bug, please report it.'.format(prop_dic['vm'])) + return + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_SL_pclone_set(cfg, SL_name, parent_name): + log_debug('render_SL_pclone_set() SL_name "{}"'.format(SL_name)) + log_debug('render_SL_pclone_set() parent_name "{}"'.format(parent_name)) + view_mode_property = cfg.settings['sl_view_mode'] + log_debug('render_SL_pclone_set() view_mode_property = {}'.format(view_mode_property)) + + # Load Software List ROMs. + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + SL_PClone_dic = utils_load_JSON_file(cfg.SL_PCLONE_DIC_PATH.getPath()) + file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_items.json' + SL_DB_FN = cfg.SL_DB_DIR.pjoin(file_name) + log_debug('render_SL_pclone_set() ROMs JSON "{}"'.format(SL_DB_FN.getPath())) + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath()) + + assets_file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_asset_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + + # Render parent first. + SL_proper_name = SL_catalog_dic[SL_name]['display_name'] + set_Kodi_all_sorting_methods(cfg) + ROM = SL_roms[parent_name] + assets = SL_asset_dic[parent_name] if parent_name in SL_asset_dic else db_new_SL_asset() + ROM['genre'] = SL_proper_name # >> Add the SL name as 'genre' + render_SL_ROM_row(cfg, SL_name, parent_name, ROM, assets, False, view_mode_property) + + # Render clones belonging to parent in this category. + for clone_name in sorted(SL_PClone_dic[SL_name][parent_name]): + ROM = SL_roms[clone_name] + assets = SL_asset_dic[clone_name] if clone_name in SL_asset_dic else db_new_SL_asset() + ROM['genre'] = SL_proper_name # >> Add the SL name as 'genre' + render_SL_ROM_row(cfg, SL_name, clone_name, ROM, assets) + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_SL_list_row(cfg, SL_name, SL): + # --- Display number of ROMs and CHDs --- + # if SL['num_with_CHDs'] == 0: + # if SL['num_with_ROMs'] == 1: f_str = '{} [COLOR orange]({} ROM)[/COLOR]' + # else: f_str = '{} [COLOR orange]({} ROMs)[/COLOR]' + # display_name = f_str.format(SL['display_name'], SL['num_with_ROMs']) + # elif SL['num_with_ROMs'] == 0: + # if SL['num_with_CHDs'] == 1: f_str = '{} [COLOR orange]({} CHD)[/COLOR]' + # else: f_str = '{} [COLOR orange]({} CHDs)[/COLOR]' + # display_name = f_str.format(SL['display_name'], SL['num_with_CHDs']) + # else: + # display_name = '{} [COLOR orange]({} ROMs and {} CHDs)[/COLOR]'.format( + # SL['display_name'], SL['num_with_ROMs'], SL['num_with_CHDs']) + + # --- Display Parents or Total SL items --- + view_mode_property = cfg.settings['sl_view_mode'] + if view_mode_property == VIEW_MODE_PCLONE: + if SL['num_parents'] == 1: f_str = '{} [COLOR orange]({} parent)[/COLOR]' + else: f_str = '{} [COLOR orange]({} parents)[/COLOR]' + display_name = f_str.format(SL['display_name'], SL['num_parents']) + elif view_mode_property == VIEW_MODE_FLAT: + if SL['num_items'] == 1: f_str = '{} [COLOR orange]({} item)[/COLOR]' + else: f_str = '{} [COLOR orange]({} items)[/COLOR]' + display_name = f_str.format(SL['display_name'], SL['num_items']) + else: + raise TypeError('Wrong view_mode_property {}'.format(view_mode_property)) + + # --- Create listitem row --- + ICON_OVERLAY = 6 + listitem = xbmcgui.ListItem(display_name) + listitem.setInfo('video', {'title' : display_name, 'overlay' : ICON_OVERLAY } ) + listitem.addContextMenuItems([ + ('Kodi File Manager', 'ActivateWindow(filemanager)' ), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ]) + URL = misc_url_2_arg('catalog', 'SL', 'category', SL_name) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = True) + +# TODO: render flag if SL item is in Favourites. +def render_SL_ROM_row(cfg, SL_name, rom_name, ROM, assets, flag_parent_list = False, num_clones = 0): + only_display_SL_items_available = cfg.settings['display_SL_items_available'] + display_SL_flags = cfg.settings['display_SL_flags'] + + # Skip SL item rendering if not available. Only skip SL items when the scanner + # has been done, always render if status is unknown. + item_not_available = ROM['status_ROM'] == 'r' or ROM['status_CHD'] == 'c' + if only_display_SL_items_available and item_not_available: return + display_name = ROM['description'] + if flag_parent_list and num_clones > 0: + # Print (n clones) and '--' flags. + display_name += ' [COLOR orange] ({} clones)[/COLOR]'.format(num_clones) + if display_SL_flags: + status = '{}{}'.format(ROM['status_ROM'], ROM['status_CHD']) + display_name += ' [COLOR skyblue]{}[/COLOR]'.format(status) + else: + # Print '--' flags and '[Clo]' flag. + if display_SL_flags: + status = '{}{}'.format(ROM['status_ROM'], ROM['status_CHD']) + display_name += ' [COLOR skyblue]{}[/COLOR]'.format(status) + if ROM['cloneof']: display_name += ' [COLOR orange][Clo][/COLOR]' + + # --- Assets/artwork --- + icon_path = assets[cfg.SL_icon] if assets[cfg.SL_icon] else 'DefaultProgram.png' + fanart_path = assets[cfg.SL_fanart] + poster_path = assets['3dbox'] if assets['3dbox'] else assets['boxfront'] + + # --- Create listitem row --- + ICON_OVERLAY = 6 + listitem = xbmcgui.ListItem(display_name) + # Make all the infolabels compatible with Advanced Emulator Launcher + if cfg.settings['display_hide_trailers']: + listitem.setInfo('video', { + 'title' : display_name, 'year' : ROM['year'], + 'genre' : ROM['genre'], 'studio' : ROM['publisher'], + 'plot' : ROM['plot'], 'overlay' : ICON_OVERLAY + }) + else: + listitem.setInfo('video', { + 'title' : display_name, 'year' : ROM['year'], + 'genre' : ROM['genre'], 'studio' : ROM['publisher'], + 'plot' : ROM['plot'], 'overlay' : ICON_OVERLAY, + 'trailer' : assets['trailer'] + }) + listitem.setProperty('platform', 'MAME Software List') + + # --- Assets --- + # AEL custom artwork fields. + listitem.setArt({ + 'title' : assets['title'], 'snap' : assets['snap'], + 'boxfront' : assets['boxfront'], '3dbox' : assets['3dbox'], + 'icon' : icon_path, 'fanart' : fanart_path, 'poster' : poster_path + }) + + # --- Create context menu --- + URL_view_DAT = misc_url_3_arg_RunPlugin('command', 'VIEW_DAT', 'SL', SL_name, 'ROM', rom_name) + URL_view = misc_url_3_arg_RunPlugin('command', 'VIEW', 'SL', SL_name, 'ROM', rom_name) + URL_fav = misc_url_3_arg_RunPlugin('command', 'ADD_SL_FAV', 'SL', SL_name, 'ROM', rom_name) + if flag_parent_list and num_clones > 0: + URL_show_c = misc_url_4_arg_RunPlugin('command', 'EXEC_SHOW_SL_CLONES', + 'catalog', 'SL', 'category', SL_name, 'parent', rom_name) + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Show clones', URL_show_c), + ('Add ROM to SL Favourites', URL_fav), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + else: + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Add ROM to SL Favourites', URL_fav), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + listitem.addContextMenuItems(commands) + URL = misc_url_3_arg('command', 'LAUNCH_SL', 'SL', SL_name, 'ROM', rom_name) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = False) + +#---------------------------------------------------------------------------------------------- +# DATs +# +# catalog = 'History' / category = '32x' / machine = 'sonic' +# catalog = 'MAMEINFO' / category = '32x' / machine = 'sonic' +# catalog = 'Gameinit' / category = 'None' / machine = 'sonic' +# catalog = 'Command' / category = 'None' / machine = 'sonic' +#---------------------------------------------------------------------------------------------- +def render_DAT_list(cfg, catalog_name): + # --- Create context menu --- + commands = [ + ('View', misc_url_1_arg_RunPlugin('command', 'VIEW')), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + # --- Unrolled variables --- + ICON_OVERLAY = 6 + + if catalog_name == 'History': + # Render list of categories. + DAT_idx_dic = utils_load_JSON_file(cfg.HISTORY_IDX_PATH.getPath()) + if not DAT_idx_dic: + kodi_dialog_OK('DAT database file "{}" empty.'.format(catalog_name)) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + set_Kodi_all_sorting_methods(cfg) + for key in DAT_idx_dic: + category_name = '{} [COLOR lightgray]({})[/COLOR]'.format(DAT_idx_dic[key]['name'], key) + listitem = xbmcgui.ListItem(category_name) + listitem.setInfo('video', {'title' : category_name, 'overlay' : ICON_OVERLAY } ) + listitem.addContextMenuItems(commands) + URL = misc_url_2_arg('catalog', catalog_name, 'category', key) + xbmcplugin.addDirectoryItem(cfg.addon_handle, url = URL, listitem = listitem, isFolder = True) + elif catalog_name == 'MAMEINFO': + # Render list of categories. + DAT_idx_dic = utils_load_JSON_file(cfg.MAMEINFO_IDX_PATH.getPath()) + if not DAT_idx_dic: + kodi_dialog_OK('DAT database file "{}" empty.'.format(catalog_name)) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + set_Kodi_all_sorting_methods(cfg) + for key in DAT_idx_dic: + category_name = '{}'.format(key) + listitem = xbmcgui.ListItem(category_name) + listitem.setInfo('video', {'title' : category_name, 'overlay' : ICON_OVERLAY } ) + listitem.addContextMenuItems(commands) + URL = misc_url_2_arg('catalog', catalog_name, 'category', key) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = True) + elif catalog_name == 'Gameinit': + # Render list of machines. + DAT_idx_dic = utils_load_JSON_file(cfg.GAMEINIT_IDX_PATH.getPath()) + if not DAT_idx_dic: + kodi_dialog_OK('DAT database file "{}" empty.'.format(catalog_name)) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + set_Kodi_all_sorting_methods(cfg) + for machine_key in DAT_idx_dic: + machine_name = '{} [COLOR lightgray]({})[/COLOR]'.format(DAT_idx_dic[machine_key], machine_key) + listitem = xbmcgui.ListItem(machine_name) + listitem.setInfo('video', {'title' : machine_name, 'overlay' : ICON_OVERLAY } ) + listitem.addContextMenuItems(commands) + URL = misc_url_3_arg('catalog', catalog_name, 'category', 'None', 'machine', machine_key) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = False) + elif catalog_name == 'Command': + # Render list of machines. + DAT_idx_dic = utils_load_JSON_file(cfg.COMMAND_IDX_PATH.getPath()) + if not DAT_idx_dic: + kodi_dialog_OK('DAT database file "{}" empty.'.format(catalog_name)) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + set_Kodi_all_sorting_methods(cfg) + for machine_key in DAT_idx_dic: + machine_name = '{} [COLOR lightgray]({})[/COLOR]'.format(DAT_idx_dic[machine_key], machine_key) + listitem = xbmcgui.ListItem(machine_name) + listitem.setInfo('video', {'title' : machine_name, 'overlay' : ICON_OVERLAY } ) + listitem.addContextMenuItems(commands) + URL = misc_url_3_arg('catalog', catalog_name, 'category', 'None', 'machine', machine_key) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = False) + else: + kodi_dialog_OK( + 'DAT database file "{}" not found. Check out "Setup addon" in the context menu.'.format(catalog_name)) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +# Only History.dat and MAMEinfo.dat have categories. +def render_DAT_category(cfg, catalog_name, category_name): + # Load Software List catalog + if catalog_name == 'History': + DAT_catalog_dic = utils_load_JSON_file(cfg.HISTORY_IDX_PATH.getPath()) + elif catalog_name == 'MAMEINFO': + DAT_catalog_dic = utils_load_JSON_file(cfg.MAMEINFO_IDX_PATH.getPath()) + else: + kodi_dialog_OK('DAT database file "{}" not found. ' + 'Check out "Setup addon" in the context menu.'.format(catalog_name)) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + if not DAT_catalog_dic: + kodi_dialog_OK('DAT database file "{}" empty.'.format(catalog_name)) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + set_Kodi_all_sorting_methods(cfg) + if catalog_name == 'History': + category_machine_dic = DAT_catalog_dic[category_name]['machines'] + for machine_key in category_machine_dic: + display_name, db_list, db_machine = category_machine_dic[machine_key].split('|') + render_DAT_category_row(cfg, catalog_name, category_name, machine_key, display_name) + elif catalog_name == 'MAMEINFO': + category_machine_dic = DAT_catalog_dic[category_name] + for machine_key in category_machine_dic: + display_name = category_machine_dic[machine_key] + render_DAT_category_row(cfg, catalog_name, category_name, machine_key, display_name) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_DAT_category_row(cfg, catalog_name, category_name, machine_key, display_name): + # --- Create listitem row --- + ICON_OVERLAY = 6 + display_name = '{} [COLOR lightgray]({})[/COLOR]'.format(display_name, machine_key) + listitem = xbmcgui.ListItem(display_name) + listitem.setInfo('video', {'title' : display_name, 'overlay' : ICON_OVERLAY } ) + commands = [ + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('Add-on Settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + listitem.addContextMenuItems(commands) + URL = misc_url_3_arg('catalog', catalog_name, 'category', category_name, 'machine', machine_key) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = False) + +def render_DAT_machine_info(cfg, catalog_name, category_name, machine_name): + log_debug('render_DAT_machine_info() catalog_name "{}"'.format(catalog_name)) + log_debug('render_DAT_machine_info() category_name "{}"'.format(category_name)) + log_debug('render_DAT_machine_info() machine_name "{}"'.format(machine_name)) + + if catalog_name == 'History': + DAT_idx_dic = utils_load_JSON_file(cfg.HISTORY_IDX_PATH.getPath()) + DAT_dic = utils_load_JSON_file(cfg.HISTORY_DB_PATH.getPath()) + display_name, db_list, db_machine = DAT_idx_dic[category_name]['machines'][machine_name].split('|') + t_str = ('History for [COLOR=orange]{}[/COLOR] item [COLOR=orange]{}[/COLOR] ' + '(DB entry [COLOR=orange]{}[/COLOR] / [COLOR=orange]{}[/COLOR])') + window_title = t_str.format(category_name, machine_name, db_list, db_machine) + info_text = DAT_dic[db_list][db_machine] + elif catalog_name == 'MAMEINFO': + DAT_dic = utils_load_JSON_file(cfg.MAMEINFO_DB_PATH.getPath()) + t_str = 'MAMEINFO information for [COLOR=orange]{}[/COLOR] item [COLOR=orange]{}[/COLOR]' + window_title = t_str.format(category_name, machine_name) + info_text = DAT_dic[category_name][machine_name] + elif catalog_name == 'Gameinit': + DAT_dic = utils_load_JSON_file(cfg.GAMEINIT_DB_PATH.getPath()) + window_title = 'Gameinit information for [COLOR=orange]{}[/COLOR]'.format(machine_name) + info_text = DAT_dic[machine_name] + elif catalog_name == 'Command': + DAT_dic = utils_load_JSON_file(cfg.COMMAND_DB_PATH.getPath()) + window_title = 'Command information for [COLOR=orange]{}[/COLOR]'.format(machine_name) + info_text = DAT_dic[machine_name] + else: + kodi_dialog_OK( + 'Wrong catalog_name "{}". This is a bug, please report it.'.format(catalog_name)) + return + + # --- Show information window --- + kodi_display_text_window_mono(window_title, info_text) + +# +# Not used at the moment -> There are global display settings. +# +def command_context_display_settings_SL(cfg, SL_name): + log_debug('command_display_settings_SL() SL_name "{}"'.format(SL_name)) + + # --- Load properties DB --- + SL_properties_dic = utils_load_JSON_file(cfg.SL_MACHINES_PROP_PATH.getPath()) + prop_dic = SL_properties_dic[SL_name] + + # --- Show menu --- + dmode_str = 'Parents only' if prop_dic['vm'] == VIEW_MODE_NORMAL else 'Parents and clones' + dialog = xbmcgui.Dialog() + menu_item = dialog.select('Display settings', [ + 'Display mode (currently {})'.format(dmode_str), + 'Default Icon', 'Default Fanart', + 'Default Banner', 'Default Poster', 'Default Clearlogo' + ]) + if menu_item < 0: return + + # --- Change display mode --- + if menu_item == 0: + p_idx = 0 if prop_dic['vm'] == VIEW_MODE_NORMAL else 1 + log_debug('command_display_settings() p_idx = "{}"'.format(p_idx)) + idx = dialog.select('Display mode', ['Parents only', 'Parents and clones'], preselect = p_idx) + log_debug('command_display_settings() idx = "{}"'.format(idx)) + if idx < 0: return + prop_dic['vm'] = VIEW_MODE_NORMAL if idx == 0 else VIEW_MODE_ALL + + # --- Change default icon --- + elif menu_item == 1: + kodi_dialog_OK('Not coded yet. Sorry') + + # --- Save display settings --- + utils_write_JSON_file(cfg.SL_MACHINES_PROP_PATH.getPath(), SL_properties_dic) + kodi_refresh_container() + +# --------------------------------------------------------------------------------------------- +# Information display / Utilities +# --------------------------------------------------------------------------------------------- +def command_context_info_utils(cfg, machine_name, SL_name, SL_ROM, location): + VIEW_MAME_MACHINE = 100 + VIEW_SL_ROM = 200 + + ACTION_VIEW_HISTORY = 100 + ACTION_VIEW_MAMEINFO = 200 + ACTION_VIEW_GAMEINIT = 300 + ACTION_VIEW_COMMAND = 400 + ACTION_VIEW_FANART = 500 + ACTION_VIEW_MANUAL = 600 + ACTION_VIEW_BROTHERS = 700 + ACTION_VIEW_SAME_GENRE = 800 + ACTION_VIEW_SAME_MANUFACTURER = 900 + + # --- Determine if we are in a category, launcher or ROM --- + log_debug('command_context_info_utils() machine_name "{}"'.format(machine_name)) + log_debug('command_context_info_utils() SL_name "{}"'.format(SL_name)) + log_debug('command_context_info_utils() SL_ROM "{}"'.format(SL_ROM)) + log_debug('command_context_info_utils() location "{}"'.format(location)) + if machine_name: + view_type = VIEW_MAME_MACHINE + elif SL_name: + view_type = VIEW_SL_ROM + else: + raise TypeError('Logic error in command_context_info_utils()') + log_debug('command_context_info_utils() view_type = {}'.format(view_type)) + + if view_type == VIEW_MAME_MACHINE: + # --- Load DAT indices --- + History_idx_dic = utils_load_JSON_file(cfg.HISTORY_IDX_PATH.getPath()) + Mameinfo_idx_dic = utils_load_JSON_file(cfg.MAMEINFO_IDX_PATH.getPath()) + Gameinit_idx_list = utils_load_JSON_file(cfg.GAMEINIT_IDX_PATH.getPath()) + Command_idx_list = utils_load_JSON_file(cfg.COMMAND_IDX_PATH.getPath()) + + # --- Check if DAT information is available for this machine --- + if History_idx_dic: + History_str = 'Found' if machine_name in History_idx_dic['mame']['machines'] else 'Not found' + else: + History_str = 'Not configured' + if Mameinfo_idx_dic: + Mameinfo_str = 'Found' if machine_name in Mameinfo_idx_dic['mame'] else 'Not found' + else: + Mameinfo_str = 'Not configured' + if Gameinit_idx_list: + Gameinit_str = 'Found' if machine_name in Gameinit_idx_list else 'Not found' + else: + Gameinit_str = 'Not configured' + if Command_idx_list: + Command_str = 'Found' if machine_name in Command_idx_list else 'Not found' + else: + Command_str = 'Not configured' + + # Check Fanart and Manual. Load hashed databases. + # NOTE A ROM loading factory need to be coded to deal with the different ROM + # locations to avoid duplicate code. Have a look at ACTION_VIEW_MACHINE_DATA + # in function _command_context_view() + # Fanart_str = + # Manual_str = + + elif view_type == VIEW_SL_ROM: + History_idx_dic = utils_load_JSON_file(cfg.HISTORY_IDX_PATH.getPath()) + if History_idx_dic: + if SL_name in History_idx_dic: + History_str = 'Found' if SL_ROM in History_idx_dic[SL_name]['machines'] else 'Not found' + else: + History_str = 'SL not found' + else: + History_str = 'Not configured' + + # Check Fanart and Manual. + # Fanart_str = + # Manual_str = + + # --- Build menu base on view_type --- + if view_type == VIEW_MAME_MACHINE: + d_list = [ + 'View History DAT ({})'.format(History_str), + 'View MAMEinfo DAT ({})'.format(Mameinfo_str), + 'View Gameinit DAT ({})'.format(Gameinit_str), + 'View Command DAT ({})'.format(Command_str), + 'View Fanart', + 'View Manual', + 'Display brother machines', + 'Display machines with same Genre', + 'Display machines by same Manufacturer', + ] + elif view_type == VIEW_SL_ROM: + d_list = [ + 'View History DAT ({})'.format(History_str), + 'View Fanart', + 'View Manual', + ] + else: + kodi_dialog_OK('Wrong view_type = {}. This is a bug, please report it.'.format(view_type)) + return + selected_value = xbmcgui.Dialog().select('View', d_list) + if selected_value < 0: return + + # --- Polymorphic menu. Determine action to do. --- + if view_type == VIEW_MAME_MACHINE: + if selected_value == 0: action = ACTION_VIEW_HISTORY + elif selected_value == 1: action = ACTION_VIEW_MAMEINFO + elif selected_value == 2: action = ACTION_VIEW_GAMEINIT + elif selected_value == 3: action = ACTION_VIEW_COMMAND + elif selected_value == 4: action = ACTION_VIEW_FANART + elif selected_value == 5: action = ACTION_VIEW_MANUAL + elif selected_value == 6: action = ACTION_VIEW_BROTHERS + elif selected_value == 7: action = ACTION_VIEW_SAME_GENRE + elif selected_value == 8: action = ACTION_VIEW_SAME_MANUFACTURER + else: + kodi_dialog_OK( + 'view_type == VIEW_MAME_MACHINE and selected_value = {}. '.format(selected_value) + + 'This is a bug, please report it.') + return + elif view_type == VIEW_SL_ROM: + if selected_value == 0: action = ACTION_VIEW_HISTORY + elif selected_value == 1: action = ACTION_VIEW_FANART + elif selected_value == 2: action = ACTION_VIEW_MANUAL + else: + kodi_dialog_OK( + 'view_type == VIEW_SL_ROM and selected_value = {}. '.format(selected_value) + + 'This is a bug, please report it.') + return + + # --- Execute action --- + if action == ACTION_VIEW_HISTORY: + if view_type == VIEW_MAME_MACHINE: + if machine_name not in History_idx_dic['mame']['machines']: + kodi_dialog_OK('MAME machine {} not in History DAT'.format(machine_name)) + return + m_str = History_idx_dic['mame']['machines'][machine_name] + display_name, db_list, db_machine = m_str.split('|') + History_DAT_dic = utils_load_JSON_file(cfg.HISTORY_DB_PATH.getPath()) + t_str = ('History DAT for MAME machine [COLOR=orange]{}[/COLOR] ' + '(DB entry [COLOR=orange]{}[/COLOR])') + window_title = t_str.format(machine_name, db_machine) + elif view_type == VIEW_SL_ROM: + if SL_name not in History_idx_dic: + kodi_dialog_OK('SL {} not found in History DAT'.format(SL_name)) + return + if SL_ROM not in History_idx_dic[SL_name]['machines']: + kodi_dialog_OK('SL {} item {} not in History DAT'.format(SL_name, SL_ROM)) + return + m_str = History_idx_dic[SL_name]['machines'][SL_ROM] + display_name, db_list, db_machine = m_str.split('|') + History_DAT_dic = utils_load_JSON_file(cfg.HISTORY_DB_PATH.getPath()) + t_str = ('History DAT for SL [COLOR=orange]{}[/COLOR] item [COLOR=orange]{}[/COLOR] ' + '(DB entry [COLOR=orange]{}[/COLOR] / [COLOR=orange]{}[/COLOR])') + window_title = t_str.format(SL_name, SL_ROM, db_list, db_machine) + kodi_display_text_window_mono(window_title, History_DAT_dic[db_list][db_machine]) + + elif action == ACTION_VIEW_MAMEINFO: + if machine_name not in Mameinfo_idx_dic['mame']: + kodi_dialog_OK('Machine {} not in Mameinfo DAT'.format(machine_name)) + return + DAT_dic = utils_load_JSON_file(cfg.MAMEINFO_DB_PATH.getPath()) + t_str = 'MAMEINFO information for [COLOR=orange]{}[/COLOR] item [COLOR=orange]{}[/COLOR]' + window_title = t_str.format('mame', machine_name) + kodi_display_text_window_mono(window_title, DAT_dic['mame'][machine_name]) + + elif action == ACTION_VIEW_GAMEINIT: + if machine_name not in Gameinit_idx_list: + kodi_dialog_OK('Machine {} not in Gameinit DAT'.format(machine_name)) + return + DAT_dic = utils_load_JSON_file(cfg.GAMEINIT_DB_PATH.getPath()) + window_title = 'Gameinit information for [COLOR=orange]{}[/COLOR]'.format(machine_name) + kodi_display_text_window_mono(window_title, DAT_dic[machine_name]) + + elif action == ACTION_VIEW_COMMAND: + if machine_name not in Command_idx_list: + kodi_dialog_OK('Machine {} not in Command DAT'.format(machine_name)) + return + DAT_dic = utils_load_JSON_file(cfg.COMMAND_DB_PATH.getPath()) + window_title = 'Command information for [COLOR=orange]{}[/COLOR]'.format(machine_name) + kodi_display_text_window_mono(window_title, DAT_dic[machine_name]) + + # --- View Fanart --- + elif action == ACTION_VIEW_FANART: + if view_type == VIEW_MAME_MACHINE: + if location == 'STANDARD': + assets_dic = utils_load_JSON_file(cfg.ASSET_DB_PATH.getPath()) + m_assets = assets_dic[machine_name] + else: + mame_favs_dic = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + m_assets = mame_favs_dic[machine_name]['assets'] + if not m_assets['fanart']: + kodi_dialog_OK('Fanart for machine {} not found.'.format(machine_name)) + return + elif view_type == VIEW_SL_ROM: + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + assets_file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_asset_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + m_assets = SL_asset_dic[SL_ROM] + if not m_assets['fanart']: + kodi_dialog_OK('Fanart for SL item {} not found.'.format(SL_ROM)) + return + + # If manual found then display it. + log_debug('Rendering FS fanart "{}"'.format(m_assets['fanart'])) + xbmc.executebuiltin('ShowPicture("{}")'.format(m_assets['fanart'])) + + # --- View Manual --- + # When Pictures menu is clicked on Home, the window pictures (MyPics.xml) opens. + # Pictures are browsed with the pictures window. When an image is clicked with ENTER the + # window changes to slideshow (SlideShow.xml) and the pictures are displayed in full + # screen with not pan/zoom effects. Pictures can be changed with the arrow keys (they + # do not change automatically). The slideshow can also be started from the side menu + # "View slideshow". Initiated this way, the slideshow has a pan/zooming effects and all + # pictures in the list are changed every few seconds. + # + # Use the builtin function SlideShow("{}",pause) to show a set of pictures in full screen. + # See https://forum.kodi.tv/showthread.php?tid=329349 + # + elif action == ACTION_VIEW_MANUAL: + # --- Slideshow DEBUG snippet --- + # https://kodi.wiki/view/List_of_built-in_functions is outdated! + # See https://github.com/xbmc/xbmc/blob/master/xbmc/interfaces/builtins/PictureBuiltins.cpp + # '\' in path strings must be escaped like '\\' + # Builtin function arguments can be in any order (at least for this function). + # xbmc.executebuiltin('SlideShow("{}",pause)'.format(r'E:\\AML-stuff\\AML-assets\\fanarts\\')) + + # If manual found then display it. + # First, extract images from the PDF/CBZ. + # Put the extracted images in a directory named MANUALS_DIR/manual_name.pages/ + # Check the modification times of the PDF manual file witht the timestamp of + # the first file to regenerate the images if PDF is newer than first extracted img. + # NOTE CBZ/CBR files are supported by Kodi. It can be extracted with the builtin + # function extract. In addition to PDF extension, CBR and CBZ extensions must + # also be searched for manuals. + if view_type == VIEW_MAME_MACHINE: + log_debug('Displaying Manual for MAME machine {} ...'.format(machine_name)) + # machine = db_get_machine_main_hashed_db(cfg, machine_name) + assets_dic = utils_load_JSON_file(cfg.ASSET_DB_PATH.getPath()) + if not assets_dic[machine_name]['manual']: + kodi_dialog_OK('Manual not found in database.') + return + man_file_FN = FileName(assets_dic[machine_name]['manual']) + img_dir_FN = FileName(cfg.settings['assets_path']).pjoin('manuals').pjoin(machine_name + '.pages') + elif view_type == VIEW_SL_ROM: + log_debug('Displaying Manual for SL {} item {} ...'.format(SL_name, SL_ROM)) + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + assets_file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_asset_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + if not SL_asset_dic[SL_ROM]['manual']: + kodi_dialog_OK('Manual not found in database.') + return + man_file_FN = FileName(SL_asset_dic[SL_ROM]['manual']) + img_dir_FN = FileName(cfg.settings['assets_path']).pjoin('manuals_SL').pjoin(SL_name).pjoin(SL_ROM + '.pages') + log_debug('man_file_FN P "{}"'.format(man_file_FN.getPath())) + log_debug('img_dir_FN P "{}"'.format(img_dir_FN.getPath())) + + # --- Check for errors --- + if not man_file_FN.exists(): + kodi_dialog_OK('Manual "{}" not found.'.format(man_file_FN.getPath())) + return + + # --- Only PDF files supported at the moment --- + man_ext = man_file_FN.getExt().lower() + log_debug('Manual file extension "{}"'.format(man_ext)) + if not man_ext == '.pdf': + kodi_dialog_OK('Only PDF files supported at the moment.') + return + + # --- If output directory does not exist create it --- + if not img_dir_FN.exists(): + log_info('Creating DIR "{}"'.format(img_dir_FN.getPath())) + img_dir_FN.makedirs() + + # OLD CODE + # status_dic = { + # 'manFormat' : '', # PDF, CBZ, CBR, ... + # 'numImages' : 0, + # } + # manuals_extract_pages(status_dic, man_file_FN, img_dir_FN) + + # Check if JSON INFO file exists. If so, read it and compare the timestamp of the + # extraction of the images with the timestamp of the PDF file. Do not extract + # the images if the images are newer than the PDF. + status_dic = manuals_check_img_extraction_needed(man_file_FN, img_dir_FN) + if status_dic['extraction_needed']: + # Disable PDF image extracion in Python 3 until the problems with the pdfrw library + # are solved. + if ADDON_RUNNING_PYTHON_3: + log_error('Image extraction from PDF files is disabled in Python 3. Exiting.') + kodi_dialog_OK('Image extraction from PDF files is disabled in Python 3. ' + 'This feature will be ported to Python 3 as soon as possible.') + return + + log_info('Extracting images from PDF file.') + # --- Open manual file --- + manuals_open_PDF_file(status_dic, man_file_FN, img_dir_FN) + if status_dic['abort_extraction']: + kodi_dialog_OK('Cannot extract images from file {}'.format(man_file_FN.getPath())) + return + manuals_get_PDF_filter_list(status_dic, man_file_FN, img_dir_FN) + + # --- Extract page by page --- + pDialog = KodiProgressDialog() + pDialog.startProgress('Extracting manual images...', status_dic['numPages']) + for page_index in range(status_dic['numPages']): + pDialog.updateProgressInc() + manuals_extract_PDF_page(status_dic, man_file_FN, img_dir_FN, page_index) + manuals_close_PDF_file() + pDialog.endProgress() + + # --- Create JSON INFO file --- + manuals_create_INFO_file(status_dic, man_file_FN, img_dir_FN) + else: + log_info('Extraction of PDF images skipped.') + + # --- Display page images --- + if status_dic['numImages'] < 1: + log_info('No images found. Nothing to show.') + str_list = [ + 'Cannot find images inside the {} file. '.format(status_dic['manFormat']), + 'Check log for more details.' + ] + kodi_dialog_OK(''.join(str_list)) + return + log_info('Rendering images in "{}"'.format(img_dir_FN.getPath())) + xbmc.executebuiltin('SlideShow("{}",pause)'.format(img_dir_FN.getPath())) + + # --- Display brother machines (same driver) --- + elif action == ACTION_VIEW_BROTHERS: + machine = db_get_machine_main_hashed_db(cfg, machine_name) + # Some (important) drivers have a different name + sourcefile = machine['sourcefile'] + log_debug('Original driver "{}"'.format(sourcefile)) + mdbn_dic = mame_driver_better_name_dic + sourcefile = mdbn_dic[sourcefile] if sourcefile in mdbn_dic else sourcefile + log_debug('Final driver "{}"'.format(sourcefile)) + + # --- Replace current window by search window --- + # When user press Back in search window it returns to the previous window. + # NOTE ActivateWindow() / RunPlugin() / RunAddon() seem not to work here + url = misc_url_2_arg('catalog', 'Driver', 'category', sourcefile) + log_debug('Container.Update URL "{}"'.format(url)) + xbmc.executebuiltin('Container.Update({})'.format(url)) + + # --- Display machines with same Genre --- + elif action == ACTION_VIEW_SAME_GENRE: + machine = db_get_machine_main_hashed_db(cfg, machine_name) + genre_str = machine['genre'] + url = misc_url_2_arg('catalog', 'Genre', 'category', genre_str) + log_debug('Container.Update URL {}'.format(url)) + xbmc.executebuiltin('Container.Update({})'.format(url)) + + # --- Display machines by same Manufacturer --- + elif action == ACTION_VIEW_SAME_MANUFACTURER: + machine = db_get_machine_main_hashed_db(cfg, machine_name) + manufacturer_str = machine['manufacturer'] + url = misc_url_2_arg('catalog', 'Manufacturer', 'category', manufacturer_str) + log_debug('Container.Update URL {}'.format(url)) + xbmc.executebuiltin('Container.Update({})'.format(url)) + + else: + kodi_dialog_OK('Unknown action == {}. This is a bug, please report it.'.format(action)) + +# --------------------------------------------------------------------------------------------- +# Information display +# --------------------------------------------------------------------------------------------- +def command_context_view_audit(cfg, machine_name, SL_name, SL_ROM, location): + VIEW_MAME_MACHINE = 100 + VIEW_SL_ROM = 200 + + ACTION_VIEW_MACHINE_DATA = 100 + ACTION_VIEW_SL_ITEM_DATA = 200 + ACTION_VIEW_MACHINE_ROMS = 300 + ACTION_VIEW_MACHINE_AUDIT_ROMS = 400 + ACTION_VIEW_SL_ITEM_ROMS = 500 + ACTION_VIEW_SL_ITEM_AUDIT_ROMS = 600 + ACTION_VIEW_MANUAL_JSON = 700 + ACTION_AUDIT_MAME_MACHINE = 800 + ACTION_AUDIT_SL_ITEM = 900 + + # --- Determine view type --- + log_debug('command_context_view_audit() machine_name "{}"'.format(machine_name)) + log_debug('command_context_view_audit() SL_name "{}"'.format(SL_name)) + log_debug('command_context_view_audit() SL_ROM "{}"'.format(SL_ROM)) + log_debug('command_context_view_audit() location "{}"'.format(location)) + if machine_name: + view_type = VIEW_MAME_MACHINE + elif SL_name: + view_type = VIEW_SL_ROM + else: + kodi_dialog_OK( + 'In command_context_view_audit(), undetermined view_type. This is a bug, please report it.') + return + log_debug('command_context_view_audit() view_type = {}'.format(view_type)) + + # --- Build menu base on view_type --- + if view_type == VIEW_MAME_MACHINE: + d_list = [ + 'View MAME machine data', + 'View MAME machine ROMs (ROMs DB)', + 'View MAME machine ROMs (Audit DB)', + 'Audit MAME machine ROMs', + 'View manual INFO file', + ] + elif view_type == VIEW_SL_ROM: + d_list = [ + 'View Software List item data', + 'View Software List item ROMs (ROMs DB)', + 'View Software List item ROMs (Audit DB)', + 'Audit Software List item', + ] + else: + kodi_dialog_OK('Wrong view_type = {}. This is a bug, please report it.'.format(view_type)) + return + selected_value = xbmcgui.Dialog().select('View', d_list) + if selected_value < 0: return + + # --- Polymorphic menu. Determine action to do. --- + if view_type == VIEW_MAME_MACHINE: + if selected_value == 0: action = ACTION_VIEW_MACHINE_DATA + elif selected_value == 1: action = ACTION_VIEW_MACHINE_ROMS + elif selected_value == 2: action = ACTION_VIEW_MACHINE_AUDIT_ROMS + elif selected_value == 3: action = ACTION_AUDIT_MAME_MACHINE + elif selected_value == 4: action = ACTION_VIEW_MANUAL_JSON + else: + kodi_dialog_OK('view_type == VIEW_MAME_MACHINE and selected_value = {}. '.format(selected_value) + + 'This is a bug, please report it.') + return + elif view_type == VIEW_SL_ROM: + if selected_value == 0: action = ACTION_VIEW_SL_ITEM_DATA + elif selected_value == 1: action = ACTION_VIEW_SL_ITEM_ROMS + elif selected_value == 2: action = ACTION_VIEW_SL_ITEM_AUDIT_ROMS + elif selected_value == 3: action = ACTION_AUDIT_SL_ITEM + else: + kodi_dialog_OK('view_type == VIEW_SL_ROM and selected_value = {}. '.format(selected_value) + + 'This is a bug, please report it.') + return + else: + kodi_dialog_OK('Wrong view_type = {}. This is a bug, please report it.'.format(view_type)) + return + log_debug('command_context_view_audit() action = {}'.format(action)) + + # --- Execute action --- + if action == ACTION_VIEW_MACHINE_DATA: + action_view_machine_data(cfg, machine_name, SL_name, SL_ROM, location) + + elif action == ACTION_VIEW_SL_ITEM_DATA: + action_view_sl_item_data(cfg, machine_name, SL_name, SL_ROM, location) + + elif action == ACTION_VIEW_MACHINE_ROMS: + action_view_machine_roms(cfg, machine_name, SL_name, SL_ROM, location) + + elif action == ACTION_VIEW_MACHINE_AUDIT_ROMS: + action_view_machine_audit_roms(cfg, machine_name, SL_name, SL_ROM, location) + + elif action == ACTION_VIEW_SL_ITEM_ROMS: + action_view_sl_item_roms(cfg, machine_name, SL_name, SL_ROM, location) + + elif action == ACTION_VIEW_SL_ITEM_AUDIT_ROMS: + action_view_sl_item_audit_roms(cfg, machine_name, SL_name, SL_ROM, location) + + elif action == ACTION_AUDIT_MAME_MACHINE: + action_audit_mame_machine(cfg, machine_name, SL_name, SL_ROM, location) + + elif action == ACTION_AUDIT_SL_ITEM: + action_audit_sl_item(cfg, machine_name, SL_name, SL_ROM, location) + + # --- View manual JSON INFO file of a MAME machine --- + elif action == ACTION_VIEW_MANUAL_JSON: + d_text = 'Loading databases ...' + pDialog = KodiProgressDialog() + pDialog.startProgress('{}\n{}'.format(d_text, 'ROM hashed database'), 2) + machine = db_get_machine_main_hashed_db(cfg, machine_name) + pDialog.updateProgressInc('{}\n{}'.format(d_text, 'Assets hashed database')) + assets = db_get_machine_assets_hashed_db(cfg, machine_name) + pDialog.endProgress() + + if not assets['manual']: + kodi_dialog_OK('Manual not found in database.') + return + man_file_FN = FileName(assets['manual']) + img_dir_FN = FileName(cfg.settings['assets_path']).pjoin('manuals').pjoin(machine_name + '.pages') + rom_name = man_file_FN.getBaseNoExt() + info_FN = img_dir_FN.pjoin(rom_name + '.json') + if not info_FN.exists(): + kodi_dialog_OK('Manual JSON INFO file not found. View the manual first.') + return + + # --- Read stdout and put into a string --- + window_title = 'MAME machine manual JSON INFO file' + info_text = utils_load_file_to_str(info_FN.getPath()) + kodi_display_text_window_mono(window_title, info_text) + + else: + t = 'Wrong action == {}. This is a bug, please report it.'.format(action) + log_error(t) + kodi_dialog_OK(t) + +def action_view_machine_data(cfg, machine_name, SL_name, SL_ROM, location): + pDialog = KodiProgressDialog() + d_text = 'Loading databases...' + if location == LOCATION_STANDARD: + pDialog.startProgress('{}\n{}'.format(d_text, 'ROM hashed database'), 2) + machine = db_get_machine_main_hashed_db(cfg, machine_name) + pDialog.updateProgress(1, '{}\n{}'.format(d_text, 'Assets hashed database')) + assets = db_get_machine_assets_hashed_db(cfg, machine_name) + pDialog.endProgress() + window_title = 'MAME Machine Information' + + elif location == LOCATION_MAME_FAVS: + pDialog.startProgress('{}\n{}'.format(d_text, 'MAME Favourites database')) + machines = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + pDialog.endProgress() + machine = machines[machine_name] + assets = machine['assets'] + window_title = 'Favourite MAME Machine Information' + + elif location == LOCATION_MAME_MOST_PLAYED: + pDialog.startProgress('{}\n{}'.format(d_text, 'MAME Most Played database')) + most_played_roms_dic = utils_load_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath()) + pDialog.endProgress() + machine = most_played_roms_dic[machine_name] + assets = machine['assets'] + window_title = 'Most Played MAME Machine Information' + + elif location == LOCATION_MAME_RECENT_PLAYED: + pDialog.startProgress('{}\n{}'.format(d_text, 'MAME Recently Played database')) + recent_roms_list = utils_load_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), []) + pDialog.endProgress() + machine_index = -1 + for i, recent_rom in enumerate(recent_roms_list): + if machine_name == recent_rom['name']: + machine_index = i + break + if machine_index < 0: + kodi_dialog_OK('machine_index < 0. Please report this bug.') + return + machine = recent_roms_list[machine_index] + assets = machine['assets'] + window_title = 'Recently Played MAME Machine Information' + + # --- Make information string and display text window --- + slist = [] + mame_info_MAME_print(slist, location, machine_name, machine, assets) + kodi_display_text_window_mono(window_title, '\n'.join(slist)) + + # --- Write DEBUG TXT file --- + if cfg.settings['debug_MAME_machine_data']: + log_info('Writing file "{}"'.format(cfg.REPORT_DEBUG_MAME_MACHINE_DATA_PATH.getPath())) + text_remove_color_tags_slist(slist) + utils_write_slist_to_file(cfg.REPORT_DEBUG_MAME_MACHINE_DATA_PATH.getPath(), slist) + +def action_view_sl_item_data(cfg, machine_name, SL_name, SL_ROM, location): + if location == LOCATION_STANDARD: + # --- Load databases --- + SL_machines_dic = utils_load_JSON_file(cfg.SL_MACHINES_PATH.getPath()) + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + assets_file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_asset_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_items.json') + roms = utils_load_JSON_file(SL_DB_FN.getPath()) + + # --- Prepare data --- + rom = roms[SL_ROM] + assets = SL_asset_dic[SL_ROM] + SL_dic = SL_catalog_dic[SL_name] + SL_machine_list = SL_machines_dic[SL_name] + window_title = 'Software List ROM Information' + + elif location == LOCATION_SL_FAVS: + # --- Load databases --- + SL_machines_dic = utils_load_JSON_file(cfg.SL_MACHINES_PATH.getPath()) + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + + # --- Prepare data --- + fav_key = SL_name + '-' + SL_ROM + rom = fav_SL_roms[fav_key] + assets = rom['assets'] + SL_dic = SL_catalog_dic[SL_name] + SL_machine_list = SL_machines_dic[SL_name] + window_title = 'Favourite Software List Item Information' + + elif location == LOCATION_SL_MOST_PLAYED: + SL_machines_dic = utils_load_JSON_file(cfg.SL_MACHINES_PATH.getPath()) + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + most_played_roms_dic = utils_load_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath()) + + # --- Prepare data --- + fav_key = SL_name + '-' + SL_ROM + rom = most_played_roms_dic[fav_key] + assets = rom['assets'] + SL_dic = SL_catalog_dic[SL_name] + SL_machine_list = SL_machines_dic[SL_name] + window_title = 'Most Played SL Item Information' + + elif location == LOCATION_SL_RECENT_PLAYED: + SL_machines_dic = utils_load_JSON_file(cfg.SL_MACHINES_PATH.getPath()) + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + recent_roms_list = utils_load_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), []) + + # --- Prepare data --- + fav_key = SL_name + '-' + SL_ROM + machine_index = -1 + for i, recent_rom in enumerate(recent_roms_list): + if fav_key == recent_rom['SL_DB_key']: + machine_index = i + break + if machine_index < 0: + kodi_dialog_OK('machine_index < 0. Please report this bug.') + return + rom = recent_roms_list[machine_index] + assets = rom['assets'] + SL_dic = SL_catalog_dic[SL_name] + SL_machine_list = SL_machines_dic[SL_name] + window_title = 'Recently Played SL Item Information' + + # Build information string. + slist = [] + mame_info_SL_print(slist, location, SL_name, SL_ROM, rom, assets, SL_dic, SL_machine_list) + kodi_display_text_window_mono(window_title, '\n'.join(slist)) + + # --- Write DEBUG TXT file --- + if cfg.settings['debug_SL_item_data']: + log_info('Writing file "{}"'.format(cfg.REPORT_DEBUG_SL_ITEM_DATA_PATH.getPath())) + text_remove_color_tags_slist(slist) + utils_write_slist_to_file(cfg.REPORT_DEBUG_SL_ITEM_DATA_PATH.getPath(), slist) + +def action_view_machine_roms(cfg, machine_name, SL_name, SL_ROM, location): + # Load machine dictionary, ROM database and Devices database. + d_text = 'Loading databases ...' + pDialog = KodiProgressDialog() + pDialog.startProgress('{}\n{}'.format(d_text, 'MAME machines main'), 3) + machine = db_get_machine_main_hashed_db(cfg, machine_name) + pDialog.updateProgressInc('{}\n{}'.format(d_text, 'MAME machine ROMs')) + roms_db_dic = utils_load_JSON_file(cfg.ROMS_DB_PATH.getPath()) + pDialog.updateProgressInc('{}\n{}'.format(d_text, 'MAME machine Devices')) + devices_db_dic = utils_load_JSON_file(cfg.DEVICES_DB_PATH.getPath()) + pDialog.endProgress() + + # --- Make a dictionary with device ROMs --- + device_roms_list = [] + for device in devices_db_dic[machine_name]: + device_roms_dic = roms_db_dic[device] + for rom in device_roms_dic['roms']: + rom['device_name'] = device + device_roms_list.append(copy.deepcopy(rom)) + + # --- ROM info --- + info_text = [] + cloneof = machine['cloneof'] if machine['cloneof'] else 'None' + romof = machine['romof'] if machine['romof'] else 'None' + info_text.append('[COLOR violet]cloneof[/COLOR] {} / '.format(cloneof) + + '[COLOR violet]romof[/COLOR] {} / '.format(romof) + + '[COLOR skyblue]isBIOS[/COLOR] {} / '.format(text_type(machine['isBIOS'])) + + '[COLOR skyblue]isDevice[/COLOR] {}'.format(text_type(machine['isDevice']))) + info_text.append('') + + # --- Table header --- + # Table cell padding: left, right + table_str = [ + ['right', 'left', 'right', 'left', 'left', 'left'], + ['Type', 'ROM name', 'Size', 'CRC/SHA1', 'Merge', 'BIOS/Device'], + ] + + # --- Table: Machine ROMs --- + roms_dic = roms_db_dic[machine_name] + if roms_dic['roms']: + for rom in roms_dic['roms']: + if rom['bios'] and rom['merge']: r_type = 'BROM' + elif rom['bios'] and not rom['merge']: r_type = 'XROM' + elif not rom['bios'] and rom['merge']: r_type = 'MROM' + elif not rom['bios'] and not rom['merge']: r_type = 'ROM' + else: r_type = 'ERROR' + table_row = [r_type, text_type(rom['name']), text_type(rom['size']), + text_type(rom['crc']), text_type(rom['merge']), text_type(rom['bios'])] + table_str.append(table_row) + + # --- Table: device ROMs --- + if device_roms_list: + for rom in device_roms_list: + table_row = ['DROM', text_type(rom['name']), text_type(rom['size']), + text_type(rom['crc']), text_type(rom['merge']), text_type(rom['device_name'])] + table_str.append(table_row) + + # --- Table: machine CHDs --- + if roms_dic['disks']: + for disk in roms_dic['disks']: + table_row = ['DISK', text_type(disk['name']), '', text_type(disk['sha1'])[0:8], + text_type(disk['merge']), ''] + table_str.append(table_row) + + # --- Table: machine Samples --- + if roms_dic['samples']: + for sample in roms_dic['samples']: + table_row = ['SAM', text_type(sample['name']), '', '', '', ''] + table_str.append(table_row) + + # --- Table: BIOSes --- + if roms_dic['bios']: + bios_table_str = [] + bios_table_str.append(['right', 'left']) + bios_table_str.append(['BIOS name', 'Description']) + for bios in roms_dic['bios']: + table_row = [text_type(bios['name']), text_type(bios['description'])] + bios_table_str.append(table_row) + + # --- Render text information window --- + table_str_list = text_render_table(table_str) + info_text.extend(table_str_list) + if roms_dic['bios']: + bios_table_str_list = text_render_table(bios_table_str) + info_text.append('') + info_text.extend(bios_table_str_list) + window_title = 'Machine {} ROMs'.format(machine_name) + kodi_display_text_window_mono(window_title, '\n'.join(info_text)) + + # --- Write DEBUG TXT file --- + if cfg.settings['debug_MAME_ROM_DB_data']: + log_info('Writing file "{}"'.format(cfg.REPORT_DEBUG_MAME_MACHINE_ROM_DATA_PATH.getPath())) + text_remove_color_tags_slist(info_text) + utils_write_slist_to_file(cfg.REPORT_DEBUG_MAME_MACHINE_ROM_DATA_PATH.getPath(), info_text) + +def action_view_machine_audit_roms(cfg, machine_name, SL_name, SL_ROM, location): + log_debug('command_context_view() View Machine ROMs (Audit database)') + d_text = 'Loading databases...' + pDialog = KodiProgressDialog() + pDialog.startProgress('{}\n{}'.format(d_text, 'MAME machine hash'), 3) + machine = db_get_machine_main_hashed_db(cfg, machine_name) + pDialog.updateProgressInc('{}\n{}'.format(d_text, 'MAME ROM Audit')) + audit_roms_dic = utils_load_JSON_file(cfg.ROM_AUDIT_DB_PATH.getPath()) + pDialog.updateProgressInc('{}\n{}'.format(d_text, 'Machine archives')) + machine_archives = utils_load_JSON_file(cfg.ROM_SET_MACHINE_FILES_DB_PATH.getPath()) + pDialog.endProgress() + + # --- Grab data and settings --- + rom_list = audit_roms_dic[machine_name] + cloneof = machine['cloneof'] + romof = machine['romof'] + rom_set = ROMSET_NAME_LIST[cfg.settings['mame_rom_set']] + chd_set = CHDSET_NAME_LIST[cfg.settings['mame_chd_set']] + log_debug('command_context_view() machine {}'.format(machine_name)) + log_debug('command_context_view() cloneof {}'.format(cloneof)) + log_debug('command_context_view() romof {}'.format(romof)) + log_debug('command_context_view() rom_set {}'.format(rom_set)) + log_debug('command_context_view() chd_set {}'.format(chd_set)) + + # --- Generate report --- + info_text = [] + cloneof = machine['cloneof'] if machine['cloneof'] else 'None' + romof = machine['romof'] if machine['romof'] else 'None' + info_text.append('[COLOR violet]cloneof[/COLOR] {} / '.format(cloneof) + + '[COLOR violet]romof[/COLOR] {} / '.format(romof) + + '[COLOR skyblue]isBIOS[/COLOR] {} / '.format(text_type(machine['isBIOS'])) + + '[COLOR skyblue]isDevice[/COLOR] {}'.format(text_type(machine['isDevice']))) + info_text.append('MAME ROM set [COLOR orange]{}[/COLOR] / '.format(rom_set) + + 'MAME CHD set [COLOR orange]{}[/COLOR]'.format(chd_set)) + info_text.append('') + + # --- Audit ROM table --- + # Table cell padding: left, right + # Table columns: Type - ROM name - Size - CRC/SHA1 - Merge - BIOS - Location + table_str = [ + ['right', 'left', 'right', 'left', 'left'], + ['Type', 'ROM name', 'Size', 'CRC/SHA1', 'Location'], + ] + for m_rom in rom_list: + if m_rom['type'] == ROM_TYPE_DISK: + sha1_str = text_type(m_rom['sha1'])[0:8] + table_row = [text_type(m_rom['type']), text_type(m_rom['name']), '', sha1_str, m_rom['location']] + elif m_rom['type'] == ROM_TYPE_SAMPLE: + table_row = [text_type(m_rom['type']), text_type(m_rom['name']), '', '', text_type(m_rom['location'])] + else: + table_row = [text_type(m_rom['type']), text_type(m_rom['name']), text_type(m_rom['size']), + text_type(m_rom['crc']), text_type(m_rom['location'])] + table_str.append(table_row) + info_text.extend(text_render_table(table_str)) + info_text.append('') + + # --- ZIP/CHD/Sample file list --- + table_str = [ + ['right', 'left'], + ['Type', 'File name'], + ] + for m_file in machine_archives[machine_name]['ROMs']: + table_str.append(['ROM', '[COLOR orange]ROM_path[/COLOR]/' + m_file + '.zip']) + for m_file in machine_archives[machine_name]['CHDs']: + table_str.append(['CHD', '[COLOR orange]CHD_path[/COLOR]/' + m_file + '.chd']) + for m_file in machine_archives[machine_name]['Samples']: + table_str.append(['Sample', '[COLOR orange]Samples_path[/COLOR]/' + m_file + '.zip']) + info_text.extend(text_render_table(table_str)) + + window_title = 'Machine {} ROM audit'.format(machine_name) + kodi_display_text_window_mono(window_title, '\n'.join(info_text)) + + # --- Write DEBUG TXT file --- + if cfg.settings['debug_MAME_Audit_DB_data']: + log_info('Writing file "{}"'.format(cfg.REPORT_DEBUG_MAME_MACHINE_AUDIT_DATA_PATH.getPath())) + text_remove_color_tags_slist(info_text) + utils_write_slist_to_file(cfg.REPORT_DEBUG_MAME_MACHINE_AUDIT_DATA_PATH.getPath(), info_text) + +def action_view_sl_item_roms(cfg, machine_name, SL_name, SL_ROM, location): + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_items.json') + SL_ROMS_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_ROMs.json') + # SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + # SL_machines_dic = utils_load_JSON_file(cfg.SL_MACHINES_PATH.getPath()) + # assets_file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json' + # SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + # SL_asset_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + # SL_dic = SL_catalog_dic[SL_name] + # SL_machine_list = SL_machines_dic[SL_name] + # assets = SL_asset_dic[SL_ROM] if SL_ROM in SL_asset_dic else db_new_SL_asset() + roms = utils_load_JSON_file(SL_DB_FN.getPath()) + roms_db = utils_load_JSON_file(SL_ROMS_DB_FN.getPath()) + rom = roms[SL_ROM] + rom_db_list = roms_db[SL_ROM] + + cloneof = rom['cloneof'] if rom['cloneof'] else 'None' + info_text = [] + info_text.append('[COLOR violet]SL_name[/COLOR] {}'.format(SL_name)) + info_text.append('[COLOR violet]SL_ROM[/COLOR] {}'.format(SL_ROM)) + info_text.append('[COLOR violet]description[/COLOR] {}'.format(rom['description'])) + info_text.append('[COLOR violet]cloneof[/COLOR] {}'.format(cloneof)) + info_text.append('') + + table_str = [] + table_str.append(['left', 'left', 'left', 'left', 'left', 'left', 'left']) + table_str.append(['Part name', 'Part iface', 'Area type', 'A name', 'ROM/CHD name', 'Size', 'CRC/SHA1']) + # Iterate Parts + for part_dic in rom_db_list: + part_name = part_dic['part_name'] + part_interface = part_dic['part_interface'] + if 'dataarea' in part_dic: + # Iterate Dataareas + for dataarea_dic in part_dic['dataarea']: + dataarea_name = dataarea_dic['name'] + # Interate ROMs in dataarea + for rom_dic in dataarea_dic['roms']: + table_row = [part_name, part_interface, 'dataarea', dataarea_name, + rom_dic['name'], text_type(rom_dic['size']), rom_dic['crc']] + table_str.append(table_row) + if 'diskarea' in part_dic: + # Iterate Diskareas + for diskarea_dic in part_dic['diskarea']: + diskarea_name = diskarea_dic['name'] + # Iterate DISKs in diskarea + for rom_dic in diskarea_dic['disks']: + table_row = [part_name, part_interface, 'diskarea', diskarea_name, + rom_dic['name'], '', rom_dic['sha1'][0:8]] + table_str.append(table_row) + table_str_list = text_render_table(table_str) + info_text.extend(table_str_list) + window_title = 'Software List ROM List (ROMs DB)' + kodi_display_text_window_mono(window_title, '\n'.join(info_text)) + + # --- Write DEBUG TXT file --- + if cfg.settings['debug_SL_ROM_DB_data']: + log_info('Writing file "{}"'.format(cfg.REPORT_DEBUG_SL_ITEM_ROM_DATA_PATH.getPath())) + text_remove_color_tags_slist(info_text) + utils_write_slist_to_file(cfg.REPORT_DEBUG_SL_ITEM_ROM_DATA_PATH.getPath(), info_text) + +def action_view_sl_item_audit_roms(cfg, machine_name, SL_name, SL_ROM, location): + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_items.json') + # SL_ROMs_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_roms.json') + SL_ROM_Audit_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_ROM_audit.json') + + roms = utils_load_JSON_file(SL_DB_FN.getPath()) + rom_audit_db = utils_load_JSON_file(SL_ROM_Audit_DB_FN.getPath()) + rom = roms[SL_ROM] + rom_db_list = rom_audit_db[SL_ROM] + + cloneof = rom['cloneof'] if rom['cloneof'] else 'None' + info_text = [] + info_text.append('[COLOR violet]SL_name[/COLOR] {}'.format(SL_name)) + info_text.append('[COLOR violet]SL_ROM[/COLOR] {}'.format(SL_ROM)) + info_text.append('[COLOR violet]description[/COLOR] {}'.format(rom['description'])) + info_text.append('[COLOR violet]cloneof[/COLOR] {}'.format(cloneof)) + info_text.append('') + + # table_str = [ ['left', 'left', 'left', 'left', 'left'] ] + # table_str.append(['Type', 'ROM/CHD name', 'Size', 'CRC/SHA1', 'Location']) + table_str = [ ['left', 'left', 'left', 'left'] ] + table_str.append(['Type', 'Size', 'CRC/SHA1', 'Location']) + for rom_dic in rom_db_list: + if rom_dic['type'] == ROM_TYPE_DISK: + table_row = [rom_dic['type'], # rom_dic['name'], + '', rom_dic['sha1'][0:8], rom_dic['location']] + table_str.append(table_row) + else: + table_row = [rom_dic['type'], # rom_dic['name'], + text_type(rom_dic['size']), rom_dic['crc'], rom_dic['location']] + table_str.append(table_row) + table_str_list = text_render_table(table_str) + info_text.extend(table_str_list) + window_title = 'Software List ROM List (Audit DB)' + kodi_display_text_window_mono(window_title, '\n'.join(info_text)) + + # --- Write DEBUG TXT file --- + if cfg.settings['debug_SL_Audit_DB_data']: + log_info('Writing file "{}"'.format(cfg.REPORT_DEBUG_SL_ITEM_AUDIT_DATA_PATH.getPath())) + text_remove_color_tags_slist(info_text) + utils_write_slist_to_file(cfg.REPORT_DEBUG_SL_ITEM_AUDIT_DATA_PATH.getPath(), info_text) + +def action_audit_mame_machine(cfg, machine_name, SL_name, SL_ROM, location): + # --- Load machine dictionary and ROM database --- + rom_set = ['MERGED', 'SPLIT', 'NONMERGED'][cfg.settings['mame_rom_set']] + log_debug('command_context_view() Auditing Machine ROMs\n') + log_debug('command_context_view() rom_set {}\n'.format(rom_set)) + + d_text = 'Loading databases...' + pDialog = KodiProgressDialog() + pDialog.startProgress('{}\n{}'.format(d_text, 'MAME machine hash'), 2) + machine = db_get_machine_main_hashed_db(cfg, machine_name) + pDialog.updateProgressInc('{}\n{}'.format(d_text, 'MAME ROM Audit')) + audit_roms_dic = utils_load_JSON_file(cfg.ROM_AUDIT_DB_PATH.getPath()) + pDialog.endProgress() + + # --- Grab data and settings --- + rom_list = audit_roms_dic[machine_name] + cloneof = machine['cloneof'] + romof = machine['romof'] + log_debug('command_context_view() machine {}\n'.format(machine_name)) + log_debug('command_context_view() cloneof {}\n'.format(cloneof)) + log_debug('command_context_view() romof {}\n'.format(romof)) + + # --- Open ZIP file, check CRC32 and also CHDs --- + audit_dic = db_new_audit_dic() + mame_audit_MAME_machine(cfg, rom_list, audit_dic) + + # --- Generate report --- + info_text = [] + cloneof = machine['cloneof'] if machine['cloneof'] else 'None' + romof = machine['romof'] if machine['romof'] else 'None' + info_text.append('[COLOR violet]cloneof[/COLOR] {} / '.format(cloneof) + + '[COLOR violet]romof[/COLOR] {} / '.format(romof) + + '[COLOR skyblue]isBIOS[/COLOR] {} / '.format(text_type(machine['isBIOS'])) + + '[COLOR skyblue]isDevice[/COLOR] {}'.format(text_type(machine['isDevice']))) + info_text.append('') + + # --- Table header --- + # Table cell padding: left, right + # Table columns: Type - ROM name - Size - CRC/SHA1 - Merge - BIOS - Location + table_str = [] + table_str.append(['right', 'left', 'right', 'left', 'left', 'left']) + table_str.append(['Type', 'ROM name', 'Size', 'CRC/SHA1', 'Location', 'Status']) + + # --- Table rows --- + for m_rom in rom_list: + if m_rom['type'] == ROM_TYPE_DISK: + sha1_srt = m_rom['sha1'][0:8] + table_row = [m_rom['type'], m_rom['name'], '', sha1_srt, + m_rom['location'], m_rom['status_colour']] + elif m_rom['type'] == ROM_TYPE_SAMPLE: + table_row = [text_type(m_rom['type']), text_type(m_rom['name']), '', '', + m_rom['location'], m_rom['status_colour']] + else: + table_row = [text_type(m_rom['type']), text_type(m_rom['name']), + text_type(m_rom['size']), text_type(m_rom['crc']), m_rom['location'], m_rom['status_colour']] + table_str.append(table_row) + table_str_list = text_render_table(table_str) + info_text.extend(table_str_list) + window_title = 'Machine {} ROM audit'.format(machine_name) + kodi_display_text_window_mono(window_title, '\n'.join(info_text)) + +def action_audit_sl_item(cfg, machine_name, SL_name, SL_ROM, location): + # --- Load machine dictionary and ROM database --- + log_debug('command_context_view() Auditing SL Software ROMs\n') + log_debug('command_context_view() SL_name {}\n'.format(SL_name)) + log_debug('command_context_view() SL_ROM {}\n'.format(SL_ROM)) + + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_items.json') + SL_ROM_Audit_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_ROM_audit.json') + + roms = utils_load_JSON_file(SL_DB_FN.getPath()) + roms_audit_db = utils_load_JSON_file(SL_ROM_Audit_DB_FN.getPath()) + rom = roms[SL_ROM] + rom_db_list = roms_audit_db[SL_ROM] + + # --- Open ZIP file and check CRC32 --- + audit_dic = db_new_audit_dic() + SL_ROM_path_FN = FileName(cfg.settings['SL_rom_path']) + SL_CHD_path_FN = FileName(cfg.settings['SL_chd_path']) + mame_audit_SL_machine(SL_ROM_path_FN, SL_CHD_path_FN, SL_name, SL_ROM, rom_db_list, audit_dic) + + info_text = [ + '[COLOR violet]SL_name[/COLOR] {}'.format(SL_name), + '[COLOR violet]SL_ROM[/COLOR] {}'.format(SL_ROM), + '[COLOR violet]description[/COLOR] {}'.format(rom['description']), + '', + ] + + # --- Table header and rows --- + # Do not render ROM name in SLs, cos they are really long. + # table_str = [ ['right', 'left', 'right', 'left', 'left', 'left'] ] + # table_str.append(['Type', 'ROM name', 'Size', 'CRC/SHA1', 'Location', 'Status']) + table_str = [ ['right', 'right', 'left', 'left', 'left'] ] + table_str.append(['Type', 'Size', 'CRC/SHA1', 'Location', 'Status']) + for m_rom in rom_db_list: + if m_rom['type'] == ROM_TYPE_DISK: + table_row = [m_rom['type'], # m_rom['name'], + '', m_rom['sha1'][0:8], m_rom['location'], m_rom['status_colour']] + table_str.append(table_row) + else: + table_row = [m_rom['type'], # m_rom['name'], + text_type(m_rom['size']), m_rom['crc'], m_rom['location'], m_rom['status_colour']] + table_str.append(table_row) + table_str_list = text_render_table(table_str) + info_text.extend(table_str_list) + window_title = 'SL {} Software {} ROM audit'.format(SL_name, SL_ROM) + kodi_display_text_window_mono(window_title, '\n'.join(info_text)) + +def command_context_utilities(cfg, catalog_name, category_name): + log_debug('command_context_utilities() catalog_name "{}"'.format(catalog_name)) + log_debug('command_context_utilities() category_name "{}"'.format(category_name)) + + d_list = [ + 'Export AEL Virtual Launcher', + ] + selected_value = xbmcgui.Dialog().select('View', d_list) + if selected_value < 0: return + + # --- Export AEL Virtual Launcher --- + if selected_value == 0: + log_debug('command_context_utilities() Export AEL Virtual Launcher') + + # Ask user for a path to export the launcher configuration + vlauncher_str_name = 'AML_VLauncher_' + catalog_name + '_' + category_name + '.xml' + dir_path = kodi_dialog_get_directory('Select XML export directory') + if not dir_path: return + export_FN = FileName(dir_path).pjoin(vlauncher_str_name) + if export_FN.exists(): + ret = kodi_dialog_yesno('Overwrite file {}?'.format(export_FN.getPath())) + if not ret: + kodi_notify_warn('Export of Launcher XML cancelled') + return + + kodi_dialog_OK('Not implemented yet, sorry!') + return + + # --- Open databases and get list of machines of this filter --- + # This can be optimized: load stuff from the cache instead of the main databases. + db_files = [ + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Print error message is something goes wrong writing file --- + try: + catalog_dic = db_get_cataloged_dic_parents(cfg, catalog_name) + db_export_Virtual_Launcher(export_FN, catalog_dic[category_name], + db_dic['machines'], db_dic['renderdb'], db_dic['assetsdb']) + except KodiAddonError as ex: + kodi_display_exception(ex) + else: + kodi_notify('Exported Virtual Launcher "{}"'.format(vlauncher_str_name)) + +# ------------------------------------------------------------------------------------------------- +# MAME Favorites/Recently Played/Most played +# ------------------------------------------------------------------------------------------------- +# Favorites use the main hashed database, not the main and render databases. +def command_context_add_mame_fav(cfg, machine_name): + log_debug('command_add_mame_fav() Machine_name "{}"'.format(machine_name)) + + # Get Machine database entry. Use MAME hashed database for speed. + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + machine = db_get_machine_main_hashed_db(cfg, machine_name) + assets = db_get_machine_assets_hashed_db(cfg, machine_name) + fav_machines = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + + # If machine already in Favourites ask user if overwrite. + if machine_name in fav_machines: + ret = kodi_dialog_yesno('Machine {} ({}) '.format(machine['description'], machine_name) + + 'already in MAME Favourites. Overwrite?') + if ret < 1: return + + # Add machine. Add database version to Favourite. + fav_machine = db_get_MAME_Favourite_simple(machine_name, machine, assets, control_dic) + fav_machines[machine_name] = fav_machine + log_info('command_add_mame_fav() Added machine "{}"'.format(machine_name)) + + # Save Favourites + utils_write_JSON_file(cfg.FAV_MACHINES_PATH.getPath(), fav_machines) + kodi_notify('Machine {} added to MAME Favourites'.format(machine_name)) + kodi_refresh_container() + +def render_fav_machine_row(cfg, m_name, machine, m_assets, location): + # --- Default values for flags --- + AEL_PClone_stat_value = AEL_PCLONE_STAT_VALUE_NONE + + # --- Mark Flags, BIOS, Devices, BIOS, Parent/Clone and Driver status --- + display_name = machine['description'] + display_name += ' [COLOR skyblue]{}[/COLOR]'.format(m_assets['flags']) + if machine['isBIOS']: display_name += ' [COLOR cyan][BIOS][/COLOR]' + if machine['isDevice']: display_name += ' [COLOR violet][Dev][/COLOR]' + if machine['cloneof']: display_name += ' [COLOR orange][Clo][/COLOR]' + if machine['driver_status'] == 'imperfect': display_name += ' [COLOR yellow][Imp][/COLOR]' + elif machine['driver_status'] == 'preliminary': display_name += ' [COLOR red][Pre][/COLOR]' + # Render number of number the ROM has been launched + if location == LOCATION_MAME_MOST_PLAYED: + if machine['launch_count'] == 1: + display_name = '{} [COLOR orange][{} time][/COLOR]'.format(display_name, machine['launch_count']) + else: + display_name = '{} [COLOR orange][{} times][/COLOR]'.format(display_name, machine['launch_count']) + + # --- Skin flags --- + AEL_PClone_stat_value = AEL_PCLONE_STAT_VALUE_CLONE if machine['cloneof'] else AEL_PCLONE_STAT_VALUE_PARENT + + # --- Assets/artwork --- + icon_path = m_assets[cfg.mame_icon] if m_assets[cfg.mame_icon] else 'DefaultProgram.png' + fanart_path = m_assets[cfg.mame_fanart] + banner_path = m_assets['marquee'] + clearlogo_path = m_assets['clearlogo'] + poster_path = m_assets['3dbox'] if m_assets['3dbox'] else m_assets['flyer'] + + # --- Create listitem row --- + ICON_OVERLAY = 6 + listitem = xbmcgui.ListItem(display_name) + + # --- Metadata --- + # Make all the infotables compatible with Advanced Emulator Launcher + if cfg.settings['display_hide_trailers']: + listitem.setInfo('video', { + 'title' : display_name, 'year' : machine['year'], + 'genre' : machine['genre'], 'studio' : machine['manufacturer'], + 'plot' : m_assets['plot'], + 'overlay' : ICON_OVERLAY + }) + else: + listitem.setInfo('video', { + 'title' : display_name, 'year' : machine['year'], + 'genre' : machine['genre'], 'studio' : machine['manufacturer'], + 'plot' : m_assets['plot'], 'trailer' : m_assets['trailer'], + 'overlay' : ICON_OVERLAY + }) + listitem.setProperty('nplayers', machine['nplayers']) + listitem.setProperty('platform', 'MAME') + + # --- Assets --- + # AEL custom artwork fields + listitem.setArt({ + 'title' : m_assets['title'], 'snap' : m_assets['snap'], + 'boxfront' : m_assets['cabinet'], 'boxback' : m_assets['cpanel'], + 'cartridge' : m_assets['PCB'], 'flyer' : m_assets['flyer'], + '3dbox' : m_assets['3dbox'], + 'icon' : icon_path, 'fanart' : fanart_path, + 'banner' : banner_path, 'clearlogo' : clearlogo_path, + 'poster' : poster_path, + }) + + # --- ROM flags (Skins will use these flags to render icons) --- + listitem.setProperty(AEL_PCLONE_STAT_LABEL, AEL_PClone_stat_value) + + # --- Create context menu --- + URL_view_DAT = misc_url_3_arg_RunPlugin('command', 'VIEW_DAT', 'machine', m_name, 'location', location) + URL_view = misc_url_3_arg_RunPlugin('command', 'VIEW', 'machine', m_name, 'location', location) + if location == LOCATION_MAME_FAVS: + URL_manage = misc_url_2_arg_RunPlugin('command', 'MANAGE_MAME_FAV', 'machine', m_name) + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Manage Favourites', URL_manage), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + elif location == LOCATION_MAME_MOST_PLAYED: + URL_manage = misc_url_2_arg_RunPlugin('command', 'MANAGE_MAME_MOST_PLAYED', 'machine', m_name) + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Manage Most Played', URL_manage), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + elif location == LOCATION_MAME_RECENT_PLAYED: + URL_manage = misc_url_2_arg_RunPlugin('command', 'MANAGE_MAME_RECENT_PLAYED', 'machine', m_name) + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Manage Recently Played', URL_manage), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + listitem.addContextMenuItems(commands) + URL = misc_url_3_arg('command', 'LAUNCH', 'machine', m_name, 'location', location) + xbmcplugin.addDirectoryItem(handle = cfg.addon_handle, url = URL, listitem = listitem, isFolder = False) + +def command_show_mame_fav(cfg): + log_debug('command_show_mame_fav() Starting ...') + + # --- Open Favourite Machines dictionary --- + fav_machines = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + if not fav_machines: + kodi_dialog_OK('No Favourite MAME machines. Add some machines to MAME Favourites first.') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + # --- Render Favourites --- + set_Kodi_all_sorting_methods(cfg) + for m_name in fav_machines: + machine = fav_machines[m_name] + assets = machine['assets'] + render_fav_machine_row(cfg, m_name, machine, assets, LOCATION_MAME_FAVS) + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +# +# Context menu "Manage Favourite machines" +# +def command_context_manage_mame_fav(cfg, machine_name): + VIEW_ROOT_MENU = 100 + VIEW_INSIDE_MENU = 200 + + ACTION_DELETE_MACHINE = 100 + ACTION_DELETE_MISSING = 200 + ACTION_DELETE_ALL = 300 + + menus_dic = { + VIEW_ROOT_MENU : [ + ('Delete missing machines from MAME Favourites', ACTION_DELETE_MISSING), + ('Delete all machines from MAME Favourites', ACTION_DELETE_ALL), + ], + VIEW_INSIDE_MENU : [ + ('Delete machine from MAME Favourites', ACTION_DELETE_MACHINE), + ('Delete missing machines from MAME Favourites', ACTION_DELETE_MISSING), + ('Delete all machines from MAME Favourites', ACTION_DELETE_ALL), + ], + } + + # --- Determine view type --- + log_debug('command_context_manage_mame_fav() BEGIN ...') + log_debug('machine_name "{}"'.format(machine_name)) + if machine_name: + view_type = VIEW_INSIDE_MENU + else: + view_type = VIEW_ROOT_MENU + log_debug('view_type = {}'.format(view_type)) + + # --- Build menu base on view_type (Polymorphic menu, determine action) --- + d_list = [menu[0] for menu in menus_dic[view_type]] + selected_value = xbmcgui.Dialog().select('Manage MAME Favourite machines', d_list) + if selected_value < 0: return + action = menus_dic[view_type][selected_value][1] + log_debug('action = {}'.format(action)) + + # --- Execute actions --- + if action == ACTION_DELETE_MACHINE: + log_debug('command_context_manage_mame_fav() ACTION_DELETE_MACHINE') + log_debug('machine_name "{}"'.format(machine_name)) + db_files = [ + ['fav_machines', 'MAME Favourite machines', cfg.FAV_MACHINES_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Ask user for confirmation --- + desc = db_dic['fav_machines'][machine_name]['description'] + ret = kodi_dialog_yesno('Delete Machine {} ({})?'.format(desc, machine_name)) + if ret < 1: + kodi_notify('MAME Favourites unchanged') + return + + # --- Delete machine and save DB --- + del db_dic['fav_machines'][machine_name] + log_info('Deleted machine "{}"'.format(machine_name)) + utils_write_JSON_file(cfg.FAV_MACHINES_PATH.getPath(), db_dic['fav_machines']) + kodi_refresh_container() + kodi_notify('Machine {} deleted from MAME Favourites'.format(machine_name)) + + elif action == ACTION_DELETE_ALL: + log_debug('command_context_manage_mame_fav() ACTION_DELETE_ALL') + db_files = [ + ['fav_machines', 'MAME Favourite machines', cfg.FAV_MACHINES_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # Confirm with user + ret = kodi_dialog_yesno( + 'You have {} MAME Favourites. Delete them all?'.format(len(db_dic['fav_machines']))) + if ret < 1: + kodi_notify('MAME Favourites unchanged') + return + + # Database is an empty dictionary + utils_write_JSON_file(cfg.FAV_MACHINES_PATH.getPath(), dict()) + kodi_refresh_container() + kodi_notify('Deleted all MAME Favourites') + + elif action == ACTION_DELETE_MISSING: + log_debug('command_context_manage_mame_fav() ACTION_DELETE_MISSING') + + # --- Ensure MAME Catalog have been built --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + options = check_MAME_DB_status(st_dic, MAME_CATALOG_BUILT, control_dic) + if kodi_display_status_message(st_dic): return False + + # --- Load databases --- + db_files = [ + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['fav_machines', 'MAME Favourite machines', cfg.FAV_MACHINES_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Delete missing MAME machines --- + num_deleted_machines = 0 + if len(db_dic['fav_machines']) >= 1: + pDialog = KodiProgressDialog() + pDialog.startProgress('Delete missing MAME Favourites...', len(db_dic['fav_machines'])) + new_fav_machines = {} + for fav_key in sorted(db_dic['fav_machines']): + pDialog.updateProgressInc() + log_debug('Checking Favourite "{}"'.format(fav_key)) + if fav_key in db_dic['machines']: + new_fav_machines[fav_key] = db_dic['fav_machines'][fav_key] + else: + num_deleted_machines += 1 + utils_write_JSON_file(cfg.FAV_MACHINES_PATH.getPath(), new_fav_machines) + pDialog.endProgress() + kodi_refresh_container() + if num_deleted_machines > 0: + kodi_notify('Deleted {} missing MAME machines'.format(num_deleted_machines)) + else: + kodi_notify('No missing machines found') + + else: + t = 'Wrong action == {}. This is a bug, please report it.'.format(action) + log_error(t) + kodi_dialog_OK(t) + +def command_show_mame_most_played(cfg): + most_played_roms_dic = utils_load_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath()) + if not most_played_roms_dic: + kodi_dialog_OK('No Most Played MAME machines. Play a bit and try later.') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + set_Kodi_unsorted_method(cfg) + sorted_dic = sorted(most_played_roms_dic, key = lambda x : most_played_roms_dic[x]['launch_count'], reverse = True) + for machine_name in sorted_dic: + machine = most_played_roms_dic[machine_name] + render_fav_machine_row(cfg, machine['name'], machine, machine['assets'], LOCATION_MAME_MOST_PLAYED) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def command_context_manage_mame_most_played(cfg, machine_name): + VIEW_ROOT_MENU = 100 + VIEW_INSIDE_MENU = 200 + + ACTION_DELETE_MACHINE = 100 + ACTION_DELETE_MISSING = 200 + ACTION_DELETE_ALL = 300 + + menus_dic = { + VIEW_ROOT_MENU : [ + ('Delete missing machines from MAME Most Played', ACTION_DELETE_MISSING), + ('Delete all machines from MAME Most Played', ACTION_DELETE_ALL), + ], + VIEW_INSIDE_MENU : [ + ('Delete machine from MAME Most Played', ACTION_DELETE_MACHINE), + ('Delete missing machines from MAME Most Played', ACTION_DELETE_MISSING), + ('Delete all machines from MAME Most Played', ACTION_DELETE_ALL), + ], + } + + # --- Determine view type --- + log_debug('command_context_manage_mame_most_played() BEGIN ...') + log_debug('machine_name "{}"'.format(machine_name)) + if machine_name: + view_type = VIEW_INSIDE_MENU + else: + view_type = VIEW_ROOT_MENU + log_debug('view_type = {}'.format(view_type)) + + # --- Build menu base on view_type (Polymorphic menu, determine action) --- + d_list = [menu[0] for menu in menus_dic[view_type]] + selected_value = xbmcgui.Dialog().select('Manage MAME Most Played machines', d_list) + if selected_value < 0: return + action = menus_dic[view_type][selected_value][1] + log_debug('action = {}'.format(action)) + + # --- Execute actions --- + if action == ACTION_DELETE_MACHINE: + log_debug('command_context_manage_mame_most_played() ACTION_DELETE_MACHINE') + db_files = [ + ['most_played_roms', 'MAME Most Played machines', cfg.MAME_MOST_PLAYED_FILE_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Ask user for confirmation --- + desc = db_dic['most_played_roms'][machine_name]['description'] + ret = kodi_dialog_yesno('Delete Machine {} ({})?'.format(desc, machine_name)) + if ret < 1: + kodi_notify('MAME Most Played unchanged') + return + + # --- Delete machine and save DB --- + del db_dic['most_played_roms'][machine_name] + log_info('Deleted machine "{}"'.format(machine_name)) + utils_write_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath(), db_dic['most_played_roms']) + kodi_refresh_container() + kodi_notify('Machine {} deleted from MAME Most Played'.format(machine_name)) + + elif action == ACTION_DELETE_ALL: + log_debug('command_context_manage_mame_most_played() ACTION_DELETE_ALL') + db_files = [ + ['most_played_roms', 'MAME Most Played machines', cfg.MAME_MOST_PLAYED_FILE_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # Confirm with user + num_machines = len(db_dic['most_played_roms']) + ret = kodi_dialog_yesno( + 'You have {} MAME Most Played machines. Delete them all?'.format(num_machines)) + if ret < 1: + kodi_notify('MAME Most Played unchanged') + return + + # Database is an empty dictionary + utils_write_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath(), dict()) + kodi_refresh_container() + kodi_notify('Deleted all MAME Most Played'.format(machine_name)) + + elif action == ACTION_DELETE_MISSING: + log_debug('command_context_manage_mame_most_played() ACTION_DELETE_MISSING') + + # --- Ensure MAME Catalog have been built --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + options = check_MAME_DB_status(st_dic, MAME_CATALOG_BUILT, control_dic) + if kodi_display_status_message(st_dic): return False + + # --- Load databases --- + db_files = [ + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['most_played_roms', 'MAME Most Played machines', cfg.MAME_MOST_PLAYED_FILE_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Delete missing MAME machines --- + num_deleted_machines = 0 + if len(db_dic['most_played_roms']) >= 1: + pDialog = xbmcgui.DialogProgress() + pDialog.startProgress('Delete missing MAME Most Played...', len(db_dic['most_played_roms'])) + new_fav_machines = {} + for fav_key in sorted(db_dic['most_played_roms']): + pDialog.updateProgressInc() + log_debug('Checking Favourite "{}"'.format(fav_key)) + if fav_key in db_dic['machines']: + new_fav_machines[fav_key] = db_dic['most_played_roms'][fav_key] + else: + num_deleted_machines += 1 + utils_write_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath(), new_fav_machines) + pDialog.endProgress() + kodi_refresh_container() + if num_deleted_machines > 0: + kodi_notify('Deleted {} missing MAME machines'.format(num_deleted_machines)) + else: + kodi_notify('No missing machines found') + else: + t = 'Wrong action == {}. This is a bug, please report it.'.format(action) + log_error(t) + kodi_dialog_OK(t) + +def command_show_mame_recently_played(cfg): + recent_roms_list = utils_load_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), []) + if not recent_roms_list: + kodi_dialog_OK('No Recently Played MAME machines. Play a bit and try later.') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + set_Kodi_unsorted_method(cfg) + for machine in recent_roms_list: + render_fav_machine_row(cfg, machine['name'], machine, machine['assets'], LOCATION_MAME_RECENT_PLAYED) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def command_context_manage_mame_recent_played(cfg, machine_name): + VIEW_ROOT_MENU = 100 + VIEW_INSIDE_MENU = 200 + + ACTION_DELETE_MACHINE = 100 + ACTION_DELETE_MISSING = 200 + ACTION_DELETE_ALL = 300 + + menus_dic = { + VIEW_ROOT_MENU : [ + ('Delete missing machines from MAME Recently Played', ACTION_DELETE_MISSING), + ('Delete all machines from MAME Recently Played', ACTION_DELETE_ALL), + ], + VIEW_INSIDE_MENU : [ + ('Delete machine from MAME Recently Played', ACTION_DELETE_MACHINE), + ('Delete missing machines from MAME Recently Played', ACTION_DELETE_MISSING), + ('Delete all machines from MAME Recently Played', ACTION_DELETE_ALL), + ], + } + + # --- Determine view type --- + log_debug('command_context_manage_mame_recent_played() BEGIN ...') + log_debug('machine_name "{}"'.format(machine_name)) + if machine_name: + view_type = VIEW_INSIDE_MENU + else: + view_type = VIEW_ROOT_MENU + log_debug('view_type = {}'.format(view_type)) + + # --- Build menu base on view_type (Polymorphic menu, determine action) --- + d_list = [menu[0] for menu in menus_dic[view_type]] + selected_value = xbmcgui.Dialog().select('Manage MAME Recently Played machines', d_list) + if selected_value < 0: return + action = menus_dic[view_type][selected_value][1] + log_debug('action = {}'.format(action)) + + # --- Execute actions --- + if action == ACTION_DELETE_MACHINE: + log_debug('command_context_manage_mame_recent_played() ACTION_DELETE_MACHINE') + log_debug('machine_name "{}"'.format(machine_name)) + + # --- Load Recently Played machine list --- + recent_roms_list = utils_load_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), []) + + # --- Search index of this machine in the list --- + machine_index = db_locate_idx_by_name(recent_roms_list, machine_name) + if machine_index < 0: + a = 'Machine {} cannot be located in Recently Played list. This is a bug.' + kodi_dialog_OK(a.format(machine_name)) + return + + # --- Ask user for confirmation --- + desc = recent_roms_list[machine_index]['description'] + ret = kodi_dialog_yesno('Delete Machine {} ({})?'.format(desc, machine_name)) + if ret < 1: + kodi_notify('MAME Recently Played unchanged') + return + + # --- Delete machine and save DB --- + recent_roms_list.pop(machine_index) + log_info('Deleted machine "{}"'.format(machine_name)) + utils_write_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), recent_roms_list) + kodi_refresh_container() + kodi_notify('Machine {} deleted from MAME Recently Played'.format(machine_name)) + + elif action == ACTION_DELETE_ALL: + log_debug('command_context_manage_mame_recent_played() ACTION_DELETE_ALL') + recent_roms_list = utils_load_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), []) + + # Confirm with user + num_machines = len(recent_roms_list) + ret = kodi_dialog_yesno( + 'You have {} MAME Recently Played. Delete them all?'.format(num_machines)) + if ret < 1: + kodi_notify('MAME Recently Played unchanged') + return + + # Database is an empty list. + utils_write_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), list()) + kodi_refresh_container() + kodi_notify('Deleted all MAME Recently Played'.format(machine_name)) + + elif action == ACTION_DELETE_MISSING: + log_debug('command_context_manage_mame_recent_played() ACTION_DELETE_MISSING') + + # --- Ensure MAME Catalog have been built --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + options = check_MAME_DB_status(st_dic, MAME_CATALOG_BUILT, control_dic) + if kodi_display_status_message(st_dic): return False + + # --- Load databases --- + db_files = [ + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + recent_roms_list = utils_load_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), []) + + # --- Delete missing MAME machines --- + num_deleted_machines = 0 + if len(recent_roms_list) >= 1: + pDialog = KodiProgressDialog() + pDialog.startProgress('Delete missing MAME Recently Played...', len(recent_roms_list)) + new_recent_roms_list = [] + for i, recent_rom in enumerate(recent_roms_list): + pDialog.updateProgressInc() + fav_key = recent_rom['name'] + log_debug('Checking Favourite "{}"'.format(fav_key)) + if fav_key in db_dic['machines']: + new_recent_roms_list.append(recent_rom) + else: + num_deleted_machines += 1 + utils_write_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), new_recent_roms_list) + pDialog.endProgress() + kodi_refresh_container() + if num_deleted_machines > 0: + kodi_notify('Deleted {} missing MAME machines'.format(num_deleted_machines)) + else: + kodi_notify('No missing machines found') + + else: + t = 'Wrong action == {}. This is a bug, please report it.'.format(action) + log_error(t) + kodi_dialog_OK(t) + +# ------------------------------------------------------------------------------------------------- +# SL Favourites/Recently Played/Most played +# ------------------------------------------------------------------------------------------------- +def command_context_add_sl_fav(cfg, SL_name, ROM_name): + log_debug('command_add_sl_fav() SL_name "{}"'.format(SL_name)) + log_debug('command_add_sl_fav() ROM_name "{}"'.format(ROM_name)) + + # --- Load databases --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_items.json' + SL_DB_FN = cfg.SL_DB_DIR.pjoin(file_name) + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath()) + assets_file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_assets_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + + # Open Favourite Machines dictionary. + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + SL_fav_key = SL_name + '-' + ROM_name + log_debug('command_add_sl_fav() SL_fav_key "{}"'.format(SL_fav_key)) + + # If machine already in Favourites ask user if overwrite. + if SL_fav_key in fav_SL_roms: + ret = kodi_dialog_yesno('Machine {} ({}) '.format(ROM_name, SL_name) + + 'already in SL Favourites. Overwrite?') + if ret < 1: return + + # Add machine to SL Favourites. + SL_ROM = SL_roms[ROM_name] + # SL_assets = SL_assets_dic[ROM_name] if ROM_name in SL_assets_dic else db_new_SL_asset() + SL_assets = SL_assets_dic[ROM_name] + fav_ROM = db_get_SL_Favourite(SL_name, ROM_name, SL_ROM, SL_assets, control_dic) + fav_SL_roms[SL_fav_key] = fav_ROM + log_info('command_add_sl_fav() Added machine "{}" ("{}")'.format(ROM_name, SL_name)) + + # Save Favourites + utils_write_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath(), fav_SL_roms) + kodi_notify('ROM {} added to SL Favourite ROMs'.format(ROM_name)) + +def render_sl_fav_machine_row(cfg, SL_fav_key, ROM, assets, location): + SL_name = ROM['SL_name'] + SL_ROM_name = ROM['SL_ROM_name'] + display_name = ROM['description'] + + # --- Mark Status and Clones --- + status = '{}{}'.format(ROM['status_ROM'], ROM['status_CHD']) + display_name += ' [COLOR skyblue]{}[/COLOR]'.format(status) + if ROM['cloneof']: display_name += ' [COLOR orange][Clo][/COLOR]' + # Render number of number the ROM has been launched + if location == LOCATION_SL_MOST_PLAYED: + if ROM['launch_count'] == 1: + display_name = '{} [COLOR orange][{} time][/COLOR]'.format(display_name, ROM['launch_count']) + else: + display_name = '{} [COLOR orange][{} times][/COLOR]'.format(display_name, ROM['launch_count']) + + # --- Assets/artwork --- + icon_path = assets[cfg.SL_icon] if assets[cfg.SL_icon] else 'DefaultProgram.png' + fanart_path = assets[cfg.SL_fanart] + poster_path = assets['3dbox'] if assets['3dbox'] else assets['boxfront'] + + # --- Create listitem row --- + ICON_OVERLAY = 6 + listitem = xbmcgui.ListItem(display_name) + # Make all the infolabels compatible with Advanced Emulator Launcher + if cfg.settings['display_hide_trailers']: + listitem.setInfo('video', { + 'title' : display_name, 'year' : ROM['year'], + 'genre' : ROM['genre'], 'studio' : ROM['publisher'], + 'plot' : ROM['plot'], 'overlay' : ICON_OVERLAY, + }) + else: + listitem.setInfo('video', { + 'title' : display_name, 'year' : ROM['year'], + 'genre' : ROM['genre'], 'studio' : ROM['publisher'], + 'plot' : ROM['plot'], 'overlay' : ICON_OVERLAY, + 'trailer' : assets['trailer'], + }) + listitem.setProperty('platform', 'MAME Software List') + + # --- Assets --- + # AEL custom artwork fields + listitem.setArt({ + 'title' : assets['title'], 'snap' : assets['snap'], + 'boxfront' : assets['boxfront'], '3dbox' : assets['3dbox'], + 'icon' : icon_path, 'fanart' : fanart_path, 'poster' : poster_path + }) + + # --- Create context menu --- + URL_view_DAT = misc_url_4_arg_RunPlugin('command', 'VIEW_DAT', 'SL', SL_name, 'ROM', SL_ROM_name, 'location', location) + URL_view = misc_url_4_arg_RunPlugin('command', 'VIEW', 'SL', SL_name, 'ROM', SL_ROM_name, 'location', location) + if location == LOCATION_SL_FAVS: + URL_manage = misc_url_3_arg_RunPlugin('command', 'MANAGE_SL_FAV', 'SL', SL_name, 'ROM', SL_ROM_name) + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Manage SL Favourites', URL_manage), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)), + ] + elif location == LOCATION_SL_MOST_PLAYED: + URL_manage = misc_url_3_arg_RunPlugin('command', 'MANAGE_SL_MOST_PLAYED', 'SL', SL_name, 'ROM', SL_ROM_name) + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Manage SL Most Played', URL_manage), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + elif location == LOCATION_SL_RECENT_PLAYED: + URL_manage = misc_url_3_arg_RunPlugin('command', 'MANAGE_SL_RECENT_PLAYED', 'SL', SL_name, 'ROM', SL_ROM_name) + commands = [ + ('Info / Utils', URL_view_DAT), + ('View / Audit', URL_view), + ('Manage SL Recently Played', URL_manage), + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + listitem.addContextMenuItems(commands) + URL = misc_url_4_arg('command', 'LAUNCH_SL', 'SL', SL_name, 'ROM', SL_ROM_name, 'location', location) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = False) + +def command_show_sl_fav(cfg): + log_debug('command_show_sl_fav() Starting ...') + + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + if not fav_SL_roms: + kodi_dialog_OK('No Favourite Software Lists ROMs. Add some ROMs to SL Favourites first.') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + # Render Favourites + set_Kodi_all_sorting_methods(cfg) + for SL_fav_key in fav_SL_roms: + SL_fav_ROM = fav_SL_roms[SL_fav_key] + assets = SL_fav_ROM['assets'] + # Add the SL name as 'genre' + SL_name = SL_fav_ROM['SL_name'] + SL_fav_ROM['genre'] = SL_catalog_dic[SL_name]['display_name'] + render_sl_fav_machine_row(cfg, SL_fav_key, SL_fav_ROM, assets, LOCATION_SL_FAVS) + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + +# +# Context menu "Manage SL Favourite ROMs" +# +def command_context_manage_sl_fav(cfg, SL_name, ROM_name): + VIEW_ROOT_MENU = 100 + VIEW_INSIDE_MENU = 200 + + ACTION_DELETE_MACHINE = 100 + ACTION_DELETE_MISSING = 200 + ACTION_DELETE_ALL = 300 + ACTION_CHOOSE_DEFAULT = 400 + + menus_dic = { + VIEW_ROOT_MENU : [ + ('Delete missing items from SL Favourites', ACTION_DELETE_MISSING), + ('Delete all machines from SL Favourites', ACTION_DELETE_ALL), + ], + VIEW_INSIDE_MENU : [ + ('Choose default machine for SL item', ACTION_CHOOSE_DEFAULT), + ('Delete item from SL Favourites', ACTION_DELETE_MACHINE), + ('Delete missing items from SL Favourites', ACTION_DELETE_MISSING), + ('Delete all machines from SL Favourites', ACTION_DELETE_ALL), + ], + } + + # --- Determine view type --- + log_debug('command_context_manage_sl_fav() BEGIN ...') + log_debug('SL_name "{}" / ROM_name "{}"'.format(SL_name, ROM_name)) + if SL_name and ROM_name: + view_type = VIEW_INSIDE_MENU + else: + view_type = VIEW_ROOT_MENU + log_debug('view_type = {}'.format(view_type)) + + # --- Build menu base on view_type (Polymorphic menu, determine action) --- + d_list = [menu[0] for menu in menus_dic[view_type]] + selected_value = xbmcgui.Dialog().select('Manage SL Favourite itmes', d_list) + if selected_value < 0: return + action = menus_dic[view_type][selected_value][1] + log_debug('action = {}'.format(action)) + + # --- Execute actions --- + if action == ACTION_CHOOSE_DEFAULT: + log_debug('command_context_manage_sl_fav() ACTION_CHOOSE_DEFAULT') + + # --- Load Favs --- + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + SL_fav_key = SL_name + '-' + ROM_name + + # --- Get a list of machines that can launch this SL ROM. User chooses. --- + SL_machines_dic = utils_load_JSON_file(cfg.SL_MACHINES_PATH.getPath()) + SL_machine_list = SL_machines_dic[SL_name] + SL_machine_names_list = [] + SL_machine_desc_list = [] + SL_machine_names_list.append('') + SL_machine_desc_list.append('[ Not set ]') + for SL_machine in SL_machine_list: + SL_machine_names_list.append(SL_machine['machine']) + SL_machine_desc_list.append(SL_machine['description']) + # Krypton feature: preselect current machine. + # Careful with the preselect bug. + pre_idx = SL_machine_names_list.index(fav_SL_roms[SL_fav_key]['launch_machine']) + if pre_idx < 0: pre_idx = 0 + dialog = xbmcgui.Dialog() + m_index = dialog.select('Select machine', SL_machine_desc_list, preselect = pre_idx) + if m_index < 0 or m_index == pre_idx: return + machine_name = SL_machine_names_list[m_index] + machine_desc = SL_machine_desc_list[m_index] + + # --- Edit and save --- + fav_SL_roms[SL_fav_key]['launch_machine'] = machine_name + utils_write_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath(), fav_SL_roms) + kodi_notify('Deafult machine set to {} ({})'.format(machine_name, machine_desc)) + + # --- Delete ROM from SL Favourites --- + elif action == ACTION_DELETE_MACHINE: + log_debug('command_context_manage_sl_fav() ACTION_DELETE_MACHINE') + + # --- Open Favourite Machines dictionary --- + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + SL_fav_key = SL_name + '-' + ROM_name + log_debug('SL_fav_key "{}"'.format(SL_fav_key)) + + # --- Ask user for confirmation --- + desc = most_played_roms_dic[SL_fav_key]['description'] + a = 'Delete SL Item {} ({} / {})?' + ret = kodi_dialog_yesno(a.format(desc, SL_name, ROM_name)) + if ret < 1: + kodi_notify('SL Favourites unchanged') + return + + # --- Delete machine and save DB --- + del fav_SL_roms[SL_fav_key] + log_info('Deleted machine {} ({})'.format(SL_name, ROM_name)) + utils_write_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath(), fav_SL_roms) + kodi_refresh_container() + kodi_notify('SL Item {}-{} deleted from SL Favourites'.format(SL_name, ROM_name)) + + elif action == ACTION_DELETE_ALL: + log_debug('command_context_manage_sl_fav() ACTION_DELETE_ALL') + + # --- Open Favourite Machines dictionary --- + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + SL_fav_key = SL_name + '-' + ROM_name + log_debug('SL_fav_key "{}"'.format(SL_fav_key)) + + # --- Ask user for confirmation --- + ret = kodi_dialog_yesno( + 'You have {} SL Favourites. Delete them all?'.format(len(fav_SL_roms))) + if ret < 1: + kodi_notify('SL Favourites unchanged') + return + + # --- Delete machine and save DB --- + utils_write_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath(), dict()) + kodi_refresh_container() + kodi_notify('Deleted all SL Favourites') + + elif action == ACTION_DELETE_MISSING: + log_debug('command_context_manage_sl_fav() ACTION_DELETE_MISSING BEGIN...') + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + if len(fav_SL_roms) < 1: + kodi_notify('SL Favourites empty') + return + pDialog = KodiProgressDialog() + pDialog.startProgress('Advanced MAME Launcher', len(fav_SL_roms)) + num_items_deleted = 0 + for fav_SL_key in sorted(fav_SL_roms): + fav_SL_name = fav_SL_roms[fav_SL_key]['SL_name'] + fav_ROM_name = fav_SL_roms[fav_SL_key]['SL_ROM_name'] + log_debug('Checking SL Favourite "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + + # Update progress dialog. + pDialog.updateProgressInc('Checking SL Favourites...\nItem "{}"'.format(fav_ROM_name)) + + # --- Load SL ROMs DB and assets --- + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_catalog_dic[fav_SL_name]['rom_DB_noext'] + '_items.json') + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + + # --- Check --- + if fav_ROM_name not in SL_roms: + num_items_deleted += 1 + del fav_SL_roms[fav_ROM_name] + log_info('Deleted machine {} ({})'.format(fav_SL_name, fav_ROM_name)) + else: + log_debug('Machine {} ({}) OK'.format(fav_SL_name, fav_ROM_name)) + utils_write_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath(), fav_SL_roms) + pDialog.close() + if num_items_deleted > 0: + kodi_notify('Deleted {} items'.format(num_items_deleted)) + else: + kodi_notify('No items deleted') + + else: + t = 'Wrong action == {}. This is a bug, please report it.'.format(action) + log_error(t) + kodi_dialog_OK(t) + +def command_show_SL_most_played(cfg): + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + most_played_roms_dic = utils_load_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath()) + if not most_played_roms_dic: + kodi_dialog_OK('No Most Played SL machines. Play a bit and try later.') + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + set_Kodi_unsorted_method(cfg) + sorted_dic = sorted(most_played_roms_dic, key = lambda x : most_played_roms_dic[x]['launch_count'], reverse = True) + for SL_fav_key in sorted_dic: + SL_fav_ROM = most_played_roms_dic[SL_fav_key] + assets = SL_fav_ROM['assets'] + # Add the SL name as 'genre' + SL_name = SL_fav_ROM['SL_name'] + SL_fav_ROM['genre'] = SL_catalog_dic[SL_name]['display_name'] + render_sl_fav_machine_row(cfg, SL_fav_key, SL_fav_ROM, assets, LOCATION_SL_MOST_PLAYED) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def command_context_manage_SL_most_played(cfg, SL_name, ROM_name): + VIEW_ROOT_MENU = 100 + VIEW_INSIDE_MENU = 200 + + ACTION_DELETE_MACHINE = 100 + ACTION_DELETE_MISSING = 200 + ACTION_DELETE_ALL = 300 + ACTION_CHOOSE_DEFAULT = 400 + + menus_dic = { + VIEW_ROOT_MENU : [ + ('Delete missing items from SL Most Played', ACTION_DELETE_MISSING), + ('Delete all machines from SL Most Played', ACTION_DELETE_ALL), + ], + VIEW_INSIDE_MENU : [ + ('Choose default machine for SL item', ACTION_CHOOSE_DEFAULT), + ('Delete item from SL Most Played', ACTION_DELETE_MACHINE), + ('Delete missing items from SL Most Played', ACTION_DELETE_MISSING), + ('Delete all machines from SL Most Played', ACTION_DELETE_ALL), + ], + } + + # --- Determine view type --- + log_debug('command_context_manage_SL_most_played() BEGIN ...') + log_debug('SL_name "{}" / ROM_name "{}"'.format(SL_name, ROM_name)) + if SL_name and ROM_name: + view_type = VIEW_INSIDE_MENU + else: + view_type = VIEW_ROOT_MENU + log_debug('view_type = {}'.format(view_type)) + + # --- Build menu base on view_type (Polymorphic menu, determine action) --- + d_list = [menu[0] for menu in menus_dic[view_type]] + selected_value = xbmcgui.Dialog().select('Manage SL Most Played', d_list) + if selected_value < 0: return + action = menus_dic[view_type][selected_value][1] + log_debug('action = {}'.format(action)) + + # --- Execute actions --- + if action == ACTION_CHOOSE_DEFAULT: + log_debug('command_context_manage_sl_most_played() ACTION_CHOOSE_DEFAULT') + kodi_dialog_OK('ACTION_CHOOSE_DEFAULT not implemented yet. Sorry.') + + elif action == ACTION_DELETE_MACHINE: + log_debug('command_context_manage_sl_most_played() ACTION_DELETE_MACHINE') + + # --- Load Most Played items dictionary --- + most_played_roms_dic = utils_load_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath()) + SL_fav_key = SL_name + '-' + ROM_name + log_debug('SL_fav_key "{}"'.format(SL_fav_key)) + + # --- Ask user for confirmation --- + desc = most_played_roms_dic[SL_fav_key]['description'] + a = 'Delete SL Item {} ({} / {})?' + ret = kodi_dialog_yesno(a.format(desc, SL_name, ROM_name)) + if ret < 1: + kodi_notify('SL Most Played unchanged') + return + + # --- Delete machine and save DB --- + del most_played_roms_dic[SL_fav_key] + a = 'Deleted SL_name "{}" / ROM_name "{}"' + log_info(a.format(SL_name, ROM_name)) + utils_write_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath(), most_played_roms_dic) + kodi_refresh_container() + kodi_notify('Item {}-{} deleted from SL Most Played'.format(SL_name, ROM_name)) + + elif action == ACTION_DELETE_ALL: + log_debug('command_context_manage_sl_most_played() ACTION_DELETE_ALL') + + # --- Open Favourite Machines dictionary --- + fav_SL_roms = utils_load_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath()) + SL_fav_key = SL_name + '-' + ROM_name + log_debug('SL_fav_key "{}"'.format(SL_fav_key)) + + # --- Ask user for confirmation --- + ret = kodi_dialog_yesno( + 'You have {} SL Most Played. Delete them all?'.format(len(fav_SL_roms))) + if ret < 1: + kodi_notify('SL Most Played unchanged') + return + + # --- Delete machine and save DB --- + utils_write_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath(), dict()) + kodi_refresh_container() + kodi_notify('Deleted all SL Most Played') + + elif action == ACTION_DELETE_MISSING: + log_debug('command_context_manage_sl_most_played() ACTION_DELETE_MISSING') + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + fav_SL_roms = utils_load_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath()) + if len(fav_SL_roms) < 1: + kodi_notify('SL Most Played empty') + return + d_text = 'Checking SL Most Played...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(fav_SL_roms)) + num_items_deleted = 0 + for fav_SL_key in sorted(fav_SL_roms): + fav_SL_name = fav_SL_roms[fav_SL_key]['SL_name'] + fav_ROM_name = fav_SL_roms[fav_SL_key]['SL_ROM_name'] + log_debug('Checking SL Most Played "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + pDialog.updateProgressInc('{}\nItem "{}"'.format(d_text, fav_ROM_name)) + + # --- Load SL ROMs DB and assets --- + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_catalog_dic[fav_SL_name]['rom_DB_noext'] + '_items.json') + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + + # --- Check --- + if fav_ROM_name not in SL_roms: + num_items_deleted += 1 + del fav_SL_roms[fav_ROM_name] + log_info('Deleted machine {} ({})'.format(fav_SL_name, fav_ROM_name)) + else: + log_debug('Machine {} ({}) OK'.format(fav_SL_name, fav_ROM_name)) + utils_write_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath(), fav_SL_roms) + pDialog.endProgress() + if num_items_deleted > 0: + kodi_notify('Deleted {} items'.format(num_items_deleted)) + else: + kodi_notify('No items deleted') + + else: + t = 'Wrong action == {}. This is a bug, please report it.'.format(action) + log_error(t) + kodi_dialog_OK(t) + +def command_show_SL_recently_played(cfg): + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + recent_roms_list = utils_load_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), []) + if not recent_roms_list: + kodi_dialog_OK('No Recently Played SL machines. Play a bit and try later.') + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + set_Kodi_unsorted_method(cfg) + for SL_fav_ROM in recent_roms_list: + SL_fav_key = SL_fav_ROM['SL_DB_key'] + assets = SL_fav_ROM['assets'] + # Add the SL name as 'genre' + SL_name = SL_fav_ROM['SL_name'] + SL_fav_ROM['genre'] = SL_catalog_dic[SL_name]['display_name'] + render_sl_fav_machine_row(cfg, SL_fav_key, SL_fav_ROM, assets, LOCATION_SL_RECENT_PLAYED) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def command_context_manage_SL_recent_played(cfg, SL_name, ROM_name): + VIEW_ROOT_MENU = 100 + VIEW_INSIDE_MENU = 200 + + ACTION_DELETE_MACHINE = 100 + ACTION_DELETE_MISSING = 200 + ACTION_DELETE_ALL = 300 + ACTION_CHOOSE_DEFAULT = 400 + + menus_dic = { + VIEW_ROOT_MENU : [ + ('Delete missing items from SL Recently Played', ACTION_DELETE_MISSING), + ('Delete all machines from SL Recently Played', ACTION_DELETE_ALL), + ], + VIEW_INSIDE_MENU : [ + ('Choose default machine for SL item', ACTION_CHOOSE_DEFAULT), + ('Delete item from SL Recently Played', ACTION_DELETE_MACHINE), + ('Delete missing items from SL Recently Played', ACTION_DELETE_MISSING), + ('Delete all machines from SL Recently Played', ACTION_DELETE_ALL), + ], + } + + # --- Determine view type --- + log_debug('command_context_manage_SL_recent_played() BEGIN ...') + log_debug('SL_name "{}" / ROM_name "{}"'.format(SL_name, ROM_name)) + if SL_name and ROM_name: + view_type = VIEW_INSIDE_MENU + else: + view_type = VIEW_ROOT_MENU + log_debug('view_type = {}'.format(view_type)) + + # --- Build menu base on view_type (Polymorphic menu, determine action) --- + d_list = [menu[0] for menu in menus_dic[view_type]] + selected_value = xbmcgui.Dialog().select('Manage SL Recently Played', d_list) + if selected_value < 0: return + action = menus_dic[view_type][selected_value][1] + log_debug('action = {}'.format(action)) + + # --- Execute actions --- + if action == ACTION_CHOOSE_DEFAULT: + log_debug('command_context_manage_SL_recent_played() ACTION_CHOOSE_DEFAULT') + kodi_dialog_OK('ACTION_CHOOSE_DEFAULT not implemented yet. Sorry.') + + elif action == ACTION_DELETE_MACHINE: + log_debug('command_context_manage_SL_recent_played() Delete SL Recently Played machine') + + # --- Load Recently Played machine list --- + recent_roms_list = utils_load_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), []) + machine_index = db_locate_idx_by_SL_item_name(recent_roms_list, SL_name, ROM_name) + if machine_index < 0: + a = 'Item {}-{} cannot be located in SL Recently Played list. This is a bug.' + kodi_dialog_OK(a.format(SL_name, ROM_name)) + return + + # --- Ask user for confirmation --- + desc = recent_roms_list[machine_index]['description'] + a = 'Delete SL Item {} ({} / {})?' + ret = kodi_dialog_yesno(a.format(desc, SL_name, ROM_name)) + if ret < 1: + kodi_notify('SL Recently Played unchanged') + return + + # --- Delete machine and save DB --- + recent_roms_list.pop(machine_index) + a = 'Deleted SL_name "{}" / ROM_name "{}"' + log_info(a.format(SL_name, ROM_name)) + utils_write_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), recent_roms_list) + kodi_refresh_container() + kodi_notify('SL Item {}-{} deleted from SL Recently Played'.format(SL_name, ROM_name)) + + elif action == ACTION_DELETE_ALL: + log_debug('command_context_manage_SL_recent_played() ACTION_DELETE_ALL') + + # --- Open Favourite Machines dictionary --- + fav_SL_roms = utils_load_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath()) + SL_fav_key = SL_name + '-' + ROM_name + log_debug('SL_fav_key "{}"'.format(SL_fav_key)) + + # --- Ask user for confirmation --- + ret = kodi_dialog_yesno( + 'You have {} SL Recently Played. Delete them all?'.format(len(fav_SL_roms))) + if ret < 1: + kodi_notify('SL Recently Played unchanged') + return + + # --- Delete machine and save DB --- + utils_write_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), list()) + kodi_refresh_container() + kodi_notify('Deleted all SL Recently Played') + + elif action == ACTION_DELETE_MISSING: + # Careful because here fav_SL_roms is a list and not a dictionary. + log_debug('command_context_manage_SL_recent_played() ACTION_DELETE_MISSING') + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + fav_SL_roms = utils_load_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath()) + if len(fav_SL_roms) < 1: + kodi_notify_warn('SL Recently Played empty') + return + d_text = 'Checking SL Recently Played...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(fav_SL_roms)) + num_items_deleted = 0 + new_fav_SL_roms = [] + # fav_SL_roms is a list, do not sort it! + for fav_SL_item in fav_SL_roms: + fav_SL_name = fav_SL_item['SL_name'] + fav_ROM_name = fav_SL_item['SL_ROM_name'] + log_debug('Checking SL Recently Played "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + pDialog.updateProgressInc('{}\nItem "{}"'.format(d_text, fav_ROM_name)) + + # --- Load SL ROMs DB and assets --- + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_catalog_dic[fav_SL_name]['rom_DB_noext'] + '_items.json') + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + + # --- Check --- + if fav_ROM_name not in SL_roms: + num_items_deleted += 1 + log_info('Deleted machine {} ({})'.format(fav_SL_name, fav_ROM_name)) + else: + new_fav_SL_roms.append(fav_SL_item) + log_debug('Machine {} ({}) OK'.format(fav_SL_name, fav_ROM_name)) + utils_write_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), new_fav_SL_roms) + pDialog.endProgress() + if num_items_deleted > 0: + kodi_notify('Deleted {} items'.format(num_items_deleted)) + else: + kodi_notify('No items deleted') + else: + t = 'Wrong action == {}. This is a bug, please report it.'.format(action) + log_error(t) + kodi_dialog_OK(t) + +# --------------------------------------------------------------------------------------------- +# Custom MAME filters +# Custom filters behave like standard catalogs. +# Custom filters are defined in a XML file, the XML file is processed and the custom catalogs +# created from the main database. +# Custom filters do not have parent and all machines lists. They are always rendered in flat mode. +# --------------------------------------------------------------------------------------------- +def command_context_setup_custom_filters(cfg): + menu_item = xbmcgui.Dialog().select('Setup AML custom filters', [ + 'Build custom filter databases', + 'Test custom filter XML', + 'View custom filter XML', + 'View filter histogram report', + 'View filter XML syntax report', + 'View filter database report', + ]) + if menu_item < 0: return + + # --- Build custom filter databases --- + if menu_item == 0: + # Open main ROM databases + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME machines render', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME machine assets', cfg.ASSET_DB_PATH.getPath()], + ['machine_archives', 'Machine archives', cfg.ROM_SET_MACHINE_FILES_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + # Compatibility with "All in one" code. + audit_dic = { 'machine_archives' : db_dic['machine_archives'] } + + # --- Make a dictionary of machines to be filtered --- + (main_filter_dic, sets_dic) = filter_get_filter_DB(cfg, db_dic) + + # --- Parse custom filter XML and check for errors --- + # 1) Check the filter XML syntax and filter semantic errors. + # 2) Produces report cfg.REPORT_CF_XML_SYNTAX_PATH + (filter_list, f_st_dic) = filter_custom_filters_load_XML(cfg, db_dic, main_filter_dic, sets_dic) + # If no filters defined sayonara. + if len(filter_list) < 1: + kodi_notify_warn('Filter XML has no filter definitions') + return + # If errors found in the XML sayonara. + if f_st_dic['XML_errors']: + kodi_dialog_OK('Custom filter database build cancelled because the XML filter ' + 'definition file contains errors. Have a look at the XML filter file report, ' + 'correct the mistakes and try again.') + return + + # --- Build filter database --- + # 1) Saves control_dic (updated custom filter build timestamp). + # 2) Generates cfg.REPORT_CF_DB_BUILD_PATH + filter_build_custom_filters(cfg, db_dic, filter_list, main_filter_dic) + + # --- So long and thanks for all the fish --- + kodi_notify('Custom filter database built') + + # --- Test custom filter XML --- + elif menu_item == 1: + # Open main ROM databases + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME machines render', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME machine assets', cfg.ASSET_DB_PATH.getPath()], + ['machine_archives', 'Machine archives list', cfg.ROM_SET_MACHINE_FILES_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Make a dictionary of machines to be filtered --- + # This currently includes all MAME parent machines. + # However, it must include all machines (parent and clones). + (main_filter_dic, sets_dic) = filter_get_filter_DB(cfg, db_dic) + + # --- Parse custom filter XML and check for errors --- + # This function also check the filter XML syntax and produces a report. + (filter_list, f_st_dic) = filter_custom_filters_load_XML(cfg, db_dic, main_filter_dic, sets_dic) + # If no filters sayonara + if len(filter_list) < 1: + kodi_notify_warn('Filter XML has no filter definitions') + return + # If errors found in the XML sayonara + elif f_st_dic['XML_errors']: + kodi_dialog_OK( + 'The XML filter definition file contains errors. Have a look at the ' + 'XML filter file report, fix the mistakes and try again.') + return + kodi_notify('Custom filter XML check succesful') + + # --- View custom filter XML --- + elif menu_item == 2: + cf_XML_path_str = cfg.settings['filter_XML'] + log_debug('cf_XML_path_str = "{}"'.format(cf_XML_path_str)) + if not cf_XML_path_str: + log_debug('Using default XML custom filter.') + XML_FN = cfg.CUSTOM_FILTER_PATH + else: + log_debug('Using user-defined in addon settings XML custom filter.') + XML_FN = FileName(cf_XML_path_str) + log_debug('command_context_setup_custom_filters() Displaying "{}"'.format(XML_FN.getOriginalPath())) + if not XML_FN.exists(): + kodi_dialog_OK('Custom filter XML file not found.') + return + kodi_display_text_window_mono('Custom filter XML', utils_load_file_to_str(XML_FN.getPath())) + + # --- View filter histogram report --- + elif menu_item == 3: + filename_FN = cfg.REPORT_CF_HISTOGRAMS_PATH + log_debug('command_context_setup_custom_filters() Displaying "{}"'.format(filename_FN.getOriginalPath())) + if not filename_FN.exists(): + kodi_dialog_OK('Filter histogram report not found.') + return + kodi_display_text_window_mono('Filter histogram report', utils_load_file_to_str(filename_FN.getPath())) + + # --- View filter XML syntax report --- + elif menu_item == 4: + filename_FN = cfg.REPORT_CF_XML_SYNTAX_PATH + log_debug('command_context_setup_custom_filters() Displaying "{}"'.format(filename_FN.getOriginalPath())) + if not filename_FN.exists(): + kodi_dialog_OK('Filter XML filter syntax report not found.') + return + fstring = utils_load_file_to_str(filename_FN.getPath()) + kodi_display_text_window_mono('Custom filter XML syntax report', fstring) + + # --- View filter report --- + elif menu_item == 5: + filename_FN = cfg.REPORT_CF_DB_BUILD_PATH + log_debug('command_context_setup_custom_filters() Displaying "{}"'.format(filename_FN.getOriginalPath())) + if not filename_FN.exists(): + kodi_dialog_OK('Custom filter database report not found.') + return + fstring = utils_load_file_to_str(filename_FN.getPath()) + kodi_display_text_window_mono('Custom filter XML syntax report', fstring) + +def command_show_custom_filters(cfg): + log_debug('command_show_custom_filters() Starting ...') + + # Open Custom filter count database and index + filter_index_dic = utils_load_JSON_file(cfg.FILTERS_INDEX_PATH.getPath()) + if not filter_index_dic: + kodi_dialog_OK('MAME custom filter index is empty. Please rebuild your filters.') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + # Check if filters need to be rebuilt + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + if control_dic['t_Custom_Filter_build'] < control_dic['t_MAME_Catalog_build']: + kodi_dialog_OK('MAME custom filters need to be rebuilt.') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + # --- Render Custom Filters, always in flat mode --- + mame_view_mode = cfg.settings['mame_view_mode'] + set_Kodi_all_sorting_methods(cfg) + for f_name in sorted(filter_index_dic, key = lambda x: filter_index_dic[x]['order'], reverse = False): + num_machines = filter_index_dic[f_name]['num_machines'] + machine_str = 'machine' if num_machines == 1 else 'machines' + render_custom_filter_item_row(cfg, f_name, num_machines, machine_str, filter_index_dic[f_name]['plot']) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + +def render_custom_filter_item_row(cfg, f_name, num_machines, machine_str, plot): + # --- Create listitem row --- + ICON_OVERLAY = 6 + title_str = '{} [COLOR orange]({} {})[/COLOR]'.format(f_name, num_machines, machine_str) + listitem = xbmcgui.ListItem(title_str) + listitem.setInfo('video', {'title' : title_str, 'plot' : plot, 'overlay' : ICON_OVERLAY}) + + # --- Artwork --- + icon_path = cfg.ICON_FILE_PATH.getPath() + fanart_path = cfg.FANART_FILE_PATH.getPath() + listitem.setArt({'icon' : icon_path, 'fanart' : fanart_path}) + + # --- Create context menu --- + commands = [ + ('Kodi File Manager', 'ActivateWindow(filemanager)'), + ('AML addon settings', 'Addon.OpenSettings({})'.format(cfg.addon.info_id)) + ] + listitem.addContextMenuItems(commands) + URL = misc_url_2_arg('catalog', 'Custom', 'category', f_name) + xbmcplugin.addDirectoryItem(cfg.addon_handle, URL, listitem, isFolder = True) + +# +# Renders a custom filter list of machines, always in flat mode. +# +def render_custom_filter_machines(cfg, filter_name): + log_debug('render_custom_filter_machines() filter_name = {}'.format(filter_name)) + + # Global properties. + view_mode_property = cfg.settings['mame_view_mode'] + log_debug('render_custom_filter_machines() view_mode_property = {}'.format(view_mode_property)) + + # Check id main DB exists. + if not cfg.RENDER_DB_PATH.exists(): + kodi_dialog_OK('MAME database not found. Check out "Setup addon" in the context menu.') + xbmcplugin.endOfDirectory(handle = cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + # Load main MAME info DB and catalog. + l_cataloged_dic_start = time.time() + Filters_index_dic = utils_load_JSON_file(cfg.FILTERS_INDEX_PATH.getPath()) + rom_DB_noext = Filters_index_dic[filter_name]['rom_DB_noext'] + l_cataloged_dic_end = time.time() + l_render_db_start = time.time() + render_db_dic = utils_load_JSON_file(cfg.FILTERS_DB_DIR.pjoin(rom_DB_noext + '_render.json').getPath()) + l_render_db_end = time.time() + l_assets_db_start = time.time() + assets_db_dic = utils_load_JSON_file(cfg.FILTERS_DB_DIR.pjoin(rom_DB_noext + '_assets.json').getPath()) + l_assets_db_end = time.time() + l_favs_start = time.time() + fav_machines = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + l_favs_end = time.time() + + # Compute loading times. + catalog_t = l_cataloged_dic_end - l_cataloged_dic_start + render_t = l_render_db_end - l_render_db_start + assets_t = l_assets_db_end - l_assets_db_start + favs_t = l_favs_end - l_favs_start + loading_time = catalog_t + render_t + assets_t + favs_t + + # Check if catalog is empty + if not render_db_dic: + kodi_dialog_OK('Catalog is empty. Check out "Setup addon" in the context menu.') + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + return + + # --- Process ROMs --- + processing_ticks_start = time.time() + catalog_name = 'Custom' + category_name = filter_name + c_dic = {} + for m_name in render_db_dic: + c_dic[m_name] = render_db_dic[m_name]['description'] + catalog_dic = {category_name : c_dic} + # Render a flat list and ignore filters. + r_list = render_process_machines(cfg, catalog_dic, catalog_name, category_name, + render_db_dic, assets_db_dic, fav_machines) + processing_ticks_end = time.time() + processing_time = processing_ticks_end - processing_ticks_start + + # --- Commit ROMs --- + rendering_ticks_start = time.time() + set_Kodi_all_sorting_methods(cfg) + render_commit_machines(cfg, r_list) + xbmcplugin.endOfDirectory(cfg.addon_handle, succeeded = True, cacheToDisc = False) + rendering_ticks_end = time.time() + rendering_time = rendering_ticks_end - rendering_ticks_start + + # --- DEBUG Data loading/rendering statistics --- + # log_debug('Loading catalog {0:.4f} s'.format(catalog_t)) + # log_debug('Loading render db {0:.4f} s'.format(render_t)) + # log_debug('Loading assets db {0:.4f} s'.format(assets_t)) + # log_debug('Loading MAME favs {0:.4f} s'.format(favs_t)) + log_debug('Loading time {0:.4f} s'.format(loading_time)) + log_debug('Processing time {0:.4f} s'.format(processing_time)) + log_debug('Rendering time {0:.4f} s'.format(rendering_ticks_end - rendering_ticks_start)) + +# ------------------------------------------------------------------------------------------------- +# Check AML status +# ------------------------------------------------------------------------------------------------- +# +# Recursive function. Conditions are a fall-trough. For example, if user checks +# MAME_MAIN_DB_BUILT but XML has not been extracted/processed then MAME_MAIN_DB_BUILT fails. +# +def check_MAME_DB_status(st_dic, condition, ctrl_dic): + if not ctrl_dic: + log_debug('check_MAME_DB_status() ERROR: Control dictionary empty.') + t = ('MAME control file not found. You need to build the MAME main database ' + 'using the context menu "Setup addon" in the AML main window.') + kodi_set_error_status(st_dic, t) + elif condition == MAME_MAIN_DB_BUILT: + test_MAIN_DB_BUILT = True if ctrl_dic['t_MAME_DB_build'] > 0.0 else False + if not test_MAIN_DB_BUILT: + log_debug('check_MAME_DB_status() ERROR: MAME_MAIN_DB_BUILT fails.') + t = 'MAME Main database needs to be built. Use the context menu "Setup addon" in the main window.' + kodi_set_error_status(st_dic, t) + else: + log_debug('check_MAME_DB_status() MAME_MAIN_DB_BUILT OK') + elif condition == MAME_AUDIT_DB_BUILT: + test_AUDIT_DB_BUILT = True if ctrl_dic['t_MAME_Audit_DB_build'] > ctrl_dic['t_MAME_DB_build'] else False + if not test_AUDIT_DB_BUILT: + log_debug('check_MAME_DB_status() ERROR: MAME_AUDIT_DB_BUILT fails.') + t = 'MAME Audit database needs to be built. Use the context menu "Setup addon " in the main window.' + kodi_set_error_status(st_dic, t) + else: + log_debug('check_MAME_DB_status() MAME_AUDIT_DB_BUILT OK') + check_MAME_DB_status(st_dic, MAME_MAIN_DB_BUILT, ctrl_dic) + elif condition == MAME_CATALOG_BUILT: + test_CATALOG_BUILT = True if ctrl_dic['t_MAME_Catalog_build'] > ctrl_dic['t_MAME_Audit_DB_build'] else False + if not test_CATALOG_BUILT: + log_debug('check_MAME_DB_status() ERROR: MAME_CATALOG_BUILT fails.') + t = 'MAME Catalog database needs to be built. Use the context menu "Setup addon" in the main window.' + kodi_set_error_status(st_dic, t) + else: + log_debug('check_MAME_DB_status() MAME_CATALOG_BUILT OK') + check_MAME_DB_status(st_dic, MAME_AUDIT_DB_BUILT, ctrl_dic) + elif condition == MAME_MACHINES_SCANNED: + test_MACHINES_SCANNED = True if ctrl_dic['t_MAME_ROMs_scan'] > ctrl_dic['t_MAME_Catalog_build'] else False + if not test_MACHINES_SCANNED: + log_debug('check_MAME_DB_status() ERROR: MAME_MACHINES_SCANNED fails.') + t = 'MAME machines need to be scanned. Use the context menu "Setup addon" in the main window.' + kodi_set_error_status(st_dic, t) + else: + log_debug('check_MAME_DB_status() MAME_MACHINES_SCANNED OK') + check_MAME_DB_status(st_dic, MAME_CATALOG_BUILT, ctrl_dic) + elif condition == MAME_ASSETS_SCANNED: + test_ASSETS_SCANNED = True if ctrl_dic['t_MAME_assets_scan'] > ctrl_dic['t_MAME_ROMs_scan'] else False + if not test_ASSETS_SCANNED: + log_debug('check_MAME_DB_status() ERROR: MAME_ASSETS_SCANNED fails.') + t = 'MAME assets need to be scanned. Use the context menu "Setup addon" in the main window.' + kodi_set_error_status(st_dic, t) + else: + log_debug('check_MAME_DB_status() MAME_ASSETS_SCANNED OK') + check_MAME_DB_status(st_dic, MAME_MACHINES_SCANNED, ctrl_dic) + + else: + raise ValueError('check_MAME_DB_status() Recursive logic error. condition = {}'.format(condition)) + +# +# Look at check_MAME_DB_status() +# +def check_SL_DB_status(st_dic, condition, ctrl_dic): + if not ctrl_dic: + log_debug('check_SL_DB_status() ERROR: Control dictionary empty.') + t = ('MAME control file not found. You need to build the MAME main database ' + 'using the context menu "Setup addon" in the AML main window.') + kodi_set_error_status(st_dic, t) + elif condition == SL_MAIN_DB_BUILT: + test_MAIN_DB_BUILT = True if ctrl_dic['t_SL_DB_build'] > ctrl_dic['t_MAME_DB_build'] else False + if not test_MAIN_DB_BUILT: + log_debug('check_SL_DB_status() SL_MAIN_DB_BUILT fails') + t = 'Software List databases not built or outdated. Use the context menu "Setup addon" in the main window.' + kodi_set_error_status(st_dic, t) + else: + log_debug('check_SL_DB_status() SL_MAIN_DB_BUILT OK') + elif condition == SL_ITEMS_SCANNED: + test_ITEMS_SCANNED = True if ctrl_dic['t_SL_ROMs_scan'] > ctrl_dic['t_SL_DB_build'] else False + if not test_ITEMS_SCANNED: + log_debug('check_SL_DB_status() SL_ITEMS_SCANNED fails') + t = 'Software List items not scanned. Use the context menu "Setup addon" in the main window.' + kodi_set_error_status(st_dic, t) + else: + log_debug('check_SL_DB_status() SL_ITEMS_SCANNED OK') + check_SL_DB_status(st_dic, SL_MAIN_DB_BUILT, ctrl_dic) + elif condition == SL_ASSETS_SCANNED: + test_ASSETS_SCANNED = True if ctrl_dic['t_SL_assets_scan'] > ctrl_dic['t_SL_ROMs_scan'] else False + if not test_ASSETS_SCANNED: + log_debug('check_SL_DB_status() SL_ASSETS_SCANNED fails') + t = 'Software List assets not scanned. Use the context menu "Setup addon" in the main window.' + kodi_set_error_status(st_dic, t) + else: + log_debug('check_SL_DB_status() SL_ASSETS_SCANNED OK') + check_SL_DB_status(st_dic, SL_ITEMS_SCANNED, ctrl_dic) + else: + raise ValueError('check_SL_DB_status() Recursive logic error. condition = {}'.format(condition)) + +# This function is called before rendering a Catalog. +def check_MAME_DB_before_rendering_catalog(cfg, st_dic, control_dic): + # Check if MAME catalogs are built. + check_MAME_DB_status(st_dic, MAME_CATALOG_BUILT, control_dic) + if kodi_is_error_status(st_dic): return + log_debug('check_MAME_DB_before_rendering_catalog() All good!') + +# This function checks if the database is OK and machines inside a Category can be rendered. +# This function is called before rendering machines. +# This function does not affect MAME Favourites, Recently Played, etc. Those can always be rendered. +# This function relies in the timestamps in control_dic. +# +# Returns True if everything is OK and machines inside a Category can be rendered. +# Returns False and prints warning message if machines inside a category cannot be rendered. +def check_MAME_DB_before_rendering_machines(cfg, st_dic, control_dic): + # Check if MAME catalogs are built. + check_MAME_DB_status(st_dic, MAME_CATALOG_BUILT, control_dic) + if kodi_is_error_status(st_dic): return + + # If MAME render cache is enabled then check that it is up-to-date. + if cfg.settings['debug_enable_MAME_render_cache'] and \ + (control_dic['t_MAME_render_cache_build'] < control_dic['t_MAME_Catalog_build']): + log_warning('t_MAME_render_cache_build < t_MAME_Catalog_build') + t = ('MAME render cache needs to be updated. ' + 'Open the context menu "Setup addon", then ' + '"Step by Step", and then "Rebuild MAME machine and asset caches."') + kodi_set_error_status(st_dic, t) + return + + if cfg.settings['debug_enable_MAME_asset_cache'] and \ + (control_dic['t_MAME_asset_cache_build'] < control_dic['t_MAME_Catalog_build']): + log_warning('t_MAME_asset_cache_build < t_MAME_Catalog_build') + t = ('MAME asset cache needs to be updated. ' + 'Open the context menu "Setup addon", then ' + '"Step by Step", and then "Rebuild MAME machine and asset caches."') + kodi_set_error_status(st_dic, t) + return + + log_debug('check_MAME_DB_before_rendering_machines() All good.') + +# Same functions for Software Lists. Called before rendering SL Items inside a Software List. +# WARNING This must be completed!!! Look at the MAME functions. +def check_SL_DB_before_rendering_catalog(cfg, st_dic, control_dic): + # Check if SL databases are built. + check_SL_DB_status(st_dic, SL_MAIN_DB_BUILT, control_dic) + if kodi_is_error_status(st_dic): return + log_debug('check_SL_DB_before_rendering_catalog() All good.') + +def check_SL_DB_before_rendering_machines(cfg, st_dic, control_dic): + # Check if SL databases are built. + check_SL_DB_status(st_dic, SL_MAIN_DB_BUILT, control_dic) + if kodi_is_error_status(st_dic): return + log_debug('check_SL_DB_before_rendering_machines() All good.') + +# ------------------------------------------------------------------------------------------------- +# Setup plugin databases +# ------------------------------------------------------------------------------------------------- +def command_context_setup_plugin(cfg): + menu_item = xbmcgui.Dialog().select('Setup AML addon', [ + 'All in one (Build DB, Scan, Plots, Filters)', + 'All in one (Build DB, Scan, Plots, Filters, Audit)', + 'Build all databases', + 'Scan everything and build plots', + 'Build missing Fanarts and 3D boxes', + 'Audit MAME machine ROMs/CHDs', + 'Audit SL ROMs/CHDs', + 'Step by step ...', + 'Build Fanarts/3D Boxes ...', + ]) + if menu_item < 0: return + + # --- All in one (Build, Scan, Plots, Filters) --- + # --- All in one (Build, Scan, Plots, Filters, Audit) --- + if menu_item == 0 or menu_item == 1: + DO_AUDIT = True if menu_item == 1 else False + log_info('command_context_setup_plugin() All in one step starting ...') + log_info('Operation mode {}'.format(cfg.settings['op_mode'])) + log_info('DO_AUDIT {}'.format(DO_AUDIT)) + + # --- Build main MAME database, PClone list and MAME hashed database (mandatory) --- + # control_dic is created or reseted in this function. + # This uses the modern GUI error reporting functions. + st_dic = kodi_new_status_dic() + db_dic = mame_build_MAME_main_database(cfg, st_dic) + if kodi_display_status_message(st_dic): return + + # --- Build ROM audit/scanner databases (mandatory) --- + mame_check_before_build_ROM_audit_databases(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + mame_build_ROM_audit_databases(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Build MAME catalogs (mandatory) --- + mame_check_before_build_MAME_catalogs(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + mame_build_MAME_catalogs(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Build Software Lists ROM/CHD databases, SL indices and SL catalogs (optional) --- + if cfg.settings['global_enable_SL']: + mame_check_before_build_SL_databases(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + mame_build_SoftwareLists_databases(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + else: + log_info('SL disabled. Skipping mame_build_SoftwareLists_databases()') + + # --- Scan ROMs/CHDs/Samples and updates ROM status (optional) --- + # Abort if ROM path not found. CHD and Samples paths are optional. + options_dic = {} + mame_check_before_scan_MAME_ROMs(cfg, st_dic, options_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + mame_scan_MAME_ROMs(cfg, st_dic, options_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Scans MAME assets/artwork (optional) --- + mame_check_before_scan_MAME_assets(cfg, st_dic, db_dic['control_dic']) + if not kodi_display_status_message(st_dic): + # Scanning of assets is optional. + mame_scan_MAME_assets(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Scan SL ROMs/CHDs (optional) --- + if cfg.settings['global_enable_SL']: + options_dic = {} + mame_check_before_scan_SL_ROMs(cfg, st_dic, options_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + mame_scan_SL_ROMs(cfg, st_dic, options_dic, db_dic) + if kodi_display_status_message(st_dic): return + else: + log_info('SL disabled. Skipping mame_scan_SL_ROMs()') + + # --- Scan SL assets/artwork (optional) --- + if cfg.settings['global_enable_SL']: + mame_check_before_scan_SL_assets(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + mame_scan_SL_assets(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + else: + log_info('SL disabled. Skipping mame_scan_SL_assets()') + + # --- Build MAME machine plots --- + mame_build_MAME_plots(cfg, db_dic) + + # --- Build Software List items plot --- + if cfg.settings['global_enable_SL']: + mame_build_SL_plots(cfg, db_dic) + else: + log_info('SL disabled. Skipping mame_build_SL_plots()') + + # --- Regenerate the custom filters --- + (main_filter_dic, sets_dic) = filter_get_filter_DB(cfg, db_dic) + (filter_list, f_st_dic) = filter_custom_filters_load_XML(cfg, db_dic, main_filter_dic, sets_dic) + if len(filter_list) >= 1 and not f_st_dic['XML_errors']: + filter_build_custom_filters(cfg, db_dic, filter_list, main_filter_dic) + else: + log_info('Custom XML filters not built.') + + # --- Regenerate MAME asset hashed database --- + db_build_asset_hashed_db(cfg, db_dic['control_dic'], db_dic['assetdb']) + + # --- Regenerate MAME machine render and assets cache --- + db_build_render_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['renderdb']) + db_build_asset_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['assetdb']) + + if DO_AUDIT: + mame_audit_MAME_all(cfg, db_dic) + if cfg.settings['global_enable_SL']: + mame_audit_SL_all(cfg, db_dic) + else: + log_info('SL disabled. Skipping mame_audit_SL_all()') + + # --- So long and thanks for all the fish --- + if DO_AUDIT: + kodi_notify('Finished extracting, DB build, scanning, filters and audit') + else: + kodi_notify('Finished extracting, DB build, scanning and filters') + + # --- Build all databases --- + elif menu_item == 2: + log_info('command_context_setup_plugin() Build everything starting...') + + # --- Build main MAME database, PClone list and hashed database (mandatory) --- + # Extract/process MAME.xml, creates XML control file, resets control_dic and creates + # main MAME databases. + st_dic = kodi_new_status_dic() + db_dic = mame_build_MAME_main_database(cfg, st_dic) + if kodi_display_status_message(st_dic): return + + # --- Build ROM audit/scanner databases (mandatory) --- + mame_check_before_build_ROM_audit_databases(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + mame_build_ROM_audit_databases(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Release some memory before building the catalogs --- + del db_dic['devices'] + del db_dic['history_idx_dic'] + del db_dic['mameinfo_idx_dic'] + del db_dic['gameinit_idx_dic'] + del db_dic['command_idx_dic'] + del db_dic['audit_roms'] + del db_dic['machine_archives'] + # Force garbage collection here to free memory? + + # --- Build MAME catalogs (mandatory) --- + mame_check_before_build_MAME_catalogs(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + mame_build_MAME_catalogs(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Regenerate the render and assets cache --- + # Check whether cache must be rebuilt is done internally. + db_build_render_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['renderdb']) + db_build_asset_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['assetdb']) + + # --- Release some memory before building the SL databases --- + del db_dic['assetdb'] + del db_dic['roms'] + del db_dic['main_pclone_dic'] + del db_dic['cache_index'] + + # --- Build Software Lists ROM/CHD databases, SL indices and SL catalogs (optional) --- + if cfg.settings['global_enable_SL']: + mame_check_before_build_SL_databases(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + mame_build_SoftwareLists_databases(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + else: + log_info('SL globally disabled. Skipping mame_build_SoftwareLists_databases()') + + # So long and thanks for all the fish. + kodi_notify('All databases built') + + # --- Scan everything --- + elif menu_item == 3: + log_info('command_setup_plugin() Scanning everything starting...') + + # --- MAME ------------------------------------------------------------------------------- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + ['main_pclone_dic', 'MAME PClone dictionary', cfg.MAIN_PCLONE_DB_PATH.getPath()], + ['machine_archives', 'Machine file list', cfg.ROM_SET_MACHINE_FILES_DB_PATH.getPath()], + ['cache_index', 'MAME cache index', cfg.CACHE_INDEX_PATH.getPath()], + ['history_idx_dic', 'History DAT index', cfg.HISTORY_IDX_PATH.getPath()], + ['mameinfo_idx_dic', 'Mameinfo DAT index', cfg.MAMEINFO_IDX_PATH.getPath()], + ['gameinit_idx_dic', 'Gameinit DAT index', cfg.GAMEINIT_IDX_PATH.getPath()], + ['command_idx_dic', 'Command DAT index', cfg.COMMAND_IDX_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Scan MAME ROMs/CHDs/Samples and updates ROM status (optional) --- + st_dic = kodi_new_status_dic() + options_dic = {} + mame_check_before_scan_MAME_ROMs(cfg, st_dic, options_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + mame_scan_MAME_ROMs(cfg, st_dic, options_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Scans MAME assets/artwork (optional) --- + mame_check_before_scan_MAME_assets(cfg, st_dic, db_dic['control_dic']) + if not kodi_display_status_message(st_dic): + # Scanning of assets is optional. + mame_scan_MAME_assets(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Build MAME machines plot (mandatory) --- + mame_build_MAME_plots(cfg, db_dic) + + # --- Regenerate asset hashed database --- + db_build_asset_hashed_db(cfg, db_dic['control_dic'], db_dic['assetdb']) + + # --- Regenerate MAME asset cache --- + # Note that scanning only changes the assets, never the machines or render DBs. + db_build_asset_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['assetdb']) + + # --- Software Lists --------------------------------------------------------------------- + if cfg.settings['global_enable_SL']: + # --- Load databases --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['SL_index', 'Software Lists index', cfg.SL_INDEX_PATH.getPath()], + ['SL_PClone_dic', 'Software Lists Parent/Clone database', cfg.SL_PCLONE_DIC_PATH.getPath()], + ['SL_machines', 'Software Lists machines', cfg.SL_MACHINES_PATH.getPath()], + ['history_idx_dic', 'History DAT index', cfg.HISTORY_IDX_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Scan SL ROMs/CHDs (optional) --- + mame_check_before_scan_SL_ROMs(cfg, st_dic, db_dic['control_dic'], db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + mame_scan_SL_ROMs(cfg, st_dic, db_dic['control_dic'], db_dic) + if kodi_display_status_message(st_dic): return + + # --- Scan SL assets/artwork (optional) --- + mame_check_before_scan_SL_assets(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + mame_scan_SL_assets(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + + # --- Build Software List items plot (mandatory) --- + mame_build_SL_plots(cfg, db_dic) + else: + log_info('SL globally disabled. Skipping SL scanning and plot building.') + + # --- So long and thanks for all the fish --- + kodi_notify('All ROM/asset scanning finished') + + # --- Build missing Fanarts and 3D boxes --- + elif menu_item == 4: + BUILD_MISSING = True + log_info('command_context_setup_plugin() Building missing Fanarts and 3D boxes...') + st_dic = kodi_new_status_dic() + + # Check if Pillow library is available. Abort if not. + if not PILLOW_AVAILABLE: + kodi_dialog_OK('Pillow Python library is not available. Aborting image generation.') + return + + # Build mussing Fanarts and 3DBoxes. + data_dic = graphs_load_MAME_Fanart_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_MAME_Fanart_all(cfg, st_dic, data_dic) + if kodi_display_status_message(st_dic): return + + data_dic = graphs_load_MAME_3DBox_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_MAME_3DBox_all(cfg, st_dic, data_dic) + if kodi_display_status_message(st_dic): return + + # MAME asset DB has changed so rebuild MAME asset hashed database and MAME asset cache. + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + db_build_asset_hashed_db(cfg, control_dic, data_dic['assetdb']) + cache_index = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + db_build_asset_cache(cfg, control_dic, cache_index, data_dic['assetdb']) + + if cfg.settings['global_enable_SL']: + data_dic_SL = graphs_load_SL_Fanart_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_SL_Fanart_all(cfg, st_dic, data_dic_SL) + if kodi_display_status_message(st_dic): return + + data_dic_SL = graphs_load_SL_3DBox_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_SL_3DBox_all(cfg, st_dic, data_dic_SL) + if kodi_display_status_message(st_dic): return + else: + log_info('SL globally disabled. Skipping SL Fanart and 3DBox generation.') + + # --- Audit MAME machine ROMs/CHDs --- + # It is likely that this function will take a looong time. It is important that the + # audit process can be canceled and a partial report is written. + elif menu_item == 5: + log_info('command_context_setup_plugin() Audit MAME machines ROMs/CHDs ...') + + # --- Load machines, ROMs and CHDs databases --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['audit_roms', 'MAME ROM Audit', cfg.ROM_AUDIT_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Check for requirements/errors --- + + # --- Audit all MAME machines --- + # 1) Updates control_dic statistics and timestamp. + mame_audit_MAME_all(cfg, db_dic) + kodi_notify('MAME audit finished') + + # --- Audit SL ROMs/CHDs --- + elif menu_item == 6: + log_info('command_context_setup_plugin() Audit SL ROMs/CHDs ...') + + # Load databases. + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['SL_index', 'Software Lists index', cfg.SL_INDEX_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Check for requirements/errors --- + + # --- Audit all Software List items --- + # 1) Updates control_dic statistics and timestamps and saves it. + mame_audit_SL_all(cfg, db_dic) + kodi_notify('Software Lists audit finished') + + # --- Build Step by Step (database and scanner) --- + elif menu_item == 7: + submenu = xbmcgui.Dialog().select('Setup AML addon (step by step)', [ + 'Extract/Process MAME.xml', + 'Build MAME main database', + 'Build MAME audit/scanner databases', + 'Build MAME catalogs', + 'Build Software List databases', + 'Scan MAME ROMs/CHDs/Samples', + 'Scan MAME assets/artwork', + 'Scan Software List ROMs/CHDs', + 'Scan Software List assets/artwork', + 'Build MAME machine plots', + 'Build Software List item plots', + 'Rebuild MAME machine and asset caches', + ]) + if submenu < 0: return + + # --- Extract/Process MAME.xml --- + if submenu == 0: + st_dic = kodi_new_status_dic() + MAME_XML_path, XML_control_FN = mame_init_MAME_XML(cfg, st_dic) + if kodi_display_status_message(st_dic): return + XML_control_dic = utils_load_JSON_file(XML_control_FN.getPath()) + # Give user some data. + mame_v_str = XML_control_dic['ver_mame_str'] + size_MB = int(XML_control_dic['st_size'] / 1000000) + num_m = XML_control_dic['total_machines'] + t = ('MAME XML version [COLOR orange]{}[/COLOR], size is [COLOR orange]{}[/COLOR] MB ' + 'and there are [COLOR orange]{:,}[/COLOR] machines.') + kodi_dialog_OK(t.format(mame_v_str, size_MB, num_m)) + + # --- Build main MAME database, PClone list and hashed database --- + elif submenu == 1: + log_info('command_context_setup_plugin() Generating MAME main database and PClone list...') + # Extract/process MAME.xml, creates XML control file, resets control_dic and creates + # main MAME databases. + # 1) Creates control_dic. + st_dic = kodi_new_status_dic() + db_dic = mame_build_MAME_main_database(cfg, st_dic) + if kodi_display_status_message(st_dic): return + kodi_notify('Main MAME databases built') + + # --- Build ROM audit/scanner databases --- + elif submenu == 2: + log_info('command_context_setup_plugin() Generating ROM audit/scanner databases...') + + # Load databases. + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['devices', 'MAME machine devices', cfg.DEVICES_DB_PATH.getPath()], + ['roms', 'MAME machine ROMs', cfg.ROMS_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # Check for requirements/errors. + st_dic = kodi_new_status_dic() + mame_check_before_build_ROM_audit_databases(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + # Generate ROM audit databases. + # 1) Updates db_dic and saves databases on disk. + # 2) Updates t_MAME_Audit_DB_build and control_dic and saves it. + mame_build_ROM_audit_databases(cfg, st_dic, db_dic) + kodi_notify('ROM audit/scanner databases built') + + # --- Build MAME catalogs --- + elif submenu == 3: + log_info('command_context_setup_plugin() Building MAME catalogs...') + + # --- Load databases --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + ['roms', 'MAME machine ROMs', cfg.ROMS_DB_PATH.getPath()], + ['main_pclone_dic', 'MAME PClone dictionary', cfg.MAIN_PCLONE_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Check for requirements/errors --- + st_dic = kodi_new_status_dic() + mame_check_before_build_MAME_catalogs(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + # --- Build MAME catalog --- + # At this time the asset database will be empty (scanner has not been run). However, + # the asset cache with an empty database is required to render the machines in the catalogs. + # 1) Updates db_dic (adds cache_index field). + # 2) Creates cache_index_dic and saves it. + # 3) Updates control_dic and saves it. + # 4) Does not require to rebuild the render hashed database. + # 5) Requires rebuilding of the render cache. + # 6) Requires rebuilding of the asset cache. + mame_build_MAME_catalogs(cfg, st_dic, db_dic) + if kodi_display_status_message(st_dic): return + # Check whether cache must be rebuilt is done internally. + db_build_render_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['renderdb']) + db_build_asset_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['assetdb']) + kodi_notify('MAME Catalogs built') + + # --- Build Software Lists ROM/CHD databases, SL indices and SL catalogs --- + elif submenu == 4: + log_info('command_context_setup_plugin() Building Software Lists ROM/CHD databases, SL indices and SL catalogs...') + if not cfg.settings['global_enable_SL']: + kodi_dialog_OK('Software Lists globally disabled.') + return + + # Read main database and control dic. + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # Check for requirements/errors. + st_dic = kodi_new_status_dic() + mame_check_before_build_SL_databases(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + # Build SL databases. + # 1) Updates db_dic (adds cache_index field). + # 2) Modifies and saves control_dic + mame_build_SoftwareLists_databases(cfg, st_dic, db_dic) + kodi_notify('Software Lists database built') + + # --- Scan ROMs/CHDs/Samples and updates ROM status --- + elif submenu == 5: + log_info('command_context_setup_plugin() Scanning MAME ROMs/CHDs/Samples...') + + # Load machine database and control_dic and scan + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['cache_index', 'MAME cache index', cfg.CACHE_INDEX_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + ['machine_archives', 'Machine archive list', cfg.ROM_SET_MACHINE_FILES_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # Check for requirements/errors. + st_dic = kodi_new_status_dic() + options_dic = {} + mame_check_before_scan_MAME_ROMs(cfg, st_dic, options_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + # --- Scan MAME ROMs/CHDs/Samples --- + # 1) Updates 'flags' field in assets_dic + # 2) Updates timestamp t_MAME_ROM_scan and statistics in control_dic. + # 3) Saves control_dic and assets_dic. + # 4) Requires rebuilding the asset hashed DB. + # 5) Requires rebuilding the asset cache. + mame_scan_MAME_ROMs(cfg, st_dic, options_dic, db_dic) + db_build_asset_hashed_db(cfg, db_dic['control_dic'], db_dic['assetdb']) + db_build_asset_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['assetdb']) + kodi_notify('Scanning of MAME ROMs, CHDs and Samples finished') + + # --- Scans MAME assets/artwork --- + elif submenu == 6: + log_info('command_context_setup_plugin() Scanning MAME assets/artwork ...') + + # Load machine database and scan. + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + ['main_pclone_dic', 'MAME PClone dictionary', cfg.MAIN_PCLONE_DB_PATH.getPath()], + ['cache_index', 'MAME cache index', cfg.CACHE_INDEX_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Check for requirements/errors --- + st_dic = kodi_new_status_dic() + mame_check_before_scan_MAME_assets(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + # --- Scan MAME assets --- + # 1) Mutates assets_dic and control_dic (timestamp and stats) + # 2) Saves assets_dic and control_dic. + # 2) Requires rebuilding of the asset hashed DB. + # 3) Requires rebuilding of the asset cache. + mame_scan_MAME_assets(cfg, st_dic, db_dic) + db_build_asset_hashed_db(cfg, db_dic['control_dic'], db_dic['assetdb']) + db_build_asset_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['assetdb']) + kodi_notify('Scanning of assets/artwork finished') + + # --- Scan SL ROMs/CHDs --- + elif submenu == 7: + log_info('command_context_setup_plugin() Scanning SL ROMs/CHDs...') + if not cfg.settings['global_enable_SL']: + kodi_dialog_OK('Software Lists globally disabled.') + return + + # --- Load SL and scan ROMs/CHDs --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['SL_index', 'Software Lists index', cfg.SL_INDEX_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Check for requirements/errors --- + st_dic = kodi_new_status_dic() + options_dic = {} + mame_check_before_scan_SL_ROMs(cfg, st_dic, options_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + # 1) Mutates control_dic (timestamp and statistics) + # 2) Saves control_dic + mame_scan_SL_ROMs(cfg, st_dic, options_dic, db_dic) + kodi_notify('Scanning of SL ROMs finished') + + # --- Scan SL assets/artwork --- + # Database format: ADDON_DATA_DIR/db_SoftwareLists/32x_assets.json + # { 'ROM_name' : {'asset1' : 'path', 'asset2' : 'path', ... }, ... } + elif submenu == 8: + log_info('command_context_setup_plugin() Scanning SL assets/artwork...') + if not cfg.settings['global_enable_SL']: + kodi_dialog_OK('Software Lists globally disabled.') + return + + # --- Load SL databases --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['SL_index', 'Software Lists index', cfg.SL_INDEX_PATH.getPath()], + ['SL_PClone_dic', 'Software Lists Parent/Clone database', cfg.SL_PCLONE_DIC_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Check for requirements/errors --- + st_dic = kodi_new_status_dic() + mame_check_before_scan_SL_assets(cfg, st_dic, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return + + # --- Scan SL --- + # 1) Mutates control_dic (timestamp and statistics) and saves it. + mame_scan_SL_assets(cfg, st_dic, db_dic) + kodi_notify('Scanning of SL assets finished') + + # --- Build MAME machines plot --- + elif submenu == 9: + log_debug('Rebuilding MAME machine plots...') + + # --- Load databases --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + ['cache_index', 'MAME cache index', cfg.CACHE_INDEX_PATH.getPath()], + ['history_idx_dic', 'History DAT index', cfg.HISTORY_IDX_PATH.getPath()], + ['mameinfo_idx_dic', 'Mameinfo DAT index', cfg.MAMEINFO_IDX_PATH.getPath()], + ['gameinit_idx_dic', 'Gameinit DAT index', cfg.GAMEINIT_IDX_PATH.getPath()], + ['command_idx_dic', 'Command DAT index', cfg.COMMAND_IDX_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Check for requirements/errors --- + + # --- Traverse MAME machines and build plot --- + # 1) Mutates and saves the assets database + # 2) Requires rebuilding of the MAME asset hashed DB. + # 3) Requires rebuilding if the MAME asset cache. + mame_build_MAME_plots(cfg, db_dic) + db_build_asset_hashed_db(cfg, db_dic['control_dic'], db_dic['assetdb']) + db_build_asset_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['assetdb']) + kodi_notify('MAME machines plot generation finished') + + # --- Buils Software List items plot --- + elif submenu == 10: + log_debug('Rebuilding Software List items plots...') + + # --- Load databases --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['SL_index', 'Software Lists index', cfg.SL_INDEX_PATH.getPath()], + ['SL_machines', 'Software Lists machines', cfg.SL_MACHINES_PATH.getPath()], + ['history_idx_dic', 'History DAT index', cfg.HISTORY_IDX_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Check for requirements/errors --- + + # --- Build SL plots --- + mame_build_SL_plots(cfg, db_dic) + kodi_notify('SL item plot generation finished') + + # --- Regenerate MAME machine render and assets cache --- + elif submenu == 11: + log_debug('Rebuilding MAME machine and assets cache ...') + + # --- Load databases --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['cache_index', 'Cache index', cfg.CACHE_INDEX_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Regenerate ROM and asset caches --- + db_build_render_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['renderdb'], force_build = True) + db_build_asset_cache(cfg, db_dic['control_dic'], db_dic['cache_index'], db_dic['assetdb'], force_build = True) + kodi_notify('MAME machine and asset caches rebuilt') + + else: + kodi_dialog_OK('In command_context_setup_plugin() wrong submenu = {}'.format(submenu)) + + # --- Build Fanarts/3D boxes --- + elif menu_item == 8: + submenu = xbmcgui.Dialog().select('Build Fanarts', [ + 'Test MAME Fanart', + 'Test Software List item Fanart', + 'Test MAME 3D Box', + 'Test Software List item 3D Box', + 'Build all missing Fanarts', + 'Build all missing 3D boxes', + 'Build missing MAME Fanarts', + 'Build missing Software Lists Fanarts', + 'Build missing MAME 3D Boxes', + 'Build missing Software Lists 3D Boxes', + 'Rebuild all MAME Fanarts', + 'Rebuild all Software Lists Fanarts', + 'Rebuild all MAME 3D Boxes', + 'Rebuild all Software Lists 3D Boxes', + ]) + if submenu < 0: return + + # Check if Pillow library is available. Abort if not. + if not PILLOW_AVAILABLE: + kodi_dialog_OK('Pillow Python library is not available. Aborting Fanart generation.') + return + + # --- Test MAME Fanart --- + if submenu == 0: + log_info('command_context_setup_plugin() Testing MAME Fanart generation...') + Template_FN = cfg.ADDON_CODE_DIR.pjoin('templates/AML-MAME-Fanart-template.xml') + Asset_path_FN = cfg.ADDON_CODE_DIR.pjoin('media/MAME_assets') + Fanart_FN = cfg.ADDON_DATA_DIR.pjoin('MAME_Fanart.png') + log_debug('Testing MAME Fanart generation ...') + log_debug('Template_FN "{}"'.format(Template_FN.getPath())) + log_debug('Fanart_FN "{}"'.format(Fanart_FN.getPath())) + log_debug('Asset_path_FN "{}"'.format(Asset_path_FN.getPath())) + + # --- Load Fanart template from XML file --- + layout = graphs_load_MAME_Fanart_template(Template_FN) + if not layout: + kodi_dialog_OK('Error loading XML MAME Fanart layout.') + return + + # ---Use hard-coded assets --- + m_name = 'dino' + assets_dic = { + m_name : { + 'title' : Asset_path_FN.pjoin('dino_title.png').getPath(), + 'snap' : Asset_path_FN.pjoin('dino_snap.png').getPath(), + 'flyer' : Asset_path_FN.pjoin('dino_flyer.png').getPath(), + 'cabinet' : Asset_path_FN.pjoin('dino_cabinet.png').getPath(), + 'artpreview' : Asset_path_FN.pjoin('dino_artpreview.png').getPath(), + 'PCB' : Asset_path_FN.pjoin('dino_PCB.png').getPath(), + 'clearlogo' : Asset_path_FN.pjoin('dino_clearlogo.png').getPath(), + 'cpanel' : Asset_path_FN.pjoin('dino_cpanel.png').getPath(), + 'marquee' : Asset_path_FN.pjoin('dino_marquee.png').getPath(), + } + } + graphs_build_MAME_Fanart(cfg, layout, m_name, assets_dic, Fanart_FN, + CANVAS_COLOR = (25, 25, 50), test_flag = True) + + # Display Fanart + log_debug('Rendering fanart "{}"'.format(Fanart_FN.getPath())) + xbmc.executebuiltin('ShowPicture("{}")'.format(Fanart_FN.getPath())) + + # --- Test SL Fanart --- + elif submenu == 1: + log_info('command_context_setup_plugin() Testing SL Fanart generation...') + Template_FN = cfg.ADDON_CODE_DIR.pjoin('templates/AML-SL-Fanart-template.xml') + Asset_path_FN = cfg.ADDON_CODE_DIR.pjoin('media/SL_assets') + Fanart_FN = cfg.ADDON_DATA_DIR.pjoin('SL_Fanart.png') + log_debug('Testing Software List Fanart generation ...') + log_debug('Template_FN "{}"'.format(Template_FN.getPath())) + log_debug('Fanart_FN "{}"'.format(Fanart_FN.getPath())) + log_debug('Asset_path_FN "{}"'.format(Asset_path_FN.getPath())) + + # --- Load Fanart template from XML file --- + layout = graphs_load_SL_Fanart_template(Template_FN) + if not layout: + kodi_dialog_OK('Error loading XML Software List Fanart layout.') + return + + # --- Use hard-coded assets --- + SL_name = '32x' + m_name = 'doom' + assets_dic = { + m_name : { + 'title' : Asset_path_FN.pjoin('doom_title.png').getPath(), + 'snap' : Asset_path_FN.pjoin('doom_snap.png').getPath(), + 'boxfront' : Asset_path_FN.pjoin('doom_boxfront.png').getPath(), + } + } + graphs_build_SL_Fanart(cfg, layout, SL_name, m_name, assets_dic, Fanart_FN, + CANVAS_COLOR = (50, 50, 75), test_flag = True) + + # --- Display Fanart --- + log_debug('Displaying image "{}"'.format(Fanart_FN.getPath())) + xbmc.executebuiltin('ShowPicture("{}")'.format(Fanart_FN.getPath())) + + # --- Test MAME 3D Box --- + elif submenu == 2: + log_info('command_context_setup_plugin() Testing MAME 3D Box generation ...') + Fanart_FN = cfg.ADDON_DATA_DIR.pjoin('MAME_3dbox.png') + Asset_path_FN = cfg.ADDON_CODE_DIR.pjoin('media/MAME_assets') + # TProjection_FN = cfg.ADDON_CODE_DIR.pjoin('templates/3dbox_angleY_56.json') + TProjection_FN = cfg.ADDON_CODE_DIR.pjoin('templates/3dbox_angleY_60.json') + log_debug('Testing Software List Fanart generation ...') + log_debug('Fanart_FN "{}"'.format(Fanart_FN.getPath())) + log_debug('Asset_path_FN "{}"'.format(Asset_path_FN.getPath())) + log_debug('TProjection_FN "{}"'.format(TProjection_FN.getPath())) + + # Load 3D texture projection matrix + t_projection = utils_load_JSON_file(TProjection_FN.getPath()) + + # Create fake asset dictionaries + # m_name = 'dino' + # assets_dic = { + # m_name : { + # 'flyer' : Asset_path_FN.pjoin('dino_flyer.png').getPath(), + # 'clearlogo' : Asset_path_FN.pjoin('dino_clearlogo.png').getPath(), + # } + # } + SL_name = 'MAME' + m_name = 'mslug' + assets_dic = { + m_name : { + 'flyer' : Asset_path_FN.pjoin('mslug_flyer.png').getPath(), + 'clearlogo' : Asset_path_FN.pjoin('mslug_clearlogo.png').getPath(), + } + } + + pDialog = KodiProgressDialog() + pDialog.startProgress('Generating test MAME 3D Box...') + graphs_build_MAME_3DBox(cfg, t_projection, SL_name, m_name, assets_dic, Fanart_FN, + CANVAS_COLOR = (50, 50, 75), test_flag = True) + pDialog.endProgress() + + # --- Display Fanart --- + log_debug('Displaying image "{}"'.format(Fanart_FN.getPath())) + xbmc.executebuiltin('ShowPicture("{}")'.format(Fanart_FN.getPath())) + + # --- Test SL 3D Box --- + elif submenu == 3: + log_info('command_context_setup_plugin() Testing SL 3D Box generation ...') + # TProjection_FN = cfg.ADDON_CODE_DIR.pjoin('templates/3dbox_angleY_56.json') + TProjection_FN = cfg.ADDON_CODE_DIR.pjoin('templates/3dbox_angleY_60.json') + Fanart_FN = cfg.ADDON_DATA_DIR.pjoin('SL_3dbox.png') + Asset_path_FN = cfg.ADDON_CODE_DIR.pjoin('media/SL_assets') + log_debug('Testing Software List Fanart generation ...') + log_debug('TProjection_FN "{}"'.format(TProjection_FN.getPath())) + log_debug('Fanart_FN "{}"'.format(Fanart_FN.getPath())) + log_debug('Asset_path_FN "{}"'.format(Asset_path_FN.getPath())) + + # Load 3D texture projection matrix + t_projection = utils_load_JSON_file(TProjection_FN.getPath()) + + # Create fake asset dictionaries + # SL items in AML don't have clearlogo, but for testing is OK. + SL_name = 'genesis' + m_name = 'sonic3' + assets_dic = { + m_name : { + 'boxfront' : Asset_path_FN.pjoin('sonic3_boxfront.png').getPath(), + 'clearlogo' : Asset_path_FN.pjoin('sonic3_clearlogo.png').getPath(), + } + } + + pDialog = KodiProgressDialog() + pDialog.startProgress('Generating test SL 3D Box...') + graphs_build_MAME_3DBox(cfg, t_projection, SL_name, m_name, assets_dic, Fanart_FN, + CANVAS_COLOR = (50, 50, 75), test_flag = True) + pDialog.endProgress() + + # --- Display Fanart --- + log_debug('Displaying image "{}"'.format(Fanart_FN.getPath())) + xbmc.executebuiltin('ShowPicture("{}")'.format(Fanart_FN.getPath())) + + # --- Build all missing Fanarts --- + elif submenu == 4: + BUILD_MISSING = True + log_info('command_context_setup_plugin() Building all missing Fanarts...') + st_dic = kodi_new_status_dic() + if not PILLOW_AVAILABLE: + kodi_dialog_OK('Pillow Python library is not available. Aborting image generation.') + return + + # Kodi notifications inside graphs_build_*() functions. + data_dic = graphs_load_MAME_Fanart_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_MAME_Fanart_all(cfg, st_dic, data_dic) + if kodi_display_status_message(st_dic): return + + # MAME asset DB has changed so rebuild MAME asset hashed database and MAME asset cache. + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + db_build_asset_hashed_db(cfg, control_dic, data_dic['assetdb']) + cache_index = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + db_build_asset_cache(cfg, control_dic, cache_index, data_dic['assetdb']) + + if cfg.settings['global_enable_SL']: + data_dic_SL = graphs_load_SL_Fanart_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_SL_Fanart_all(cfg, st_dic, data_dic_SL) + if kodi_display_status_message(st_dic): return + else: + log_info('SL globally disabled. Skipping SL Fanart generation.') + + # --- Build all missing 3D boxes --- + elif submenu == 5: + BUILD_MISSING = True + log_info('command_context_setup_plugin() Building all missing 3D boxes...') + st_dic = kodi_new_status_dic() + if not PILLOW_AVAILABLE: + kodi_dialog_OK('Pillow Python library is not available. Aborting image generation.') + return + + # Kodi notifications inside graphs_build_*() functions. + data_dic = graphs_load_MAME_3DBox_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_MAME_3DBox_all(cfg, st_dic, data_dic) + if kodi_display_status_message(st_dic): return + + # MAME asset DB has changed so rebuild MAME asset hashed database and MAME asset cache. + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + db_build_asset_hashed_db(cfg, control_dic, data_dic['assetdb']) + cache_index = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + db_build_asset_cache(cfg, control_dic, cache_index, data_dic['assetdb']) + + if cfg.settings['global_enable_SL']: + data_dic_SL = graphs_load_SL_3DBox_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_SL_3DBox_all(cfg, st_dic, data_dic_SL) + if kodi_display_status_message(st_dic): return + else: + log_info('SL globally disabled. Skipping SL Fanart generation.') + + # --- Build missing/Rebuild all MAME Fanarts --- + # For a complete MAME artwork collection, rebuilding all Fanarts will take hours! + elif submenu == 6 or submenu == 10: + BUILD_MISSING = True if submenu == 6 else False + log_info('command_context_setup_plugin() Build missing/Rebuild all MAME Fanarts...') + log_info('BUILD_MISSING is {}'.format(BUILD_MISSING)) + st_dic = kodi_new_status_dic() + data_dic = graphs_load_MAME_Fanart_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_MAME_Fanart_all(cfg, st_dic, data_dic) + if kodi_display_status_message(st_dic): return + # MAME asset DB has changed so rebuild MAME asset hashed database and MAME asset cache. + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + db_build_asset_hashed_db(cfg, control_dic, data_dic['assetdb']) + cache_index = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + db_build_asset_cache(cfg, control_dic, cache_index, data_dic['assetdb']) + + # --- Build missing/Rebuild all SL Fanarts --- + elif submenu == 7 or submenu == 11: + BUILD_MISSING = True if submenu == 7 else False + log_info('command_context_setup_plugin() Build missing/Rebuild all Software Lists Fanarts...') + log_info('BUILD_MISSING is {}'.format(BUILD_MISSING)) + st_dic = kodi_new_status_dic() + if not cfg.settings['global_enable_SL']: + kodi_dialog_OK('SL globally disabled. Skipping image generation.') + return + data_dic_SL = graphs_load_SL_Fanart_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_SL_Fanart_all(cfg, st_dic, data_dic_SL) + if kodi_display_status_message(st_dic): return + + # --- Build missing/Rebuild all MAME 3D Boxes --- + elif submenu == 8 or submenu == 12: + BUILD_MISSING = True if submenu == 8 else False + log_info('command_context_setup_plugin() Rebuilding all MAME 3D Boxes...') + log_info('BUILD_MISSING is {}'.format(BUILD_MISSING)) + st_dic = kodi_new_status_dic() + data_dic = graphs_load_MAME_3DBox_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_MAME_3DBox_all(cfg, st_dic, data_dic) + if kodi_display_status_message(st_dic): return + # MAME asset DB has changed so rebuild MAME asset hashed database and MAME asset cache. + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + db_build_asset_hashed_db(cfg, control_dic, data_dic['assetdb']) + cache_index = utils_load_JSON_file(cfg.CACHE_INDEX_PATH.getPath()) + db_build_asset_cache(cfg, control_dic, cache_index, data_dic['assetdb']) + + # --- Build missing/Rebuild all SL 3D Boxes --- + elif submenu == 9 or submenu == 13: + BUILD_MISSING = True if submenu == 9 else False + log_info('command_context_setup_plugin() Rebuilding all Software Lists 3D Boxes...') + log_info('BUILD_MISSING is {}'.format(BUILD_MISSING)) + st_dic = kodi_new_status_dic() + if not cfg.settings['global_enable_SL']: + kodi_dialog_OK('SL globally disabled. Skipping image generation.') + return + data_dic_SL = graphs_load_SL_3DBox_stuff(cfg, st_dic, BUILD_MISSING) + if kodi_display_status_message(st_dic): return + graphs_build_SL_3DBox_all(cfg, st_dic, data_dic_SL) + if kodi_display_status_message(st_dic): return + + else: + kodi_dialog_OK('In command_context_setup_plugin() wrong submenu = {}'.format(submenu)) + + else: + kodi_dialog_OK('In command_context_setup_plugin() wrong menu_item = {}'.format(menu_item)) + +# +# Execute utilities. +# +def command_exec_utility(cfg, which_utility): + log_debug('command_exec_utility() which_utility = "{}" starting ...'.format(which_utility)) + + # Check MAME version + # Run 'mame -version' and extract version from stdout + if which_utility == 'CHECK_MAME_VERSION': + # --- Check for errors --- + if not cfg.settings['mame_prog']: + kodi_dialog_OK('MAME executable is not set.') + return + + # Check MAME version. + mame_prog_FN = FileName(cfg.settings['mame_prog']) + if not mame_prog_FN.exists(): + kodi_dialog_OK('Vanilla MAME executable not found.') + return + mame_version_str = mame_get_MAME_exe_version(cfg, mame_prog_FN) + kodi_dialog_OK('MAME version is {}'.format(mame_version_str)) + + # Check AML configuration + elif which_utility == 'CHECK_CONFIG': + # Functions defined here can see local variables defined in this code block. + def aux_check_dir_ERR(slist, dir_str, msg): + if dir_str: + if FileName(dir_str).exists(): + slist.append('{} {} "{}"'.format(OK, msg, dir_str)) + else: + slist.append('{} {} not found'.format(ERR, msg)) + else: + slist.append('{} {} not set'.format(ERR, msg)) + + def aux_check_dir_WARN(slist, dir_str, msg): + if dir_str: + if FileName(dir_str).exists(): + slist.append('{} {} "{}"'.format(OK, msg, dir_str)) + else: + slist.append('{} {} not found'.format(WARN, msg)) + else: + slist.append('{} {} not set'.format(WARN, msg)) + + def aux_check_file_WARN(slist, file_str, msg): + if file_str: + if FileName(file_str).exists(): + slist.append('{} {} "{}"'.format(OK, msg, file_str)) + else: + slist.append('{} {} not found'.format(WARN, msg)) + else: + slist.append('{} {} not set'.format(WARN, msg)) + + def aux_check_asset_dir(slist, dir_FN, msg): + if dir_FN.exists(): + slist.append('{} Found {} path "{}"'.format(OK, msg, dir_FN.getPath())) + else: + slist.append('{} {} path does not exist'.format(WARN, msg)) + slist.append(' Tried "{}"'.format(dir_FN.getPath())) + + # Checks AML configuration and informs users of potential problems. + log_info('command_exec_utility() Checking AML configuration ...') + OK = '[COLOR green]OK [/COLOR]' + WARN = '[COLOR yellow]WARN[/COLOR]' + ERR = '[COLOR red]ERR [/COLOR]' + slist = [] + + # --- Check main stuff --- + slist.append('Operation mode [COLOR orange]{}[/COLOR]'.format(cfg.settings['op_mode'])) + if cfg.settings['global_enable_SL']: + slist.append('Software Lists [COLOR orange]enabled[/COLOR]') + else: + slist.append('Software Lists [COLOR orange]disabled[/COLOR]') + slist.append('') + + # --- Mandatory stuff --- + slist.append('[COLOR orange]MAME executable[/COLOR]') + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + # ROM path is mandatory. + aux_check_dir_ERR(slist, cfg.settings['rom_path_vanilla'], 'MAME ROM path') + # Vanilla MAME checks. + if cfg.settings['mame_prog']: + if FileName(cfg.settings['mame_prog']).exists(): + slist.append('{} MAME executable "{}"'.format(OK, cfg.settings['mame_prog'])) + else: + slist.append('{} MAME executable not found'.format(ERR)) + else: + slist.append('{} MAME executable not set'.format(ERR)) + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + # MAME 2003 Plus checks. + # ROM path is mandatory. + aux_check_dir_ERR(slist, cfg.settings['rom_path_2003_plus'], 'MAME ROM path') + # Retroarch executable. + if cfg.settings['retroarch_prog']: + if FileName(cfg.settings['retroarch_prog']).exists(): + slist.append('{} Retroarch executable "{}"'.format(OK, cfg.settings['retroarch_prog'])) + else: + slist.append('{} Retroarch executable not found'.format(ERR)) + else: + slist.append('{} Retroarch executable not set'.format(ERR)) + # Libretro directory. + if cfg.settings['libretro_dir']: + if FileName(cfg.settings['libretro_dir']).exists(): + slist.append('{} Libretro directory "{}"'.format(OK, cfg.settings['libretro_dir'])) + else: + slist.append('{} Libretro directory not found'.format(ERR)) + else: + slist.append('{} Libretro directory not set'.format(ERR)) + # MAME XML path. + if cfg.settings['xml_2003_path']: + if FileName(cfg.settings['xml_2003_path']).exists(): + slist.append('{} MAME 2003 Plus XML "{}"'.format(OK, cfg.settings['xml_2003_path'])) + else: + slist.append('{} MAME 2003 Plus XML not found'.format(ERR)) + else: + slist.append('{} MAME 2003 Plus XML not set'.format(ERR)) + else: + slist.append('{} Unknown op_mode {}'.format(ERR, cfg.settings['op_mode'])) + slist.append('') + + slist.append('[COLOR orange]MAME optional paths[/COLOR]') + aux_check_dir_WARN(slist, cfg.settings['chd_path'], 'MAME CHD path') + aux_check_dir_WARN(slist, cfg.settings['samples_path'], 'MAME Samples path') + slist.append('') + + # --- MAME assets --- + slist.append('[COLOR orange]MAME assets[/COLOR]') + if cfg.settings['assets_path']: + if FileName(cfg.settings['assets_path']).exists(): + slist.append('{} MAME Asset path "{}"'.format(OK, cfg.settings['assets_path'])) + + # Check that artwork subdirectories exist + Asset_path_FN = FileName(cfg.settings['assets_path']) + + _3dboxes_FN = Asset_path_FN.pjoin('3dboxes') + artpreview_FN = Asset_path_FN.pjoin('artpreviews') + artwork_FN = Asset_path_FN.pjoin('artwork') + cabinets_FN = Asset_path_FN.pjoin('cabinets') + clearlogos_FN = Asset_path_FN.pjoin('clearlogos') + cpanels_FN = Asset_path_FN.pjoin('cpanels') + fanarts_FN = Asset_path_FN.pjoin('fanarts') + flyers_FN = Asset_path_FN.pjoin('flyers') + manuals_FN = Asset_path_FN.pjoin('manuals') + marquees_FN = Asset_path_FN.pjoin('marquees') + PCB_FN = Asset_path_FN.pjoin('PCBs') + snaps_FN = Asset_path_FN.pjoin('snaps') + titles_FN = Asset_path_FN.pjoin('titles') + videosnaps_FN = Asset_path_FN.pjoin('videosnaps') + + aux_check_asset_dir(slist, _3dboxes_FN, '3D Boxes') + aux_check_asset_dir(slist, artpreview_FN, 'Artpreviews') + aux_check_asset_dir(slist, artwork_FN, 'Artwork') + aux_check_asset_dir(slist, cabinets_FN, 'Cabinets') + aux_check_asset_dir(slist, clearlogos_FN, 'Clearlogos') + aux_check_asset_dir(slist, cpanels_FN, 'CPanels') + aux_check_asset_dir(slist, fanarts_FN, 'Fanarts') + aux_check_asset_dir(slist, flyers_FN, 'Flyers') + aux_check_asset_dir(slist, manuals_FN, 'Manuals') + aux_check_asset_dir(slist, marquees_FN, 'Marquees') + aux_check_asset_dir(slist, PCB_FN, 'PCB') + aux_check_asset_dir(slist, snaps_FN, 'Snaps') + aux_check_asset_dir(slist, titles_FN, 'Titles') + aux_check_asset_dir(slist, videosnaps_FN, 'Trailers') + else: + slist.append('{} MAME Asset path not found'.format(ERR)) + else: + slist.append('{} MAME Asset path not set'.format(WARN)) + slist.append('') + + # --- Software Lists paths --- + if cfg.settings['global_enable_SL']: + slist.append('[COLOR orange]Software List paths[/COLOR]') + aux_check_dir_WARN(slist, cfg.settings['SL_hash_path'], 'SL hash path') + aux_check_dir_WARN(slist, cfg.settings['SL_rom_path'], 'SL ROM path') + aux_check_dir_WARN(slist, cfg.settings['SL_chd_path'], 'SL CHD path') + slist.append('') + + slist.append('[COLOR orange]Software Lists assets[/COLOR]') + if cfg.settings['assets_path']: + if FileName(cfg.settings['assets_path']).exists(): + slist.append('{} MAME Asset path "{}"'.format(OK, cfg.settings['assets_path'])) + + # >> Check that artwork subdirectories exist + Asset_path_FN = FileName(cfg.settings['assets_path']) + + _3dboxes_FN = Asset_path_FN.pjoin('3dboxes_SL') + covers_FN = Asset_path_FN.pjoin('covers_SL') + fanarts_FN = Asset_path_FN.pjoin('fanarts_SL') + manuals_FN = Asset_path_FN.pjoin('manuals_SL') + snaps_FN = Asset_path_FN.pjoin('snaps_SL') + titles_FN = Asset_path_FN.pjoin('titles_SL') + videosnaps_FN = Asset_path_FN.pjoin('videosnaps_SL') + + aux_check_asset_dir(slist, _3dboxes_FN, '3D Boxes') + aux_check_asset_dir(slist, covers_FN, 'SL Covers') + aux_check_asset_dir(slist, fanarts_FN, 'SL Fanarts') + aux_check_asset_dir(slist, manuals_FN, 'SL Manuals') + aux_check_asset_dir(slist, snaps_FN, 'SL Snaps') + aux_check_asset_dir(slist, titles_FN, 'SL Titles') + aux_check_asset_dir(slist, videosnaps_FN, 'SL Trailers') + else: + slist.append('{} MAME Asset path not found'.format(ERR)) + else: + slist.append('{} MAME Asset path not set'.format(WARN)) + slist.append('') + + # --- Optional INI files --- + slist.append('[COLOR orange]INI/DAT files[/COLOR]') + if cfg.settings['dats_path']: + if FileName(cfg.settings['dats_path']).exists(): + slist.append('{} MAME INI/DAT path "{}"'.format(OK, cfg.settings['dats_path'])) + + DATS_dir_FN = FileName(cfg.settings['dats_path']) + ALLTIME_FN = DATS_dir_FN.pjoin(ALLTIME_INI) + ARTWORK_FN = DATS_dir_FN.pjoin(ARTWORK_INI) + BESTGAMES_FN = DATS_dir_FN.pjoin(BESTGAMES_INI) + CATEGORY_FN = DATS_dir_FN.pjoin(CATEGORY_INI) + CATLIST_FN = DATS_dir_FN.pjoin(CATLIST_INI) + CATVER_FN = DATS_dir_FN.pjoin(CATVER_INI) + GENRE_FN = DATS_dir_FN.pjoin(GENRE_INI) + MATURE_FN = DATS_dir_FN.pjoin(MATURE_INI) + NPLAYERS_FN = DATS_dir_FN.pjoin(NPLAYERS_INI) + SERIES_FN = DATS_dir_FN.pjoin(SERIES_INI) + COMMAND_FN = DATS_dir_FN.pjoin(COMMAND_DAT) + GAMEINIT_FN = DATS_dir_FN.pjoin(GAMEINIT_DAT) + HISTORY_XML_FN = DATS_dir_FN.pjoin(HISTORY_XML) + HISTORY_DAT_FN = DATS_dir_FN.pjoin(HISTORY_DAT) + MAMEINFO_FN = DATS_dir_FN.pjoin(MAMEINFO_DAT) + + aux_check_file_WARN(slist, ALLTIME_FN.getPath(), ALLTIME_INI + ' file') + aux_check_file_WARN(slist, ARTWORK_FN.getPath(), ARTWORK_INI + ' file') + aux_check_file_WARN(slist, BESTGAMES_FN.getPath(), BESTGAMES_INI + ' file') + aux_check_file_WARN(slist, CATEGORY_FN.getPath(), CATEGORY_INI + ' file') + aux_check_file_WARN(slist, CATLIST_FN.getPath(), CATLIST_INI + ' file') + aux_check_file_WARN(slist, CATVER_FN.getPath(), CATVER_INI + ' file') + aux_check_file_WARN(slist, GENRE_FN.getPath(), GENRE_INI + ' file') + aux_check_file_WARN(slist, MATURE_FN.getPath(), MATURE_INI + ' file') + aux_check_file_WARN(slist, NPLAYERS_FN.getPath(), NPLAYERS_INI + ' file') + aux_check_file_WARN(slist, SERIES_FN.getPath(), SERIES_INI + ' file') + aux_check_file_WARN(slist, COMMAND_FN.getPath(), COMMAND_DAT + ' file') + aux_check_file_WARN(slist, GAMEINIT_FN.getPath(), GAMEINIT_DAT + ' file') + aux_check_file_WARN(slist, HISTORY_XML_FN.getPath(), HISTORY_XML + ' file') + aux_check_file_WARN(slist, HISTORY_DAT_FN.getPath(), HISTORY_DAT + ' file') + aux_check_file_WARN(slist, MAMEINFO_FN.getPath(), MAMEINFO_DAT + ' file') + else: + slist.append('{} MAME INI/DAT path not found'.format(ERR)) + else: + slist.append('{} MAME INI/DAT path not set'.format(WARN)) + + # --- Display info to the user --- + kodi_display_text_window_mono('AML configuration check report', '\n'.join(slist)) + + # Check and update all favourite objects. + # Check if Favourites can be found in current MAME main database. It may happen that + # a machine is renamed between MAME version although I think this is very unlikely. + # MAME Favs can not be relinked. If the machine is not found in current database it must + # be deleted by the user and a new Favourite created. + # If the machine is found in the main database, then update the Favourite database + # with data from the main database. + elif which_utility == 'CHECK_ALL_FAV_OBJECTS': + log_debug('command_exec_utility() Executing CHECK_ALL_FAV_OBJECTS...') + + # --- Load databases --- + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME machines render', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME machine assets', cfg.ASSET_DB_PATH.getPath()], + ['SL_index', 'Software Lists index', cfg.SL_INDEX_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # --- Ensure databases are built and assets scanned before updating Favourites --- + st_dic = kodi_new_status_dic() + check_MAME_DB_status(st_dic, MAME_ASSETS_SCANNED, db_dic['control_dic']) + if kodi_display_status_message(st_dic): return False + + mame_update_MAME_Fav_objects(cfg, db_dic) + mame_update_MAME_MostPlay_objects(cfg, db_dic) + mame_update_MAME_RecentPlay_objects(cfg, db_dic) + mame_update_SL_Fav_objects(cfg, db_dic) + mame_update_SL_MostPlay_objects(cfg, db_dic) + mame_update_SL_RecentPlay_objects(cfg, db_dic) + kodi_notify('All Favourite objects checked') + kodi_refresh_container() + + # Check MAME and SL CRC 32 hash collisions. + # The assumption in this function is that there is not SHA1 hash collisions. + # Implicit ROM merging must not be confused with a collision. + elif which_utility == 'CHECK_MAME_COLLISIONS': + log_info('command_check_MAME_CRC_collisions() Initialising ...') + + # --- Check database --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + options = check_MAME_DB_status(st_dic, MAME_CATALOG_BUILT, control_dic) + if kodi_display_status_message(st_dic): return False + + # --- Open ROMs database --- + db_files = [ + ['machine_roms', 'MAME machine ROMs', cfg.ROMS_DB_PATH.getPath()], + ['roms_sha1_dic', 'MAME ROMs SHA1 dictionary', cfg.SHA1_HASH_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # Detect implicit ROM merging using the SHA1 hash and check for CRC32 collisions for + # non-implicit merged ROMs. + pDialog = KodiProgressDialog() + pDialog.startProgress('Checking for MAME CRC32 hash collisions...', len(db_dic['machine_roms'])) + crc_roms_dic = {} + sha1_roms_dic = {} + num_collisions = 0 + table_str = [] + table_str.append(['right', 'left', 'left', 'left', 'left']) + table_str.append(['Status', 'ROM name', 'Size', 'CRC', 'SHA1']) + for m_name in sorted(db_dic['machine_roms']): + pDialog.updateProgressInc() + m_roms = db_dic['machine_roms'][m_name] + for rom in m_roms['roms']: + rom_nonmerged_location = m_name + '/' + rom['name'] + # Skip invalid ROMs (no CRC, no SHA1 + if rom_nonmerged_location not in db_dic['roms_sha1_dic']: + continue + sha1 = db_dic['roms_sha1_dic'][rom_nonmerged_location] + if sha1 in sha1_roms_dic: + # ROM implicit merging (using SHA1). No check of CRC32 collision. + pass + else: + # No ROM implicit mergin. Check CRC32 collision + sha1_roms_dic[sha1] = rom_nonmerged_location + if rom['crc'] in crc_roms_dic: + num_collisions += 1 + coliding_name = crc_roms_dic[rom['crc']] + coliding_crc = rom['crc'] + coliding_sha1 = db_dic['roms_sha1_dic'][coliding_name] + table_str.append( + ['Collision', rom_nonmerged_location, text_type(rom['size']), rom['crc'], sha1]) + table_str.append(['with', coliding_name, ' ', coliding_crc, coliding_sha1]) + else: + crc_roms_dic[rom['crc']] = rom_nonmerged_location + pDialog.endProgress() + log_debug('MAME has {:,d} valid ROMs in total'.format(len(db_dic['roms_sha1_dic']))) + log_debug('There are {} CRC32 collisions'.format(num_collisions)) + + # --- Write report and debug file --- + slist = [ + '*** AML MAME ROMs CRC32 hash collision report ***', + 'MAME has {:,d} valid ROMs in total'.format(len(db_dic['roms_sha1_dic'])), + 'There are {} CRC32 collisions'.format(num_collisions), + '', + ] + table_str_list = text_render_table(table_str) + slist.extend(table_str_list) + kodi_display_text_window_mono('AML MAME CRC32 hash collision report', '\n'.join(slist)) + log_info('Writing "{}"'.format(cfg.REPORT_DEBUG_MAME_COLLISIONS_PATH.getPath())) + utils_write_slist_to_file(cfg.REPORT_DEBUG_MAME_COLLISIONS_PATH.getPath(), slist) + + elif which_utility == 'CHECK_SL_COLLISIONS': + log_info('command_exec_utility() Initialising CHECK_SL_COLLISIONS ...') + + # --- Load SL catalog and check for errors --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + options = check_SL_DB_status(st_dic, SL_MAIN_DB_BUILT, control_dic) + if kodi_display_status_message(st_dic): return False + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + + # --- Process all SLs --- + d_text = 'Scanning Sofware Lists ROMs/CHDs...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(SL_catalog_dic)) + roms_sha1_dic = {} + crc_roms_dic = {} + sha1_roms_dic = {} + num_collisions = 0 + table_str = [] + table_str.append(['right', 'left', 'left', 'left', 'left']) + table_str.append(['Status', 'ROM name', 'Size', 'CRC', 'SHA1']) + for SL_name in sorted(SL_catalog_dic): + pDialog.updateProgressInc('{}\nSoftware List "{}"'.format(d_text, SL_name)) + + # Load SL databases + # SL_SETS_DB_FN = SL_hash_dir_FN.pjoin(SL_name + '.json') + # sl_sets = utils_load_JSON_file(SL_SETS_DB_FN.getPath(), verbose = False) + SL_ROMS_DB_FN = cfg.SL_DB_DIR.pjoin(SL_name + '_ROMs.json') + sl_roms = utils_load_JSON_file(SL_ROMS_DB_FN.getPath(), verbose = False) + + # First step: make a SHA1 dictionary of all SL item hashes. + for set_name in sorted(sl_roms): + set_rom_list = sl_roms[set_name] + for area in set_rom_list: + if 'dataarea' not in area: continue + for da_dict in area['dataarea']: + for rom in da_dict['roms']: + sha1 = rom['sha1'] + if sha1: + rom_nonmerged_location = SL_name + '/' + set_name + '/' + rom['name'] + roms_sha1_dic[rom_nonmerged_location] = sha1 + + # Second step: make. + for set_name in sorted(sl_roms): + set_rom_list = sl_roms[set_name] + for area in set_rom_list: + if 'dataarea' not in area: continue + for da_dict in area['dataarea']: + for rom in da_dict['roms']: + rom_nonmerged_location = SL_name + '/' + set_name + '/' + rom['name'] + # >> Skip invalid ROMs (no CRC, no SHA1 + if rom_nonmerged_location not in roms_sha1_dic: + continue + sha1 = roms_sha1_dic[rom_nonmerged_location] + if sha1 in sha1_roms_dic: + # >> ROM implicit merging (using SHA1). No check of CRC32 collision. + pass + else: + # >> No ROM implicit mergin. Check CRC32 collision + sha1_roms_dic[sha1] = rom_nonmerged_location + if rom['crc'] in crc_roms_dic: + num_collisions += 1 + coliding_name = crc_roms_dic[rom['crc']] + coliding_crc = rom['crc'] + coliding_sha1 = roms_sha1_dic[coliding_name] + table_str.append([ + 'Collision', rom_nonmerged_location, + text_type(rom['size']), rom['crc'], sha1 + ]) + table_str.append([ + 'with', coliding_name, ' ', + coliding_crc, coliding_sha1 + ]) + else: + crc_roms_dic[rom['crc']] = rom_nonmerged_location + pDialog.endProgress() + log_debug('The SL have {:,d} valid ROMs in total'.format(len(roms_sha1_dic))) + log_debug('There are {} CRC32 collisions'.format(num_collisions)) + + # --- Write report --- + slist = [ + '*** AML SL ROMs CRC32 hash collision report ***', + 'The Software Lists have {:,d} valid ROMs in total'.format(len(roms_sha1_dic)), + 'There are {} CRC32 collisions'.format(num_collisions), + '', + ] + table_str_list = text_render_table(table_str) + slist.extend(table_str_list) + kodi_display_text_window_mono('AML Software Lists CRC32 hash collision report', '\n'.join(slist)) + log_info('Writing "{}"'.format(cfg.REPORT_DEBUG_SL_COLLISIONS_PATH.getPath())) + utils_write_slist_to_file(cfg.REPORT_DEBUG_SL_COLLISIONS_PATH.getPath(), slist) + + # Open the ROM audit database and calculate the size of all ROMs. + # Sort the list by size and print it. + elif which_utility == 'SHOW_BIGGEST_ROMS' or which_utility == 'SHOW_SMALLEST_ROMS': + show_BIG = True if which_utility == 'SHOW_BIGGEST_ROMS' else False + log_info('command_exec_utility() Initialising SHOW_BIGGEST_ROMS/SHOW_SMALLEST_ROMS...') + log_info('command_exec_utility() show_BIG {}'.format(show_BIG)) + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['renderdb', 'MAME machines Render', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME machine Assets', cfg.ASSET_DB_PATH.getPath()], + ['roms', 'MAME machine ROMs', cfg.ROMS_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + st_dic = kodi_new_status_dic() + options = check_MAME_DB_status(st_dic, MAME_MAIN_DB_BUILT, control_dic) + if kodi_display_status_message(st_dic): return False + + # { mname : size (int), ... } + m_size_dic = {} + for mname in db_dic['renderdb']: + roms = db_dic['roms'][mname] + ROMs_size = 0 + invalid_ROMs = False + for rom_dic in roms['roms']: + ROMs_size += rom_dic['size'] + # If total size of ROMs is 0 is because all ROMs are invalid. + # Do not add this machine to the dictionary. + if ROMs_size == 0: continue + m_size_dic[mname] = ROMs_size + + table_str = [] + table_str.append(['right', 'left', 'left', 'right']) + NUM_MACHINES = 512 + DESC_MAX_LENGTH = 64 + machine_i = 0 + sorted_machine_list = [] + if show_BIG: + table_str.append(['Short name', 'Flags', 'Long name', 'Size (MiB)']) + for mname in sorted(m_size_dic, key = lambda item: m_size_dic[item], reverse = True): + sorted_machine_list.append(mname) + machine_i += 1 + if machine_i >= NUM_MACHINES: break + for mname in sorted_machine_list: + table_str.append([mname, db_dic['assetdb'][mname]['flags'], + text_limit_string(db_dic['renderdb'][mname]['description'], DESC_MAX_LENGTH), + '{:7.2f}'.format(m_size_dic[mname] / 1024**2), + ]) + else: + table_str.append(['Short name', 'Flags', 'Long name', 'Size']) + for mname in sorted(m_size_dic, key = lambda item: m_size_dic[item], reverse = False): + sorted_machine_list.append(mname) + machine_i += 1 + if machine_i >= NUM_MACHINES: break + for mname in sorted_machine_list: + table_str.append([mname, db_dic['assetdb'][mname]['flags'], + text_limit_string(db_dic['renderdb'][mname]['description'], DESC_MAX_LENGTH), + '{:,d}'.format(m_size_dic[mname]), + ]) + + # --- Write report --- + slist = [] + table_str_list = text_render_table(table_str) + slist.extend(table_str_list) + if show_BIG: + window_title = 'MAME machines with biggest ROMs' + else: + window_title = 'MAME machines with smallest ROMs' + kodi_display_text_window_mono(window_title, '\n'.join(slist)) + + # Export MAME information in Billyc999 XML format to use with RCB. + elif which_utility == 'EXPORT_MAME_INFO_BILLYC999_XML': + log_info('command_exec_utility() Initialising EXPORT_MAME_INFO_BILLYC999_XML...') + dir_path = kodi_dialog_get_wdirectory('Chose directory to write MAME info XML') + if not dir_path: return + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['assetdb', 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + mame_write_MAME_ROM_Billyc999_XML(cfg, FileName(dir_path), db_dic) + + # Export a MAME ROM DAT XML file with Logiqx format. + # The DAT will be Merged, Split, Non-merged or Fully Non-merged same as the current + # AML database. + elif which_utility == 'EXPORT_MAME_ROM_DAT': + log_info('command_exec_utility() Initialising EXPORT_MAME_ROM_DAT...') + dir_path = kodi_dialog_get_wdirectory('Chose directory to write MAME ROMs DAT') + if not dir_path: return + + # Open databases. + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['audit_roms', 'MAME ROM Audit', cfg.ROM_AUDIT_DB_PATH.getPath()], + ['roms_sha1_dic', 'MAME ROMs SHA1 dictionary', cfg.SHA1_HASH_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # Write MAME ROM dat. Notifies the user if successful. + mame_write_MAME_ROM_XML_DAT(cfg, FileName(dir_path), db_dic) + + elif which_utility == 'EXPORT_MAME_CHD_DAT': + log_info('command_exec_utility() Initialising EXPORT_MAME_CHD_DAT ...') + dir_path = kodi_dialog_get_wdirectory('Chose directory to write MAME CHDs DAT') + if not dir_path: return + + # Open databases. + db_files = [ + ['control_dic', 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ['machines', 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + ['renderdb', 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + ['audit_roms', 'MAME ROM Audit', cfg.ROM_AUDIT_DB_PATH.getPath()], + ] + db_dic = db_load_files(db_files) + + # Write MAME ROM dat. Notifies the user if successful. + mame_write_MAME_CHD_XML_DAT(cfg, FileName(dir_path), db_dic) + + elif which_utility == 'EXPORT_SL_ROM_DAT': + log_info('command_exec_utility() Initialising EXPORT_SL_ROM_DAT ...') + kodi_dialog_OK('EXPORT_SL_ROM_DAT not implemented yet. Sorry.') + + elif which_utility == 'EXPORT_SL_CHD_DAT': + log_info('command_exec_utility() Initialising EXPORT_SL_CHD_DAT ...') + kodi_dialog_OK('EXPORT_SL_CHD_DAT not implemented yet. Sorry.') + + else: + u = 'Utility "{}" not found. This is a bug, please report it.'.format(which_utility) + log_error(u) + kodi_dialog_OK(u) + +# +# Execute view reports. +# +def command_exec_report(cfg, which_report): + log_debug('command_exec_report() which_report = "{}" starting ...'.format(which_report)) + + if which_report == 'VIEW_EXEC_OUTPUT': + if not cfg.MAME_OUTPUT_PATH.exists(): + kodi_dialog_OK('MAME output file not found. Execute MAME and try again.') + return + info_text = utils_load_file_to_str(cfg.MAME_OUTPUT_PATH.getPath()) + kodi_display_text_window_mono('MAME last execution output', info_text) + + # --- View database information and statistics stored in control dictionary ------------------ + elif which_report == 'VIEW_STATS_MAIN': + if not cfg.MAIN_CONTROL_PATH.exists(): + kodi_dialog_OK('MAME database not found. Please setup the addon first.') + return + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + XML_ctrl_dic = utils_load_JSON_file(cfg.MAME_XML_CONTROL_PATH.getPath()) + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + XML_ctrl_dic = utils_load_JSON_file(cfg.MAME_2003_PLUS_XML_CONTROL_PATH.getPath()) + else: + XML_ctrl_dic = db_new_MAME_XML_control_dic() + info_text = [] + mame_stats_main_print_slist(cfg, info_text, control_dic, XML_ctrl_dic) + kodi_display_text_window_mono('Database main statistics', '\n'.join(info_text)) + + elif which_report == 'VIEW_STATS_SCANNER': + if not cfg.MAIN_CONTROL_PATH.exists(): + kodi_dialog_OK('MAME database not found. Please setup the addon first.') + return + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + info_text = [] + mame_stats_scanner_print_slist(cfg, info_text, control_dic) + kodi_display_text_window_mono('Scanner statistics', '\n'.join(info_text)) + + elif which_report == 'VIEW_STATS_AUDIT': + if not cfg.MAIN_CONTROL_PATH.exists(): + kodi_dialog_OK('MAME database not found. Please setup the addon first.') + return + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + info_text = [] + mame_stats_audit_print_slist(cfg, info_text, control_dic) + kodi_display_text_window_mono('Database information and statistics', '\n'.join(info_text)) + + elif which_report == 'VIEW_STATS_TIMESTAMPS': + if not cfg.MAIN_CONTROL_PATH.exists(): + kodi_dialog_OK('MAME database not found. Please setup the addon first.') + return + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + info_text = [] + mame_stats_timestamps_slist(cfg, info_text, control_dic) + kodi_display_text_window_mono('Database information and statistics', '\n'.join(info_text)) + + # --- All statistics --- + elif which_report == 'VIEW_STATS_ALL': + if not cfg.MAIN_CONTROL_PATH.exists(): + kodi_dialog_OK('MAME database not found. Please setup the addon first.') + return + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + XML_ctrl_dic = utils_load_JSON_file(cfg.MAME_XML_CONTROL_PATH.getPath()) + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + XML_ctrl_dic = utils_load_JSON_file(cfg.MAME_2003_PLUS_XML_CONTROL_PATH.getPath()) + else: + XML_ctrl_dic = db_new_MAME_XML_control_dic() + + info_text = [] + mame_stats_main_print_slist(cfg, info_text, control_dic, XML_ctrl_dic) + info_text.append('') + mame_stats_scanner_print_slist(cfg, info_text, control_dic) + info_text.append('') + mame_stats_audit_print_slist(cfg, info_text, control_dic) + info_text.append('') + mame_stats_timestamps_slist(cfg, info_text, control_dic) + kodi_display_text_window_mono('Database full statistics', '\n'.join(info_text)) + + elif which_report == 'VIEW_STATS_WRITE_FILE': + if not cfg.MAIN_CONTROL_PATH.exists(): + kodi_dialog_OK('MAME database not found. Please setup the addon first.') + return + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + XML_ctrl_dic = utils_load_JSON_file(cfg.MAME_XML_CONTROL_PATH.getPath()) + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + XML_ctrl_dic = utils_load_JSON_file(cfg.MAME_2003_PLUS_XML_CONTROL_PATH.getPath()) + else: + XML_ctrl_dic = db_new_MAME_XML_control_dic() + + # --- Generate stats string and remove Kodi colours --- + info_text = [] + mame_stats_main_print_slist(cfg, info_text, control_dic, XML_ctrl_dic) + info_text.append('') + mame_stats_scanner_print_slist(cfg, info_text, control_dic) + info_text.append('') + mame_stats_audit_print_slist(cfg, info_text, control_dic) + info_text.append('') + mame_stats_timestamps_slist(cfg, info_text, control_dic) + + # --- Write file to disk and inform user --- + log_info('Writing AML statistics report...') + log_info('File "{}"'.format(cfg.REPORT_STATS_PATH.getPath())) + text_remove_color_tags_slist(info_text) + utils_write_slist_to_file(cfg.REPORT_STATS_PATH.getPath(), info_text) + kodi_notify('Exported AML statistics') + + # --- MAME scanner reports ------------------------------------------------------------------- + elif which_report == 'VIEW_SCANNER_MAME_ARCH_FULL': + if not cfg.REPORT_MAME_SCAN_MACHINE_ARCH_FULL_PATH.exists(): + kodi_dialog_OK('Full MAME machines archives scanner report not found. ' + 'Please scan MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_FULL_PATH.getPath()) + kodi_display_text_window_mono('Full MAME machines archives scanner report', fstring) + + elif which_report == 'VIEW_SCANNER_MAME_ARCH_HAVE': + if not cfg.REPORT_MAME_SCAN_MACHINE_ARCH_HAVE_PATH.exists(): + kodi_dialog_OK('Have MAME machines archives scanner report not found. ' + 'Please scan MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_HAVE_PATH.getPath()) + kodi_display_text_window_mono('Have MAME machines archives scanner report', fstring) + + elif which_report == 'VIEW_SCANNER_MAME_ARCH_MISS': + if not cfg.REPORT_MAME_SCAN_MACHINE_ARCH_MISS_PATH.exists(): + kodi_dialog_OK('Missing MAME machines archives scanner report not found. ' + 'Please scan MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_MISS_PATH.getPath()) + kodi_display_text_window_mono('Missing MAME machines archives scanner report', fstring) + + elif which_report == 'VIEW_SCANNER_MAME_ROM_LIST_MISS': + if not cfg.REPORT_MAME_SCAN_ROM_LIST_MISS_PATH.exists(): + kodi_dialog_OK('Missing MAME ROM ZIP list scanner report not found. ' + 'Please scan MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_SCAN_ROM_LIST_MISS_PATH.getPath()) + kodi_display_text_window_mono('Missing MAME ROM ZIP list scanner report', fstring) + + elif which_report == 'VIEW_SCANNER_MAME_SAM_LIST_MISS': + if not cfg.REPORT_MAME_SCAN_SAM_LIST_MISS_PATH.exists(): + kodi_dialog_OK('Missing MAME Sample ZIP list scanner report not found. ' + 'Please scan MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_SCAN_SAM_LIST_MISS_PATH.getPath()) + kodi_display_text_window_mono('Missing MAME Sample ZIP list scanner report', fstring) + + elif which_report == 'VIEW_SCANNER_MAME_CHD_LIST_MISS': + if not cfg.REPORT_MAME_SCAN_CHD_LIST_MISS_PATH.exists(): + kodi_dialog_OK('Missing MAME CHD list scanner report not found. ' + 'Please scan MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_SCAN_CHD_LIST_MISS_PATH.getPath()) + kodi_display_text_window_mono('Missing MAME CHD list scanner report', fstring) + + # --- SL scanner reports --------------------------------------------------------------------- + elif which_report == 'VIEW_SCANNER_SL_FULL': + if not cfg.REPORT_SL_SCAN_MACHINE_ARCH_FULL_PATH.exists(): + kodi_dialog_OK('Full Software Lists item archives scanner report not found. ' + 'Please scan SL ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_SCAN_MACHINE_ARCH_FULL_PATH.getPath()) + kodi_display_text_window_mono('Full Software Lists item archives scanner report', fstring) + + elif which_report == 'VIEW_SCANNER_SL_HAVE': + if not cfg.REPORT_SL_SCAN_MACHINE_ARCH_HAVE_PATH.exists(): + kodi_dialog_OK('Have Software Lists item archives scanner report not found. ' + 'Please scan SL ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_SCAN_MACHINE_ARCH_HAVE_PATH.getPath()) + kodi_display_text_window_mono('Have Software Lists item archives scanner report', fstring) + + elif which_report == 'VIEW_SCANNER_SL_MISS': + if not cfg.REPORT_SL_SCAN_MACHINE_ARCH_MISS_PATH.exists(): + kodi_dialog_OK('Missing Software Lists item archives scanner report not found. ' + 'Please scan SL ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_SCAN_MACHINE_ARCH_MISS_PATH.getPath()) + kodi_display_text_window_mono('Missing Software Lists item archives scanner report', fstring) + + # --- Asset scanner reports ------------------------------------------------------------------ + elif which_report == 'VIEW_SCANNER_MAME_ASSETS': + if not cfg.REPORT_MAME_ASSETS_PATH.exists(): + kodi_dialog_OK('MAME asset report report not found. ' + 'Please scan MAME assets and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_ASSETS_PATH.getPath()) + kodi_display_text_window_mono('MAME asset report', fstring) + + elif which_report == 'VIEW_SCANNER_SL_ASSETS': + if not cfg.REPORT_SL_ASSETS_PATH.exists(): + kodi_dialog_OK('Software Lists asset report not found. ' + 'Please scan Software List assets and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_ASSETS_PATH.getPath()) + kodi_display_text_window_mono('Software Lists asset report', fstring) + + # --- MAME audit reports --------------------------------------------------------------------- + elif which_report == 'VIEW_AUDIT_MAME_FULL': + if not cfg.REPORT_MAME_AUDIT_FULL_PATH.exists(): + kodi_dialog_OK('MAME audit report (Full) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_FULL_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (Full)', fstring) + + elif which_report == 'VIEW_AUDIT_MAME_GOOD': + if not cfg.REPORT_MAME_AUDIT_GOOD_PATH.exists(): + kodi_dialog_OK('MAME audit report (Good) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_GOOD_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (Good)', fstring) + + elif which_report == 'VIEW_AUDIT_MAME_BAD': + if not cfg.REPORT_MAME_AUDIT_ERRORS_PATH.exists(): + kodi_dialog_OK('MAME audit report (Errors) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_ERRORS_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (Errors)', fstring) + + elif which_report == 'VIEW_AUDIT_MAME_ROM_GOOD': + if not cfg.REPORT_MAME_AUDIT_ROM_GOOD_PATH.exists(): + kodi_dialog_OK('MAME audit report (ROMs Good) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_ROM_GOOD_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (ROMs Good)', fstring) + + elif which_report == 'VIEW_AUDIT_MAME_ROM_BAD': + if not cfg.REPORT_MAME_AUDIT_ROM_ERRORS_PATH.exists(): + kodi_dialog_OK('MAME audit report (ROM Errors) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_ROM_ERRORS_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (ROM Errors)', fstring) + + elif which_report == 'VIEW_AUDIT_MAME_SAM_GOOD': + if not cfg.REPORT_MAME_AUDIT_SAMPLES_GOOD_PATH.exists(): + kodi_dialog_OK('MAME audit report (Samples Good) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_SAMPLES_GOOD_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (Samples Good)', fstring) + + elif which_report == 'VIEW_AUDIT_MAME_SAM_BAD': + if not cfg.REPORT_MAME_AUDIT_SAMPLES_ERRORS_PATH.exists(): + kodi_dialog_OK('MAME audit report (Sample Errors) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_SAMPLES_ERRORS_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (Sample Errors)', fstring) + + elif which_report == 'VIEW_AUDIT_MAME_CHD_GOOD': + if not cfg.REPORT_MAME_AUDIT_CHD_GOOD_PATH.exists(): + kodi_dialog_OK('MAME audit report (CHDs Good) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_CHD_GOOD_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (CHDs Good)', fstring) + + elif which_report == 'VIEW_AUDIT_MAME_CHD_BAD': + if not cfg.REPORT_MAME_AUDIT_CHD_ERRORS_PATH.exists(): + kodi_dialog_OK('MAME audit report (CHD Errors) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_MAME_AUDIT_CHD_ERRORS_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (CHD Errors)', fstring) + + # --- SL audit reports ----------------------------------------------------------------------- + elif which_report == 'VIEW_AUDIT_SL_FULL': + if not cfg.REPORT_SL_AUDIT_FULL_PATH.exists(): + kodi_dialog_OK('SL audit report (Full) not found. ' + 'Please audit your SL ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_AUDIT_FULL_PATH.getPath()) + kodi_display_text_window_mono('SL audit report (Full)', fstring) + + elif which_report == 'VIEW_AUDIT_SL_GOOD': + if not cfg.REPORT_SL_AUDIT_GOOD_PATH.exists(): + kodi_dialog_OK('SL audit report (Good) not found. ' + 'Please audit your SL ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_AUDIT_GOOD_PATH.getPath()) + kodi_display_text_window_mono('SL audit report (Good)', fstring) + + elif which_report == 'VIEW_AUDIT_SL_BAD': + if not cfg.REPORT_SL_AUDIT_ERRORS_PATH.exists(): + kodi_dialog_OK('SL audit report (Errors) not found. ' + 'Please audit your SL ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_AUDIT_ERRORS_PATH.getPath()) + kodi_display_text_window_mono('SL audit report (Errors)', fstring) + + elif which_report == 'VIEW_AUDIT_SL_ROM_GOOD': + if not cfg.REPORT_SL_AUDIT_ROMS_GOOD_PATH.exists(): + kodi_dialog_OK('MAME audit report (ROM Good) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_AUDIT_ROMS_GOOD_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (ROM Good)', fstring) + + elif which_report == 'VIEW_AUDIT_SL_ROM_BAD': + if not cfg.REPORT_SL_AUDIT_ROMS_ERRORS_PATH.exists(): + kodi_dialog_OK('MAME audit report (ROM Errors) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_AUDIT_ROMS_ERRORS_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (ROM Errors)', fstring) + + elif which_report == 'VIEW_AUDIT_SL_CHD_GOOD': + if not cfg.REPORT_SL_AUDIT_CHDS_GOOD_PATH.exists(): + kodi_dialog_OK('MAME audit report (CHD Good) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_AUDIT_CHDS_GOOD_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (CHD Good)', fstring) + + elif which_report == 'VIEW_AUDIT_SL_CHD_BAD': + if not cfg.REPORT_SL_AUDIT_CHDS_ERRORS_PATH.exists(): + kodi_dialog_OK('MAME audit report (CHD Errors) not found. ' + 'Please audit your MAME ROMs and try again.') + return + fstring = utils_load_file_to_str(cfg.REPORT_SL_AUDIT_CHDS_ERRORS_PATH.getPath()) + kodi_display_text_window_mono('MAME audit report (CHD Errors)', fstring) + + # --- Error ---------------------------------------------------------------------------------- + else: + u = 'Report "{}" not found. This is a bug, please report it.'.format(which_report) + log_error(u) + kodi_dialog_OK(u) + +# +# Launch MAME machine. Syntax: $ mame <machine_name> [options] +# Example: $ mame dino +# +def run_machine(cfg, machine_name, location): + log_info('run_machine() Launching MAME machine "{}"'.format(machine_name)) + log_info('run_machine() Launching MAME location "{}"'.format(location)) + + # --- Load databases --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + if location == LOCATION_STANDARD: + log_debug('Reading info from hashed DBs') + machine = db_get_machine_main_hashed_db(cfg, machine_name) + assets = db_get_machine_assets_hashed_db(cfg, machine_name) + elif location == LOCATION_MAME_FAVS: + log_debug('Reading info from MAME Favourites') + fav_machines = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + machine = fav_machines[machine_name] + assets = machine['assets'] + elif location == LOCATION_MAME_MOST_PLAYED: + log_debug('Reading info from MAME Most Played DB') + most_played_roms_dic = utils_load_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath()) + machine = most_played_roms_dic[machine_name] + assets = machine['assets'] + elif location == LOCATION_MAME_RECENT_PLAYED: + log_debug('Reading info from MAME Recently Played DB') + recent_roms_list = utils_load_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), []) + machine_index = db_locate_idx_by_MAME_name(recent_roms_list, machine_name) + if machine_index < 0: + a = 'Machine {} cannot be located in Recently Played list. This is a bug.' + kodi_dialog_OK(a.format(machine_name)) + return + machine = recent_roms_list[machine_index] + assets = machine['assets'] + else: + kodi_dialog_OK('Unknown location = "{}". This is a bug, please report it.'.format(location)) + return + + # Check if ROM ZIP file exists. + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + rom_path = cfg.settings['rom_path_vanilla'] + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + rom_path = cfg.settings['rom_path_2003_plus'] + else: + raise TypeError('Unknown op_mode "{}"'.format(cfg.settings['op_mode'])) + if not rom_path: + kodi_dialog_OK('ROM directory not configured.') + return + ROM_path_FN = FileName(rom_path) + if not ROM_path_FN.isdir(): + kodi_dialog_OK('ROM directory does not exist.') + return + ROM_FN = ROM_path_FN.pjoin(machine_name + '.zip') + # if not ROM_FN.exists(): + # kodi_dialog_OK('ROM "{}" not found.'.format(ROM_FN.getBase())) + # return + + # Choose BIOS (only available for Favourite Machines). + # Not implemented at the moment + # if location and location == 'MAME_FAV' and len(machine['bios_name']) > 1: + # dialog = xbmcgui.Dialog() + # m_index = dialog.select('Select BIOS', machine['bios_desc']) + # if m_index < 0: return + # BIOS_name = machine['bios_name'][m_index] + # else: + # BIOS_name = '' + BIOS_name = '' + + # Launch machine using subprocess module. + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + mame_prog_FN = FileName(cfg.settings['mame_prog']) + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + mame_prog_FN = FileName(cfg.settings['retroarch_prog']) + else: + raise TypeError('Unknown op_mode "{}"'.format(cfg.settings['op_mode'])) + (mame_dir, mame_exec) = os.path.split(mame_prog_FN.getPath()) + log_debug('run_machine() mame_prog_FN "{}"'.format(mame_prog_FN.getPath())) + log_debug('run_machine() mame_dir "{}"'.format(mame_dir)) + log_debug('run_machine() mame_exec "{}"'.format(mame_exec)) + log_debug('run_machine() machine_name "{}"'.format(machine_name)) + log_debug('run_machine() BIOS_name "{}"'.format(BIOS_name)) + + # --- Compute ROM recently played list --- + # If the machine is already in the list remove it and place it on the first position. + MAX_RECENT_PLAYED_ROMS = 100 + recent_rom = db_get_MAME_Favourite_simple(machine_name, machine, assets, control_dic) + recent_roms_list = utils_load_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), []) + # Machine names are unique in this list. + recent_roms_list = [machine for machine in recent_roms_list if machine_name != machine['name']] + recent_roms_list.insert(0, recent_rom) + if len(recent_roms_list) > MAX_RECENT_PLAYED_ROMS: + log_debug('run_machine() len(recent_roms_list) = {}'.format(len(recent_roms_list))) + log_debug('run_machine() Trimming list to {} ROMs'.format(MAX_RECENT_PLAYED_ROMS)) + temp_list = recent_roms_list[:MAX_RECENT_PLAYED_ROMS] + recent_roms_list = temp_list + utils_write_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), recent_roms_list) + + # --- Compute most played ROM statistics --- + most_played_roms_dic = utils_load_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath()) + if recent_rom['name'] in most_played_roms_dic: + rom_name = recent_rom['name'] + most_played_roms_dic[rom_name]['launch_count'] += 1 + else: + # Add field launch_count to recent_rom to count how many times have been launched. + recent_rom['launch_count'] = 1 + most_played_roms_dic[recent_rom['name']] = recent_rom + utils_write_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath(), most_played_roms_dic) + + # --- Build final arguments to launch MAME --- + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + # arg_list = [mame_prog_FN.getPath(), '-window', machine_name] + arg_list = [mame_prog_FN.getPath(), machine_name] + if BIOS_name: arg_list.extend(['-bios', BIOS_name]) + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + if is_windows(): + core_path = os.path.join(cfg.settings['libretro_dir'], 'mame2003_plus_libretro.dll') + elif is_linux(): + core_path = os.path.join(cfg.settings['libretro_dir'], 'mame2003_plus_libretro.so') + else: + raise TypeError('Unsupported platform "{}"'.format(cached_sys_platform)) + machine_path = os.path.join(cfg.settings['rom_path_2003_plus'], machine_name + '.zip') + arg_list = [mame_prog_FN.getPath(), '-L', core_path, machine_path] + else: + raise TypeError('Unknown op_mode "{}"'.format(cfg.settings['op_mode'])) + log_info('arg_list = {}'.format(arg_list)) + + # --- User notification --- + if cfg.settings['display_launcher_notify']: + kodi_notify('Launching MAME machine "{}"'.format(machine_name)) + if DISABLE_MAME_LAUNCHING: + log_info('run_machine() MAME launching disabled. Exiting function.') + return + + # --- Run MAME --- + run_before_execution(cfg) + run_process(cfg, arg_list, mame_dir) + run_after_execution(cfg) + # Refresh list so Most Played and Recently played get updated. + log_info('run_machine() Exiting function.') + kodi_refresh_container() + +# +# Launch a SL machine. See http://docs.mamedev.org/usingmame/usingmame.html +# Complex syntax: $ mame <system> <media> <software> [options] +# Easy syntax: $ mame <system> <software> [options] +# Valid example: $ mame smspal -cart sonic +# +# Software list <part> tag has an 'interface' attribute that tells how to virtually plug the +# cartridge/cassete/disk/etc. into the MAME <device> with same 'interface' attribute. The +# <media> argument in the command line is the <device> <instance> 'name' attribute. +# +# Launching cases: +# A) Machine has only one device (defined by a <device> tag) with a valid <instance> and +# SL ROM has only one part (defined by a <part> tag). +# Valid examples:$ mame smspal -cart sonic +# Launch as: $ mame machine_name -part_attrib_name SL_ROM_name +# +# <device type="cartridge" tag="slot" interface="sms_cart"> +# <instance name="cartridge" briefname="cart"/> +# <extension name="bin"/> +# <extension name="sms"/> +# </device> +# <software name="sonic"> +# <part name="cart" interface="sms_cart"> +# <!-- PCB info based on SMS Power --> +# <feature name="pcb" value="171-5507" /> +# <feature name="ic1" value="MPR-14271-F" /> +# <dataarea name="rom" size="262144"> +# <rom name="mpr-14271-f.ic1" size="262144" crc="b519e833" sha1="6b9..." offset="000000" /> +# </dataarea> +# </part> +# </software> +# +# B) Machine has only one device with a valid <instance> and SL ROM has multiple parts. +# In this case, user should choose which part to plug. +# Currently not implemented and launch using easy syntax. +# Valid examples: +# Launch as: $ mame machine_name -part_attrib_name SL_ROM_name +# +# C) Machine has two or more devices with a valid <instance> and SL ROM has only one part. +# Traverse the machine devices until there is a match of the <part> interface attribute +# with the <machine> interface attribute. After the match is found, check also that +# SL ROM <part> name attribute matches with machine <device> <intance> briefname attribute. +# Valid examples: +# MSX2 cartridge vampkill (in msx2_cart.xml) with MSX machine. +# vampkill is also in msx_flop SL.xml. MSX2 machines always have two or more interfaces. +# $ mame hbf700p -cart vampkill +# Launch as: $ mame machine_name -part_attrib_name SL_ROM_name +# +# D) Machine has two or more devices with a valid <instance> and SL ROM has two or more parts. +# In this case it is not clear how to launch the machine. +# Not implemented and launch using easy syntax. +# +# Most common cases are A) and C). +# +def run_SL_machine(cfg, SL_name, SL_ROM_name, location): + SL_LAUNCH_WITH_MEDIA = 100 + SL_LAUNCH_NO_MEDIA = 200 + log_info('run_SL_machine() Launching SL machine (location = {}) ...'.format(location)) + log_info('run_SL_machine() SL_name "{}"'.format(SL_name)) + log_info('run_SL_machine() SL_ROM_name "{}"'.format(SL_ROM_name)) + + # --- Get paths --- + mame_prog_FN = FileName(cfg.settings['mame_prog']) + + # --- Get a list of launch machine <devices> and SL ROM <parts> --- + # --- Load SL ROMs and SL assets databases --- + control_dic = utils_load_JSON_file(cfg.MAIN_CONTROL_PATH.getPath()) + if location == LOCATION_STANDARD: + # >> Load DBs + log_info('run_SL_machine() SL ROM is in Standard Location') + SL_catalog_dic = utils_load_JSON_file(cfg.SL_INDEX_PATH.getPath()) + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_catalog_dic[SL_name]['rom_DB_noext'] + '_items.json') + log_info('run_SL_machine() SL ROMs JSON "{}"'.format(SL_DB_FN.getPath())) + SL_ROMs = utils_load_JSON_file(SL_DB_FN.getPath()) + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json') + SL_asset_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath()) + # >> Get ROM and assets + SL_fav_DB_key = SL_name + '-' + SL_ROM_name + SL_ROM = SL_ROMs[SL_ROM_name] + SL_assets = SL_asset_dic[SL_ROM_name] + part_list = SL_ROM['parts'] + # >> Launch machine + launch_machine_name = '' + launch_machine_desc = '' + elif location == LOCATION_SL_FAVS: + # >> Load DBs + log_info('run_SL_machine() SL ROM is in Favourites') + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + # >> Get ROM and assets + SL_fav_DB_key = SL_name + '-' + SL_ROM_name + SL_ROM = fav_SL_roms[SL_fav_DB_key] + SL_assets = SL_ROM['assets'] + part_list = fav_SL_roms[SL_fav_DB_key]['parts'] + # >> Launch machine + launch_machine_name = fav_SL_roms[SL_fav_DB_key]['launch_machine'] + launch_machine_desc = '[ Not available ]' + elif location == LOCATION_SL_MOST_PLAYED: + log_debug('Reading info from MAME Most Played DB') + most_played_roms_dic = utils_load_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath()) + SL_fav_DB_key = SL_name + '-' + SL_ROM_name + SL_ROM = most_played_roms_dic[SL_fav_DB_key] + SL_assets = SL_ROM['assets'] + part_list = most_played_roms_dic[SL_fav_DB_key]['parts'] + launch_machine_name = most_played_roms_dic[SL_fav_DB_key]['launch_machine'] + launch_machine_desc = '[ Not available ]' + elif location == LOCATION_SL_RECENT_PLAYED: + log_debug('Reading info from MAME Recently Played DB') + recent_roms_list = utils_load_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), []) + machine_index = db_locate_idx_by_SL_item_name(recent_roms_list, SL_name, SL_ROM_name) + if machine_index < 0: + a = 'SL Item {} cannot be located in Recently Played list. This is a bug.' + kodi_dialog_OK(a.format(SL_fav_DB_key)) + return + SL_fav_DB_key = SL_name + '-' + SL_ROM_name + SL_ROM = recent_roms_list[machine_index] + SL_assets = SL_ROM['assets'] + part_list = recent_roms_list[machine_index]['parts'] + launch_machine_name = recent_roms_list[machine_index]['launch_machine'] + launch_machine_desc = '[ Not available ]' + else: + kodi_dialog_OK('Unknown location = "{}". This is a bug, please report it.'.format(location)) + return + log_info('run_SL_machine() launch_machine_name = "{}"'.format(launch_machine_name)) + log_info('run_SL_machine() launch_machine_desc = "{}"'.format(launch_machine_desc)) + + # --- Load SL machines --- + SL_machines_dic = utils_load_JSON_file(cfg.SL_MACHINES_PATH.getPath()) + SL_machine_list = SL_machines_dic[SL_name] + if not launch_machine_name: + # >> Get a list of machines that can launch this SL ROM. User chooses in a select dialog + log_info('run_SL_machine() User selecting SL run machine ...') + SL_machine_names_list = [] + SL_machine_desc_list = [] + SL_machine_devices = [] + for SL_machine in sorted(SL_machine_list, key = lambda x: x['description'].lower()): + SL_machine_names_list.append(SL_machine['machine']) + SL_machine_desc_list.append(SL_machine['description']) + SL_machine_devices.append(SL_machine['devices']) + m_index = xbmcgui.Dialog().select('Select machine', SL_machine_desc_list) + if m_index < 0: return + launch_machine_name = SL_machine_names_list[m_index] + launch_machine_desc = SL_machine_desc_list[m_index] + launch_machine_devices = SL_machine_devices[m_index] + log_info('run_SL_machine() User chose machine "{}" ({})'.format(launch_machine_name, launch_machine_desc)) + else: + # >> User configured a machine to launch this SL item. Find the machine in the machine list. + log_info('run_SL_machine() Searching configured SL item running machine ...') + machine_found = False + for SL_machine in SL_machine_list: + if SL_machine['machine'] == launch_machine_name: + selected_SL_machine = SL_machine + machine_found = True + break + if machine_found: + log_info('run_SL_machine() Found machine "{}"'.format(launch_machine_name)) + launch_machine_desc = SL_machine['description'] + launch_machine_devices = SL_machine['devices'] + else: + log_error('run_SL_machine() Machine "{}" not found'.format(launch_machine_name)) + log_error('run_SL_machine() Aborting launch') + kodi_dialog_OK('Machine "{}" not found. Aborting launch.'.format(launch_machine_name)) + return + + # --- DEBUG --- + log_info('run_SL_machine() Machine "{}" has {} interfaces'.format(launch_machine_name, len(launch_machine_devices))) + log_info('run_SL_machine() SL ROM "{}" has {} parts'.format(SL_ROM_name, len(part_list))) + for device_dic in launch_machine_devices: + u = '<device type="{}" interface="{}">'.format(device_dic['att_type'], device_dic['att_interface']) + log_info(u) + for part_dic in part_list: + u = '<part name="{}" interface="{}">'.format(part_dic['name'], part_dic['interface']) + log_info(u) + + # --- Select media depending on SL launching case --- + num_machine_interfaces = len(launch_machine_devices) + num_SL_ROM_parts = len(part_list) + + # >> Error + if num_machine_interfaces == 0: + kodi_dialog_OK('Machine has no inferfaces! Aborting launch.') + return + if num_SL_ROM_parts == 0: + kodi_dialog_OK('SL ROM has no parts! Aborting launch.') + return + + # >> Case A + elif num_machine_interfaces == 1 and num_SL_ROM_parts == 1: + log_info('run_SL_machine() Launch case A)') + launch_case = SL_LAUNCH_CASE_A + media_name = launch_machine_devices[0]['instance']['name'] + sl_launch_mode = SL_LAUNCH_WITH_MEDIA + + # >> Case B + # User chooses media to launch? + elif num_machine_interfaces == 1 and num_SL_ROM_parts > 1: + log_info('run_SL_machine() Launch case B)') + launch_case = SL_LAUNCH_CASE_B + media_name = '' + sl_launch_mode = SL_LAUNCH_NO_MEDIA + + # >> Case C + elif num_machine_interfaces > 1 and num_SL_ROM_parts == 1: + log_info('run_SL_machine() Launch case C)') + launch_case = SL_LAUNCH_CASE_C + m_interface_found = False + for device in launch_machine_devices: + if device['att_interface'] == part_list[0]['interface']: + media_name = device['instance']['name'] + m_interface_found = True + break + if not m_interface_found: + kodi_dialog_OK('SL launch case C), not machine interface found! Aborting launch.') + return + log_info('run_SL_machine() Matched machine device interface "{}" '.format(device['att_interface']) + + 'to SL ROM part "{}"'.format(part_list[0]['interface'])) + sl_launch_mode = SL_LAUNCH_WITH_MEDIA + + # >> Case D. + # >> User chooses media to launch? + elif num_machine_interfaces > 1 and num_SL_ROM_parts > 1: + log_info('run_SL_machine() Launch case D)') + launch_case = SL_LAUNCH_CASE_D + media_name = '' + sl_launch_mode = SL_LAUNCH_NO_MEDIA + + else: + log_info(text_type(machine_interfaces)) + log_warning('run_SL_machine() Logical error in SL launch case.') + launch_case = SL_LAUNCH_CASE_ERROR + kodi_dialog_OK('Logical error in SL launch case. This is a bug, please report it.') + media_name = '' + sl_launch_mode = SL_LAUNCH_NO_MEDIA + + # >> Display some DEBUG information. + kodi_dialog_OK('Launch case {}. '.format(launch_case) + + 'Machine has {} device interface/s and '.format(num_machine_interfaces) + + 'SL ROM has {} part/s. '.format(num_SL_ROM_parts) + + 'Media name is "{}"'.format(media_name)) + + # --- Launch machine using subprocess module --- + (mame_dir, mame_exec) = os.path.split(mame_prog_FN.getPath()) + log_debug('run_SL_machine() mame_prog_FN "{}"'.format(mame_prog_FN.getPath())) + log_debug('run_SL_machine() mame_dir "{}"'.format(mame_dir)) + log_debug('run_SL_machine() mame_exec "{}"'.format(mame_exec)) + log_debug('run_SL_machine() launch_machine_name "{}"'.format(launch_machine_name)) + log_debug('run_SL_machine() launch_machine_desc "{}"'.format(launch_machine_desc)) + log_debug('run_SL_machine() media_name "{}"'.format(media_name)) + + # --- Compute ROM recently played list --- + # If the machine is already in the list remove it and place it on the first position. + MAX_RECENT_PLAYED_ROMS = 100 + recent_ROM = db_get_SL_Favourite(SL_name, SL_ROM_name, SL_ROM, SL_assets, control_dic) + recent_roms_list = utils_load_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), []) + # Machine names are unique in this list + recent_roms_list = [item for item in recent_roms_list if SL_fav_DB_key != item['SL_DB_key']] + recent_roms_list.insert(0, recent_ROM) + if len(recent_roms_list) > MAX_RECENT_PLAYED_ROMS: + log_debug('run_SL_machine() len(recent_roms_list) = {}'.format(len(recent_roms_list))) + log_debug('run_SL_machine() Trimming list to {} ROMs'.format(MAX_RECENT_PLAYED_ROMS)) + temp_list = recent_roms_list[:MAX_RECENT_PLAYED_ROMS] + recent_roms_list = temp_list + utils_write_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), recent_roms_list) + + # --- Compute most played ROM statistics --- + most_played_roms_dic = utils_load_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath()) + if SL_fav_DB_key in most_played_roms_dic: + most_played_roms_dic[SL_fav_DB_key]['launch_count'] += 1 + else: + # >> Add field launch_count to recent_ROM to count how many times have been launched. + recent_ROM['launch_count'] = 1 + most_played_roms_dic[SL_fav_DB_key] = recent_ROM + utils_write_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath(), most_played_roms_dic) + + # --- Build MAME arguments --- + if sl_launch_mode == SL_LAUNCH_WITH_MEDIA: + arg_list = [mame_prog_FN.getPath(), launch_machine_name, '-{}'.format(media_name), SL_ROM_name] + elif sl_launch_mode == SL_LAUNCH_NO_MEDIA: + arg_list = [mame_prog_FN.getPath(), launch_machine_name, '{}:{}'.format(SL_name, SL_ROM_name)] + else: + kodi_dialog_OK('Unknown sl_launch_mode = {}. This is a bug, please report it.'.format(sl_launch_mode)) + return + log_info('arg_list = {}'.format(arg_list)) + + # --- User notification --- + if cfg.settings['display_launcher_notify']: + kodi_notify('Launching MAME SL item "{}"'.format(SL_ROM_name)) + if DISABLE_MAME_LAUNCHING: + log_info('run_machine() MAME launching disabled. Exiting function.') + return + + # --- Run MAME --- + run_before_execution(cfg) + run_process(cfg, arg_list, mame_dir) + run_after_execution(cfg) + # Refresh list so Most Played and Recently played get updated. + kodi_refresh_container() + log_info('run_SL_machine() Exiting function.') + +def run_before_execution(cfg): + global g_flag_kodi_was_playing + global g_flag_kodi_audio_suspended + global g_flag_kodi_toggle_fullscreen + log_info('run_before_execution() Function BEGIN ...') + + # --- Stop/Pause Kodi mediaplayer if requested in settings --- + # id = "media_state_action" default = "0" values = "Stop|Pause|Keep playing" + g_flag_kodi_was_playing = False + media_state_action = cfg.settings['media_state_action'] + media_state_str = ['Stop', 'Pause', 'Keep playing'][media_state_action] + a = 'run_before_execution() media_state_action is "{}" ({})' + log_debug(a.format(media_state_str, media_state_action)) + kodi_is_playing = xbmc.getCondVisibility('Player.HasMedia') + if media_state_action == 0 and kodi_is_playing: + log_debug('run_before_execution() Executing built-in PlayerControl(stop)') + xbmc.executebuiltin('PlayerControl(stop)') + xbmc.sleep(100) + g_flag_kodi_was_playing = True + elif media_state_action == 1 and kodi_is_playing: + log_debug('run_before_execution() Executing built-in PlayerControl(pause)') + xbmc.executebuiltin('PlayerControl(pause)') + xbmc.sleep(100) + g_flag_kodi_was_playing = True + + # --- Force audio suspend if requested in "Settings" --> "Advanced" + # >> See http://forum.kodi.tv/showthread.php?tid=164522 + g_flag_kodi_audio_suspended = False + if cfg.settings['suspend_audio_engine']: + log_debug('run_before_execution() Suspending Kodi audio engine') + xbmc.audioSuspend() + xbmc.enableNavSounds(False) + xbmc.sleep(100) + g_flag_kodi_audio_suspended = True + else: + log_debug('run_before_execution() DO NOT suspend Kodi audio engine') + + # --- Force joystick suspend if requested in "Settings" --> "Advanced" + # NOT IMPLEMENTED YET. + # See https://forum.kodi.tv/showthread.php?tid=287826&pid=2627128#pid2627128 + # See https://forum.kodi.tv/showthread.php?tid=157499&pid=1722549&highlight=input.enablejoystick#pid1722549 + # See https://forum.kodi.tv/showthread.php?tid=313615 + + # --- Toggle Kodi windowed/fullscreen if requested --- + g_flag_kodi_toggle_fullscreen = False + if cfg.settings['toggle_window']: + log_debug('run_before_execution() Toggling Kodi from fullscreen to window') + kodi_toogle_fullscreen() + g_flag_kodi_toggle_fullscreen = True + else: + log_debug('run_before_execution() Toggling Kodi fullscreen/windowed DISABLED') + + # Disable screensaver + if cfg.settings['suspend_screensaver']: + kodi_disable_screensaver() + else: + screensaver_mode = kodi_get_screensaver_mode() + log_debug('run_before_execution() Screensaver status "{}"'.format(screensaver_mode)) + + # --- Pause Kodi execution some time --- + delay_tempo_ms = cfg.settings['delay_tempo'] + log_debug('run_before_execution() Pausing {} ms'.format(delay_tempo_ms)) + xbmc.sleep(delay_tempo_ms) + log_debug('run_before_execution() function ENDS') + +def run_process(cfg, arg_list, mame_dir): + log_info('run_process() Function BEGIN...') + + # --- Prevent a console window to be shown in Windows. Not working yet! --- + if is_windows(): + log_info('run_process() Platform is win32. Creating _info structure') + _info = subprocess.STARTUPINFO() + _info.dwFlags = subprocess.STARTF_USESHOWWINDOW + # See https://msdn.microsoft.com/en-us/library/ms633548(v=vs.85).aspx + # See https://docs.python.org/2/library/subprocess.html#subprocess.STARTUPINFO + # SW_HIDE = 0 + # Does not work: MAME console window is not shown, graphical window not shown either, + # process run in background. + # _info.wShowWindow = subprocess.SW_HIDE + # SW_SHOWMINIMIZED = 2 + # Both MAME console and graphical window minimized. + # _info.wShowWindow = 2 + # SW_SHOWNORMAL = 1 + # MAME console window is shown, MAME graphical window on top, Kodi on bottom. + _info.wShowWindow = 1 + elif is_linux(): + log_info('run_process() _info is None') + _info = None + else: + raise TypeError('Unsupported platform "{}"'.format(cached_sys_platform)) + + # --- Run MAME --- + f = io.open(cfg.MAME_OUTPUT_PATH.getPath(), 'wb') + p = subprocess.Popen(arg_list, cwd = mame_dir, startupinfo = _info, stdout = f, stderr = subprocess.STDOUT) + p.wait() + f.close() + log_debug('run_process() function ENDS') + +def run_after_execution(cfg): + log_info('run_after_execution() Function BEGIN ...') + + # --- Stop Kodi some time --- + delay_tempo_ms = cfg.settings['delay_tempo'] + log_debug('run_after_execution() Pausing {} ms'.format(delay_tempo_ms)) + xbmc.sleep(delay_tempo_ms) + + # --- Toggle Kodi windowed/fullscreen if requested --- + if g_flag_kodi_toggle_fullscreen: + log_debug('run_after_execution() Toggling Kodi fullscreen') + kodi_toogle_fullscreen() + else: + log_debug('run_after_execution() Toggling Kodi fullscreen DISABLED') + + # --- Resume joystick engine if it was suspended --- + # NOT IMPLEMENTED + + # --- Resume audio engine if it was suspended --- + # Calling xmbc.audioResume() takes a loong time (2/4 secs) if audio was not properly suspended! + # Also produces this in Kodi's log: + # WARNING: CActiveAE::StateMachine - signal: 0 from port: OutputControlPort not handled for state: 7 + # ERROR: ActiveAE::Resume - failed to init + if g_flag_kodi_audio_suspended: + log_debug('run_after_execution() Kodi audio engine was suspended before launching') + log_debug('run_after_execution() Resuming Kodi audio engine') + xbmc.audioResume() + xbmc.enableNavSounds(True) + xbmc.sleep(100) + else: + log_debug('run_after_execution() DO NOT resume Kodi audio engine') + + # Restore screensaver status. + if cfg.settings['suspend_screensaver']: + kodi_restore_screensaver() + else: + screensaver_mode = kodi_get_screensaver_mode() + log_debug('run_after_execution() Screensaver status "{}"'.format(screensaver_mode)) + + # --- Resume Kodi playing if it was paused. If it was stopped, keep it stopped. --- + # >> id="media_state_action" default="0" values="Stop|Pause|Keep playing" + media_state_action = cfg.settings['media_state_action'] + media_state_str = ['Stop', 'Pause', 'Keep playing'][media_state_action] + a = 'run_after_execution() media_state_action is "{}" ({})' + log_debug(a.format(media_state_str, media_state_action)) + log_debug('run_after_execution() g_flag_kodi_was_playing is {}'.format(g_flag_kodi_was_playing)) + if g_flag_kodi_was_playing and media_state_action == 1: + log_debug('run_after_execution() Executing built-in PlayerControl(play)') + # When Kodi is in "pause" mode, resume is used to continue play. + xbmc.executebuiltin('PlayerControl(resume)') + log_debug('run_after_execution() Function ENDS') diff --git a/plugin.program.AML/resources/mame.py b/plugin.program.AML/resources/mame.py new file mode 100644 index 0000000000..331100fd80 --- /dev/null +++ b/plugin.program.AML/resources/mame.py @@ -0,0 +1,8354 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher MAME specific stuff. + +# --- AEL packages --- +from .constants import * +from .utils import * +from .misc import * +from .db import * +from .filters import * +from .mame_misc import * + +# --- Kodi modules --- +import xbmcgui + +# --- Python standard library --- +import binascii +import struct +import xml.etree.ElementTree as ET +import zipfile as z + +# ------------------------------------------------------------------------------------------------- +# Data structures +# ------------------------------------------------------------------------------------------------- +# Substitute notable drivers with a proper name +# Drivers are located in https://github.com/mamedev/mame/blob/master/src/mame/drivers/<driver_name>.cpp +mame_driver_better_name_dic = { + # --- Atari --- + 'atari_s1.cpp' : 'Atari Generation/System 1', + 'atari_s2.cpp' : 'Atari Generation/System 2 and 3', + 'atarifb.cpp' : 'Atari Football hardware', + 'atarittl.cpp' : 'Atari / Kee Games Driver', + 'asteroid.cpp' : 'Atari Asteroids hardware', + 'atetris.cpp' : 'Atari Tetris hardware', + 'avalnche.cpp' : 'Atari Avalanche hardware', + 'bzone.cpp' : 'Atari Battlezone hardware', + 'bwidow.cpp' : 'Atari Black Widow hardware', + 'boxer.cpp' : 'Atari Boxer (prototype) driver', + 'canyon.cpp' : 'Atari Canyon Bomber hardware', + 'cball.cpp' : 'Atari Cannonball (prototype) driver', + 'ccastles.cpp' : 'Atari Crystal Castles hardware', + 'centiped.cpp' : 'Atari Centipede hardware', + 'cloak.cpp' : 'Atari Cloak & Dagger hardware', + 'destroyr.cpp' : 'Atari Destroyer driver', + 'mhavoc.cpp' : 'Atari Major Havoc hardware', + 'mgolf.cpp' : 'Atari Mini Golf (prototype) driver', + 'pong.cpp' : 'Atari Pong hardware', + + # --- Capcom --- + '1942.cpp' : 'Capcom 1942', + '1943.cpp' : 'Capcom 1943: The Battle of Midway', + 'capcom.cpp' : 'Capcom A0015405', + 'gng.cpp' : "Capcom Ghosts'n Goblins", + 'cps1.cpp' : 'Capcom Play System 1', + 'cps2.cpp' : 'Capcom Play System 2', + 'cps3.cpp' : 'Capcom Play System 3', + + # --- Konami --- + '88games.cpp' : 'Konami 88 Games', + 'ajax.cpp' : 'Konami GX770', + 'aliens.cpp' : 'Konami Aliens', + 'asterix.cpp' : 'Konami Asterix', + 'konamigv.cpp' : 'Konami GV System (PSX Hardware)', + 'konblands.cpp' : 'Konami GX455 - Konami Badlands', + 'konamigx.cpp' : 'Konami System GX', + 'konamim2.cpp' : 'Konami M2 Hardware', + + # --- Midway --- + 'midtunit.cpp' : 'Midway T-unit system', + 'midvunit.cpp' : 'Midway V-Unit games', + 'midwunit.cpp' : 'Midway Wolf-unit system', + 'midxunit.cpp' : 'Midway X-unit system', + 'midyunit.cpp' : 'Williams/Midway Y/Z-unit system', + 'midzeus.cpp' : 'Midway Zeus games', + + # --- Namco --- + 'galaxian.cpp' : 'Namco Galaxian-derived hardware', + 'namcops2.cpp' : 'Namco System 246 / System 256 (Sony PS2 based)', + + # --- SNK --- + 'neodriv.hxx' : 'SNK NeoGeo AES', + 'neogeo.cpp' : 'SNK NeoGeo MVS', + + # --- Misc important drivers (important enough to have a fancy name!) --- + 'seta.cpp' : 'Seta Hardware', + + # --- SEGA --- + # Lesser known boards + 'segajw.cpp' : 'SEGA GOLDEN POKER SERIES', + 'segam1.cpp' : 'SEGA M1 hardware', + 'segaufo.cpp' : 'SEGA UFO Catcher, Z80 type hardware', + # Boards listed in wikipedia + # Sega Z80 board is included in galaxian.cpp + 'vicdual.cpp' : 'SEGA VIC Dual Game board', + 'segag80r.cpp' : 'SEGA G-80 raster hardware', + 'segag80v.cpp' : 'SEGA G-80 vector hardware', + 'zaxxon.cpp' : 'SEGA Zaxxon hardware', + 'segald.cpp' : 'SEGA LaserDisc Hardware', + 'system1.cpp' : 'SEGA System1 / System 2', + 'segac2.cpp' : 'SEGA System C (System 14)', + 'segae.cpp' : 'SEGA System E', + 'segas16a.cpp' : 'SEGA System 16A', + 'segas16b.cpp' : 'SEGA System 16B', + 'system16.cpp' : 'SEGA System 16 / 18 bootlegs', + 'segas24.cpp' : 'SEGA System 24', + 'segas18.cpp' : 'SEGA System 18', + 'kyugo.cpp' : 'SEGA Kyugo Hardware', + 'segahang.cpp' : 'SEGA Hang On hardware', # AKA Sega Space Harrier + 'segaorun.cpp' : 'SEGA Out Run hardware', + 'segaxbd.cpp' : 'SEGA X-board', + 'segaybd.cpp' : 'SEGA Y-board', + 'segas32.cpp' : 'SEGA System 32', + 'model1.cpp' : 'SEGA Model 1', + 'model2.cpp' : 'SEGA Model 2', + 'model3.cpp' : 'SEGA Model 3', + 'stv.cpp' : 'SEGA ST-V hardware', + 'naomi.cpp' : 'SEGA Naomi / Naomi 2 / Atomiswave', + 'segasp.cpp' : 'SEGA System SP (Spider)', # Naomi derived + 'chihiro.cpp' : 'SEGA Chihiro (Xbox-based)', + 'triforce.cpp' : 'SEGA Triforce Hardware', + 'lindbergh.cpp' : 'SEGA Lindbergh', + + # --- Taito --- + # Ordered alphabetically + 'taito_b.cpp' : 'Taito B System', + 'taito_f2.cpp' : 'Taito F2 System', + 'taito_f3.cpp' : 'Taito F3 System', + 'taito_h.cpp' : 'Taito H system', + 'taito_l.cpp' : 'Taito L System', + 'taito_o.cpp' : 'Taito O system (Gambling)', + 'taito_x.cpp' : 'Taito X system', + 'taito_z.cpp' : 'Taito Z System (twin 68K with optional Z80)', + 'taitoair.cpp' : 'Taito Air System', + 'taitogn.cpp' : 'Taito GNET Motherboard', + 'taitojc.cpp' : 'Taito JC System', + 'taitopjc.cpp' : 'Taito Power-JC System', + 'taitosj.cpp' : 'Taito SJ system', + 'taitottl.cpp' : 'Taito Discrete Hardware Games', + 'taitotz.cpp' : 'Taito Type-Zero hardware', + 'taitowlf.cpp' : 'Taito Wolf System', + + # --- SONY --- + 'zn.cpp' : 'Sony ZN1/ZN2 (Arcade PSX)', +} + +# Some Software Lists don't follow the convention of adding the company name at the beginning. +# I will try to create pull requests to fix theses and if the PRs are not accepted then +# SL names will be changed using the data here. +# Develop a test script to check wheter this substitutsion are used or not. +SL_better_name_dic = { + 'Amiga AGA disk images' : 'Commodore Amiga AGA disk images', + 'Amiga CD-32 CD-ROMs' : 'Commodore Amiga CD-32 CD-ROMs', + 'Amiga CDTV CD-ROMs' : 'Commodore Amiga CDTV CD-ROMs', + 'Amiga ECS disk images' : 'Commodore Amiga ECS disk images', + 'Amiga OCS disk images' : 'Commodore Amiga OCS disk images', + 'CC-40 cartridges' : 'Texas Instruments CC-40 cartridges', + 'CD-i CD-ROMs' : 'Philips/Sony CD-i CD-ROMs', + 'COMX-35 diskettes' : 'COMX COMX-35 diskettes', + 'EPSON PX-4 ROM capsules' : 'Epson PX-4 ROM capsules', + 'EPSON PX-8 ROM capsules' : 'Epson PX-8 ROM capsules', + 'IQ-151 cartridges' : 'ZPA Nový Bor IQ-151 cartridges', + 'IQ-151 disk images' : 'ZPA Nový Bor IQ-151 disk images', + 'Mac Harddisks' : 'Apple Mac Harddisks', + 'Macintosh 400K/800K Disk images' : 'Apple Macintosh 400K/800K Disk images', + 'Macintosh High Density Disk images' : 'Apple Macintosh High Density Disk images', + 'MC-1502 disk images' : 'Elektronika MC-1502 disk images', + 'MD-2 disk images' : 'Morrow Micro Decision MD-2 disk images', + 'Mega CD (Euro) CD-ROMs' : 'Sega Mega CD (Euro) CD-ROMs', + 'Mega CD (Jpn) CD-ROMs' : 'Sega Mega CD (Jpn) CD-ROMs', + 'MZ-2000 cassettes' : 'Sharp MZ-2000 cassettes', + 'MZ-2000 disk images' : 'Sharp MZ-2000 disk images', + 'MZ-2500 disk images' : 'Sharp MZ-2500 disk images', + 'Pippin CD-ROMs' : 'Apple/Bandai Pippin CD-ROMs', + 'Pippin disk images' : 'Apple/Bandai Pippin disk images', + 'SEGA Computer 3000 cartridges' : 'Sega Computer 3000 cartridges', + 'SEGA Computer 3000 cassettes' : 'Sega Computer 3000 cassettes', + 'Z88 ROM cartridges' : 'Cambridge Computer Z88 ROM cartridges', + 'ZX80 cassettes' : 'Sinclair ZX80 cassettes', + 'ZX81 cassettes' : 'Sinclair ZX81 cassettes', + 'ZX Spectrum +3 disk images' : 'Sinclair ZX Spectrum +3 disk images', + 'ZX Spectrum Beta Disc / TR-DOS disk images' : 'Sinclair ZX Spectrum Beta Disc / TR-DOS disk images', +} + +# +# Numerical MAME version. Allows for comparisons like ver_mame >= MAME_VERSION_0190 +# Support MAME versions higher than 0.53 August 12th 2001. +# See header of MAMEINFO.dat for a list of all MAME versions. +# +# M.mmm.Xbb +# | | | |-> Beta flag 0, 1, ..., 99 +# | | |---> Release kind flag +# | | 5 for non-beta, non-alpha, non RC versions. +# | | 2 for RC versions +# | | 1 for beta versions +# | | 0 for alpha versions +# | |-----> Minor version 0, 1, ..., 999 +# |---------> Major version 0, ..., infinity +# +# See https://retropie.org.uk/docs/MAME/ +# See https://www.mamedev.org/oldrel.html +# +# Examples: +# '0.37b5' -> 37105 (mame4all-pi, lr-mame2000 released 27 Jul 2000) +# '0.37b16' -> 37116 (Last unconsistent MAME version, released 02 Jul 2001) +# '0.53' -> 53500 (MAME versioning is consistent from this release, released 12 Aug 2001) +# '0.78' -> 78500 (lr-mame2003, lr-mame2003-plus) +# '0.139' -> 139500 (lr-mame2010) +# '0.160' -> 160500 (lr-mame2015) +# '0.174' -> 174500 (lr-mame2016) +# '0.206' -> 206500 +# +# mame_version_raw examples: +# a) '0.194 (mame0194)' from '<mame build="0.194 (mame0194)" debug="no" mameconfig="10">' +# +# re.search() returns a MatchObject https://docs.python.org/2/library/re.html#re.MatchObject +def mame_get_numerical_version(mame_version_str): + log_debug('mame_get_numerical_version() mame_version_str = "{}"'.format(mame_version_str)) + mame_version_int = 0 + # Search for old version scheme x.yyybzz + m_obj_old = re.search('^(\d+)\.(\d+)b(\d+)', mame_version_str) + # Search for modern, consistent versioning system x.yyy + m_obj_modern = re.search('^(\d+)\.(\d+)', mame_version_str) + if m_obj_old: + major = int(m_obj_old.group(1)) + minor = int(m_obj_old.group(2)) + beta = int(m_obj_old.group(3)) + release_flag = 1 + # log_debug('mame_get_numerical_version() major = {}'.format(major)) + # log_debug('mame_get_numerical_version() minor = {}'.format(minor)) + # log_debug('mame_get_numerical_version() beta = {}'.format(beta)) + mame_version_int = major * 1000000 + minor * 1000 + release_flag * 100 + beta + elif m_obj_modern: + major = int(m_obj_modern.group(1)) + minor = int(m_obj_modern.group(2)) + release_flag = 5 + # log_debug('mame_get_numerical_version() major = {}'.format(major)) + # log_debug('mame_get_numerical_version() minor = {}'.format(minor)) + mame_version_int = major * 1000000 + minor * 1000 + release_flag * 100 + else: + t = 'MAME version "{}" cannot be parsed.'.format(mame_version_str) + log_error(t) + raise TypeError(t) + log_debug('mame_get_numerical_version() mame_version_int = {}'.format(mame_version_int)) + + return mame_version_int + +# Returns a string like '0.224 (mame0224)'. +def mame_get_MAME_exe_version(cfg, mame_prog_FN): + (mame_dir, mame_exec) = os.path.split(mame_prog_FN.getPath()) + log_info('mame_get_MAME_exe_version() mame_prog_FN "{}"'.format(mame_prog_FN.getPath())) + # log_info('mame_get_MAME_exe_version() mame_dir "{}"'.format(mame_dir)) + # log_info('mame_get_MAME_exe_version() mame_exec "{}"'.format(mame_exec)) + stdout_f = cfg.MAME_STDOUT_VER_PATH.getPath() + err_f = cfg.MAME_STDERR_VER_PATH.getPath() + with io.open(stdout_f, 'wb') as out, io.open(err_f, 'wb') as err: + p = subprocess.Popen([mame_prog_FN.getPath(), '-version'], stdout = out, stderr = err, cwd = mame_dir) + p.wait() + + # Read MAME version. + lines = utils_load_file_to_slist(cfg.MAME_STDOUT_VER_PATH.getPath()) + # log_debug('mame_get_MAME_exe_version() Number of lines {}'.format(len(lines))) + version_str = lines[0] + # version_str = '' + # for line in lines: + # m = re.search('^([0-9\.]+?) \(([a-z0-9]+?)\)$', line.strip()) + # if m: + # version_str = m.group(1) + # break + # log_debug('mame_get_MAME_exe_version() Returning "{}"'.format(version_str)) + + return version_str + +# Counts MAME machines in MAME XML file. +def mame_count_MAME_machines(XML_path_FN): + log_debug('mame_count_MAME_machines_modern() BEGIN...') + log_debug('XML "{}"'.format(XML_path_FN.getPath())) + num_machines_modern = 0 + num_machines_legacy = 0 + with io.open(XML_path_FN.getPath(), 'rt', encoding = 'utf-8') as f: + for line in f: + if line.find('<machine name=') > 0: + num_machines_modern += 1 + continue + if line.find('<game name=') > 0: + num_machines_legacy += 1 + continue + if num_machines_modern and num_machines_legacy: + log_error('num_machines_modern = {}'.format(num_machines_modern)) + log_error('num_machines_legacy = {}'.format(num_machines_legacy)) + log_error('Both cannot be > 0!') + raise TypeError + num_machines = num_machines_modern if num_machines_modern > num_machines_legacy else num_machines_legacy + + return num_machines + +# 1) Extracts MAME XML. +# 2) Counts number of MAME machines. +# 3) Gets MAME version from the XML file. +# 4) Creates MAME XML control file. +def mame_extract_MAME_XML(cfg, st_dic): + pDialog = KodiProgressDialog() + + # Extract XML from MAME executable. + mame_prog_FN = FileName(cfg.settings['mame_prog']) + (mame_dir, mame_exec) = os.path.split(mame_prog_FN.getPath()) + log_info('mame_extract_MAME_XML() mame_prog_FN "{}"'.format(mame_prog_FN.getPath())) + log_info('mame_extract_MAME_XML() Saving XML "{}"'.format(cfg.MAME_XML_PATH.getPath())) + log_info('mame_extract_MAME_XML() mame_dir "{}"'.format(mame_dir)) + log_info('mame_extract_MAME_XML() mame_exec "{}"'.format(mame_exec)) + pDialog.startProgress('Extracting MAME XML database. Progress bar is not accurate.') + XML_path_FN = cfg.MAME_XML_PATH + with io.open(XML_path_FN.getPath(), 'wb') as out, io.open(cfg.MAME_STDERR_PATH.getPath(), 'wb') as err: + p = subprocess.Popen([mame_prog_FN.getPath(), '-listxml'], stdout = out, stderr = err, cwd = mame_dir) + count = 0 + while p.poll() is None: + time.sleep(1) + count += 1 + pDialog.updateProgress(count) + pDialog.endProgress() + time_extracting = time.time() + + # Count number of machines. Useful for later progress dialogs and statistics. + log_info('mame_extract_MAME_XML() Counting number of machines ...') + pDialog.startProgress('Counting number of MAME machines...') + total_machines = mame_count_MAME_machines(cfg.MAME_XML_PATH) + pDialog.endProgress() + log_info('mame_extract_MAME_XML() Found {} machines.'.format(total_machines)) + + # Get XML file stat info. + # See https://docs.python.org/3/library/os.html#os.stat_result + statinfo = os.stat(XML_path_FN.getPath()) + + # Get MAME version from the XML. + xml_f = io.open(XML_path_FN.getPath(), 'rt', encoding = 'utf-8') + xml_iter = ET.iterparse(xml_f, events = ("start", "end")) + event, root = next(xml_iter) + xml_f.close() + ver_mame_str = root.attrib['build'] + ver_mame_int = mame_get_numerical_version(ver_mame_str) + + # Create the MAME XML control file. Only change used fields. + XML_control_dic = db_new_MAME_XML_control_dic() + db_safe_edit(XML_control_dic, 't_XML_extraction', time_extracting) + db_safe_edit(XML_control_dic, 't_XML_preprocessing', time.time()) + db_safe_edit(XML_control_dic, 'total_machines', total_machines) + db_safe_edit(XML_control_dic, 'st_size', statinfo.st_size) + db_safe_edit(XML_control_dic, 'st_mtime', statinfo.st_mtime) + db_safe_edit(XML_control_dic, 'ver_mame_int', ver_mame_int) + db_safe_edit(XML_control_dic, 'ver_mame_str', ver_mame_str) + utils_write_JSON_file(cfg.MAME_XML_CONTROL_PATH.getPath(), XML_control_dic, verbose = True) + +# 1) Counts number of MAME machines +# 2) Creates MAME XML control file. +def mame_preprocess_RETRO_MAME2003PLUS(cfg, st_dic): + pDialog = KodiProgressDialog() + + # In MAME 2003 Plus MAME XML is already extracted. + XML_path_FN = FileName(cfg.settings['xml_2003_path']) + + # Count number of machines. Useful for later progress dialogs and statistics. + log_info('mame_process_RETRO_MAME2003PLUS() Counting number of machines ...') + pDialog.startProgress('Counting number of MAME machines...') + total_machines = mame_count_MAME_machines(XML_path_FN) + pDialog.endProgress() + log_info('mame_process_RETRO_MAME2003PLUS() Found {} machines.'.format(total_machines)) + + # Get XML file stat info. + # See https://docs.python.org/3/library/os.html#os.stat_result + statinfo = os.stat(XML_path_FN.getPath()) + + # Get MAME version from the XML (although we know is MAME 2003 Plus). + # In MAME 2003 Plus the MAME version is not in the XML file. + ver_mame_str = MAME2003PLUS_VERSION_RAW + ver_mame_int = mame_get_numerical_version(ver_mame_str) + + # Create the MAME XML control file. Only change used fields. + XML_control_dic = db_new_MAME_XML_control_dic() + db_safe_edit(XML_control_dic, 't_XML_preprocessing', time.time()) + db_safe_edit(XML_control_dic, 'total_machines', total_machines) + db_safe_edit(XML_control_dic, 'st_size', statinfo.st_size) + db_safe_edit(XML_control_dic, 'st_mtime', statinfo.st_mtime) + db_safe_edit(XML_control_dic, 'ver_mame_int', ver_mame_int) + db_safe_edit(XML_control_dic, 'ver_mame_str', ver_mame_str) + utils_write_JSON_file(cfg.MAME_2003_PLUS_XML_CONTROL_PATH.getPath(), XML_control_dic, verbose = True) + +# After this function of code we have: +# 1) Valid and verified for existence MAME_XML_path. +# 2) A valid XML_control_dic and the XML control file is created and/or current. +# +# Returns tuple (MAME_XML_path [FileName object], XML_control_FN [FileName object]) +def mame_init_MAME_XML(cfg, st_dic, force_rebuild = False): + log_info('mame_init_MAME_XML() Beginning extract/process of MAME.xml...') + if cfg.settings['op_mode'] == OP_MODE_VANILLA and force_rebuild: + log_info('Forcing rebuilding of Vanilla MAME XML.') + MAME_XML_path = cfg.MAME_XML_PATH + XML_control_FN = cfg.MAME_XML_CONTROL_PATH + # Extract, count number of machines and create XML control file. + mame_extract_MAME_XML(cfg, st_dic) + if st_dic['abort']: return + elif cfg.settings['op_mode'] == OP_MODE_VANILLA and not force_rebuild: + process_XML_flag = False + MAME_exe_path = FileName(cfg.settings['mame_prog']) + MAME_XML_path = cfg.MAME_XML_PATH + XML_control_FN = cfg.MAME_XML_CONTROL_PATH + # Check that MAME executable exists. + if not cfg.settings['mame_prog']: + log_info('Vanilla MAME executable path is not set. Aborting.') + kodi_set_error_status(st_dic, 'Vanilla MAME executable path is not set.') + return + if not MAME_exe_path.exists(): + log_info('Vanilla MAME executable file not found. Aborting.') + kodi_set_error_status(st_dic, 'Vanilla MAME executable file not found.') + return + log_info('Vanilla MAME executable found.') + # Check that extracted MAME XML exists. + # In Vanilla MAME the XML file is extracted from the executable. + if MAME_XML_path.exists(): + log_info('Vanilla MAME XML file found.') + # Check that the XML control file exists. + if XML_control_FN.exists(): + # Open the XML control file and check if the current version of the MAME executable + # is the same as in the XML control file. + # If so reset everything, if not use the cached information in the XML control file. + log_info('Vanilla MAME XML control file found.') + XML_control_dic = utils_load_JSON_file(XML_control_FN.getPath()) + mame_exe_version_str = mame_get_MAME_exe_version(cfg, MAME_exe_path) + log_debug('XML_control_dic["ver_mame_str"] "{}"'.format(XML_control_dic['ver_mame_str'])) + log_debug('mame_exe_version_str "{}"'.format(mame_exe_version_str)) + if mame_exe_version_str != XML_control_dic['ver_mame_str']: + log_info('Vanilla MAME version is different from the version in the XML control file. ' + 'Forcing new preprocessing.') + process_XML_flag = True + else: + log_info('XML control file up to date.') + process_XML_flag = False + else: + log_info('XML control file NOT found. Forcing XML preprocessing.') + process_XML_flag = True + else: + log_info('Vanilla MAME XML file NOT found. Forcing XML preprocessing.') + process_XML_flag = True + # Only process MAME XML if needed. + if process_XML_flag: + # Extract, count number of machines and create XML control file. + mame_extract_MAME_XML(cfg, st_dic) + if st_dic['abort']: return + else: + log_info('Reusing previosly preprocessed Vanilla MAME XML.') + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS and force_rebuild: + log_info('Forcing rebuilding of MAME 2003 Plus XML.') + MAME_XML_path = FileName(cfg.settings['xml_2003_path']) + XML_control_FN = cfg.MAME_2003_PLUS_XML_CONTROL_PATH + # Count number of machines and create XML control file. + mame_preprocess_RETRO_MAME2003PLUS(cfg, st_dic) + if st_dic['abort']: return + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS and not force_rebuild: + process_XML_flag = False + MAME_XML_path = FileName(cfg.settings['xml_2003_path']) + XML_control_FN = cfg.MAME_2003_PLUS_XML_CONTROL_PATH + # Check that MAME 2003 Plus XML exists. + if not cfg.settings['xml_2003_path']: + log_info('MAME 2003 Plus XML path is not set. Aborting.') + kodi_set_error_status(st_dic, 'MAME 2003 Plus XML path is not set.') + return + if not MAME_XML_path.exists(): + log_info('MAME 2003 Plus XML file not found. Aborting.') + kodi_set_error_status(st_dic, 'MAME 2003 Plus XML file not found.') + return + log_info('MAME 2003 Plus XML found.') + # Check that the XML control file exists. + if XML_control_FN.exists(): + # Open the XML control file and check if mtime of current file is older than + # the one stored in the XML control file. + # If so reset everything, if not use the cached information in the XML control file. + log_info('MAME 2003 XML control file found.') + XML_control_dic = utils_load_JSON_file(XML_control_FN.getPath()) + statinfo = os.stat(MAME_XML_path.getPath()) + log_debug('XML_control_dic["st_mtime"] "{}"'.format(XML_control_dic['st_mtime'])) + log_debug('statinfo.st_mtime "{}"'.format(statinfo.st_mtime)) + if statinfo.st_mtime > XML_control_dic['st_mtime']: + log_info('XML file is more recent than last preprocessing. Forcing new preprocessing.') + process_XML_flag = True + else: + log_info('XML control up to date.') + process_XML_flag = False + else: + log_info('XML control file not found. Forcing XML preprocessing.') + process_XML_flag = True + # Only process MAME XML if needed. + if process_XML_flag: + # Count number of machines and create XML control file. + mame_preprocess_RETRO_MAME2003PLUS(cfg, st_dic) + if st_dic['abort']: return + else: + log_info('Reusing previosly preprocessed MAME 2003 XML.') + else: + log_error('mame_build_MAME_main_database() Unknown op_mode "{}"'.format(cfg.settings['op_mode'])) + kodi_set_error_status(st_dic, 'Unknown operation mode {}'.format(cfg.settings['op_mode'])) + return + + return MAME_XML_path, XML_control_FN + +# ------------------------------------------------------------------------------------------------- +# Loading of data files +# ------------------------------------------------------------------------------------------------- +# Catver.ini is very special so it has a custom loader. +# It provides data for two catalogs: categories and version added. In other words, it +# has 2 folders defined in the INI file. +# +# --- Example ----------------------------------- +# ;; Comment +# [special_folder_name or no mae] +# machine_name_1 = category_name_1 +# machine_name_2 = category_name_2 +# ----------------------------------------------- +# +# Returns two dictionaries with struct similar a mame_load_INI_datfile_simple() +# catver_dic, veradded_dic +# +def mame_load_Catver_ini(filename): + __debug_do_list_categories = False + log_info('mame_load_Catver_ini() Parsing "{}"'.format(filename)) + catver_dic = { + 'version' : 'unknown', + 'unique_categories' : True, + 'single_category' : False, + 'isValid' : False, + 'data' : {}, + 'categories' : set(), + } + veradded_dic = { + 'version' : 'unknown', + 'unique_categories' : True, + 'single_category' : False, + 'isValid' : False, + 'data' : {}, + 'categories' : set(), + } + + # --- read_status FSM values --- + # 0 -> Looking for '[Category]' tag + # 1 -> Reading categories + # 2 -> Looking for '[VerAdded]' tag. + # 3 -> Reading version added + # 4 -> END + read_status = 0 + try: + f = io.open(filename, 'rt', encoding = 'utf-8') + except IOError: + log_error('mame_load_Catver_ini() Exception IOError') + log_error('mame_load_Catver_ini() File "{}"'.format(filename)) + return (catver_dic, veradded_dic) + for cat_line in f: + stripped_line = cat_line.strip() + if __debug_do_list_categories: log_debug('Line "' + stripped_line + '"') + if read_status == 0: + # >> Look for Catver version + m = re.search(r'^;; CatVer ([0-9\.]+) / ', stripped_line) + if m: + catver_dic['version'] = m.group(1) + veradded_dic['version'] = m.group(1) + m = re.search(r'^;; CATVER.ini ([0-9\.]+) / ', stripped_line) + if m: + catver_dic['version'] = m.group(1) + veradded_dic['version'] = m.group(1) + if stripped_line == '[Category]': + if __debug_do_list_categories: log_debug('Found [Category]') + read_status = 1 + elif read_status == 1: + line_list = stripped_line.split("=") + if len(line_list) == 1: + # log_debug('mame_load_Catver_ini() Reached end of categories parsing.') + read_status = 2 + else: + if __debug_do_list_categories: log_debug(line_list) + machine_name = line_list[0] + current_category = line_list[1] + catver_dic['categories'].add(current_category) + if machine_name in catver_dic['data']: + catver_dic['data'][machine_name].append(current_category) + else: + catver_dic['data'][machine_name] = [current_category] + elif read_status == 2: + if stripped_line == '[VerAdded]': + if __debug_do_list_categories: log_debug('Found [VerAdded]') + read_status = 3 + elif read_status == 3: + line_list = stripped_line.split("=") + if len(line_list) == 1: + # log_debug('mame_load_Catver_ini() Reached end of veradded parsing.') + read_status = 4 + else: + if __debug_do_list_categories: log_debug(line_list) + machine_name = line_list[0] + current_category = line_list[1] + veradded_dic['categories'].add(current_category) + if machine_name in veradded_dic['data']: + veradded_dic['data'][machine_name].append(current_category) + else: + veradded_dic['data'][machine_name] = [current_category] + elif read_status == 4: + log_debug('End parsing') + break + else: + raise CriticalError('Unknown read_status FSM value') + f.close() + catver_dic['single_category'] = True if len(catver_dic['categories']) == 1 else False + for m_name in sorted(catver_dic['data']): + if len(catver_dic['data'][m_name]) > 1: + catver_dic['unique_categories'] = False + break + catver_dic['single_category'] = True + veradded_dic['single_category'] = True if len(veradded_dic['categories']) == 1 else False + for m_name in sorted(veradded_dic['data']): + if len(veradded_dic['data'][m_name]) > 1: + veradded_dic['unique_categories'] = False + break + veradded_dic['single_category'] = True + # If categories are unique for each machine transform lists into strings + if catver_dic['unique_categories']: + for m_name in catver_dic['data']: + catver_dic['data'][m_name] = catver_dic['data'][m_name][0] + if veradded_dic['unique_categories']: + for m_name in veradded_dic['data']: + veradded_dic['data'][m_name] = veradded_dic['data'][m_name][0] + log_info('mame_load_Catver_ini() Catver Machines {:6d}'.format(len(catver_dic['data']))) + log_info('mame_load_Catver_ini() Catver Categories {:6d}'.format(len(catver_dic['categories']))) + log_info('mame_load_Catver_ini() Catver Version "{}"'.format(catver_dic['version'])) + log_info('mame_load_Catver_ini() Catver unique_categories {}'.format(catver_dic['unique_categories'])) + log_info('mame_load_Catver_ini() Catver single_category {}'.format(catver_dic['single_category'])) + log_info('mame_load_Catver_ini() Veradded Machines {:6d}'.format(len(veradded_dic['data']))) + log_info('mame_load_Catver_ini() Veradded Categories {:6d}'.format(len(veradded_dic['categories']))) + log_info('mame_load_Catver_ini() Veradded Version "{}"'.format(veradded_dic['version'])) + log_info('mame_load_Catver_ini() Veradded unique_categories {}'.format(veradded_dic['unique_categories'])) + log_info('mame_load_Catver_ini() Veradded single_category {}'.format(veradded_dic['single_category'])) + + return (catver_dic, veradded_dic) + +# +# nplayers.ini does not have [ROOT_FOLDER], only [NPlayers]. +# nplayers.ini has an structure very similar to catver.ini, and it is also supported here. +# Returns a ini_dic with same structue as mame_load_INI_datfile_simple() +# +# NOTE nplayers.ini has defects like having repeated entries for some machines. +# Do not crash because of this! For example (in verrsion 0.194 04-feb-18) +# 1943=2P sim +# 1943=2P sim +# +def mame_load_nplayers_ini(filename): + __debug_do_list_categories = False + log_info('mame_load_nplayers_ini() Parsing "{}"'.format(filename)) + ini_dic = { + 'version' : 'unknown', + 'unique_categories' : True, + 'single_category' : False, + 'isValid' : False, + 'data' : {}, + 'categories' : set(), + } + + # --- read_status FSM values --- + # 0 -> Looking for '[NPlayers]' tag + # 1 -> Reading categories + # 2 -> Categories finished. STOP + read_status = 0 + try: + f = io.open(filename, 'rt', encoding = 'utf-8') + except IOError: + log_info('mame_load_nplayers_ini() (IOError) opening "{}"'.format(filename)) + return ini_dic + for cat_line in f: + stripped_line = cat_line.strip() + if __debug_do_list_categories: log_debug('Line "' + stripped_line + '"') + if read_status == 0: + m = re.search(r'NPlayers ([0-9\.]+) / ', stripped_line) + if m: ini_dic['version'] = m.group(1) + if stripped_line == '[NPlayers]': + if __debug_do_list_categories: log_debug('Found [NPlayers]') + read_status = 1 + elif read_status == 1: + line_list = stripped_line.split("=") + if len(line_list) == 1: + read_status = 2 + continue + else: + machine_name, current_category = text_type(line_list[0]), text_type(line_list[1]) + if __debug_do_list_categories: log_debug('"{}" / "{}"'.format(machine_name, current_category)) + ini_dic['categories'].add(current_category) + if machine_name in ini_dic['data']: + # Force a single category to avoid nplayers.ini bugs. + pass + # ini_dic['data'][machine_name].add(current_category) + # log_debug('machine "{}"'.format(machine_name)) + # log_debug('current_category "{}"'.format(current_category)) + # log_debug('"{}"'.format(text_type(ini_dic['data'][machine_name]))) + # raise ValueError('unique_categories False') + else: + ini_dic['data'][machine_name] = [current_category] + elif read_status == 2: + log_info('mame_load_nplayers_ini() Reached end of nplayers parsing.') + break + else: + raise ValueError('Unknown read_status FSM value') + f.close() + ini_dic['single_category'] = True if len(ini_dic['categories']) == 1 else False + # nplayers.ini has repeated machines, so checking for unique_cateogories is here. + for m_name in sorted(ini_dic['data']): + if len(ini_dic['data'][m_name]) > 1: + ini_dic['unique_categories'] = False + break + # If categories are unique for each machine transform lists into strings + if ini_dic['unique_categories']: + for m_name in ini_dic['data']: + ini_dic['data'][m_name] = ini_dic['data'][m_name][0] + log_info('mame_load_nplayers_ini() Machines {0:6d}'.format(len(ini_dic['data']))) + log_info('mame_load_nplayers_ini() Categories {0:6d}'.format(len(ini_dic['categories']))) + log_info('mame_load_nplayers_ini() Version "{}"'.format(ini_dic['version'])) + log_info('mame_load_nplayers_ini() unique_categories {}'.format(ini_dic['unique_categories'])) + log_info('mame_load_nplayers_ini() single_category {}'.format(ini_dic['single_category'])) + + # DEBUG: print machines with more than one category. + # for m_name in sorted(ini_dic['data']): + # if len(ini_dic['data'][m_name]) > 1: + # for cat_name in ini_dic['data'][m_name]: + # log_debug('machine {} nplayers {}'.format(m_name, cat_name)) + + return ini_dic + +# +# Load mature.ini file. +# Returns a ini_dic similar to mame_load_INI_datfile_simple() +# +def mame_load_Mature_ini(filename): + # FSM statuses + FSM_HEADER = 0 # Looking for and process '[ROOT_FOLDER]' directory tag. + # Initial status. + FSM_FOLDER_NAME = 1 # Searching for [category_name] and/or adding machines. + + log_info('mame_load_Mature_ini() Parsing "{}"'.format(filename)) + ini_dic = { + 'version' : 'unknown', + 'unique_categories' : True, + 'single_category' : False, + 'isValid' : False, + 'data' : {}, + 'categories' : set(), + } + slist = [] + try: + f = io.open(filename, 'rt', encoding = 'utf-8') + for file_line in f: + stripped_line = file_line.strip() + if stripped_line == '': continue # Skip blanks + slist.append(stripped_line) + f.close() + except IOError: + log_info('mame_load_Mature_ini() (IOError) opening "{}"'.format(filename)) + return ini_dic + + fsm_status = FSM_HEADER + for stripped_line in slist: + if fsm_status == FSM_HEADER: + # Skip comments: lines starting with ';;' + # Look for version string in comments + if re.search(r'^;;', stripped_line): + # log_debug('mame_load_Mature_ini() Comment line "{}"'.format(stripped_line)) + m = re.search(r';; (\w+)\.ini ([0-9\.]+) / ', stripped_line) + if m: + ini_dic['version'] = m.group(2) + continue + if stripped_line == '[ROOT_FOLDER]': + fsm_status = FSM_FOLDER_NAME + # Create default category + current_category = 'default' + ini_dic['categories'].add(current_category) + elif fsm_status == FSM_FOLDER_NAME: + machine_name = stripped_line + if machine_name in ini_dic['data']: + ini_dic['data'][machine_name].append(current_category) + else: + ini_dic['data'][machine_name] = [current_category] + else: + raise ValueError('Unknown FSM fsm_status {}'.format(fsm_status)) + ini_dic['single_category'] = True if len(ini_dic['categories']) == 1 else False + for m_name in sorted(ini_dic['data']): + if len(ini_dic['data'][m_name]) > 1: + ini_dic['unique_categories'] = False + break + # If categories are unique for each machine transform lists into strings + if ini_dic['unique_categories']: + for m_name in ini_dic['data']: + ini_dic['data'][m_name] = ini_dic['data'][m_name][0] + log_info('mame_load_Mature_ini() Machines {0:6d}'.format(len(ini_dic['data']))) + log_info('mame_load_Mature_ini() Categories {0:6d}'.format(len(ini_dic['categories']))) + log_info('mame_load_Mature_ini() Version "{}"'.format(ini_dic['version'])) + log_info('mame_load_Mature_ini() unique_categories {}'.format(ini_dic['unique_categories'])) + log_info('mame_load_Mature_ini() single_category {}'.format(ini_dic['single_category'])) + + return ini_dic + +# +# Generic MAME INI file loader. +# Supports Alltime.ini, Artwork.ini, bestgames.ini, Category.ini, catlist.ini, +# genre.ini and series.ini. +# +# --- Example ----------------------------------- +# ;; Comment +# [FOLDER_SETTINGS] +# RootFolderIcon mame +# SubFolderIcon folder +# +# [ROOT_FOLDER] +# +# [category_name_1] +# machine_name_1 +# machine_name_2 +# +# [category_name_2] +# machine_name_1 +# ----------------------------------------------- +# +# Note that some INIs, for example Artwork.ini, may have the same machine on different +# categories. This must be supported in this function. +# +# ini_dic = { +# 'version' : string, +# 'unique_categories' : bool, +# 'single_category' : bool, +# 'data' : { +# 'machine_name' : { 'category_1', 'category_2', ... } +# } +# 'categories' : { +# 'category_1', 'category_2', ... +# } +# } +# +# categories is a set of (unique) categories. By definition of set, each category appears +# only once. +# unique_categories is True is each machine has a unique category, False otherwise. +# single_category is True if only one category is defined, for example in mature.ini. +# +def mame_load_INI_datfile_simple(filename): + # FSM statuses + FSM_HEADER = 0 # Looking for and process '[ROOT_FOLDER]' directory tag. + # Initial status. + FSM_FOLDER_NAME = 1 # Searching for [category_name] and/or adding machines. + + # Read file and put it in a list of strings. + # Strings in this list are stripped. + log_info('mame_load_INI_datfile_simple() Parsing "{}"'.format(filename)) + ini_dic = { + 'version' : 'unknown', + 'unique_categories' : True, + 'single_category' : False, + 'isValid' : False, + 'data' : {}, + 'categories' : set(), + } + slist = [] + try: + f = io.open(filename, 'rt', encoding = 'utf-8', errors = 'replace') + for file_line in f: + stripped_line = file_line.strip() + if stripped_line == '': continue # Skip blanks + slist.append(stripped_line) + f.close() + except IOError: + log_info('mame_load_INI_datfile_simple() (IOError) opening "{}"'.format(filename)) + return ini_dic + + # Compile regexes to increase performance => It is no necessary. According to the docs: The + # compiled versions of the most recent patterns passed to re.match(), re.search() or + # re.compile() are cached, so programs that use only a few regular expressions at a + # time needn’t worry about compiling regular expressions. + fsm_status = FSM_HEADER + for stripped_line in slist: + # log_debug('{}'.format(stripped_line)) + if fsm_status == FSM_HEADER: + # log_debug('FSM_HEADER "{}"'.format(stripped_line)) + # Skip comments: lines starting with ';;' + # Look for version string in comments + if re.search(r'^;;', stripped_line): + m = re.search(r';; (\w+)\.ini ([0-9\.]+) / ', stripped_line) + if m: ini_dic['version'] = m.group(2) + continue + if stripped_line.find('[ROOT_FOLDER]') >= 0: + fsm_status = FSM_FOLDER_NAME + elif fsm_status == FSM_FOLDER_NAME: + m = re.search(r'^\[(.*)\]', stripped_line) + if m: + current_category = text_type(m.group(1)) + if current_category in ini_dic['categories']: + raise ValueError('Repeated category {}'.format(current_category)) + ini_dic['categories'].add(current_category) + else: + machine_name = stripped_line + if machine_name in ini_dic['data']: + ini_dic['unique_categories'] = False + ini_dic['data'][machine_name].append(current_category) + else: + ini_dic['data'][machine_name] = [current_category] + else: + raise ValueError('Unknown FSM fsm_status {}'.format(fsm_status)) + ini_dic['single_category'] = True if len(ini_dic['categories']) == 1 else False + for m_name in sorted(ini_dic['data']): + if len(ini_dic['data'][m_name]) > 1: + ini_dic['unique_categories'] = False + break + # If categories are unique for each machine transform lists into strings + if ini_dic['unique_categories']: + for m_name in ini_dic['data']: + ini_dic['data'][m_name] = ini_dic['data'][m_name][0] + log_info('mame_load_INI_datfile_simple() Machines {0:6d}'.format(len(ini_dic['data']))) + log_info('mame_load_INI_datfile_simple() Categories {0:6d}'.format(len(ini_dic['categories']))) + log_info('mame_load_INI_datfile_simple() Version "{}"'.format(ini_dic['version'])) + log_info('mame_load_INI_datfile_simple() unique_categories {}'.format(ini_dic['unique_categories'])) + log_info('mame_load_INI_datfile_simple() single_category {}'.format(ini_dic['single_category'])) + + return ini_dic + +# --- BEGIN code in dev-parsers/test_parser_history_dat.py ---------------------------------------- +# Loads History.dat. This function is deprecated in favour of the XML format. +# +# One description can be for several MAME machines: +# $info=99lstwar,99lstwara,99lstwarb, +# $bio +# +# One description can be for several SL items and several SL lists: +# $amigaocs_flop=alloallo,alloallo1, +# $amigaaga_flop=alloallo,alloallo1, +# $amiga_flop=alloallo,alloallo1, +# $bio +# +# key_in_history_dic is the first machine on the list on the first line. +# +# history_idx = { +# 'nes' : { +# 'name': string, +# 'machines' : { +# 'machine_name' : "beautiful_name|db_list_name|db_machine_name", +# '100mandk' : "beautiful_name|nes|100mandk", +# '89denku' : "beautiful_name|nes|89denku", +# }, +# } +# 'mame' : { +# 'name' : string, +# 'machines': { +# '88games' : "beautiful_name|db_list_name|db_machine_name", +# 'flagrall' : "beautiful_name|db_list_name|db_machine_name", +# }, +# } +# } +# +# history_dic = { +# 'nes' : { +# '100mandk' : string, +# '89denku' : string, +# }, +# 'mame' : { +# '88games' : string, +# 'flagrall' : string, +# }, +# } +def mame_load_History_DAT(filename): + log_info('mame_load_History_DAT() Parsing "{}"'.format(filename)) + history_dic = { + 'version' : 'Unknown', + 'date' : 'Unknown', + 'index' : {}, + 'data' : {}, + } + __debug_function = False + line_number = 0 + num_header_line = 0 + # Due to syntax errors in History.dat m_data may have invalid data, for example + # exmpty strings as list_name and/or machine names. + # m_data = [ + # (line_number, list_name, [machine1, machine2, ...]), + # ... + # ] + m_data = [] + + # Convenience variables. + history_idx = history_dic['index'] + history_data = history_dic['data'] + + # --- read_status FSM values --- + # History.dat has some syntax errors, like empty machine names. To fix this, do + # the parsing on two stages: first read the raw data and the bio and then + # check if the data is OK before adding it to the index and the DB. + # 0 -> Looking for '$info=machine_name_1,machine_name_2,' or '$SL_name=item_1,item_2,' + # If '$bio' found go to 1. + # 1 -> Reading information. If '$end' found go to 2. + # 2 -> Add information to database if no errors. Then go to 0. + read_status = 0 + try: + f = io.open(filename, 'rt', encoding = 'utf-8') + except IOError: + log_info('mame_load_History_DAT() (IOError) opening "{}"'.format(filename)) + return history_dic + for file_line in f: + line_number += 1 + line_uni = file_line.strip() + if __debug_function: log_debug('Line "{}"'.format(line_uni)) + if read_status == 0: + # Skip comments: lines starting with '##' + # Look for version string in comments + if re.search(r'^##', line_uni): + m = re.search(r'## REVISION\: ([0-9\.]+)$', line_uni) + if m: history_dic['version'] = m.group(1) + ' DAT' + continue + if line_uni == '': continue + # Machine list line + # Parses lines like "$info=99lstwar,99lstwara,99lstwarb," + # Parses lines like "$info=99lstwar,99lstwara,99lstwarb" + # History.dat has syntactic errors like "$dc=,". + # History.dat has syntactic errors like "$megadriv=". + m = re.search(r'^\$(.+?)=(.*?),?$', line_uni) + if m: + num_header_line += 1 + list_name = m.group(1) + machine_name_raw = m.group(2) + # Remove trailing ',' to fix history.dat syntactic errors like + # "$snes_bspack=bsfami,," + if len(machine_name_raw) > 1 and machine_name_raw[-1] == ',': + machine_name_raw = machine_name_raw[:-1] + # Transform some special list names + if list_name in {'info', 'info,megatech', 'info,stv'}: list_name = 'mame' + mname_list = machine_name_raw.split(',') + m_data.append([num_header_line, list_name, mname_list]) + continue + if line_uni == '$bio': + read_status = 1 + info_str_list = [] + continue + # If we reach this point it's an error. + raise TypeError('Wrong header "{}" (line {:,})'.format(line_uni, line_number)) + elif read_status == 1: + if line_uni == '$end': + # Generate biography text. + bio_str = '\n'.join(info_str_list) + bio_str = bio_str[1:] if bio_str[0] == '\n' else bio_str + bio_str = bio_str[:-1] if bio_str[-1] == '\n' else bio_str + bio_str = bio_str.replace('\n\t\t', '') + + # Clean m_data of bad data due to History.dat syntax errors, for example + # empty machine names. + # clean_m_data = [ + # (list_name, [machine_name_1, machine_name_2, ...] ), + # ..., + # ] + clean_m_data = [] + for dtuple in m_data: + line_num, list_name, mname_list = dtuple + # If list_name is empty drop the full line + if not list_name: continue + # Clean empty machine names. + clean_mname_list = [] + for machine_name in mname_list: + # Skip bad/wrong machine names. + if not machine_name: continue + if machine_name == ',': continue + clean_mname_list.append(machine_name) + clean_m_data.append((list_name, clean_mname_list)) + + # Reset FSM status + read_status = 2 + num_header_line = 0 + m_data = [] + info_str_list = [] + else: + info_str_list.append(line_uni) + elif read_status == 2: + # Go to state 0 of the FSM. + read_status = 0 + + # Ignore machine if no valid data at all. + if len(clean_m_data) == 0: + log_warning('On History.dat line {:,}'.format(line_number)) + log_warning('clean_m_data is empty.') + log_warning('Ignoring entry in History.dat database') + continue + # Ignore if empty list name. + if not clean_m_data[0][0]: + log_warning('On History.dat line {:,}'.format(line_number)) + log_warning('clean_m_data empty list name.') + log_warning('Ignoring entry in History.dat database') + continue + # Ignore if empty machine list. + if not clean_m_data[0][1]: + log_warning('On History.dat line {:,}'.format(line_number)) + log_warning('Empty machine name list.') + log_warning('db_list_name "{}"'.format(clean_m_data[0][0])) + log_warning('Ignoring entry in History.dat database') + continue + if not clean_m_data[0][1][0]: + log_warning('On History.dat line {:,}'.format(line_number)) + log_warning('Empty machine name first element.') + log_warning('db_list_name "{}"'.format(clean_m_data[0][0])) + log_warning('Ignoring entry in History.dat database') + continue + db_list_name = clean_m_data[0][0] + db_machine_name = clean_m_data[0][1][0] + + # Add list and machine names to index database. + for dtuple in clean_m_data: + list_name, machine_name_list = dtuple + if list_name not in history_idx: + history_idx[list_name] = {'name' : list_name, 'machines' : {}} + for machine_name in machine_name_list: + m_str = misc_build_db_str_3(machine_name, db_list_name, db_machine_name) + history_idx[list_name]['machines'][machine_name] = m_str + + # Add biography string to main database. + if db_list_name not in history_data: history_data[db_list_name] = {} + history_data[db_list_name][db_machine_name] = bio_str + else: + raise TypeError('Wrong read_status = {} (line {:,})'.format(read_status, line_number)) + f.close() + log_info('mame_load_History_DAT() Version "{}"'.format(history_dic['version'])) + log_info('mame_load_History_DAT() Rows in index {}'.format(len(history_dic['index']))) + log_info('mame_load_History_DAT() Rows in data {}'.format(len(history_dic['data']))) + return history_dic +# --- END code in dev-parsers/test_parser_history_dat.py ------------------------------------------ + +# --- BEGIN code in dev-parsers/test_parser_history_xml.py ---------------------------------------- +# Loads History.xml, a new XML version of History.dat +# +# MAME machine: +# <entry> +# <systems> +# <system name="dino" /> +# <system name="dinou" /> +# </systems> +# <text /> +# </entry> +# +# One description can be for several SL items and several SL lists: +# <entry> +# <software> +# <item list="snes" name="smw2jb" /> +# <item list="snes" name="smw2ja" /> +# <item list="snes" name="smw2j" /> +# </software> +# <text /> +# </entry> +# +# Example of a problematic entry: +# <entry> +# <systems> +# <system name="10yardj" /> +# </systems> +# <software> +# <item list="vgmplay" name="10yard" /> +# </software> +# <text /> +# </entry> +# +# The key in the data dictionary is the first machine found on history.xml +# +# history_dic = { +# 'version' : '2.32', # string +# 'date' : '2021-05-28', # string +# 'index' : { +# 'nes' : { +# 'name': 'nes', # string, later changed with beautiful name +# 'machines' : { +# 'machine_name' : "beautiful_name|db_list_name|db_machine_name", +# '100mandk' : "beautiful_name|nes|100mandk", +# '89denku' : "beautiful_name|nes|89denku", +# }, +# }, +# 'mame' : { +# 'name' : string, +# 'machines': { +# '88games' : "beautiful_name|db_list_name|db_machine_name", +# 'flagrall' : "beautiful_name|db_list_name|db_machine_name", +# }, +# }, +# }, +# 'data' = { +# 'nes' : { +# '100mandk' : string, +# '89denku' : string, +# }, +# 'mame' : { +# '88games' : string, +# 'flagrall' : string, +# }, +# } +# } +def mame_load_History_xml(filename): + log_info('mame_load_History_xml() Parsing "{}"'.format(filename)) + history_dic = { + 'version' : 'Unknown', + 'date' : 'Unknown', + 'index' : {}, + 'data' : {}, + } + __debug_xml_parser = False + entry_counter = 0 + # Convenience variables. + history_idx = history_dic['index'] + history_data = history_dic['data'] + + xml_tree = utils_load_XML_to_ET(filename) + if not xml_tree: return history_dic + xml_root = xml_tree.getroot() + history_dic['version'] = xml_root.attrib['version'] + ' XML ' + xml_root.attrib['date'] + history_dic['date'] = xml_root.attrib['date'] + for root_el in xml_root: + if __debug_xml_parser: log_debug('Root child tag "{}"'.format(root_el.tag)) + if root_el.tag != 'entry': + log_error('Unknown tag <{}>'.format(root_el.tag)) + raise TypeError + entry_counter += 1 + item_list = [] + for entry_el in root_el: + if __debug_xml_parser: log_debug('Entry child tag "{}"'.format(entry_el.tag)) + if entry_el.tag == 'software': + for software_el in entry_el: + if software_el.tag != 'item': + log_error('Unknown <software> child tag <{}>'.format(software_el.tag)) + raise TypeError + item_list.append((software_el.attrib['list'], software_el.attrib['name'])) + elif entry_el.tag == 'systems': + for system_el in entry_el: + if system_el.tag != 'system': + log_error('Unknown <systems> child tag <{}>'.format(software_el.tag)) + raise TypeError + item_list.append(('mame', software_el.attrib['name'])) + elif entry_el.tag == 'text': + # Generate biography text. + bio_str = entry_el.text + bio_str = bio_str[1:] if bio_str[0] == '\n' else bio_str + bio_str = bio_str[:-1] if bio_str[-1] == '\n' else bio_str + bio_str = bio_str.replace('\n\t\t', '') + + # Add list and machine names to index database. + if len(item_list) < 1: + log_warning('Empty item_list in entry_counter = {}'.format(entry_counter)) + continue + db_list_name = item_list[0][0] + db_machine_name = item_list[0][1] + for list_name, machine_name in item_list: + m_str = misc_build_db_str_3(machine_name, db_list_name, db_machine_name) + try: + history_idx[list_name]['machines'][machine_name] = m_str + except: + history_idx[list_name] = {'name' : list_name, 'machines' : {}} + history_idx[list_name]['machines'][machine_name] = m_str + + # Add biography string to main database. + try: + history_data[db_list_name][db_machine_name] = bio_str + except: + history_data[db_list_name] = {} + history_data[db_list_name][db_machine_name] = bio_str + else: + log_error('Unknown tag <{}>'.format(root_el.tag)) + raise TypeError + if __debug_xml_parser and entry_counter > 100: break + log_info('mame_load_History_xml() Version "{}"'.format(history_dic['version'])) + log_info('mame_load_History_xml() Date "{}"'.format(history_dic['date'])) + log_info('mame_load_History_xml() Rows in index {}'.format(len(history_dic['index']))) + log_info('mame_load_History_xml() Rows in data {}'.format(len(history_dic['data']))) + return history_dic +# --- END code in dev-parsers/test_parser_history_xml.py ------------------------------------------ + +# --- BEGIN code in dev-parsers/test_parser_mameinfo_dat.py --------------------------------------- +# mameinfo.dat has information for both MAME machines and MAME drivers. +# +# idx_dic = { +# 'mame' : { +# '88games' : 'beautiful_name', +# 'flagrall' : 'beautiful_name', +# }, +# 'drv' : { +# '88games.cpp' : 'beautiful_name'], +# 'flagrall.cpp' : 'beautiful_name'], +# } +# } +# data_dic = { +# 'mame' : { +# '88games' : string, +# 'flagrall' : string, +# }, +# 'drv' : { +# '1942.cpp' : string, +# '1943.cpp' : string, +# } +# } +def mame_load_MameInfo_DAT(filename): + log_info('mame_load_MameInfo_DAT() Parsing "{}"'.format(filename)) + ret_dic = { + 'version' : 'Unknown', + 'index' : { + 'mame' : {}, + 'drv' : {}, + }, + 'data' : {}, + } + __debug_function = False + line_counter = 0 + + # --- read_status FSM values --- + # 0 -> Looking for '$(xxxx)=(machine_name)' + # 1 -> Looking for $bio + # 2 -> Reading information. If '$end' found go to 0. + # 3 -> Ignoring information. If '$end' found go to 0. + read_status = 0 + try: + f = io.open(filename, 'rt', encoding = 'utf-8') + except IOError: + log_info('mame_load_MameInfo_DAT() (IOError) opening "{}"'.format(filename)) + return ret_dic + for file_line in f: + line_counter += 1 + line_uni = file_line.strip() + # if __debug_function: log_debug('Line "{}"'.format(line_uni)) + if read_status == 0: + # Skip comments: lines starting with '#' + # Look for version string in comments + if re.search(r'^#', line_uni): + m = re.search(r'# MAMEINFO.DAT v([0-9\.]+)', line_uni) + if m: ret_dic['version'] = m.group(1) + continue + if line_uni == '': continue + # New machine or driver information + m = re.search(r'^\$info=(.+?)$', line_uni) + if m: + machine_name = m.group(1) + if __debug_function: log_debug('Machine "{}"'.format(machine_name)) + read_status = 1 + elif read_status == 1: + if __debug_function: log_debug('Second line "{}"'.format(line_uni)) + if line_uni == '$mame': + read_status = 2 + info_str_list = [] + list_name = 'mame' + ret_dic['index'][list_name][machine_name] = machine_name + elif line_uni == '$drv': + read_status = 2 + info_str_list = [] + list_name = 'drv' + ret_dic['index'][list_name][machine_name] = machine_name + # Ignore empty lines between "$info=xxxxx" and "$mame" or "$drv" + elif line_uni == '': + continue + else: + raise TypeError('Wrong second line = "{}" (line {:,})'.format(line_uni, line_counter)) + elif read_status == 2: + if line_uni == '$end': + if list_name not in ret_dic['data']: ret_dic['data'][list_name] = {} + ret_dic['data'][list_name][machine_name] = '\n'.join(info_str_list).strip() + read_status = 0 + else: + info_str_list.append(line_uni) + else: + raise TypeError('Wrong read_status = {} (line {:,})'.format(read_status, line_counter)) + f.close() + log_info('mame_load_MameInfo_DAT() Version "{}"'.format(ret_dic['version'])) + log_info('mame_load_MameInfo_DAT() Rows in index {}'.format(len(ret_dic['index']))) + log_info('mame_load_MameInfo_DAT() Rows in data {}'.format(len(ret_dic['data']))) + return ret_dic +# --- END code in dev-parsers/test_parser_mameinfo_dat.py ----------------------------------------- + +# NOTE set objects are not JSON-serializable. Use lists and transform lists to sets if +# necessary after loading the JSON file. +# +# idx_dic = { +# '88games', 'beautiful_name', +# 'flagrall', 'beautiful_name', +# } +# data_dic = { +# '88games' : 'string', +# 'flagrall' : 'string', +# } +def mame_load_GameInit_DAT(filename): + log_info('mame_load_GameInit_DAT() Parsing "{}"'.format(filename)) + ret_dic = { + 'version' : 'Unknown', + 'index' : {}, + 'data' : {}, + } + __debug_function = False + + # --- read_status FSM values --- + # 0 -> Looking for '$info=(machine_name)' + # 1 -> Looking for $mame + # 2 -> Reading information. If '$end' found go to 0. + # 3 -> Ignoring information. If '$end' found go to 0. + read_status = 0 + try: + f = io.open(filename, 'rt', encoding = 'utf-8') + except IOError: + log_info('mame_load_GameInit_DAT() (IOError) opening "{}"'.format(filename)) + return ret_dic + for file_line in f: + line_uni = file_line.strip() + if __debug_function: log_debug('read_status {} | Line "{}"'.format(read_status, line_uni)) + # Note that Gameinit.dat may have a BOM 0xEF,0xBB,0xBF + # See https://en.wikipedia.org/wiki/Byte_order_mark + # Remove BOM if present. + if line_uni and line_uni[0] == '\ufeff': line_uni = line_uni[1:] + if read_status == 0: + # Skip comments: lines starting with '#' + # Look for version string in comments + if re.search(r'^#', line_uni): + if __debug_function: log_debug('Comment | "{}"'.format(line_uni)) + m = re.search(r'# MAME GAMEINIT\.DAT v([0-9\.]+) ', line_uni) + if m: ret_dic['version'] = m.group(1) + continue + if line_uni == '': continue + # New machine or driver information + m = re.search(r'^\$info=(.+?)$', line_uni) + if m: + machine_name = m.group(1) + if __debug_function: log_debug('Machine "{}"'.format(machine_name)) + ret_dic['index'][machine_name] = machine_name + read_status = 1 + elif read_status == 1: + if __debug_function: log_debug('Second line "{}"'.format(line_uni)) + if line_uni == '$mame': + read_status = 2 + info_str_list = [] + else: + raise TypeError('Wrong second line = "{}"'.format(line_uni)) + elif read_status == 2: + if line_uni == '$end': + ret_dic['data'][machine_name] = '\n'.join(info_str_list) + info_str_list = [] + read_status = 0 + else: + info_str_list.append(line_uni) + else: + raise TypeError('Wrong read_status = {}'.format(read_status)) + f.close() + log_info('mame_load_GameInit_DAT() Version "{}"'.format(ret_dic['version'])) + log_info('mame_load_GameInit_DAT() Rows in index {}'.format(len(ret_dic['index']))) + log_info('mame_load_GameInit_DAT() Rows in data {}'.format(len(ret_dic['data']))) + return ret_dic + +# NOTE set objects are not JSON-serializable. Use lists and transform lists to sets if +# necessary after loading the JSON file. +# +# idx_dic = { +# '88games', 'beautiful_name', +# 'flagrall', 'beautiful_name', +# } +# data_dic = { +# '88games' : 'string', +# 'flagrall' : 'string', +# } +def mame_load_Command_DAT(filename): + log_info('mame_load_Command_DAT() Parsing "{}"'.format(filename)) + ret_dic = { + 'version' : 'Unknown', + 'index' : {}, + 'data' : {}, + } + # Temporal storage. + idx_dic = {} + data_dic = {} + __debug_function = False + + # --- read_status FSM values --- + # 0 -> Looking for '$info=(machine_name)' + # 1 -> Looking for $cmd + # 2 -> Reading information. If '$end' found go to 0. + read_status = 0 + try: + f = io.open(filename, 'rt', encoding = 'utf-8') + except IOError: + log_info('mame_load_Command_DAT() (IOError) opening "{}"'.format(filename)) + return ret_dic + for file_line in f: + line_uni = file_line.strip() + # if __debug_function: log_debug('Line "{}"'.format(line_uni)) + if read_status == 0: + # Skip comments: lines starting with '#' + # Look for version string in comments + if re.search(r'^#', line_uni): + m = re.search(r'# Command List-[\w]+[\s]+([0-9\.]+) #', line_uni) + if m: ret_dic['version'] = m.group(1) + continue + if line_uni == '': continue + # New machine or driver information + m = re.search(r'^\$info=(.+?)$', line_uni) + if m: + machine_name = m.group(1) + if __debug_function: log_debug('Machine "{}"'.format(machine_name)) + idx_dic[machine_name] = machine_name + read_status = 1 + elif read_status == 1: + if __debug_function: log_debug('Second line "{}"'.format(line_uni)) + if line_uni == '$cmd': + read_status = 2 + info_str_list = [] + else: + raise TypeError('Wrong second line = "{}"'.format(line_uni)) + elif read_status == 2: + if line_uni == '$end': + data_dic[machine_name] = '\n'.join(info_str_list) + info_str_list = [] + read_status = 0 + else: + info_str_list.append(line_uni) + else: + raise TypeError('Wrong read_status = {}'.format(read_status)) + f.close() + log_info('mame_load_Command_DAT() Version "{}"'.format(ret_dic['version'])) + log_info('mame_load_Command_DAT() Rows in idx_dic {}'.format(len(idx_dic))) + log_info('mame_load_Command_DAT() Rows in data_dic {}'.format(len(data_dic))) + + # Many machines share the same entry. Expand the database. + for original_name in idx_dic: + for expanded_name in original_name.split(','): + # Skip empty strings + if not expanded_name: continue + expanded_name = expanded_name.strip() + ret_dic['index'][expanded_name] = expanded_name + ret_dic['data'][expanded_name] = data_dic[original_name] + log_info('mame_load_Command_DAT() Entries in proper index {}'.format(len(ret_dic['index']))) + log_info('mame_load_Command_DAT() Entries in proper data {}'.format(len(ret_dic['data']))) + return ret_dic + +# ------------------------------------------------------------------------------------------------- +# DAT export +# ------------------------------------------------------------------------------------------------- +# +# Writes a XML text tag line, indented 2 spaces by default. +# Both tag_name and tag_text must be Unicode strings. +# Returns an Unicode string. +# +def XML_t(tag_name, tag_text = '', num_spaces = 4): + if tag_text: + tag_text = text_escape_XML(tag_text) + line = '{}<{}>{}</{}>'.format(' ' * num_spaces, tag_name, tag_text, tag_name) + else: + # Empty tag + line = '{}<{} />'.format(' ' * num_spaces, tag_name) + + return line + +# Export a MAME information file in Billyc999 XML format to use with RCB. +# https://forum.kodi.tv/showthread.php?tid=70115&pid=2949624#pid2949624 +# https://github.com/billyc999/Game-database-info +def mame_write_MAME_ROM_Billyc999_XML(cfg, out_dir_FN, db_dic): + log_debug('mame_write_MAME_ROM_Billyc999_XML() BEGIN...') + control_dic = db_dic['control_dic'] + + # Get output filename + # DAT filename: AML 0.xxx ROMs (merged|split|non-merged|fully non-merged).xml + mame_version_str = control_dic['ver_mame_str'] + log_info('MAME version "{}"'.format(mame_version_str)) + DAT_basename_str = 'AML MAME {} Billyc999.xml'.format(mame_version_str) + DAT_FN = out_dir_FN.pjoin(DAT_basename_str) + log_info('XML "{}"'.format(DAT_FN.getPath())) + + # XML file header. + sl = [] + sl.append('<?xml version="1.0" encoding="UTF-8"?>') + sl.append('<menu>') + sl.append(' <header>') + sl.append(XML_t('listname', 'Exported by Advanced MAME Launcher')) + sl.append(XML_t('lastlistupdate', misc_time_to_str(time.time()))) + sl.append(XML_t('listversion', '{}'.format(mame_version_str))) + sl.append(XML_t('exporterversion', 'MAME {}'.format(mame_version_str))) + sl.append(' </header>') + + # Traverse ROMs and write DAT. + machine_counter = 0 + pDialog = KodiProgressDialog() + pDialog.startProgress('Creating MAME Billyc999 XML...', len(db_dic['renderdb'])) + for m_name in sorted(db_dic['renderdb']): + render = db_dic['renderdb'][m_name] + assets = db_dic['assetdb'][m_name] + sl.append(' <game name="{}">'.format(m_name)) + sl.append(XML_t('description', render['description'])) + sl.append(XML_t('year', render['year'])) + sl.append(XML_t('rating', 'ESRB - E (Everyone)')) + sl.append(XML_t('manufacturer', render['manufacturer'])) + sl.append(XML_t('dev')) + sl.append(XML_t('genre', render['genre'])) + sl.append(XML_t('score')) + sl.append(XML_t('player', render['nplayers'])) + sl.append(XML_t('story', assets['plot'])) + sl.append(XML_t('enabled', 'Yes')) + sl.append(XML_t('crc')) + sl.append(XML_t('cloneof', render['cloneof'])) + sl.append(' </game>') + machine_counter += 1 + pDialog.updateProgress(machine_counter) + sl.append('</menu>') + pDialog.endProgress() + + # Open output file name. + pDialog.startProgress('Writing MAME Billyc999 XML...') + utils_write_slist_to_file(DAT_FN.getPath(), sl) + pDialog.endProgress() + +# +# Only valid ROMs in DAT file. +# +def mame_write_MAME_ROM_XML_DAT(cfg, out_dir_FN, db_dic): + log_debug('mame_write_MAME_ROM_XML_DAT() BEGIN...') + control_dic = db_dic['control_dic'] + machines = db_dic['machines'] + render = db_dic['renderdb'] + audit_roms = db_dic['audit_roms'] + roms_sha1_dic = db_dic['roms_sha1_dic'] + + # Get output filename + # DAT filename: AML 0.xxx ROMs (merged|split|non-merged|fully non-merged).xml + mame_version_str = control_dic['ver_mame_str'] + rom_set = ['MERGED', 'SPLIT', 'NONMERGED', 'FULLYNONMERGED'][cfg.settings['mame_rom_set']] + rom_set_str = ['Merged', 'Split', 'Non-merged', 'Fully Non-merged'][cfg.settings['mame_rom_set']] + log_info('MAME version "{}"'.format(mame_version_str)) + log_info('ROM set is "{}"'.format(rom_set_str)) + DAT_basename_str = 'AML MAME {} ROMs ({}).xml'.format(mame_version_str, rom_set_str) + DAT_FN = out_dir_FN.pjoin(DAT_basename_str) + log_info('XML "{}"'.format(DAT_FN.getPath())) + + # XML file header. + slist = [] + slist.append('<?xml version="1.0" encoding="UTF-8"?>') + slist.append('<!DOCTYPE datafile PUBLIC "{}" "{}">'.format( + '-//Logiqx//DTD ROM Management Datafile//EN', 'http://www.logiqx.com/Dats/datafile.dtd')) + slist.append('<datafile>') + + desc_str = 'AML MAME {} ROMs {} set'.format(mame_version_str, rom_set_str) + slist.append('<header>') + slist.append(XML_t('name', desc_str)) + slist.append(XML_t('description', desc_str)) + slist.append(XML_t('version', '{}'.format(mame_version_str))) + slist.append(XML_t('date', misc_time_to_str(time.time()))) + slist.append(XML_t('author', 'Exported by Advanced MAME Launcher')) + slist.append('</header>') + + # Traverse ROMs and write DAT. + pDialog = KodiProgressDialog() + pDialog.startProgress('Creating MAME ROMs XML DAT...', len(audit_roms)) + for m_name in sorted(audit_roms): + pDialog.updateProgressInc() + # If machine has no ROMs then skip it + rom_list, actual_rom_list, num_ROMs = audit_roms[m_name], [], 0 + for rom in rom_list: + # Skip CHDs and samples + if rom['type'] == ROM_TYPE_ERROR: raise ValueError + if rom['type'] in [ROM_TYPE_DISK, ROM_TYPE_SAMPLE]: continue + # Skip machine ROMs not in this machine ZIP file. + zip_name, rom_name = rom['location'].split('/') + if zip_name != m_name: continue + # Skip invalid ROMs + if not rom['crc']: continue + # Add SHA1 field + rom['sha1'] = roms_sha1_dic[rom['location']] + actual_rom_list.append(rom) + num_ROMs += 1 + # Machine has no ROMs, skip it + if num_ROMs == 0: continue + + # Print ROMs in the XML. + slist.append('<machine name="{}">'.format(m_name)) + slist.append(XML_t('description', render[m_name]['description'])) + slist.append(XML_t('year', render[m_name]['year'])) + slist.append(XML_t('manufacturer', render[m_name]['manufacturer'])) + if render[m_name]['cloneof']: + slist.append(XML_t('cloneof', render[m_name]['cloneof'])) + for rom in actual_rom_list: + t = ' <rom name="{}" size="{}" crc="{}" sha1="{}"/>'.format( + rom['name'], rom['size'], rom['crc'], rom['sha1']) + slist.append(t) + slist.append('</machine>') + slist.append('</datafile>') + pDialog.endProgress() + + # Open output file name. + pDialog.startProgress('Writing MAME ROMs XML DAT...') + utils_write_slist_to_file(DAT_FN.getPath(), slist) + pDialog.endProgress() + +# +# Only valid CHDs in DAT file. +# +def mame_write_MAME_CHD_XML_DAT(cfg, out_dir_FN, db_dic): + log_debug('mame_write_MAME_CHD_XML_DAT() BEGIN ...') + control_dic = db_dic['control_dic'] + machines = db_dic['machines'] + render = db_dic['renderdb'] + audit_roms = db_dic['audit_roms'] + + # Get output filename + # DAT filename: AML 0.xxx ROMs (merged|split|non-merged|fully non-merged).xml + mame_version_str = control_dic['ver_mame_str'] + chd_set = ['MERGED', 'SPLIT', 'NONMERGED'][cfg.settings['mame_chd_set']] + chd_set_str = ['Merged', 'Split', 'Non-merged'][cfg.settings['mame_chd_set']] + log_info('MAME version "{}"'.format(mame_version_str)) + log_info('CHD set is "{}"'.format(chd_set_str)) + DAT_basename_str = 'AML MAME {} CHDs ({}).xml'.format(mame_version_str, chd_set_str) + DAT_FN = out_dir_FN.pjoin(DAT_basename_str) + log_info('XML "{}"'.format(DAT_FN.getPath())) + + # XML file header. + slist = [] + slist.append('<?xml version="1.0" encoding="UTF-8"?>') + str_a = '-//Logiqx//DTD ROM Management Datafile//EN' + str_b = 'http://www.logiqx.com/Dats/datafile.dtd' + slist.append('<!DOCTYPE datafile PUBLIC "{}" "{}">'.format(str_a, str_b)) + slist.append('<datafile>') + + desc_str = 'AML MAME {} CHDs {} set'.format(mame_version_str, chd_set_str) + slist.append('<header>') + slist.append(XML_t('name', desc_str)) + slist.append(XML_t('description', desc_str)) + slist.append(XML_t('version', '{}'.format(mame_version_str))) + slist.append(XML_t('date', misc_time_to_str(time.time()))) + slist.append(XML_t('author', 'Exported by Advanced MAME Launcher')) + slist.append('</header>') + + # Traverse ROMs and write DAT. + pDialog = KodiProgressDialog() + pDialog.startProgress('Creating MAME CHDs XML DAT...', len(audit_roms)) + for m_name in sorted(audit_roms): + pDialog.updateProgressInc() + # If machine has no ROMs then skip it + chd_list, actual_chd_list, num_CHDs = audit_roms[m_name], [], 0 + for chd in chd_list: + # Only include CHDs + if chd['type'] != ROM_TYPE_DISK: continue + # Skip machine ROMs not in this machine ZIP file. + zip_name, chd_name = chd['location'].split('/') + if zip_name != m_name: continue + # Skip invalid CHDs + if not chd['sha1']: continue + actual_chd_list.append(chd) + num_CHDs += 1 + if num_CHDs == 0: continue + + # Print CHDs in the XML. + slist.append('<machine name="{}">'.format(m_name)) + slist.append(XML_t('description', render[m_name]['description'])) + slist.append(XML_t('year', render[m_name]['year'])) + slist.append(XML_t('manufacturer', render[m_name]['manufacturer'])) + if render[m_name]['cloneof']: + slist.append(XML_t('cloneof', render[m_name]['cloneof'])) + for chd in actual_chd_list: + t = ' <rom name="{}" sha1="{}"/>'.format(chd['name'], chd['sha1']) + slist.append(t) + slist.append('</machine>') + slist.append('</datafile>') + pDialog.endProgress() + + # Open output file name. + pDialog.startProgress('Creating MAME ROMs XML DAT...') + utils_write_slist_to_file(DAT_FN.getPath(), slist) + pDialog.endProgress() + +# +# ------------------------------------------------------------------------------------------------- +# CHD manipulation functions +# ------------------------------------------------------------------------------------------------- +# Reference in https://github.com/rtissera/libchdr/blob/master/src/chd.h +# Reference in https://github.com/mamedev/mame/blob/master/src/lib/util/chd.h +# +# Open CHD and return stat information. +# +# chd_info = { +# 'status' : CHD_OK or CHD_BAD, +# 'version' : int, +# 'sha1' : string, +# } +# +CHD_OK = 0 +CHD_BAD_CHD = 1 +CHD_BAD_VERSION = 2 +def _mame_stat_chd(chd_path): + __debug_this_function = False + chd_info = { + 'status' : CHD_OK, + 'version' : 0, + 'sha1' : '', + } + + # --- Open CHD file and read first 124 bytes --- + if __debug_this_function: log_debug('_mame_stat_chd() Opening "{}"'.format(chd_path)) + try: + f = io.open(chd_path, 'rb') + chd_data_str = f.read(124) + f.close() + except IOError as E: + chd_info['status'] = CHD_BAD_CHD + return chd_info + + # --- Check CHD magic string to skip fake files --- + if chd_data_str[0:8] != 'MComprHD': + if __debug_this_function: log_debug('_mame_stat_chd() Magic string not found!') + chd_info['status'] = CHD_BAD_CHD + return chd_info + + # --- Parse CHD header --- + # All values in the CHD header are stored in big endian! + h_tuple = struct.unpack('>8sII', chd_data_str[0:16]) + tag, length, version = h_tuple + if __debug_this_function: + log_debug('_mame_stat_chd() Tag "{}"'.format(tag)) + log_debug('_mame_stat_chd() Length {}'.format(length)) + log_debug('_mame_stat_chd() Version {}'.format(version)) + + # Discard very old CHD that don't have SHA1 hash. Older version used MD5. + if version == 1 or version == 2 or version == 3: + chd_info['status'] = CHD_BAD_VERSION + chd_info['version'] = version + return chd_info + + # Read the whole header (must consider V3, V4 and V5) + # NOTE In MAME 0.196 some CHDs have version 4, most have version 5, version 3 is obsolete + if version == 4: + if __debug_this_function: log_debug('Reading V4 CHD header') + chd_header_v4_str = '>8sIIIIIQQI20s20s20s' + header_size = struct.calcsize(chd_header_v4_str) + t = struct.unpack(chd_header_v4_str, chd_data_str[0:108]) + tag = t[0] + length = t[1] + version = t[2] + flags = t[3] + compression = t[4] + totalhunks = t[5] + logicalbytes = t[6] + metaoffset = t[7] + hunkbytes = t[8] + rawsha1 = binascii.b2a_hex(t[9]) + sha1 = binascii.b2a_hex(t[10]) + parentsha1 = binascii.b2a_hex(t[11]) + + if __debug_this_function: + log_debug('V4 header size = {}'.format(header_size)) + log_debug('tag "{}"'.format(tag)) + log_debug('length {}'.format(length)) + log_debug('version {}'.format(version)) + log_debug('flags {}'.format(flags)) + log_debug('compression {}'.format(compression)) + log_debug('totalhunks {}'.format(totalhunks)) + log_debug('logicalbytes {}'.format(logicalbytes)) + log_debug('metaoffset {}'.format(metaoffset)) + log_debug('hunkbytes {}'.format(hunkbytes)) + log_debug('rawsha1 "{}"'.format(rawsha1)) + log_debug('sha1 "{}"'.format(sha1)) + log_debug('parentsha1 "{}"'.format(parentsha1)) + + # The CHD SHA1 string storet in MAME -listxml is the rawsha1 field in V4 CHDs. + chd_info['status'] = CHD_OK + chd_info['version'] = version + chd_info['sha1'] = rawsha1 + elif version == 5: + if __debug_this_function: log_debug('Reading V5 CHD header') + chd_header_v5_str = '>8sII16sQQQII20s20s20s' + header_size = struct.calcsize(chd_header_v5_str) + t = struct.unpack(chd_header_v5_str, chd_data_str) + tag = t[0] + length = t[1] + version = t[2] + compressors = t[3] + logicalbytes = t[4] + mapoffset = t[5] + metaoffset = t[6] + hunkbytes = t[7] + unitbytes = t[8] + rawsha1 = binascii.b2a_hex(t[9]) + sha1 = binascii.b2a_hex(t[10]) + parentsha1 = binascii.b2a_hex(t[11]) + + if __debug_this_function: + log_debug('V5 header size = {}'.format(header_size)) + log_debug('tag "{}"'.format(tag)) + log_debug('length {}'.format(length)) + log_debug('version {}'.format(version)) + log_debug('compressors "{}"'.format(compressors)) + log_debug('logicalbytes {}'.format(logicalbytes)) + log_debug('mapoffset {}'.format(mapoffset)) + log_debug('metaoffset {}'.format(metaoffset)) + log_debug('hunkbytes {}'.format(hunkbytes)) + log_debug('unitbytes {}'.format(unitbytes)) + log_debug('rawsha1 "{}"'.format(rawsha1)) + log_debug('sha1 "{}"'.format(sha1)) + log_debug('parentsha1 "{}"'.format(parentsha1)) + + # The CHD SHA1 string storet in MAME -listxml is the sha1 field (combined raw+meta SHA1). + chd_info['status'] = CHD_OK + chd_info['version'] = version + chd_info['sha1'] = sha1 + else: + raise TypeError('Unsuported version = {}'.format(version)) + + return chd_info + +# ------------------------------------------------------------------------------------------------- +# Statistic printing +# ------------------------------------------------------------------------------------------------- +def mame_info_MAME_print(slist, location, machine_name, machine, assets): + slist.append('[COLOR orange]Machine {} / Render data[/COLOR]'.format(machine_name)) + # Print MAME Favourites special fields + if 'ver_mame' in machine: + slist.append("[COLOR slateblue]name[/COLOR]: {}".format(machine['name'])) + if 'ver_mame' in machine: + slist.append("[COLOR slateblue]ver_mame[/COLOR]: {}".format(machine['ver_mame'])) + if 'ver_mame_str' in machine: + slist.append("[COLOR slateblue]ver_mame_str[/COLOR]: {}".format(machine['ver_mame_str'])) + # Most Played Favourites special fields + if 'launch_count' in machine: + slist.append("[COLOR slateblue]launch_count[/COLOR]: {}".format(text_type(machine['launch_count']))) + + # Standard fields in Render database + slist.append("[COLOR violet]cloneof[/COLOR]: '{}'".format(machine['cloneof'])) + slist.append("[COLOR violet]description[/COLOR]: '{}'".format(machine['description'])) + slist.append("[COLOR violet]driver_status[/COLOR]: '{}'".format(machine['driver_status'])) + slist.append("[COLOR violet]genre[/COLOR]: '{}'".format(machine['genre'])) + slist.append("[COLOR skyblue]isBIOS[/COLOR]: {}".format(machine['isBIOS'])) + slist.append("[COLOR skyblue]isDevice[/COLOR]: {}".format(machine['isDevice'])) + slist.append("[COLOR skyblue]isMature[/COLOR]: {}".format(machine['isMature'])) + slist.append("[COLOR violet]manufacturer[/COLOR]: '{}'".format(machine['manufacturer'])) + slist.append("[COLOR violet]nplayers[/COLOR]: '{}'".format(machine['nplayers'])) + slist.append("[COLOR violet]year[/COLOR]: '{}'".format(machine['year'])) + + # Standard fields in Main database + slist.append('\n[COLOR orange]Machine Main data[/COLOR]') + slist.append("[COLOR skyblue]alltime[/COLOR]: {}".format(text_type(machine['alltime']))) + slist.append("[COLOR skyblue]artwork[/COLOR]: {}".format(text_type(machine['artwork']))) + slist.append("[COLOR violet]bestgames[/COLOR]: '{}'".format(machine['bestgames'])) + slist.append("[COLOR skyblue]category[/COLOR]: {}".format(text_type(machine['category']))) + slist.append("[COLOR violet]catlist[/COLOR]: '{}'".format(machine['catlist'])) + slist.append("[COLOR violet]catver[/COLOR]: '{}'".format(machine['catver'])) + slist.append("[COLOR skyblue]chip_cpu_name[/COLOR]: {}".format(text_type(machine['chip_cpu_name']))) + # --- Devices list is a special case --- + if machine['devices']: + for i, device in enumerate(machine['devices']): + slist.append("[COLOR lime]devices[/COLOR][{}]:".format(i)) + slist.append(" [COLOR violet]att_type[/COLOR]: {}".format(device['att_type'])) + slist.append(" [COLOR violet]att_tag[/COLOR]: {}".format(device['att_tag'])) + slist.append(" [COLOR skyblue]att_mandatory[/COLOR]: {}".format(text_type(device['att_mandatory']))) + slist.append(" [COLOR violet]att_interface[/COLOR]: {}".format(device['att_interface'])) + slist.append(" [COLOR skyblue]instance[/COLOR]: {}".format(text_type(device['instance']))) + slist.append(" [COLOR skyblue]ext_names[/COLOR]: {}".format(text_type(device['ext_names']))) + else: + slist.append("[COLOR lime]devices[/COLOR]: []") + slist.append("[COLOR skyblue]display_height[/COLOR]: {}".format(text_type(machine['display_height']))) + slist.append("[COLOR skyblue]display_refresh[/COLOR]: {}".format(text_type(machine['display_refresh']))) + slist.append("[COLOR skyblue]display_rotate[/COLOR]: {}".format(text_type(machine['display_rotate']))) + slist.append("[COLOR skyblue]display_type[/COLOR]: {}".format(text_type(machine['display_type']))) + slist.append("[COLOR skyblue]display_width[/COLOR]: {}".format(text_type(machine['display_width']))) + slist.append("[COLOR violet]genre[/COLOR]: '{}'".format(machine['genre'])) + # --- input is a special case --- + if machine['input']: + # Print attributes + slist.append("[COLOR lime]input[/COLOR]:") + slist.append(" [COLOR skyblue]att_coins[/COLOR]: {}".format(text_type(machine['input']['att_coins']))) + slist.append(" [COLOR skyblue]att_players[/COLOR]: {}".format(text_type(machine['input']['att_players']))) + slist.append(" [COLOR skyblue]att_service[/COLOR]: {}".format(text_type(machine['input']['att_service']))) + slist.append(" [COLOR skyblue]att_tilt[/COLOR]: {}".format(text_type(machine['input']['att_tilt']))) + # Print control tag list + for i, control in enumerate(machine['input']['control_list']): + slist.append("[COLOR lime]control[/COLOR][{}]:".format(i)) + slist.append(" [COLOR violet]type[/COLOR]: {}".format(control['type'])) + slist.append(" [COLOR skyblue]player[/COLOR]: {}".format(text_type(control['player']))) + slist.append(" [COLOR skyblue]buttons[/COLOR]: {}".format(text_type(control['buttons']))) + slist.append(" [COLOR skyblue]ways[/COLOR]: {}".format(text_type(control['ways']))) + else: + slist.append("[COLOR lime]input[/COLOR]: []") + slist.append("[COLOR skyblue]isDead[/COLOR]: {}".format(text_type(machine['isDead']))) + slist.append("[COLOR skyblue]isMechanical[/COLOR]: {}".format(text_type(machine['isMechanical']))) + slist.append("[COLOR violet]romof[/COLOR]: '{}'".format(machine['romof'])) + slist.append("[COLOR violet]sampleof[/COLOR]: '{}'".format(machine['sampleof'])) + slist.append("[COLOR skyblue]series[/COLOR]: '{}'".format(machine['series'])) + slist.append("[COLOR skyblue]softwarelists[/COLOR]: {}".format(text_type(machine['softwarelists']))) + slist.append("[COLOR violet]sourcefile[/COLOR]: '{}'".format(machine['sourcefile'])) + slist.append("[COLOR violet]veradded[/COLOR]: '{}'".format(machine['veradded'])) + + slist.append('\n[COLOR orange]Machine assets/artwork[/COLOR]') + slist.append("[COLOR violet]3dbox[/COLOR]: '{}'".format(assets['3dbox'])) + slist.append("[COLOR violet]artpreview[/COLOR]: '{}'".format(assets['artpreview'])) + slist.append("[COLOR violet]artwork[/COLOR]: '{}'".format(assets['artwork'])) + slist.append("[COLOR violet]cabinet[/COLOR]: '{}'".format(assets['cabinet'])) + slist.append("[COLOR violet]clearlogo[/COLOR]: '{}'".format(assets['clearlogo'])) + slist.append("[COLOR violet]cpanel[/COLOR]: '{}'".format(assets['cpanel'])) + slist.append("[COLOR violet]fanart[/COLOR]: '{}'".format(assets['fanart'])) + slist.append("[COLOR violet]flags[/COLOR]: '{}'".format(assets['flags'])) + slist.append("[COLOR violet]flyer[/COLOR]: '{}'".format(assets['flyer'])) + slist.append("[COLOR violet]history[/COLOR]: '{}'".format(assets['history'])) + slist.append("[COLOR violet]manual[/COLOR]: '{}'".format(assets['manual'])) + slist.append("[COLOR violet]marquee[/COLOR]: '{}'".format(assets['marquee'])) + slist.append("[COLOR violet]PCB[/COLOR]: '{}'".format(assets['PCB'])) + slist.append("[COLOR violet]plot[/COLOR]: '{}'".format(assets['plot'])) + slist.append("[COLOR violet]snap[/COLOR]: '{}'".format(assets['snap'])) + slist.append("[COLOR violet]title[/COLOR]: '{}'".format(assets['title'])) + slist.append("[COLOR violet]trailer[/COLOR]: '{}'".format(assets['trailer'])) + +def mame_info_SL_print(slist, location, SL_name, SL_ROM, rom, assets, SL_dic, SL_machine_list): + # --- ROM stuff --- + slist.append('[COLOR orange]Software List {} Item {}[/COLOR]'.format(SL_name, SL_ROM)) + if 'SL_DB_key' in rom: + slist.append("[COLOR slateblue]SL_DB_key[/COLOR]: '{}'".format(rom['SL_DB_key'])) + if 'SL_ROM_name' in rom: + slist.append("[COLOR slateblue]SL_ROM_name[/COLOR]: '{}'".format(rom['SL_ROM_name'])) + if 'SL_name' in rom: + slist.append("[COLOR slateblue]SL_name[/COLOR]: '{}'".format(rom['SL_name'])) + slist.append("[COLOR violet]cloneof[/COLOR]: '{}'".format(rom['cloneof'])) + slist.append("[COLOR violet]description[/COLOR]: '{}'".format(rom['description'])) + slist.append("[COLOR skyblue]hasCHDs[/COLOR]: {}".format(text_type(rom['hasCHDs']))) + slist.append("[COLOR skyblue]hasROMs[/COLOR]: {}".format(text_type(rom['hasROMs']))) + if 'launch_count' in rom: + slist.append("[COLOR slateblue]launch_count[/COLOR]: '{}'".format(text_type(rom['launch_count']))) + if 'launch_machine' in rom: + slist.append("[COLOR slateblue]launch_machine[/COLOR]: '{}'".format(rom['launch_machine'])) + if rom['parts']: + for i, part in enumerate(rom['parts']): + slist.append("[COLOR lime]parts[/COLOR][{}]:".format(i)) + slist.append(" [COLOR violet]interface[/COLOR]: '{}'".format(part['interface'])) + slist.append(" [COLOR violet]name[/COLOR]: '{}'".format(part['name'])) + else: + slist.append('[COLOR lime]parts[/COLOR]: []') + slist.append("[COLOR violet]plot[/COLOR]: '{}'".format(rom['plot'])) + slist.append("[COLOR violet]publisher[/COLOR]: '{}'".format(rom['publisher'])) + slist.append("[COLOR violet]status_CHD[/COLOR]: '{}'".format(rom['status_CHD'])) + slist.append("[COLOR violet]status_ROM[/COLOR]: '{}'".format(rom['status_ROM'])) + if 'ver_mame' in rom: + slist.append("[COLOR slateblue]ver_mame[/COLOR]: {}".format(rom['ver_mame'])) + if 'ver_mame_str' in rom: + slist.append("[COLOR slateblue]ver_mame_str[/COLOR]: {}".format(rom['ver_mame_str'])) + slist.append("[COLOR violet]year[/COLOR]: '{}'".format(rom['year'])) + + slist.append('\n[COLOR orange]Software List assets[/COLOR]') + slist.append("[COLOR violet]3dbox[/COLOR]: '{}'".format(assets['3dbox'])) + slist.append("[COLOR violet]title[/COLOR]: '{}'".format(assets['title'])) + slist.append("[COLOR violet]snap[/COLOR]: '{}'".format(assets['snap'])) + slist.append("[COLOR violet]boxfront[/COLOR]: '{}'".format(assets['boxfront'])) + slist.append("[COLOR violet]fanart[/COLOR]: '{}'".format(assets['fanart'])) + slist.append("[COLOR violet]trailer[/COLOR]: '{}'".format(assets['trailer'])) + slist.append("[COLOR violet]manual[/COLOR]: '{}'".format(assets['manual'])) + + slist.append('\n[COLOR orange]Software List {}[/COLOR]'.format(SL_name)) + slist.append("[COLOR violet]display_name[/COLOR]: '{}'".format(SL_dic['display_name'])) + slist.append("[COLOR skyblue]num_with_CHDs[/COLOR]: {}".format(text_type(SL_dic['num_with_CHDs']))) + slist.append("[COLOR skyblue]num_with_ROMs[/COLOR]: {}".format(text_type(SL_dic['num_with_ROMs']))) + slist.append("[COLOR violet]rom_DB_noext[/COLOR]: '{}'".format(SL_dic['rom_DB_noext'])) + + slist.append('\n[COLOR orange]Runnable by[/COLOR]') + for machine_dic in sorted(SL_machine_list, key = lambda x: x['description'].lower()): + t = "[COLOR violet]machine[/COLOR]: '{}' [COLOR slateblue]{}[/COLOR]" + slist.append(t.format(machine_dic['description'], machine_dic['machine'])) + +# slist is a list of strings that will be joined like '\n'.join(slist) +# slist is a list, so it is mutable and can be changed by reference. +def mame_stats_main_print_slist(cfg, slist, control_dic, XML_ctrl_dic): + settings = cfg.settings + ctrl = control_dic + SL_str = 'enabled' if settings['global_enable_SL'] else 'disabled' + + slist.append('[COLOR orange]Main information[/COLOR]') + slist.append('AML version {:,} [COLOR violet]{}[/COLOR]'.format( + cfg.addon_version_int, cfg.addon.info_version)) + slist.append('Database version {:,} [COLOR violet]{}[/COLOR]'.format( + ctrl['ver_AML_int'], ctrl['ver_AML_str'])) + slist.append('MAME version {:,} [COLOR violet]{}[/COLOR]'.format( + ctrl['ver_mame_int'], ctrl['ver_mame_str'])) + slist.append('Operation mode [COLOR violet]{:s}[/COLOR]'.format(settings['op_mode'])) + slist.append('Software Lists [COLOR violet]{:s}[/COLOR]'.format(SL_str)) + # Information in the MAME XML control file. + if XML_ctrl_dic['t_XML_extraction']: + slist.append('XML extraction time {}'.format(misc_time_to_str(XML_ctrl_dic['t_XML_extraction']))) + else: + slist.append('XML extraction time {}'.format('no extracted')) + if XML_ctrl_dic['st_mtime']: + slist.append('XML modification time {}'.format(misc_time_to_str(XML_ctrl_dic['st_mtime']))) + else: + slist.append('XML extraction time {}'.format('undefined')) + if XML_ctrl_dic['t_XML_preprocessing']: + slist.append('XML preprocess time {}'.format(misc_time_to_str(XML_ctrl_dic['t_XML_preprocessing']))) + else: + slist.append('XML extraction time {}'.format('undefined')) + slist.append('XML size {:,} bytes'.format(XML_ctrl_dic['st_size'])) + slist.append('XML machine count {:,} machines'.format(XML_ctrl_dic['total_machines'])) + + slist.append('') + slist.append('[COLOR orange]MAME machine count[/COLOR]') + table_str = [] + table_str.append(['left', 'right', 'right', 'right']) + table_str.append(['Type', 'Total', 'Parent', 'Clones']) + table_str.append([ + 'Machines', + '{:6,d}'.format(control_dic['stats_processed_machines']), + '{:6,d}'.format(control_dic['stats_parents']), + '{:6,d}'.format(control_dic['stats_clones']), + ]) + table_str.append([ + 'Runnable', + '{:6,d}'.format(control_dic['stats_runnable']), + '{:6,d}'.format(control_dic['stats_runnable_parents']), + '{:6,d}'.format(control_dic['stats_runnable_clones']), + ]) + table_str.append([ + 'Coin', + '{:6,d}'.format(control_dic['stats_coin']), + '{:6,d}'.format(control_dic['stats_coin_parents']), + '{:6,d}'.format(control_dic['stats_coin_clones']), + ]) + table_str.append([ + 'Nocoin', + '{:6,d}'.format(control_dic['stats_nocoin']), + '{:6,d}'.format(control_dic['stats_nocoin_parents']), + '{:6,d}'.format(control_dic['stats_nocoin_clones']), + ]) + table_str.append([ + 'Mechanical', + '{:6,d}'.format(control_dic['stats_mechanical']), + '{:6,d}'.format(control_dic['stats_mechanical_parents']), + '{:6,d}'.format(control_dic['stats_mechanical_clones']), + ]) + table_str.append([ + 'Dead', + '{:6,d}'.format(control_dic['stats_dead']), + '{:6,d}'.format(control_dic['stats_dead_parents']), + '{:6,d}'.format(control_dic['stats_dead_clones']), + ]) + table_str.append([ + 'Devices', + '{:6,d}'.format(control_dic['stats_devices']), + '{:6,d}'.format(control_dic['stats_devices_parents']), + '{:6,d}'.format(control_dic['stats_devices_clones']), + ]) + # Binary filters + table_str.append([ + 'BIOS', + '{:6,d}'.format(control_dic['stats_BIOS']), + '{:6,d}'.format(control_dic['stats_BIOS_parents']), + '{:6,d}'.format(control_dic['stats_BIOS_clones']), + ]) + table_str.append([ + 'Samples', + '{:6,d}'.format(control_dic['stats_samples']), + '{:6,d}'.format(control_dic['stats_samples_parents']), + '{:6,d}'.format(control_dic['stats_samples_clones']), + ]) + slist.extend(text_render_table(table_str)) + + slist.append('') + slist.append('[COLOR orange]MAME machine statistics[/COLOR]') + table_str = [] + table_str.append(['left', 'right', 'right', 'right', 'right', 'right', 'right', 'right', 'right']) + table_str.append(['Type (parents/total)', 'Total', '', 'Good', '', 'Imperfect', '', 'Nonworking', '']) + table_str.append(['Coin slot (Normal)', + '{:,}'.format(control_dic['stats_MF_Normal_Total_parents']), + '{:,}'.format(control_dic['stats_MF_Normal_Total']), + '{:,}'.format(control_dic['stats_MF_Normal_Good_parents']), + '{:,}'.format(control_dic['stats_MF_Normal_Good']), + '{:,}'.format(control_dic['stats_MF_Normal_Imperfect_parents']), + '{:,}'.format(control_dic['stats_MF_Normal_Imperfect']), + '{:,}'.format(control_dic['stats_MF_Normal_Nonworking_parents']), + '{:,}'.format(control_dic['stats_MF_Normal_Nonworking']), + ]) + table_str.append(['Coin slot (Unusual)', + '{:,}'.format(control_dic['stats_MF_Unusual_Total_parents']), + '{:,}'.format(control_dic['stats_MF_Unusual_Total']), + '{:,}'.format(control_dic['stats_MF_Unusual_Good_parents']), + '{:,}'.format(control_dic['stats_MF_Unusual_Good']), + '{:,}'.format(control_dic['stats_MF_Unusual_Imperfect_parents']), + '{:,}'.format(control_dic['stats_MF_Unusual_Imperfect']), + '{:,}'.format(control_dic['stats_MF_Unusual_Nonworking_parents']), + '{:,}'.format(control_dic['stats_MF_Unusual_Nonworking']), + ]) + table_str.append(['No coin slot', + '{:,}'.format(control_dic['stats_MF_Nocoin_Total_parents']), + '{:,}'.format(control_dic['stats_MF_Nocoin_Total']), + '{:,}'.format(control_dic['stats_MF_Nocoin_Good_parents']), + '{:,}'.format(control_dic['stats_MF_Nocoin_Good']), + '{:,}'.format(control_dic['stats_MF_Nocoin_Imperfect_parents']), + '{:,}'.format(control_dic['stats_MF_Nocoin_Imperfect']), + '{:,}'.format(control_dic['stats_MF_Nocoin_Nonworking_parents']), + '{:,}'.format(control_dic['stats_MF_Nocoin_Nonworking']), + ]) + table_str.append(['Mechanical machines', + '{:,}'.format(control_dic['stats_MF_Mechanical_Total_parents']), + '{:,}'.format(control_dic['stats_MF_Mechanical_Total']), + '{:,}'.format(control_dic['stats_MF_Mechanical_Good_parents']), + '{:,}'.format(control_dic['stats_MF_Mechanical_Good']), + '{:,}'.format(control_dic['stats_MF_Mechanical_Imperfect_parents']), + '{:,}'.format(control_dic['stats_MF_Mechanical_Imperfect']), + '{:,}'.format(control_dic['stats_MF_Mechanical_Nonworking_parents']), + '{:,}'.format(control_dic['stats_MF_Mechanical_Nonworking']), + ]) + table_str.append(['Dead machines', + '{:,}'.format(control_dic['stats_MF_Dead_Total_parents']), + '{:,}'.format(control_dic['stats_MF_Dead_Total']), + '{:,}'.format(control_dic['stats_MF_Dead_Good_parents']), + '{:,}'.format(control_dic['stats_MF_Dead_Good']), + '{:,}'.format(control_dic['stats_MF_Dead_Imperfect_parents']), + '{:,}'.format(control_dic['stats_MF_Dead_Imperfect']), + '{:,}'.format(control_dic['stats_MF_Dead_Nonworking_parents']), + '{:,}'.format(control_dic['stats_MF_Dead_Nonworking']), + ]) + table_str.append(['Device machines', + '{:,}'.format(control_dic['stats_devices_parents']), + '{:,}'.format(control_dic['stats_devices']), + 'N/A', 'N/A', 'N/A', 'N/A', 'N/A', 'N/A']) + slist.extend(text_render_table(table_str)) + + if settings['global_enable_SL']: + slist.append('\n[COLOR orange]Software Lists item count[/COLOR]') + slist.append("SL XML files {:7,d}".format(control_dic['stats_SL_XML_files'])) + slist.append("SL software items {:7,d}".format(control_dic['stats_SL_software_items'])) + slist.append("SL items with ROMs {:7,d}".format(control_dic['stats_SL_items_with_ROMs'])) + slist.append("SL items with CHDs {:7,d}".format(control_dic['stats_SL_items_with_CHDs'])) + +def mame_stats_scanner_print_slist(cfg, slist, control_dic): + settings = cfg.settings + # MAME statistics + slist.append('[COLOR orange]MAME scanner information[/COLOR]') + t_str = [ + ['left', 'right', 'right', 'right'], + ['Stat', 'Total', 'Have', 'Missing'], + ['ROM ZIP files', + '{:,}'.format(control_dic['scan_ROM_ZIP_files_total']), + '{:,}'.format(control_dic['scan_ROM_ZIP_files_have']), + '{:,}'.format(control_dic['scan_ROM_ZIP_files_missing'])], + ['Sample ZIP files', + '{:,}'.format(control_dic['scan_Samples_ZIP_total']), + '{:,}'.format(control_dic['scan_Samples_ZIP_have']), + '{:,}'.format(control_dic['scan_Samples_ZIP_missing'])], + ['CHD files', + '{:,}'.format(control_dic['scan_CHD_files_total']), + '{:,}'.format(control_dic['scan_CHD_files_have']), + '{:,}'.format(control_dic['scan_CHD_files_missing'])], + ] + slist.extend(text_render_table(t_str)) + + slist.append('') + t_str = [ + ['left', 'right', 'right', 'right'], + ['Stat', 'Can run', 'Out of', 'Unrunnable'], + ] + t_str.append(['ROM machines', + '{:,}'.format(control_dic['scan_machine_archives_ROM_have']), + '{:,}'.format(control_dic['scan_machine_archives_ROM_total']), + '{:,}'.format(control_dic['scan_machine_archives_ROM_missing']), + ]) + t_str.append(['Sample machines', + '{:,}'.format(control_dic['scan_machine_archives_Samples_have']), + '{:,}'.format(control_dic['scan_machine_archives_Samples_total']), + '{:,}'.format(control_dic['scan_machine_archives_Samples_missing']), + ]) + t_str.append(['CHD machines', + '{:,}'.format(control_dic['scan_machine_archives_CHD_have']), + '{:,}'.format(control_dic['scan_machine_archives_CHD_total']), + '{:,}'.format(control_dic['scan_machine_archives_CHD_missing']), + ]) + slist.extend(text_render_table(t_str)) + + # SL scanner statistics + if settings['global_enable_SL']: + slist.append('') + slist.append('[COLOR orange]Software List scanner information[/COLOR]') + t_str = [ + ['left', 'right', 'right', 'right'], + ['Stat', 'Total', 'Have', 'Missing'], + ] + t_str.append(['SL ROMs', + '{:,}'.format(control_dic['scan_SL_archives_ROM_total']), + '{:,}'.format(control_dic['scan_SL_archives_ROM_have']), + '{:,}'.format(control_dic['scan_SL_archives_ROM_missing']), + ]) + t_str.append(['SL CHDs', + '{:,}'.format(control_dic['scan_SL_archives_CHD_total']), + '{:,}'.format(control_dic['scan_SL_archives_CHD_have']), + '{:,}'.format(control_dic['scan_SL_archives_CHD_missing']), + ]) + slist.extend(text_render_table(t_str)) + + # --- MAME asset scanner --- + slist.append('') + slist.append('[COLOR orange]MAME asset scanner information[/COLOR]') + # slist.append('Total number of MAME machines {0:,d}'.format(control_dic['assets_num_MAME_machines'])) + t_str = [ + ['left', 'right', 'right', 'right'], + ['Stat', 'Have', 'Missing', 'Alternate'], + ] + t_str.append(['3D Boxes', + '{:,}'.format(control_dic['assets_3dbox_have']), + '{:,}'.format(control_dic['assets_3dbox_missing']), + '{:,}'.format(control_dic['assets_3dbox_alternate']), + ]) + t_str.append(['Artpreviews', + '{:,}'.format(control_dic['assets_artpreview_have']), + '{:,}'.format(control_dic['assets_artpreview_missing']), + '{:,}'.format(control_dic['assets_artpreview_alternate']), + ]) + t_str.append(['Artwork', + '{:,}'.format(control_dic['assets_artwork_have']), + '{:,}'.format(control_dic['assets_artwork_missing']), + '{:,}'.format(control_dic['assets_artwork_alternate']), + ]) + t_str.append(['Cabinets', + '{:,}'.format(control_dic['assets_cabinets_have']), + '{:,}'.format(control_dic['assets_cabinets_missing']), + '{:,}'.format(control_dic['assets_cabinets_alternate']), + ]) + t_str.append(['Clearlogos', + '{:,}'.format(control_dic['assets_clearlogos_have']), + '{:,}'.format(control_dic['assets_clearlogos_missing']), + '{:,}'.format(control_dic['assets_clearlogos_alternate']), + ]) + t_str.append(['CPanels', + '{:,}'.format(control_dic['assets_cpanels_have']), + '{:,}'.format(control_dic['assets_cpanels_missing']), + '{:,}'.format(control_dic['assets_cpanels_alternate']), + ]) + t_str.append(['Fanart', + '{:,}'.format(control_dic['assets_fanarts_have']), + '{:,}'.format(control_dic['assets_fanarts_missing']), + '{:,}'.format(control_dic['assets_fanarts_alternate']), + ]) + t_str.append(['Flyers', + '{:,}'.format(control_dic['assets_flyers_have']), + '{:,}'.format(control_dic['assets_flyers_missing']), + '{:,}'.format(control_dic['assets_flyers_alternate']), + ]) + t_str.append(['Manuals', + '{:,}'.format(control_dic['assets_manuals_have']), + '{:,}'.format(control_dic['assets_manuals_missing']), + '{:,}'.format(control_dic['assets_manuals_alternate']), + ]) + t_str.append(['Marquees', + '{:,}'.format(control_dic['assets_marquees_have']), + '{:,}'.format(control_dic['assets_marquees_missing']), + '{:,}'.format(control_dic['assets_marquees_alternate']), + ]) + t_str.append(['PCBs', + '{:,}'.format(control_dic['assets_PCBs_have']), + '{:,}'.format(control_dic['assets_PCBs_missing']), + '{:,}'.format(control_dic['assets_PCBs_alternate']), + ]) + t_str.append(['Snaps', + '{:,}'.format(control_dic['assets_snaps_have']), + '{:,}'.format(control_dic['assets_snaps_missing']), + '{:,}'.format(control_dic['assets_snaps_alternate']), + ]) + t_str.append(['Titles', + '{:,}'.format(control_dic['assets_titles_have']), + '{:,}'.format(control_dic['assets_titles_missing']), + '{:,}'.format(control_dic['assets_titles_alternate']), + ]) + t_str.append(['Trailers', + '{:,}'.format(control_dic['assets_trailers_have']), + '{:,}'.format(control_dic['assets_trailers_missing']), + '{:,}'.format(control_dic['assets_trailers_alternate']), + ]) + slist.extend(text_render_table(t_str)) + + # --- Software List scanner --- + if settings['global_enable_SL']: + slist.append('') + slist.append('[COLOR orange]Software List asset scanner information[/COLOR]') + # slist.append('Total number of SL items {0:,d}'.format(control_dic['assets_SL_num_items'])) + t_str = [ + ['left', 'right', 'right', 'right'], + ['Stat', 'Have', 'Missing', 'Alternate'], + ] + t_str.append(['3D Boxes', + '{:,}'.format(control_dic['assets_SL_3dbox_have']), + '{:,}'.format(control_dic['assets_SL_3dbox_missing']), + '{:,}'.format(control_dic['assets_SL_3dbox_alternate']), + ]) + t_str.append(['Titles', + '{:,}'.format(control_dic['assets_SL_titles_have']), + '{:,}'.format(control_dic['assets_SL_titles_missing']), + '{:,}'.format(control_dic['assets_SL_titles_alternate']), + ]) + t_str.append(['Snaps', + '{:,}'.format(control_dic['assets_SL_snaps_have']), + '{:,}'.format(control_dic['assets_SL_snaps_missing']), + '{:,}'.format(control_dic['assets_SL_snaps_alternate']), + ]) + t_str.append(['Boxfronts', + '{:,}'.format(control_dic['assets_SL_boxfronts_have']), + '{:,}'.format(control_dic['assets_SL_boxfronts_missing']), + '{:,}'.format(control_dic['assets_SL_boxfronts_alternate']), + ]) + t_str.append(['Fanarts', + '{:,}'.format(control_dic['assets_SL_fanarts_have']), + '{:,}'.format(control_dic['assets_SL_fanarts_missing']), + '{:,}'.format(control_dic['assets_SL_fanarts_alternate']), + ]) + t_str.append(['Trailers', + '{:,}'.format(control_dic['assets_SL_trailers_have']), + '{:,}'.format(control_dic['assets_SL_trailers_missing']), + '{:,}'.format(control_dic['assets_SL_trailers_alternate']), + ]) + t_str.append(['Manuals', + '{:,}'.format(control_dic['assets_SL_manuals_have']), + '{:,}'.format(control_dic['assets_SL_manuals_missing']), + '{:,}'.format(control_dic['assets_SL_manuals_alternate']), + ]) + slist.extend(text_render_table(t_str)) + +def mame_stats_audit_print_slist(cfg, slist, control_dic): + settings = cfg.settings + rom_set = ['Merged', 'Split', 'Non-merged'][settings['mame_rom_set']] + chd_set = ['Merged', 'Split', 'Non-merged'][settings['mame_chd_set']] + + slist.append('[COLOR orange]MAME ROM audit database statistics[/COLOR]') + t = "{:7,d} runnable MAME machines" + slist.append(t.format(control_dic['stats_audit_MAME_machines_runnable'])) + t = "{:7,d} machines require ROM ZIPs, {:7,d} parents and {:7,d} clones" + slist.append(t.format(control_dic['stats_audit_machine_archives_ROM'], + control_dic['stats_audit_machine_archives_ROM_parents'], + control_dic['stats_audit_machine_archives_ROM_clones'])) + t = "{:7,d} machines require CHDs, {:7,d} parents and {:7,d} clones" + slist.append(t.format(control_dic['stats_audit_machine_archives_CHD'], + control_dic['stats_audit_machine_archives_CHD_parents'], + control_dic['stats_audit_machine_archives_CHD_clones'])) + t = "{:7,d} machines require Samples, {:7,d} parents and {:7,d} clones" + slist.append(t.format(control_dic['stats_audit_machine_archives_Samples'], + control_dic['stats_audit_machine_archives_Samples_parents'], + control_dic['stats_audit_machine_archives_Samples_clones'])) + t = "{:7,d} machines require nothing, {:7,d} parents and {:7,d} clones" + slist.append(t.format(control_dic['stats_audit_archive_less'], + control_dic['stats_audit_archive_less_parents'], + control_dic['stats_audit_archive_less_clones'])) + + t = "{:7,d} ROM ZIPs in the [COLOR darkorange]{}[/COLOR] set" + slist.append(t.format(control_dic['stats_audit_MAME_ROM_ZIP_files'], rom_set)) + t = "{:7,d} CHDs in the [COLOR darkorange]{}[/COLOR] set" + slist.append(t.format(control_dic['stats_audit_MAME_CHD_files'], chd_set)) + t = "{:7,d} Sample ZIPs in the [COLOR darkorange]{}[/COLOR] set" + slist.append(t.format(control_dic['stats_audit_MAME_Sample_ZIP_files'], rom_set)) + + t = "{:7,d} total ROMs, {:7,d} valid and {:7,d} invalid" + slist.append(t.format( + control_dic['stats_audit_ROMs_total'], + control_dic['stats_audit_ROMs_valid'], + control_dic['stats_audit_ROMs_invalid'], + )) + t = "{:7,d} total CHDs, {:7,d} valid and {:7,d} invalid" + slist.append(t.format( + control_dic['stats_audit_CHDs_total'], + control_dic['stats_audit_CHDs_valid'], + control_dic['stats_audit_CHDs_invalid'], + )) + + # SL item audit database statistics + if settings['global_enable_SL']: + slist.append('\n[COLOR orange]SL audit database statistics[/COLOR]') + t = "{:7,d} runnable Software List items" + slist.append(t.format(control_dic['stats_audit_SL_items_runnable'])) + t = "{:7,d} SL items require ROM ZIPs and/or CHDs" + slist.append(t.format(control_dic['stats_audit_SL_items_with_arch'])) + t = "{:7,d} SL items require ROM ZIPs" + slist.append(t.format(control_dic['stats_audit_SL_items_with_arch_ROM'])) + t = "{:7,d} SL items require CHDs" + slist.append(t.format(control_dic['stats_audit_SL_items_with_CHD'])) + + # MAME audit summary. + slist.append('\n[COLOR orange]MAME ROM audit information[/COLOR]') + table_str = [ + ['left', 'right', 'right', 'right'], + ['Type', 'Total', 'Good', 'Bad'], + ] + table_str.append([ + 'Machines with ROMs and/or CHDs', + '{:,d}'.format(control_dic['audit_MAME_machines_with_arch']), + '{:,d}'.format(control_dic['audit_MAME_machines_with_arch_OK']), + '{:,d}'.format(control_dic['audit_MAME_machines_with_arch_BAD']), + ]) + table_str.append([ + 'Machines with ROMs', + '{:,d}'.format(control_dic['audit_MAME_machines_with_ROMs']), + '{:,d}'.format(control_dic['audit_MAME_machines_with_ROMs_OK']), + '{:,d}'.format(control_dic['audit_MAME_machines_with_ROMs_BAD']), + ]) + table_str.append([ + 'Machines with CHDs', + '{:,d}'.format(control_dic['audit_MAME_machines_with_CHDs']), + '{:,d}'.format(control_dic['audit_MAME_machines_with_CHDs_OK']), + '{:,d}'.format(control_dic['audit_MAME_machines_with_CHDs_BAD']), + ]) + table_str.append([ + 'Machines with Samples', + '{:,d}'.format(control_dic['audit_MAME_machines_with_SAMPLES']), + '{:,d}'.format(control_dic['audit_MAME_machines_with_SAMPLES_OK']), + '{:,d}'.format(control_dic['audit_MAME_machines_with_SAMPLES_BAD']), + ]) + slist.extend(text_render_table(table_str)) + + # SL audit summary. + if settings['global_enable_SL']: + slist.append('\n[COLOR orange]SL audit information[/COLOR]') + table_str = [ + ['left', 'right', 'right', 'right'], + ['Type', 'Total', 'Good', 'Bad'], + ] + table_str.append([ + 'SL items with ROMs and/or CHDs', + '{:,d}'.format(control_dic['audit_SL_items_with_arch']), + '{:,d}'.format(control_dic['audit_SL_items_with_arch_OK']), + '{:,d}'.format(control_dic['audit_SL_items_with_arch_BAD']), + ]) + table_str.append([ + 'SL items with ROMs', + '{:,d}'.format(control_dic['audit_SL_items_with_arch_ROM']), + '{:,d}'.format(control_dic['audit_SL_items_with_arch_ROM_OK']), + '{:,d}'.format(control_dic['audit_SL_items_with_arch_ROM_BAD']), + ]) + table_str.append([ + 'SL items with CHDs', + '{:,d}'.format(control_dic['audit_SL_items_with_CHD']), + '{:,d}'.format(control_dic['audit_SL_items_with_CHD_OK']), + '{:,d}'.format(control_dic['audit_SL_items_with_CHD_BAD']), + ]) + slist.extend(text_render_table(table_str)) + +def mame_stats_timestamps_slist(cfg, slist, control_dic): + settings = cfg.settings + # DAT/INI file versions. Note than in some DAT/INIs the version is not available. + slist.append('[COLOR orange]DAT/INI versions[/COLOR]') + slist.append("Alltime.ini version {}".format(control_dic['ver_alltime'])) + slist.append("Artwork.ini version {}".format(control_dic['ver_artwork'])) + slist.append("bestgames.ini version {}".format(control_dic['ver_bestgames'])) + slist.append("Category.ini version {}".format(control_dic['ver_category'])) + slist.append("catlist.ini version {}".format(control_dic['ver_catlist'])) + slist.append("catver.ini version {}".format(control_dic['ver_catver'])) + slist.append("command.dat version {}".format(control_dic['ver_command'])) + slist.append("gameinit.dat version {}".format(control_dic['ver_gameinit'])) + slist.append("genre.ini version {}".format(control_dic['ver_genre'])) + slist.append("history.dat version {}".format(control_dic['ver_history'])) + slist.append("mameinfo.dat version {}".format(control_dic['ver_mameinfo'])) + slist.append("mature.ini version {}".format(control_dic['ver_mature'])) + slist.append("nplayers.ini version {}".format(control_dic['ver_nplayers'])) + slist.append("series.ini version {}".format(control_dic['ver_series'])) + + # Timestamps ordered if user selects "All in one step" + slist.append('') + slist.append('[COLOR orange]Timestamps[/COLOR]') + # MAME and SL databases. + if control_dic['t_MAME_DB_build']: + slist.append("MAME DB built on {}".format(misc_time_to_str(control_dic['t_MAME_DB_build']))) + else: + slist.append("MAME DB never built") + if control_dic['t_MAME_Audit_DB_build']: + slist.append("MAME Audit DB built on {}".format(misc_time_to_str(control_dic['t_MAME_Audit_DB_build']))) + else: + slist.append("MAME Audit DB never built") + if control_dic['t_MAME_Catalog_build']: + slist.append("MAME Catalog built on {}".format(misc_time_to_str(control_dic['t_MAME_Catalog_build']))) + else: + slist.append("MAME Catalog never built") + if control_dic['t_SL_DB_build']: + slist.append("SL DB built on {}".format(misc_time_to_str(control_dic['t_SL_DB_build']))) + else: + slist.append("SL DB never built") + + # MAME and SL scanner. + if control_dic['t_MAME_ROMs_scan']: + slist.append("MAME ROMs scaned on {}".format(misc_time_to_str(control_dic['t_MAME_ROMs_scan']))) + else: + slist.append("MAME ROMs never scaned") + if control_dic['t_MAME_assets_scan']: + slist.append("MAME assets scaned on {}".format(misc_time_to_str(control_dic['t_MAME_assets_scan']))) + else: + slist.append("MAME assets never scaned") + + if control_dic['t_SL_ROMs_scan']: + slist.append("SL ROMs scaned on {}".format(misc_time_to_str(control_dic['t_SL_ROMs_scan']))) + else: + slist.append("SL ROMs never scaned") + if control_dic['t_SL_assets_scan']: + slist.append("SL assets scaned on {}".format(misc_time_to_str(control_dic['t_SL_assets_scan']))) + else: + slist.append("SL assets never scaned") + + # Plots, Fanarts and 3D Boxes. + if control_dic['t_MAME_plots_build']: + slist.append("MAME Plots built on {}".format(misc_time_to_str(control_dic['t_MAME_plots_build']))) + else: + slist.append("MAME Plots never built") + if control_dic['t_SL_plots_build']: + slist.append("SL Plots built on {}".format(misc_time_to_str(control_dic['t_SL_plots_build']))) + else: + slist.append("SL Plots never built") + + if control_dic['t_MAME_fanart_build']: + slist.append("MAME Fanarts built on {}".format(misc_time_to_str(control_dic['t_MAME_fanart_build']))) + else: + slist.append("MAME Fanarts never built") + if control_dic['t_SL_fanart_build']: + slist.append("SL Fanarts built on {}".format(misc_time_to_str(control_dic['t_SL_fanart_build']))) + else: + slist.append("SL Fanarts never built") + + if control_dic['t_MAME_3dbox_build']: + slist.append("MAME 3D Boxes built on {}".format(misc_time_to_str(control_dic['t_MAME_3dbox_build']))) + else: + slist.append("MAME 3D Boxes never built") + if control_dic['t_SL_3dbox_build']: + slist.append("SL 3D Boxes built on {}".format(misc_time_to_str(control_dic['t_SL_3dbox_build']))) + else: + slist.append("SL 3D Boxes never built") + + # MAME machine hash, asset hash, render cache and asset cache. + if control_dic['t_MAME_machine_hash']: + slist.append("MAME machine hash built on {}".format(misc_time_to_str(control_dic['t_MAME_machine_hash']))) + else: + slist.append("MAME machine hash never built") + if control_dic['t_MAME_asset_hash']: + slist.append("MAME asset hash built on {}".format(misc_time_to_str(control_dic['t_MAME_asset_hash']))) + else: + slist.append("MAME asset hash never built") + if control_dic['t_MAME_render_cache_build']: + slist.append("MAME render cache built on {}".format(misc_time_to_str(control_dic['t_MAME_render_cache_build']))) + else: + slist.append("MAME render cache never built") + if control_dic['t_MAME_asset_cache_build']: + slist.append("MAME asset cache built on {}".format(misc_time_to_str(control_dic['t_MAME_asset_cache_build']))) + else: + slist.append("MAME asset cache never built") + + # Custsom filters. + if control_dic['t_Custom_Filter_build']: + slist.append("Custom filters built on {}".format(misc_time_to_str(control_dic['t_Custom_Filter_build']))) + else: + slist.append("Custom filters never built") + + # Audit stuff. + if control_dic['t_MAME_audit']: + slist.append("MAME ROMs audited on {}".format(misc_time_to_str(control_dic['t_MAME_audit']))) + else: + slist.append("MAME ROMs never audited") + if control_dic['t_SL_audit']: + slist.append("SL ROMs audited on {}".format(misc_time_to_str(control_dic['t_SL_audit']))) + else: + slist.append("SL ROMs never audited") + +# ------------------------------------------------------------------------------------------------- +# Check/Update/Repair Favourite ROM objects +# ------------------------------------------------------------------------------------------------- +def mame_update_MAME_Fav_objects(cfg, db_dic): + control_dic = db_dic['control_dic'] + machines = db_dic['machines'] + renderdb_dic = db_dic['renderdb'] + assets_dic = db_dic['assetdb'] + fav_machines = utils_load_JSON_file(cfg.FAV_MACHINES_PATH.getPath()) + # If no MAME Favourites return + if len(fav_machines) < 1: + kodi_notify('MAME Favourites empty') + return + iteration = 0 + d_text = 'Checking/Updating MAME Favourites...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(fav_machines)) + for fav_key in sorted(fav_machines): + log_debug('Checking machine "{}"'.format(fav_key)) + if fav_key in machines: + machine = machines[fav_key] + render = renderdb_dic[fav_key] + assets = assets_dic[fav_key] + else: + # Machine not found in DB. Create an empty one to update the database fields. + # The user can delete it later. + log_debug('Machine "{}" not found in MAME main DB'.format(fav_key)) + machine = db_new_machine_dic() + render = db_new_machine_render_dic() + assets = db_new_MAME_asset() + # Change plot to warn user this machine is not found in database. + t = 'Machine {} missing'.format(fav_key) + render['description'] = t + assets['plot'] = t + new_fav = db_get_MAME_Favourite_full(fav_key, machine, render, assets, control_dic) + fav_machines[fav_key] = new_fav + log_debug('Updated machine "{}"'.format(fav_key)) + iteration += 1 + pDialog.updateProgress(iteration) + utils_write_JSON_file(cfg.FAV_MACHINES_PATH.getPath(), fav_machines) + pDialog.endProgress() + +def mame_update_MAME_MostPlay_objects(cfg, db_dic): + control_dic = db_dic['control_dic'] + machines = db_dic['machines'] + renderdb_dic = db_dic['renderdb'] + assets_dic = db_dic['assetdb'] + most_played_roms_dic = utils_load_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath()) + if len(most_played_roms_dic) < 1: + kodi_notify('MAME Most Played empty') + return + iteration = 0 + pDialog = KodiProgressDialog() + pDialog.startProgress('Checking/Updating MAME Most Played machines...', len(most_played_roms_dic)) + for fav_key in sorted(most_played_roms_dic): + log_debug('Checking machine "{}"'.format(fav_key)) + if 'launch_count' in most_played_roms_dic[fav_key]: + launch_count = most_played_roms_dic[fav_key]['launch_count'] + else: + launch_count = 1 + if fav_key in machines: + machine = machines[fav_key] + render = renderdb_dic[fav_key] + assets = assets_dic[fav_key] + else: + log_debug('Machine "{}" not found in MAME main DB'.format(fav_key)) + machine = db_new_machine_dic() + render = db_new_machine_render_dic() + assets = db_new_MAME_asset() + t = 'Machine {} missing'.format(fav_key) + render['description'] = t + assets['plot'] = t + new_fav = db_get_MAME_Favourite_full(fav_key, machine, render, assets, control_dic) + new_fav['launch_count'] = launch_count + most_played_roms_dic[fav_key] = new_fav + log_debug('Updated machine "{}"'.format(fav_key)) + iteration += 1 + pDialog.updateProgress(iteration) + utils_write_JSON_file(cfg.MAME_MOST_PLAYED_FILE_PATH.getPath(), most_played_roms_dic) + pDialog.endProgress() + +def mame_update_MAME_RecentPlay_objects(cfg, db_dic): + control_dic = db_dic['control_dic'] + machines = db_dic['machines'] + renderdb_dic = db_dic['renderdb'] + assets_dic = db_dic['assetdb'] + recent_roms_list = utils_load_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), []) + if len(recent_roms_list) < 1: + kodi_notify('MAME Recently Played empty') + return + iteration = 0 + pDialog = KodiProgressDialog() + pDialog.startProgress('Checking/Updating MAME Recently Played machines...', len(recent_roms_list)) + for i, recent_rom in enumerate(recent_roms_list): + fav_key = recent_rom['name'] + log_debug('Checking machine "{}"'.format(fav_key)) + if fav_key in machines: + machine = machines[fav_key] + render = renderdb_dic[fav_key] + assets = assets_dic[fav_key] + else: + log_debug('Machine "{}" not found in MAME main DB'.format(fav_key)) + machine = db_new_machine_dic() + render = db_new_machine_render_dic() + assets = db_new_MAME_asset() + t = 'Machine {} missing'.format(fav_key) + render['description'] = t + assets['plot'] = t + new_fav = db_get_MAME_Favourite_full(fav_key, machine, render, assets, control_dic) + recent_roms_list[i] = new_fav + log_debug('Updated machine "{}"'.format(fav_key)) + iteration += 1 + pDialog.updateProgress(iteration) + utils_write_JSON_file(cfg.MAME_RECENT_PLAYED_FILE_PATH.getPath(), recent_roms_list) + pDialog.endProgress() + +def mame_update_SL_Fav_objects(cfg, db_dic): + control_dic = db_dic['control_dic'] + SL_index = db_dic['SL_index'] + pDialog = KodiProgressDialog() + pDialog.startProgress('Loading SL Most Played JSON DB...') + fav_SL_roms = utils_load_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath()) + if len(fav_SL_roms) < 1: + kodi_notify_warn('SL Most Played empty') + return + pDialog.resetProgress('Checking SL Favourites', len(fav_SL_roms)) + for fav_SL_key in sorted(fav_SL_roms): + if 'ROM_name' in fav_SL_roms[fav_SL_key]: + fav_ROM_name = fav_SL_roms[fav_SL_key]['ROM_name'] + elif 'SL_ROM_name' in fav_SL_roms[fav_SL_key]: + fav_ROM_name = fav_SL_roms[fav_SL_key]['SL_ROM_name'] + else: + raise TypeError('Cannot find SL ROM name') + fav_SL_name = fav_SL_roms[fav_SL_key]['SL_name'] + log_debug('Checking SL Favourite "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + pDialog.updateProgressInc('Checking SL Favourites...\nItem "{}"'.format(fav_ROM_name)) + + # --- Load SL ROMs DB and assets --- + file_name = SL_index[fav_SL_name]['rom_DB_noext'] + '_items.json' + SL_DB_FN = cfg.SL_DB_DIR.pjoin(file_name) + assets_file_name = SL_index[fav_SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + SL_assets_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath(), verbose = False) + + # --- Check --- + if fav_ROM_name in SL_roms: + SL_ROM = SL_roms[fav_ROM_name] + SL_assets = SL_assets_dic[fav_ROM_name] + else: + # Machine not found in DB. Create an empty one to update the database fields. + # The user can delete it later. + log_debug('Machine "{}" / "{}" not found in SL main DB'.format(fav_ROM_name, fav_SL_name)) + SL_ROM = db_new_SL_ROM() + SL_assets = db_new_SL_asset() + # Change plot to warn user this machine is not found in database. + t = 'Item "{}" missing'.format(fav_ROM_name) + SL_ROM['description'] = t + SL_ROM['plot'] = t + new_fav_ROM = db_get_SL_Favourite(fav_SL_name, fav_ROM_name, SL_ROM, SL_assets, control_dic) + fav_SL_roms[fav_SL_key] = new_fav_ROM + log_debug('Updated SL Favourite "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + utils_write_JSON_file(cfg.FAV_SL_ROMS_PATH.getPath(), fav_SL_roms) + pDialog.endProgress() + +def mame_update_SL_MostPlay_objects(cfg, db_dic): + control_dic = db_dic['control_dic'] + SL_index = db_dic['SL_index'] + pDialog = KodiProgressDialog() + pDialog.startProgress('Loading SL Most Played JSON DB...') + most_played_roms_dic = utils_load_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath()) + if len(most_played_roms_dic) < 1: + kodi_notify_warn('SL Most Played empty') + return + pDialog.resetProgress('Checking SL Most Played', len(most_played_roms_dic)) + for fav_SL_key in sorted(most_played_roms_dic): + if 'ROM_name' in most_played_roms_dic[fav_SL_key]: + fav_ROM_name = most_played_roms_dic[fav_SL_key]['ROM_name'] + elif 'SL_ROM_name' in most_played_roms_dic[fav_SL_key]: + fav_ROM_name = most_played_roms_dic[fav_SL_key]['SL_ROM_name'] + else: + raise TypeError('Cannot find SL ROM name') + if 'launch_count' in most_played_roms_dic[fav_SL_key]: + launch_count = most_played_roms_dic[fav_SL_key]['launch_count'] + else: + launch_count = 1 + fav_SL_name = most_played_roms_dic[fav_SL_key]['SL_name'] + log_debug('Checking SL Most Played "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + + # Update progress dialog. + pDialog.updateProgressInc('Checking SL Most Played...\nItem "{}"'.format(fav_ROM_name)) + + # --- Load SL ROMs DB and assets --- + file_name = SL_index[fav_SL_name]['rom_DB_noext'] + '_items.json' + SL_DB_FN = cfg.SL_DB_DIR.pjoin(file_name) + assets_file_name = SL_index[fav_SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + SL_assets_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath(), verbose = False) + + # --- Check --- + if fav_ROM_name in SL_roms: + SL_ROM = SL_roms[fav_ROM_name] + SL_assets = SL_assets_dic[fav_ROM_name] + else: + log_debug('Machine "{}" / "{}" not found in SL main DB'.format(fav_ROM_name, fav_SL_name)) + SL_ROM = db_new_SL_ROM() + SL_assets = db_new_SL_asset() + t = 'Item "{}" missing'.format(fav_ROM_name) + SL_ROM['description'] = t + SL_ROM['plot'] = t + new_fav_ROM = db_get_SL_Favourite(fav_SL_name, fav_ROM_name, SL_ROM, SL_assets, control_dic) + new_fav_ROM['launch_count'] = launch_count + most_played_roms_dic[fav_SL_key] = new_fav_ROM + log_debug('Updated SL Most Played "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + utils_write_JSON_file(cfg.SL_MOST_PLAYED_FILE_PATH.getPath(), most_played_roms_dic) + pDialog.endProgress() + +def mame_update_SL_RecentPlay_objects(cfg, db_dic): + control_dic = db_dic['control_dic'] + SL_index = db_dic['SL_index'] + pDialog = KodiProgressDialog() + pDialog.startProgress('Loading SL Recently Played JSON DB...') + recent_roms_list = utils_load_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), []) + if len(recent_roms_list) < 1: + kodi_notify_warn('SL Recently Played empty') + return + pDialog.resetProgress('Checking SL Recently Played', len(recent_roms_list)) + for i, recent_rom in enumerate(recent_roms_list): + if 'ROM_name' in recent_rom: + fav_ROM_name = recent_rom['ROM_name'] + elif 'SL_ROM_name' in recent_rom: + fav_ROM_name = recent_rom['SL_ROM_name'] + else: + raise TypeError('Cannot find SL ROM name') + fav_SL_name = recent_rom['SL_name'] + log_debug('Checking SL Recently Played "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + pDialog.updateProgressInc('Checking SL Recently Played...\nItem "{}"'.format(fav_ROM_name)) + + # --- Load SL ROMs DB and assets --- + file_name = SL_index[fav_SL_name]['rom_DB_noext'] + '_items.json' + SL_DB_FN = cfg.SL_DB_DIR.pjoin(file_name) + assets_file_name = SL_index[fav_SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + SL_assets_dic = utils_load_JSON_file(SL_asset_DB_FN.getPath(), verbose = False) + + # --- Check --- + if fav_ROM_name in SL_roms: + SL_ROM = SL_roms[fav_ROM_name] + SL_assets = SL_assets_dic[fav_ROM_name] + else: + log_debug('Machine "{}" / "{}" not found in SL main DB'.format(fav_ROM_name, fav_SL_name)) + SL_ROM = db_new_SL_ROM() + SL_assets = db_new_SL_asset() + t = 'Item "{}" missing'.format(fav_ROM_name) + SL_ROM['description'] = t + SL_ROM['plot'] = t + new_fav_ROM = db_get_SL_Favourite(fav_SL_name, fav_ROM_name, SL_ROM, SL_assets, control_dic) + recent_roms_list[i] = new_fav_ROM + log_debug('Updated SL Recently Played "{}" / "{}"'.format(fav_SL_name, fav_ROM_name)) + utils_write_JSON_file(cfg.SL_RECENT_PLAYED_FILE_PATH.getPath(), recent_roms_list) + pDialog.endProgress() + +# ------------------------------------------------------------------------------------------------ +# Build MAME and SL plots +# ------------------------------------------------------------------------------------------------ + +# Generate plot for MAME machines. +# Line 1) Controls are {Joystick} +# Line 2) {One Vertical Raster screen} +# Line 3) Machine [is|is not] mechanical and driver is neogeo.hpp +# Line 4) Machine has [no coin slots| N coin slots] +# Line 5) Artwork, Manual, History, Info, Gameinit, Command +# Line 6) Machine [supports|does not support] a Software List. +def mame_MAME_plot_slits(mname, m, assets_dic, + history_info_set, mameinfo_info_set, gameinit_idx_dic, command_idx_dic): + Flag_list = [] + if assets_dic[mname]['artwork']: Flag_list.append('Artwork') + if assets_dic[mname]['manual']: Flag_list.append('Manual') + if mname in history_info_set: Flag_list.append('History') + if mname in mameinfo_info_set: Flag_list.append('Info') + if mname in gameinit_idx_dic: Flag_list.append('Gameinit') + if mname in command_idx_dic: Flag_list.append('Command') + Flag_str = ', '.join(Flag_list) + if m['input']: + control_list = [ctrl_dic['type'] for ctrl_dic in m['input']['control_list']] + else: + control_list = [] + if control_list: + controls_str = 'Controls {}'.format(misc_get_mame_control_str(control_list)) + else: + controls_str = 'No controls' + mecha_str = 'Mechanical' if m['isMechanical'] else 'Non-mechanical' + n_coins = m['input']['att_coins'] if m['input'] else 0 + coin_str = 'Machine has {} coin slots'.format(n_coins) if n_coins > 0 else 'Machine has no coin slots' + SL_str = ', '.join(m['softwarelists']) if m['softwarelists'] else '' + + plot_str_list = [] + plot_str_list.append('{}'.format(controls_str)) + plot_str_list.append('{}'.format(misc_get_mame_screen_str(mname, m))) + plot_str_list.append('{} / Driver is {}'.format(mecha_str, m['sourcefile'])) + plot_str_list.append('{}'.format(coin_str)) + if Flag_str: plot_str_list.append('{}'.format(Flag_str)) + if SL_str: plot_str_list.append('SL {}'.format(SL_str)) + + return plot_str_list + +# Setting id="MAME_plot" values="Info|History DAT|Info + History DAT" +def mame_build_MAME_plots(cfg, db_dic_in): + log_info('mame_build_MAME_plots() Building machine plots/descriptions ...') + control_dic = db_dic_in['control_dic'] + machines = db_dic_in['machines'] + renderdb_dic = db_dic_in['renderdb'] + assetdb_dic = db_dic_in['assetdb'] + history_idx_dic = db_dic_in['history_idx_dic'] + mameinfo_idx_dic = db_dic_in['mameinfo_idx_dic'] + gameinit_idx_dic = db_dic_in['gameinit_idx_dic'] + command_idx_dic = db_dic_in['command_idx_dic'] + + # Do not crash if DAT files are not configured. + history_info_set = {m for m in history_idx_dic['mame']['machines']} if history_idx_dic else set() + mameinfo_info_set = {m for m in mameinfo_idx_dic['mame']} if mameinfo_idx_dic else set() + + # --- Built machine plots --- + pDialog = KodiProgressDialog() + pDialog.startProgress('Generating MAME machine plots...', len(machines)) + for mname, m in machines.items(): + pDialog.updateProgressInc() + plot_str_list = mame_MAME_plot_slits(mname, m, assetdb_dic, + history_info_set, mameinfo_info_set, gameinit_idx_dic, command_idx_dic) + assetdb_dic[mname]['plot'] = '\n'.join(plot_str_list) + pDialog.endProgress() + + # Timestamp, save the MAME asset database. Save control_dic at the end. + db_safe_edit(control_dic, 't_MAME_plots_build', time.time()) + db_files = [ + (assetdb_dic, 'MAME machine assets', cfg.ASSET_DB_PATH.getPath()), + (control_dic, 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()), + ] + db_save_files(db_files) + +# --------------------------------------------------------------------------------------------- +# Generate plot for Software Lists +# Line 1) SL item has {} parts +# Line 2) {} ROMs and {} disks +# Line 3) Manual, History +# Line 4) Machines: machine list ... +# --------------------------------------------------------------------------------------------- +def mame_build_SL_plots(cfg, SL_dic): + control_dic = SL_dic['control_dic'] + SL_index_dic = SL_dic['SL_index'] + SL_machines_dic = SL_dic['SL_machines'] + History_idx_dic = SL_dic['history_idx_dic'] + + d_text = 'Generating SL item plots ...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(SL_index_dic)) + for SL_name in sorted(SL_index_dic): + pDialog.updateProgressInc('{}\nSoftware List {}'.format(d_text, SL_name)) + + # Open database + SL_DB_prefix = SL_index_dic[SL_name]['rom_DB_noext'] + SL_ROMs_FN = cfg.SL_DB_DIR.pjoin(SL_DB_prefix + '_items.json') + SL_assets_FN = cfg.SL_DB_DIR.pjoin(SL_DB_prefix + '_assets.json') + SL_ROM_audit_FN = cfg.SL_DB_DIR.pjoin(SL_DB_prefix + '_ROM_audit.json') + SL_roms = utils_load_JSON_file(SL_ROMs_FN.getPath(), verbose = False) + SL_assets_dic = utils_load_JSON_file(SL_assets_FN.getPath(), verbose = False) + SL_ROM_audit_dic = utils_load_JSON_file(SL_ROM_audit_FN.getPath(), verbose = False) + History_SL_set = {m for m in History_idx_dic[SL_name]['machines']} if SL_name in History_idx_dic else set() + # Machine_list = [ m['machine'] for m in SL_machines_dic[SL_name] ] + # Machines_str = 'Machines: {}'.format(', '.join(sorted(Machine_list))) + + # Traverse SL ROMs and make plot. + for rom_key in sorted(SL_roms): + SL_rom = SL_roms[rom_key] + num_parts = len(SL_rom['parts']) + if num_parts == 0: parts_str = 'SL item has no parts' + elif num_parts == 1: parts_str = 'SL item has {} part'.format(num_parts) + elif num_parts > 1: parts_str = 'SL item has {} parts'.format(num_parts) + num_ROMs = 0 + num_disks = 0 + for SL_rom in SL_ROM_audit_dic[rom_key]: + if SL_rom['type'] == 'ROM': num_ROMs += 1 + elif SL_rom['type'] == 'DISK': num_disks += 1 + ROM_str = 'ROM' if num_ROMs == 1 else 'ROMs' + disk_str = 'disk' if num_disks == 1 else 'disks' + roms_str = '{} {} and {} {}'.format(num_ROMs, ROM_str, num_disks, disk_str) + Flag_list = [] + if SL_assets_dic[rom_key]['manual']: Flag_list.append('Manual') + if rom_key in History_SL_set: Flag_list.append('History') + Flag_str = ', '.join(Flag_list) + # SL_roms[rom_key]['plot'] = '\n'.join([parts_str, roms_str, Flag_str, Machines_str]) + SL_roms[rom_key]['plot'] = '\n'.join([parts_str, roms_str, Flag_str]) + utils_write_JSON_file(SL_ROMs_FN.getPath(), SL_roms, verbose = False) + pDialog.endProgress() + + # --- Timestamp --- + db_safe_edit(control_dic, 't_SL_plots_build', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + +# ------------------------------------------------------------------------------------------------- +# MAME ROM/CHD audit code +# ------------------------------------------------------------------------------------------------- +# This code is very un-optimised! But it is better to get something that works +# and then optimise. "Premature optimization is the root of all evil" -- Donald Knuth +# +# MAME loads ROMs by hash, not by filename. This is the reason MAME is able to load ROMs even +# if they have a wrong name and providing they are in the correct ZIP file (parent or clone set). +# +# Adds new field 'status': ROMS 'OK', 'OK (invalid ROM)', 'ZIP not found', 'Bad ZIP file', +# 'ROM not in ZIP', 'ROM bad size', 'ROM bad CRC'. +# DISKS 'OK', 'OK (invalid CHD)', 'CHD not found', 'CHD bad SHA1' +# Adds fields 'status_colour'. +# +# rom_list = [ +# {'type' : several types, 'name' : 'avph.03d', 'crc' : '01234567', 'location' : 'avsp/avph.03d'}, ... +# {'type' : 'ROM_TYPE_DISK', 'name' : 'avph.03d', 'sha1' : '012...', 'location' : 'avsp/avph.03d'}, ... +# ] +# +# I'm not sure if the CHD sha1 value in MAME XML is the sha1 of the uncompressed data OR +# the sha1 of the CHD file. If the former, then AML can open the CHD file, get the sha1 from the +# header and verify it. See: +# http://www.mameworld.info/ubbthreads/showflat.php?Cat=&Number=342940&page=0&view=expanded&sb=5&o=&vc=1 +# +ZIP_NOT_FOUND = 0 +BAD_ZIP_FILE = 1 +ZIP_FILE_OK = 2 +def mame_audit_MAME_machine(cfg, rom_list, audit_dic): + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + rom_path = cfg.settings['rom_path_vanilla'] + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + rom_path = cfg.settings['rom_path_2003_plus'] + else: + raise TypeError('Unknown op_mode "{}"'.format(cfg.settings['op_mode'])) + + # --- Cache the ROM set ZIP files and detect wrong named files by CRC --- + # 1) Traverse ROMs, determine the set ZIP files, open ZIP files and put ZIPs in the cache. + # 2) If a ZIP file is not in the cache is because the ZIP file was not found + # 3) z_cache_exists is used to check if the ZIP file has been found the first time or not. + # + # z_cache = { + # 'zip_filename' : { + # 'fname' : {'size' : int, 'crc' : text_type}, + # 'fname' : {'size' : int, 'crc' : text_type}, ... + # } + # } + # + # z_cache_status = { + # 'zip_filename' : ZIP_NOT_FOUND, BAD_ZIP_FILE, ZIP_FILE_OK + # } + # + z_cache = {} + z_cache_status = {} + for m_rom in rom_list: + # Skip CHDs. + if m_rom['type'] == ROM_TYPE_DISK: continue + + # Process ROM ZIP files. + set_name = m_rom['location'].split('/')[0] + if m_rom['type'] == ROM_TYPE_SAMPLE: + zip_FN = FileName(cfg.settings['samples_path']).pjoin(set_name + '.zip') + else: + zip_FN = FileName(rom_path).pjoin(set_name + '.zip') + zip_path = zip_FN.getPath() + + # ZIP file encountered for the first time. Skip ZIP files already in the cache. + if zip_path not in z_cache_status: + if zip_FN.exists(): + # >> Scan files in ZIP file and put them in the cache + # log_debug('Caching ZIP file {}'.format(zip_path)) + try: + zip_f = z.ZipFile(zip_path, 'r') + except z.BadZipfile as e: + z_cache_status[zip_path] = BAD_ZIP_FILE + continue + # log_debug('ZIP {} files {}'.format(m_rom['location'], z_file_list)) + zip_file_dic = {} + for zfile in zip_f.namelist(): + # >> NOTE CRC32 in Python is a decimal number: CRC32 4225815809 + # >> However, MAME encodes it as an hexadecimal number: CRC32 0123abcd + z_info = zip_f.getinfo(zfile) + z_info_file_size = z_info.file_size + z_info_crc_hex_str = '{0:08x}'.format(z_info.CRC) + zip_file_dic[zfile] = {'size' : z_info_file_size, 'crc' : z_info_crc_hex_str} + # log_debug('ZIP CRC32 {} | CRC hex {} | size {}'.format(z_info.CRC, z_crc_hex, z_info.file_size)) + # log_debug('ROM CRC hex {} | size {}'.format(m_rom['crc'], 0)) + zip_f.close() + z_cache[zip_path] = zip_file_dic + z_cache_status[zip_path] = ZIP_FILE_OK + else: + # >> Mark ZIP file as not found + z_cache_status[zip_path] = ZIP_NOT_FOUND + + # --- Audit ROM by ROM --- + for m_rom in rom_list: + if m_rom['type'] == ROM_TYPE_DISK: + split_list = m_rom['location'].split('/') + set_name = split_list[0] + disk_name = split_list[1] + # log_debug('Testing CHD {}'.format(m_rom['name'])) + # log_debug('location {}'.format(m_rom['location'])) + # log_debug('set_name "{}"'.format(set_name)) + # log_debug('disk_name "{}"'.format(disk_name)) + + # >> Invalid CHDs + if not m_rom['sha1']: + m_rom['status'] = AUDIT_STATUS_OK_INVALID_CHD + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> Test if DISK file exists + chd_FN = FileName(cfg.settings['chd_path']).pjoin(set_name).pjoin(disk_name + '.chd') + # log_debug('chd_FN P {}'.format(chd_FN.getPath())) + if not chd_FN.exists(): + m_rom['status'] = AUDIT_STATUS_CHD_NO_FOUND + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> Open CHD file and check SHA1 hash. + chd_info = _mame_stat_chd(chd_FN.getPath()) + if chd_info['status'] == CHD_BAD_CHD: + m_rom['status'] = AUDIT_STATUS_BAD_CHD_FILE + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + if chd_info['status'] == CHD_BAD_VERSION: + m_rom['status'] = AUDIT_STATUS_CHD_BAD_VERSION + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + if chd_info['sha1'] != m_rom['sha1']: + m_rom['status'] = AUDIT_STATUS_CHD_BAD_SHA1 + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> DISK is OK + m_rom['status'] = AUDIT_STATUS_OK + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + elif m_rom['type'] == ROM_TYPE_SAMPLE: + split_list = m_rom['location'].split('/') + set_name = split_list[0] + sample_name = split_list[1] + '.wav' + # log_debug('Testing SAMPLE {}'.format(m_rom['name'])) + # log_debug('location {}'.format(m_rom['location'])) + # log_debug('set_name {}'.format(set_name)) + # log_debug('sample_name {}'.format(sample_name)) + + # Test if ZIP file exists (use cached data). ZIP file must be in the cache always + # at this point. + zip_FN = FileName(cfg.settings['samples_path']).pjoin(set_name + '.zip') + zip_path = zip_FN.getPath() + # log_debug('ZIP {}'.format(zip_FN.getPath())) + if z_cache_status[zip_path] == ZIP_NOT_FOUND: + m_rom['status'] = AUDIT_STATUS_ZIP_NO_FOUND + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + elif z_cache_status[zip_path] == BAD_ZIP_FILE: + m_rom['status'] = AUDIT_STATUS_BAD_ZIP_FILE + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + # >> ZIP file is good and data was cached. + zip_file_dic = z_cache[zip_path] + + # >> At this point the ZIP file is in the cache (if it was open) + if sample_name not in zip_file_dic: + # >> File not found by filename. Check if it has renamed by looking at CRC. + # >> ROM not in ZIP (not even under other filename) + m_rom['status'] = AUDIT_STATUS_SAMPLE_NOT_IN_ZIP + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> SAMPLE is OK + m_rom['status'] = AUDIT_STATUS_OK + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + + else: + split_list = m_rom['location'].split('/') + set_name = split_list[0] + rom_name = split_list[1] + # log_debug('Testing ROM {}'.format(m_rom['name'])) + # log_debug('location {}'.format(m_rom['location'])) + # log_debug('set_name {}'.format(set_name)) + # log_debug('rom_name {}'.format(rom_name)) + + # >> Invalid ROMs are not in the ZIP file + if not m_rom['crc']: + m_rom['status'] = AUDIT_STATUS_OK_INVALID_ROM + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + continue + + # Test if ZIP file exists (use cached data). ZIP file must be in the cache always + # at this point. + zip_FN = FileName(rom_path).pjoin(set_name + '.zip') + zip_path = zip_FN.getPath() + # log_debug('ZIP {}'.format(zip_FN.getPath())) + if z_cache_status[zip_path] == ZIP_NOT_FOUND: + m_rom['status'] = AUDIT_STATUS_ZIP_NO_FOUND + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + elif z_cache_status[zip_path] == BAD_ZIP_FILE: + m_rom['status'] = AUDIT_STATUS_BAD_ZIP_FILE + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + # >> ZIP file is good and data was cached. + zip_file_dic = z_cache[zip_path] + + # >> At this point the ZIP file is in the cache (if it was open) + if rom_name in zip_file_dic: + # >> File has correct name + if zip_file_dic[rom_name]['size'] != m_rom['size']: + m_rom['status'] = AUDIT_STATUS_ROM_BAD_SIZE + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + if zip_file_dic[rom_name]['crc'] != m_rom['crc']: + m_rom['status'] = AUDIT_STATUS_ROM_BAD_CRC + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + else: + # >> File not found by filename. Check if it has renamed by looking at CRC. + rom_OK_name = '' + for fn in zip_file_dic: + if m_rom['crc'] == zip_file_dic[fn]['crc']: + rom_OK_name = fn + break + if rom_OK_name: + # >> File found by CRC + m_rom['status'] = AUDIT_STATUS_OK_WRONG_NAME_ROM + m_rom['status_colour'] = '[COLOR orange]OK (named {})[/COLOR]'.format(rom_OK_name) + continue + else: + # >> ROM not in ZIP (not even under other filename) + m_rom['status'] = AUDIT_STATUS_ROM_NOT_IN_ZIP + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> ROM is OK + m_rom['status'] = AUDIT_STATUS_OK + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + + # >> Audit results + # >> Naive and slow code, but better safe than sorry. + ROM_OK_status_list = [] + SAM_OK_status_list = [] + CHD_OK_status_list = [] + audit_dic['machine_has_ROMs_or_CHDs'] = False + audit_dic['machine_has_ROMs'] = False + audit_dic['machine_has_SAMPLES'] = False + audit_dic['machine_has_CHDs'] = False + for m_rom in rom_list: + audit_dic['machine_has_ROMs_or_CHDs'] = True + if m_rom['type'] == ROM_TYPE_DISK: + audit_dic['machine_has_CHDs'] = True + if m_rom['status'] == AUDIT_STATUS_OK or m_rom['status'] == AUDIT_STATUS_OK_INVALID_CHD: + CHD_OK_status_list.append(True) + else: + CHD_OK_status_list.append(False) + elif m_rom['type'] == ROM_TYPE_SAMPLE: + audit_dic['machine_has_SAMPLES'] = True + if m_rom['status'] == AUDIT_STATUS_OK: + SAM_OK_status_list.append(True) + else: + SAM_OK_status_list.append(False) + else: + audit_dic['machine_has_ROMs'] = True + if m_rom['status'] == AUDIT_STATUS_OK or \ + m_rom['status'] == AUDIT_STATUS_OK_INVALID_ROM or \ + m_rom['status'] == AUDIT_STATUS_OK_WRONG_NAME_ROM: + ROM_OK_status_list.append(True) + else: + ROM_OK_status_list.append(False) + audit_dic['machine_ROMs_are_OK'] = all(ROM_OK_status_list) if audit_dic['machine_has_ROMs'] else True + audit_dic['machine_SAMPLES_are_OK'] = all(SAM_OK_status_list) if audit_dic['machine_has_SAMPLES'] else True + audit_dic['machine_CHDs_are_OK'] = all(CHD_OK_status_list) if audit_dic['machine_has_CHDs'] else True + audit_dic['machine_is_OK'] = audit_dic['machine_ROMs_are_OK'] and \ + audit_dic['machine_SAMPLES_are_OK'] and audit_dic['machine_CHDs_are_OK'] + +# ------------------------------------------------------------------------------------------------- +# SL ROM/CHD audit code +# ------------------------------------------------------------------------------------------------- +def mame_audit_SL_machine(SL_ROM_path_FN, SL_CHD_path_FN, SL_name, item_name, rom_list, audit_dic): + # --- Cache the ROM set ZIP files and detect wrong named files by CRC --- + # >> Look at mame_audit_MAME_machine() for comments. + z_cache = {} + z_cache_status = {} + for m_rom in rom_list: + # >> Skip CHDs + if m_rom['type'] == ROM_TYPE_DISK: continue + + # >> Process ROM ZIP files + split_list = m_rom['location'].split('/') + SL_name = split_list[0] + zip_name = split_list[1] + '.zip' + zip_FN = SL_ROM_path_FN.pjoin(SL_name).pjoin(zip_name) + zip_path = zip_FN.getPath() + + # >> ZIP file encountered for the first time. Skip ZIP files already in the cache. + if zip_path not in z_cache_status: + if zip_FN.exists(): + # >> Scan files in ZIP file and put them in the cache + # log_debug('Caching ZIP file {}'.format(zip_path)) + try: + zip_f = z.ZipFile(zip_path, 'r') + except z.BadZipfile as e: + z_cache_status[zip_path] = BAD_ZIP_FILE + continue + # log_debug('ZIP {} files {}'.format(m_rom['location'], z_file_list)) + zip_file_dic = {} + for zfile in zip_f.namelist(): + # >> NOTE CRC32 in Python is a decimal number: CRC32 4225815809 + # >> However, MAME encodes it as an hexadecimal number: CRC32 0123abcd + z_info = zip_f.getinfo(zfile) + z_info_file_size = z_info.file_size + z_info_crc_hex_str = '{0:08x}'.format(z_info.CRC) + # Unicode filenames in ZIP files cause problems later in this function. + # zfile has type Unicode and it's not encoded in utf-8. + # How to know encoding of ZIP files? + # https://stackoverflow.com/questions/15918314/how-to-detect-string-byte-encoding/15918519 + try: + # zfile sometimes has type Unicode, sometimes str. If type is str then + # try to decode it as UTF-8. + if type(zfile) == text_type: + zfile_unicode = zfile + else: + zfile_unicode = zfile.decode('utf-8') + except UnicodeDecodeError: + log_error('mame_audit_SL_machine() Exception UnicodeDecodeError') + log_error('type(zfile) = {}'.format(type(zfile))) + log_error('SL_name "{}", item_name "{}", rom name "{}"'.format(SL_name, item_name, m_rom['name'])) + except UnicodeEncodeError: + log_error('mame_audit_SL_machine() Exception UnicodeEncodeError') + log_error('type(zfile) = {}'.format(type(zfile))) + log_error('SL_name "{}", item_name "{}", rom name "{}"'.format(SL_name, item_name, m_rom['name'])) + else: + # For now, do not add non-ASCII ROMs so the audit will fail for this ROM. + zip_file_dic[zfile_unicode] = {'size' : z_info_file_size, 'crc' : z_info_crc_hex_str} + # log_debug('ZIP CRC32 {} | CRC hex {} | size {}'.format(z_info.CRC, z_crc_hex, z_info.file_size)) + # log_debug('ROM CRC hex {} | size {}'.format(m_rom['crc'], 0)) + zip_f.close() + z_cache[zip_path] = zip_file_dic + z_cache_status[zip_path] = ZIP_FILE_OK + else: + # >> Mark ZIP file as not found + z_cache_status[zip_path] = ZIP_NOT_FOUND + + # --- Audit ROM by ROM --- + for m_rom in rom_list: + if m_rom['type'] == ROM_TYPE_DISK: + # --- Audit CHD ---------------------------------------------------------------------- + split_list = m_rom['location'].split('/') + SL_name = split_list[0] + item_name = split_list[1] + disk_name = split_list[2] + # log_debug('Testing CHD "{}"'.format(m_rom['name'])) + # log_debug('location "{}"'.format(m_rom['location'])) + # log_debug('SL_name "{}"'.format(SL_name)) + # log_debug('item_name "{}"'.format(item_name)) + # log_debug('disk_name "{}"'.format(disk_name)) + + # >> Invalid CHDs + if not m_rom['sha1']: + m_rom['status'] = AUDIT_STATUS_OK_INVALID_CHD + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> Test if DISK file exists + chd_FN = SL_CHD_path_FN.pjoin(SL_name).pjoin(item_name).pjoin(disk_name + '.chd') + # log_debug('chd_FN P {}'.format(chd_FN.getPath())) + if not chd_FN.exists(): + m_rom['status'] = AUDIT_STATUS_CHD_NO_FOUND + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> Open CHD file and check SHA1 hash. + chd_info = _mame_stat_chd(chd_FN.getPath()) + if chd_info['status'] == CHD_BAD_CHD: + m_rom['status'] = AUDIT_STATUS_BAD_CHD_FILE + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + if chd_info['status'] == CHD_BAD_VERSION: + m_rom['status'] = AUDIT_STATUS_CHD_BAD_VERSION + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + if chd_info['sha1'] != m_rom['sha1']: + m_rom['status'] = AUDIT_STATUS_CHD_BAD_SHA1 + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> DISK is OK + m_rom['status'] = AUDIT_STATUS_OK + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + else: + # --- Audit ROM ---------------------------------------------------------------------- + split_list = m_rom['location'].split('/') + SL_name = split_list[0] + item_name = split_list[1] + rom_name = split_list[2] + # log_debug('Testing ROM "{}"'.format(m_rom['name'])) + # log_debug('location "{}"'.format(m_rom['location'])) + # log_debug('SL_name "{}"'.format(SL_name)) + # log_debug('item_name "{}"'.format(item_name)) + # log_debug('rom_name "{}"'.format(rom_name)) + + # >> Invalid ROMs are not in the ZIP file + if not m_rom['crc']: + m_rom['status'] = AUDIT_STATUS_OK_INVALID_ROM + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> Test if ZIP file exists + zip_FN = SL_ROM_path_FN.pjoin(SL_name).pjoin(item_name + '.zip') + zip_path = zip_FN.getPath() + # log_debug('zip_FN P {}'.format(zip_FN.getPath())) + if z_cache_status[zip_path] == ZIP_NOT_FOUND: + m_rom['status'] = AUDIT_STATUS_ZIP_NO_FOUND + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + elif z_cache_status[zip_path] == BAD_ZIP_FILE: + m_rom['status'] = AUDIT_STATUS_BAD_ZIP_FILE + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + # >> ZIP file is good and data was cached. + zip_file_dic = z_cache[zip_path] + + # >> At this point the ZIP file is in the cache (if it was open) + if rom_name in zip_file_dic: + # >> File has correct name + if zip_file_dic[rom_name]['size'] != m_rom['size']: + m_rom['status'] = AUDIT_STATUS_ROM_BAD_SIZE + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + if zip_file_dic[rom_name]['crc'] != m_rom['crc']: + m_rom['status'] = AUDIT_STATUS_ROM_BAD_CRC + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + else: + # >> File not found by filename. Check if it has renamed by looking at CRC. + rom_OK_name = '' + for fn in zip_file_dic: + if m_rom['crc'] == zip_file_dic[fn]['crc']: + rom_OK_name = fn + break + if rom_OK_name: + # >> File found by CRC + m_rom['status'] = AUDIT_STATUS_OK_WRONG_NAME_ROM + m_rom['status_colour'] = '[COLOR orange]OK (named {})[/COLOR]'.format(rom_OK_name) + continue + else: + # >> ROM not in ZIP (not even under other filename) + m_rom['status'] = AUDIT_STATUS_ROM_NOT_IN_ZIP + m_rom['status_colour'] = '[COLOR red]{}[/COLOR]'.format(m_rom['status']) + continue + + # >> ROM is OK + m_rom['status'] = AUDIT_STATUS_OK + m_rom['status_colour'] = '[COLOR green]{}[/COLOR]'.format(m_rom['status']) + # log_debug('{}'.format(AUDIT_STATUS_OK)) + + # >> Currently exactly same code as in mame_audit_MAME_machine() + # >> Audit results + # >> Naive and slow code, but better safe than sorry. + ROM_OK_status_list = [] + CHD_OK_status_list = [] + audit_dic['machine_has_ROMs_or_CHDs'] = False + audit_dic['machine_has_ROMs'] = False + audit_dic['machine_has_CHDs'] = False + for m_rom in rom_list: + audit_dic['machine_has_ROMs_or_CHDs'] = True + if m_rom['type'] == ROM_TYPE_DISK: + audit_dic['machine_has_CHDs'] = True + if m_rom['status'] == AUDIT_STATUS_OK or \ + m_rom['status'] == AUDIT_STATUS_OK_INVALID_CHD: + CHD_OK_status_list.append(True) + else: + CHD_OK_status_list.append(False) + else: + audit_dic['machine_has_ROMs'] = True + if m_rom['status'] == AUDIT_STATUS_OK or \ + m_rom['status'] == AUDIT_STATUS_OK_INVALID_ROM or \ + m_rom['status'] == AUDIT_STATUS_OK_WRONG_NAME_ROM: + ROM_OK_status_list.append(True) + else: + ROM_OK_status_list.append(False) + audit_dic['machine_ROMs_are_OK'] = all(ROM_OK_status_list) if audit_dic['machine_has_ROMs'] else True + audit_dic['machine_CHDs_are_OK'] = all(CHD_OK_status_list) if audit_dic['machine_has_CHDs'] else True + audit_dic['machine_is_OK'] = audit_dic['machine_ROMs_are_OK'] and audit_dic['machine_CHDs_are_OK'] + +def mame_audit_MAME_all(cfg, db_dic_in): + log_debug('mame_audit_MAME_all() Initialising...') + control_dic = db_dic_in['control_dic'] + machines = db_dic_in['machines'] + renderdb_dic = db_dic_in['renderdb'] + audit_roms_dic = db_dic_in['audit_roms'] + + # Go machine by machine and audit ZIPs and CHDs. Adds new column 'status' to each ROM. + pDialog = KodiProgressDialog() + pDialog.startProgress('Auditing MAME ROMs and CHDs...', len(renderdb_dic)) + machine_audit_dic = {} + for m_name in sorted(renderdb_dic): + pDialog.updateProgressInc() + if pDialog.isCanceled(): break + # Only audit machine if it has ROMs. However, add all machines to machine_audit_dic. + # audit_roms_dic[m_name] is mutable and edited inside mame_audit_MAME_machine() + audit_dic = db_new_audit_dic() + if m_name in audit_roms_dic: + mame_audit_MAME_machine(cfg, audit_roms_dic[m_name], audit_dic) + machine_audit_dic[m_name] = audit_dic + pDialog.endProgress() + + # Audit statistics. + audit_MAME_machines_with_arch = 0 + audit_MAME_machines_with_arch_OK = 0 + audit_MAME_machines_with_arch_BAD = 0 + audit_MAME_machines_without = 0 + audit_MAME_machines_with_ROMs = 0 + audit_MAME_machines_with_ROMs_OK = 0 + audit_MAME_machines_with_ROMs_BAD = 0 + audit_MAME_machines_without_ROMs = 0 + audit_MAME_machines_with_SAMPLES = 0 + audit_MAME_machines_with_SAMPLES_OK = 0 + audit_MAME_machines_with_SAMPLES_BAD = 0 + audit_MAME_machines_without_SAMPLES = 0 + audit_MAME_machines_with_CHDs = 0 + audit_MAME_machines_with_CHDs_OK = 0 + audit_MAME_machines_with_CHDs_BAD = 0 + audit_MAME_machines_without_CHDs = 0 + for m_name in renderdb_dic: + render_dic = renderdb_dic[m_name] + audit_dic = machine_audit_dic[m_name] + # Skip unrunnable (device) machines + if render_dic['isDevice']: continue + if audit_dic['machine_has_ROMs_or_CHDs']: + audit_MAME_machines_with_arch += 1 + if audit_dic['machine_is_OK']: audit_MAME_machines_with_arch_OK += 1 + else: audit_MAME_machines_with_arch_BAD += 1 + else: + audit_MAME_machines_without += 1 + + if audit_dic['machine_has_ROMs']: + audit_MAME_machines_with_ROMs += 1 + if audit_dic['machine_ROMs_are_OK']: audit_MAME_machines_with_ROMs_OK += 1 + else: audit_MAME_machines_with_ROMs_BAD += 1 + else: + audit_MAME_machines_without_ROMs += 1 + + if audit_dic['machine_has_SAMPLES']: + audit_MAME_machines_with_SAMPLES += 1 + if audit_dic['machine_SAMPLES_are_OK']: audit_MAME_machines_with_SAMPLES_OK += 1 + else: audit_MAME_machines_with_SAMPLES_BAD += 1 + else: + audit_MAME_machines_without_SAMPLES += 1 + + if audit_dic['machine_has_CHDs']: + audit_MAME_machines_with_CHDs += 1 + if audit_dic['machine_CHDs_are_OK']: audit_MAME_machines_with_CHDs_OK += 1 + else: audit_MAME_machines_with_CHDs_BAD += 1 + else: + audit_MAME_machines_without_CHDs += 1 + + # --- Report header and statistics --- + report_full_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows full audit report', + ] + report_good_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows machines with good ROMs and/or CHDs', + ] + report_error_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows machines with bad/missing ROMs and/or CHDs', + ] + ROM_report_good_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows machines with good ROMs', + ] + ROM_report_error_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows machines with bad/missing ROMs', + ] + SAMPLES_report_good_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows machines with good Samples', + ] + SAMPLES_report_error_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows machines with bad/missing Samples', + ] + CHD_report_good_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows machines with good CHDs', + ] + CHD_report_error_list = [ + '*** Advanced MAME Launcher MAME audit report ***', + 'This report shows machines with bad/missing CHDs', + ] + h_list = [ + 'There are {} machines in total'.format(len(renderdb_dic)), + 'Of those, {} are runnable machines'.format(control_dic['stats_audit_MAME_machines_runnable']), + ] + report_full_list.extend(h_list) + report_good_list.extend(h_list) + report_error_list.extend(h_list) + ROM_report_good_list.extend(h_list) + ROM_report_error_list.extend(h_list) + SAMPLES_report_good_list.extend(h_list) + SAMPLES_report_error_list.extend(h_list) + CHD_report_good_list.extend(h_list) + CHD_report_error_list.extend(h_list) + + h_list = [ + 'Of those, {} require ROMs and or CHDSs'.format(audit_MAME_machines_with_arch), + 'Of those, {} are OK and {} have bad/missing ROMs and/or CHDs'.format( + audit_MAME_machines_with_arch_OK, audit_MAME_machines_with_arch_BAD ), + ] + report_good_list.extend(h_list) + report_error_list.extend(h_list) + h_list = [ + 'Of those, {} require ROMs'.format(audit_MAME_machines_with_ROMs), + 'Of those, {} are OK and {} have bad/missing ROMs and/or CHDs'.format( + audit_MAME_machines_with_ROMs_OK, audit_MAME_machines_with_ROMs_BAD ), + ] + ROM_report_good_list.extend(h_list) + ROM_report_error_list.extend(h_list) + h_list = [ + 'Of those, {} require ROMs and or CHDSs'.format(audit_MAME_machines_with_CHDs), + 'Of those, {} are OK and {} have bad/missing ROMs and/or CHDs'.format( + audit_MAME_machines_with_CHDs_OK, audit_MAME_machines_with_CHDs_BAD ), + ] + CHD_report_good_list.extend(h_list) + CHD_report_error_list.extend(h_list) + + report_full_list.append('') + report_good_list.append('') + report_error_list.append('') + ROM_report_good_list.append('') + ROM_report_error_list.append('') + SAMPLES_report_good_list.append('') + SAMPLES_report_error_list.append('') + CHD_report_good_list.append('') + CHD_report_error_list.append('') + + # Generate report. + pDialog.startProgress('Generating audit reports...', len(renderdb_dic)) + for m_name in sorted(renderdb_dic): + pDialog.updateProgressInc() + + # Skip ROMless and/or CHDless machines from reports, except the full report + description = renderdb_dic[m_name]['description'] + cloneof = renderdb_dic[m_name]['cloneof'] + if m_name not in audit_roms_dic: + head_list = [] + head_list.append('Machine {} "{}"'.format(m_name, description)) + if cloneof: + clone_desc = renderdb_dic[cloneof]['description'] + head_list.append('Cloneof {} "{}"'.format(cloneof, clone_desc)) + head_list.append('This machine has no ROMs and/or CHDs') + report_full_list.extend(head_list) + continue + rom_list = audit_roms_dic[m_name] + if not rom_list: continue + + # >> Check if audit was canceled. + # log_debug(text_type(rom_list)) + if 'status' not in rom_list[0]: + report_list.append('Audit was canceled at machine {}'.format(m_name)) + break + + # >> Machine header (in all reports). + head_list = [] + head_list.append('Machine {} "{}"'.format(m_name, description)) + if cloneof: + clone_desc = renderdb_dic[cloneof]['description'] + head_list.append('Cloneof {} "{}"'.format(cloneof, clone_desc)) + + # ROM/CHD report. + table_str = [ ['right', 'left', 'right', 'left', 'left', 'left'] ] + for m_rom in rom_list: + if m_rom['type'] == ROM_TYPE_DISK: + table_row = [m_rom['type'], m_rom['name'], '', m_rom['sha1'][0:8], + m_rom['location'], m_rom['status']] + elif m_rom['type'] == ROM_TYPE_SAMPLE: + table_row = [m_rom['type'], m_rom['name'], '', '', m_rom['location'], m_rom['status']] + else: + table_row = [m_rom['type'], m_rom['name'], text_type(m_rom['size']), m_rom['crc'], + m_rom['location'], m_rom['status']] + table_str.append(table_row) + local_str_list = text_render_table_NO_HEADER(table_str) + local_str_list.append('') + + # --- At this point all machines have ROMs and/or CHDs --- + # >> Full, ROMs and/or CHDs report. + audit_dic = machine_audit_dic[m_name] + report_full_list.extend(head_list + local_str_list) + if audit_dic['machine_is_OK']: + report_good_list.extend(head_list + local_str_list) + else: + report_error_list.extend(head_list + local_str_list) + + # >> ROM report + if audit_dic['machine_has_ROMs']: + if audit_dic['machine_ROMs_are_OK']: + ROM_report_good_list.extend(head_list + local_str_list) + else: + ROM_report_error_list.extend(head_list + local_str_list) + + # >> Samples report + if audit_dic['machine_has_SAMPLES']: + if audit_dic['machine_SAMPLES_are_OK']: + SAMPLES_report_good_list.extend(head_list + local_str_list) + else: + SAMPLES_report_error_list.extend(head_list + local_str_list) + + # >> CHD report. + if audit_dic['machine_has_CHDs']: + if audit_dic['machine_CHDs_are_OK']: + CHD_report_good_list.extend(head_list + local_str_list) + else: + CHD_report_error_list.extend(head_list + local_str_list) + else: + a = '*** MAME audit finished ***' + report_full_list.append(a) + report_good_list.append(a) + report_error_list.append(a) + ROM_report_good_list.append(a) + ROM_report_error_list.append(a) + SAMPLES_report_good_list.append(a) + SAMPLES_report_error_list.append(a) + CHD_report_good_list.append(a) + CHD_report_error_list.append(a) + pDialog.endProgress() + + # --- Write reports --- + num_items = 9 + pDialog.startProgress('Writing report files...', num_items) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_FULL_PATH.getPath(), report_full_list) + pDialog.updateProgress(1) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_GOOD_PATH.getPath(), report_good_list) + pDialog.updateProgress(2) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_ERRORS_PATH.getPath(), report_error_list) + pDialog.updateProgress(3) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_ROM_GOOD_PATH.getPath(), ROM_report_good_list) + pDialog.updateProgress(4) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_ROM_ERRORS_PATH.getPath(), ROM_report_error_list) + pDialog.updateProgress(5) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_SAMPLES_GOOD_PATH.getPath(), SAMPLES_report_good_list) + pDialog.updateProgress(6) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_SAMPLES_ERRORS_PATH.getPath(), SAMPLES_report_error_list) + pDialog.updateProgress(7) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_CHD_GOOD_PATH.getPath(), CHD_report_good_list) + pDialog.updateProgress(8) + utils_write_slist_to_file(cfg.REPORT_MAME_AUDIT_CHD_ERRORS_PATH.getPath(), CHD_report_error_list) + pDialog.endProgress() + + # Update MAME audit statistics. + db_safe_edit(control_dic, 'audit_MAME_machines_with_arch', audit_MAME_machines_with_arch) + db_safe_edit(control_dic, 'audit_MAME_machines_with_arch_OK', audit_MAME_machines_with_arch_OK) + db_safe_edit(control_dic, 'audit_MAME_machines_with_arch_BAD', audit_MAME_machines_with_arch_BAD) + db_safe_edit(control_dic, 'audit_MAME_machines_without', audit_MAME_machines_without) + db_safe_edit(control_dic, 'audit_MAME_machines_with_ROMs', audit_MAME_machines_with_ROMs) + db_safe_edit(control_dic, 'audit_MAME_machines_with_ROMs_OK', audit_MAME_machines_with_ROMs_OK) + db_safe_edit(control_dic, 'audit_MAME_machines_with_ROMs_BAD', audit_MAME_machines_with_ROMs_BAD) + db_safe_edit(control_dic, 'audit_MAME_machines_without_ROMs', audit_MAME_machines_without_ROMs) + db_safe_edit(control_dic, 'audit_MAME_machines_with_SAMPLES', audit_MAME_machines_with_SAMPLES) + db_safe_edit(control_dic, 'audit_MAME_machines_with_SAMPLES_OK', audit_MAME_machines_with_SAMPLES_OK) + db_safe_edit(control_dic, 'audit_MAME_machines_with_SAMPLES_BAD', audit_MAME_machines_with_SAMPLES_BAD) + db_safe_edit(control_dic, 'audit_MAME_machines_without_SAMPLES', audit_MAME_machines_without_SAMPLES) + db_safe_edit(control_dic, 'audit_MAME_machines_with_CHDs', audit_MAME_machines_with_CHDs) + db_safe_edit(control_dic, 'audit_MAME_machines_with_CHDs_OK', audit_MAME_machines_with_CHDs_OK) + db_safe_edit(control_dic, 'audit_MAME_machines_with_CHDs_BAD', audit_MAME_machines_with_CHDs_BAD) + db_safe_edit(control_dic, 'audit_MAME_machines_without_CHDs', audit_MAME_machines_without_CHDs) + + # Update timestamp of ROM audit. + db_safe_edit(control_dic, 't_MAME_audit', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + +def mame_audit_SL_all(cfg, db_dic_in): + log_debug('mame_audit_SL_all() Initialising ...') + control_dic = db_dic_in['control_dic'] + SL_index_dic = db_dic_in['SL_index'] + + # Report header and statistics + report_full_list = [ + '*** Advanced MAME Launcher Software Lists audit report ***', + 'This report shows full Software Lists audit report', + ] + report_good_list = [ + '*** Advanced MAME Launcher Software Lists audit report ***', + 'This report shows SL items with good ROMs and/or CHDs', + ] + report_error_list = [ + '*** Advanced MAME Launcher Software Lists audit report ***', + 'This report shows SL items with errors in ROMs and/or CHDs', + ] + ROM_report_good_list = [ + '*** Advanced MAME Launcher Software Lists audit report ***', + 'This report shows SL items with good ROMs', + ] + ROM_report_error_list = [ + '*** Advanced MAME Launcher Software Lists audit report ***', + 'This report shows SL items with errors in ROMs', + ] + CHD_report_good_list = [ + '*** Advanced MAME Launcher Software Lists audit report ***', + 'This report shows SL items with good CHDs', + ] + CHD_report_error_list = [ + '*** Advanced MAME Launcher Software Lists audit report ***', + 'This report shows SL items with errors in CHDs', + ] + h_list = [ + 'There are {} software lists'.format(len(SL_index_dic)), + '', + ] + report_full_list.extend(h_list) + report_good_list.extend(h_list) + report_error_list.extend(h_list) + ROM_report_good_list.extend(h_list) + ROM_report_error_list.extend(h_list) + CHD_report_good_list.extend(h_list) + CHD_report_error_list.extend(h_list) + + # DEBUG code + # SL_index_dic = { + # "32x" : { + # "display_name" : "Sega 32X cartridges", + # "num_with_CHDs" : 0, + # "num_with_ROMs" : 203, + # "rom_DB_noext" : "32x" + # } + # } + + # SL audit statistics. + audit_SL_items_runnable = 0 + audit_SL_items_with_arch = 0 + audit_SL_items_with_arch_OK = 0 + audit_SL_items_with_arch_BAD = 0 + audit_SL_items_without_arch = 0 + audit_SL_items_with_arch_ROM = 0 + audit_SL_items_with_arch_ROM_OK = 0 + audit_SL_items_with_arch_ROM_BAD = 0 + audit_SL_items_without_arch_ROM = 0 + audit_SL_items_with_CHD = 0 + audit_SL_items_with_CHD_OK = 0 + audit_SL_items_with_CHD_BAD = 0 + audit_SL_items_without_CHD = 0 + + # Iterate all SL databases and audit ROMs. + d_text = 'Auditing Sofware Lists ROMs and CHDs...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(SL_index_dic)) + SL_ROM_path_FN = FileName(cfg.settings['SL_rom_path']) + SL_CHD_path_FN = FileName(cfg.settings['SL_chd_path']) + for SL_name in sorted(SL_index_dic): + pDialog.updateProgressInc('{}\nSoftware List {}'.format(d_text, SL_name)) + + SL_dic = SL_index_dic[SL_name] + SL_DB_FN = cfg.SL_DB_DIR.pjoin(SL_dic['rom_DB_noext'] + '_items.json') + SL_AUDIT_ROMs_DB_FN = cfg.SL_DB_DIR.pjoin(SL_dic['rom_DB_noext'] + '_ROM_audit.json') + roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + audit_roms = utils_load_JSON_file(SL_AUDIT_ROMs_DB_FN.getPath(), verbose = False) + + # Iterate SL ROMs + for rom_key in sorted(roms): + # audit_roms_list and audit_dic are mutable and edited inside the function() + audit_rom_list = audit_roms[rom_key] + audit_dic = db_new_audit_dic() + mame_audit_SL_machine(SL_ROM_path_FN, SL_CHD_path_FN, SL_name, rom_key, audit_rom_list, audit_dic) + + # Audit statistics + audit_SL_items_runnable += 1 + if audit_dic['machine_has_ROMs_or_CHDs']: + audit_SL_items_with_arch += 1 + if audit_dic['machine_is_OK']: audit_SL_items_with_arch_OK += 1 + else: audit_SL_items_with_arch_BAD += 1 + else: + audit_SL_items_without_arch += 1 + + if audit_dic['machine_has_ROMs']: + audit_SL_items_with_arch_ROM += 1 + if audit_dic['machine_ROMs_are_OK']: audit_SL_items_with_arch_ROM_OK += 1 + else: audit_SL_items_with_arch_ROM_BAD += 1 + else: + audit_SL_items_without_arch_ROM += 1 + + if audit_dic['machine_has_CHDs']: + audit_SL_items_with_CHD += 1 + if audit_dic['machine_CHDs_are_OK']: audit_SL_items_with_CHD_OK += 1 + else: audit_SL_items_with_CHD_BAD += 1 + else: + audit_SL_items_without_CHD += 1 + + # Software/machine header. + # WARNING: Kodi crashes with a 22 MB text file with colours. No problem if TXT file has not colours. + rom = roms[rom_key] + cloneof = rom['cloneof'] + head_list = [] + if cloneof: + head_list.append('SL {} ROM {} (cloneof {})'.format(SL_name, rom_key, cloneof)) + else: + head_list.append('SL {} ROM {}'.format(SL_name, rom_key)) + + # ROM/CHD report. + table_str = [ ['right', 'left', 'left', 'left', 'left'] ] + for m_rom in audit_rom_list: + if m_rom['type'] == ROM_TYPE_DISK: + table_row = [m_rom['type'], '', + m_rom['sha1'][0:8], m_rom['location'], m_rom['status']] + else: + table_row = [m_rom['type'], text_type(m_rom['size']), + m_rom['crc'], m_rom['location'], m_rom['status']] + table_str.append(table_row) + local_str_list = text_render_table_NO_HEADER(table_str) + local_str_list.append('') + + # Full, ROMs and CHDs report. + report_full_list.extend(head_list + local_str_list) + if audit_dic['machine_is_OK']: + report_good_list.extend(head_list + local_str_list) + else: + report_error_list.extend(head_list + local_str_list) + + # ROM report + if audit_dic['machine_has_ROMs']: + if audit_dic['machine_ROMs_are_OK']: + ROM_report_good_list.extend(head_list + local_str_list) + else: + ROM_report_error_list.extend(head_list + local_str_list) + + # CHD report. + if audit_dic['machine_has_CHDs']: + if audit_dic['machine_CHDs_are_OK']: + CHD_report_good_list.extend(head_list + local_str_list) + else: + CHD_report_error_list.extend(head_list + local_str_list) + a = '*** Software Lists audit finished ***' + report_full_list.append(a) + report_good_list.append(a) + report_error_list.append(a) + ROM_report_good_list.append(a) + ROM_report_error_list.append(a) + CHD_report_good_list.append(a) + CHD_report_error_list.append(a) + pDialog.endProgress() + + # Write reports. + num_items = 7 + pDialog.startProgress('Writing SL audit reports...', num_items) + utils_write_slist_to_file(cfg.REPORT_SL_AUDIT_FULL_PATH.getPath(), report_full_list) + pDialog.updateProgress(1) + utils_write_slist_to_file(cfg.REPORT_SL_AUDIT_GOOD_PATH.getPath(), report_good_list) + pDialog.updateProgress(2) + utils_write_slist_to_file(cfg.REPORT_SL_AUDIT_ERRORS_PATH.getPath(), report_error_list) + pDialog.updateProgress(3) + utils_write_slist_to_file(cfg.REPORT_SL_AUDIT_ROMS_GOOD_PATH.getPath(), ROM_report_good_list) + pDialog.updateProgress(4) + utils_write_slist_to_file(cfg.REPORT_SL_AUDIT_ROMS_ERRORS_PATH.getPath(), ROM_report_error_list) + pDialog.updateProgress(5) + utils_write_slist_to_file(cfg.REPORT_SL_AUDIT_CHDS_GOOD_PATH.getPath(), CHD_report_good_list) + pDialog.updateProgress(6) + utils_write_slist_to_file(cfg.REPORT_SL_AUDIT_CHDS_ERRORS_PATH.getPath(), CHD_report_error_list) + pDialog.endProgress() + + # Update SL audit statistics. + db_safe_edit(control_dic, 'audit_SL_items_runnable', audit_SL_items_runnable) + db_safe_edit(control_dic, 'audit_SL_items_with_arch', audit_SL_items_with_arch) + db_safe_edit(control_dic, 'audit_SL_items_with_arch_OK', audit_SL_items_with_arch_OK) + db_safe_edit(control_dic, 'audit_SL_items_with_arch_BAD', audit_SL_items_with_arch_BAD) + db_safe_edit(control_dic, 'audit_SL_items_without_arch', audit_SL_items_without_arch) + db_safe_edit(control_dic, 'audit_SL_items_with_arch_ROM', audit_SL_items_with_arch_ROM) + db_safe_edit(control_dic, 'audit_SL_items_with_arch_ROM_OK', audit_SL_items_with_arch_ROM_OK) + db_safe_edit(control_dic, 'audit_SL_items_with_arch_ROM_BAD', audit_SL_items_with_arch_ROM_BAD) + db_safe_edit(control_dic, 'audit_SL_items_without_arch_ROM', audit_SL_items_without_arch_ROM) + db_safe_edit(control_dic, 'audit_SL_items_with_CHD', audit_SL_items_with_CHD) + db_safe_edit(control_dic, 'audit_SL_items_with_CHD_OK', audit_SL_items_with_CHD_OK) + db_safe_edit(control_dic, 'audit_SL_items_with_CHD_BAD', audit_SL_items_with_CHD_BAD) + db_safe_edit(control_dic, 'audit_SL_items_without_CHD', audit_SL_items_without_CHD) + + # Update timestamp and save control_dic. + db_safe_edit(control_dic, 't_SL_audit', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + +# ------------------------------------------------------------------------------------------------- +# MAME database building +# ------------------------------------------------------------------------------------------------- +# 1) Scan MAME hash dir for XML files. +# 2) For each XML file, read the first XML_READ_LINES lines. +# 3) Search for the line <softwarelist name="32x" description="Sega 32X cartridges"> +# 4) Create the file SL_NAMES_PATH with a dictionary {sl_name : description, ... } +# +# <softwarelist name="32x" description="Sega 32X cartridges"> +# <softwarelist name="vsmile_cart" description="VTech V.Smile cartridges"> +# <softwarelist name="vsmileb_cart" description="VTech V.Smile Baby cartridges"> +def mame_build_SL_names(cfg): + XML_READ_LINES = 600 + log_debug('mame_build_SL_names() Starting...') + + # If MAME hash path is not configured then create and empty file + SL_names_dic = {} + hash_dir_FN = FileName(cfg.settings['SL_hash_path']) + if not hash_dir_FN.exists(): + log_info('mame_build_SL_names() MAME hash path does not exists.') + log_info('mame_build_SL_names() Creating empty SL_NAMES_PATH') + utils_write_JSON_file(cfg.SL_NAMES_PATH.getPath(), SL_names_dic) + return + + # MAME hash path exists. Carry on. + file_list = os.listdir(hash_dir_FN.getPath()) + log_debug('mame_build_SL_names() Found {} files'.format(len(file_list))) + xml_files = [] + for file in file_list: + if file.endswith('.xml'): xml_files.append(file) + log_debug('mame_build_SL_names() Found {} XML files'.format(len(xml_files))) + for f_name in xml_files: + XML_FN = hash_dir_FN.pjoin(f_name) + # log_debug('Inspecting file "{}"'.format(XML_FN.getPath())) + # Read first XML_READ_LINES lines + try: + f = io.open(XML_FN.getPath(), 'r', encoding = 'utf-8') + except IOError: + log_error('(IOError) Exception opening {}'.format(XML_FN.getPath())) + continue + # f.readlines(XML_READ_LINES) does not work well for some files + # content_list = f.readlines(XML_READ_LINES) + line_count = 1 + content_list = [] + try: + for line in f: + content_list.append(line) + line_count += 1 + if line_count > XML_READ_LINES: break + except UnicodeDecodeError as ex: + log_error('Exception UnicodeDecodeError on line {} of file "{}"'.format(line_count, XML_FN.getBase())) + log_error('Previous line "{}"'.format(content_list[-1])) + raise TypeError + f.close() + content_list = [x.strip() for x in content_list] + for line in content_list: + # Search for SL name + if not line.startswith('<softwarelist'): continue + m = re.search(r'<softwarelist name="([^"]+?)" description="([^"]+?)"', line) + if not m: continue + SL_name, SL_desc = m.group(1), m.group(2) + # log_debug('SL "{}" -> "{}"'.format(SL_name, SL_desc)) + # Substitute SL description (long name). + if SL_desc in SL_better_name_dic: + old_SL_desc = SL_desc + SL_desc = SL_better_name_dic[SL_desc] + log_debug('Substitute SL "{}" with "{}"'.format(old_SL_desc, SL_desc)) + SL_names_dic[SL_name] = SL_desc + break + # Save database + log_debug('mame_build_SL_names() Extracted {} Software List names'.format(len(SL_names_dic))) + utils_write_JSON_file(cfg.SL_NAMES_PATH.getPath(), SL_names_dic) + +# ------------------------------------------------------------------------------------------------- +# Reads and processes MAME.xml +# +# The ROM location in the non-merged set is unique and can be used as a unique dictionary key. +# Include only ROMs and not CHDs. +# +# roms_sha1_dic = { +# rom_nonmerged_location_1 : sha1_hash, +# rom_nonmerged_location_2 : sha1_hash, +# ... +# +# } +# +# Saves: +# MAIN_DB_PATH +# RENDER_DB_PATH +# ROMS_DB_PATH +# MAIN_ASSETS_DB_PATH (empty JSON file) +# MAIN_PCLONE_DIC_PATH +# MAIN_CONTROL_PATH (updated and then JSON file saved) +# ROM_SHA1_HASH_DB_PATH +# +def _get_stats_dic(): + return { + 'parents' : 0, + 'clones' : 0, + 'devices' : 0, + 'devices_parents' : 0, + 'devices_clones' : 0, + 'runnable' : 0, + 'runnable_parents' : 0, + 'runnable_clones' : 0, + 'samples' : 0, + 'samples_parents' : 0, + 'samples_clones' : 0, + 'BIOS' : 0, + 'BIOS_parents' : 0, + 'BIOS_clones' : 0, + 'coin' : 0, + 'coin_parents' : 0, + 'coin_clones' : 0, + 'nocoin' : 0, + 'nocoin_parents' : 0, + 'nocoin_clones' : 0, + 'mechanical' : 0, + 'mechanical_parents' : 0, + 'mechanical_clones' : 0, + 'dead' : 0, + 'dead_parents' : 0, + 'dead_clones' : 0, + } + +def _update_stats(stats, machine, m_render, runnable): + if m_render['cloneof']: stats['clones'] += 1 + else: stats['parents'] += 1 + if m_render['isDevice']: + stats['devices'] += 1 + if m_render['cloneof']: + stats['devices_clones'] += 1 + else: + stats['devices_parents'] += 1 + if runnable: + stats['runnable'] += 1 + if m_render['cloneof']: + stats['runnable_clones'] += 1 + else: + stats['runnable_parents'] += 1 + if machine['sampleof']: + stats['samples'] += 1 + if m_render['cloneof']: + stats['samples_clones'] += 1 + else: + stats['samples_parents'] += 1 + if m_render['isBIOS']: + stats['BIOS'] += 1 + if m_render['cloneof']: + stats['BIOS_clones'] += 1 + else: + stats['BIOS_parents'] += 1 + + if runnable: + if machine['input']['att_coins'] > 0: + stats['coin'] += 1 + if m_render['cloneof']: + stats['coin_clones'] += 1 + else: + stats['coin_parents'] += 1 + else: + stats['nocoin'] += 1 + if m_render['cloneof']: + stats['nocoin_clones'] += 1 + else: + stats['nocoin_parents'] += 1 + if machine['isMechanical']: + stats['mechanical'] += 1 + if m_render['cloneof']: + stats['mechanical_clones'] += 1 + else: + stats['mechanical_parents'] += 1 + if machine['isDead']: + stats['dead'] += 1 + if m_render['cloneof']: + stats['dead_clones'] += 1 + else: + stats['dead_parents'] += 1 + +def mame_build_MAME_main_database(cfg, st_dic): + # Use for debug purposes. This number must be much bigger than the actual number of machines + # when releasing. + STOP_AFTER_MACHINES = 250000 + DATS_dir_FN = FileName(cfg.settings['dats_path']) + ALLTIME_FN = DATS_dir_FN.pjoin(ALLTIME_INI) + ARTWORK_FN = DATS_dir_FN.pjoin(ARTWORK_INI) + BESTGAMES_FN = DATS_dir_FN.pjoin(BESTGAMES_INI) + CATEGORY_FN = DATS_dir_FN.pjoin(CATEGORY_INI) + CATLIST_FN = DATS_dir_FN.pjoin(CATLIST_INI) + CATVER_FN = DATS_dir_FN.pjoin(CATVER_INI) + GENRE_FN = DATS_dir_FN.pjoin(GENRE_INI) + MATURE_FN = DATS_dir_FN.pjoin(MATURE_INI) + NPLAYERS_FN = DATS_dir_FN.pjoin(NPLAYERS_INI) + SERIES_FN = DATS_dir_FN.pjoin(SERIES_INI) + COMMAND_FN = DATS_dir_FN.pjoin(COMMAND_DAT) + GAMEINIT_FN = DATS_dir_FN.pjoin(GAMEINIT_DAT) + HISTORY_XML_FN = DATS_dir_FN.pjoin(HISTORY_XML) + HISTORY_DAT_FN = DATS_dir_FN.pjoin(HISTORY_DAT) + MAMEINFO_FN = DATS_dir_FN.pjoin(MAMEINFO_DAT) + + # --- Print user configuration for debug --- + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + rom_path = cfg.settings['rom_path_vanilla'] + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + rom_path = cfg.settings['rom_path_2003_plus'] + else: + raise TypeError('Unknown op_mode "{}"'.format(cfg.settings['op_mode'])) + log_info('mame_build_MAME_main_database() Starting...') + log_info('--- Paths ---') + log_info('mame_prog = "{}"'.format(cfg.settings['mame_prog'])) + log_info('ROM path = "{}"'.format(rom_path)) + log_info('assets_path = "{}"'.format(cfg.settings['assets_path'])) + log_info('DATs_path = "{}"'.format(cfg.settings['dats_path'])) + log_info('CHD_path = "{}"'.format(cfg.settings['chd_path'])) + log_info('samples_path = "{}"'.format(cfg.settings['samples_path'])) + log_info('SL_hash_path = "{}"'.format(cfg.settings['SL_hash_path'])) + log_info('SL_rom_path = "{}"'.format(cfg.settings['SL_rom_path'])) + log_info('SL_chd_path = "{}"'.format(cfg.settings['SL_chd_path'])) + log_info('--- INI paths ---') + log_info('alltime_path = "{}"'.format(ALLTIME_FN.getPath())) + log_info('artwork_path = "{}"'.format(ARTWORK_FN.getPath())) + log_info('bestgames_path = "{}"'.format(BESTGAMES_FN.getPath())) + log_info('category_path = "{}"'.format(CATEGORY_FN.getPath())) + log_info('catlist_path = "{}"'.format(CATLIST_FN.getPath())) + log_info('catver_path = "{}"'.format(CATVER_FN.getPath())) + log_info('genre_path = "{}"'.format(GENRE_FN.getPath())) + log_info('mature_path = "{}"'.format(MATURE_FN.getPath())) + log_info('nplayers_path = "{}"'.format(NPLAYERS_FN.getPath())) + log_info('series_path = "{}"'.format(SERIES_FN.getPath())) + log_info('--- DAT paths ---') + log_info('command_path = "{}"'.format(COMMAND_FN.getPath())) + log_info('gameinit_path = "{}"'.format(GAMEINIT_FN.getPath())) + log_info('history_xml_path = "{}"'.format(HISTORY_XML_FN.getPath())) + log_info('history_dat_path = "{}"'.format(HISTORY_DAT_FN.getPath())) + log_info('mameinfo_path = "{}"'.format(MAMEINFO_FN.getPath())) + + # --- Automatically extract and/or process MAME XML --- + # After this block of code we have: + # 1) a valid XML_control_dic and the XML control file is created and/or current. + # 2) valid and verified for existence MAME_XML_path. + MAME_XML_path, XML_control_FN = mame_init_MAME_XML(cfg, st_dic) + if st_dic['abort']: return + XML_control_dic = utils_load_JSON_file(XML_control_FN.getPath()) + + # Main progress dialog. + pDialog = KodiProgressDialog() + + # --- Build SL_NAMES_PATH if available, to be used later in the catalog building --- + if cfg.settings['global_enable_SL']: + pDialog.startProgress('Creating list of Software List names...') + mame_build_SL_names(cfg) + pDialog.endProgress() + else: + log_info('SL globally disabled, not creating SL names.') + + # --- Load INI files to include category information --- + num_items = 10 + pd_line1 = 'Processing INI files...' + pDialog.startProgress(pd_line1, num_items) + pDialog.updateProgress(0, '{}\nFile {}'.format(pd_line1, ALLTIME_INI)) + alltime_dic = mame_load_INI_datfile_simple(ALLTIME_FN.getPath()) + pDialog.updateProgress(1, '{}\nFile {}'.format(pd_line1, ARTWORK_INI)) + artwork_dic = mame_load_INI_datfile_simple(ARTWORK_FN.getPath()) + pDialog.updateProgress(2, '{}\nFile {}'.format(pd_line1, BESTGAMES_INI)) + bestgames_dic = mame_load_INI_datfile_simple(BESTGAMES_FN.getPath()) + pDialog.updateProgress(3, '{}\nFile {}'.format(pd_line1, CATEGORY_INI)) + category_dic = mame_load_INI_datfile_simple(CATEGORY_FN.getPath()) + pDialog.updateProgress(4, '{}\nFile {}'.format(pd_line1, CATLIST_INI)) + catlist_dic = mame_load_INI_datfile_simple(CATLIST_FN.getPath()) + pDialog.updateProgress(5, '{}\nFile {}'.format(pd_line1, CATVER_INI)) + (catver_dic, veradded_dic) = mame_load_Catver_ini(CATVER_FN.getPath()) + pDialog.updateProgress(6, '{}\nFile {}'.format(pd_line1, GENRE_INI)) + genre_dic = mame_load_INI_datfile_simple(GENRE_FN.getPath()) + pDialog.updateProgress(7, '{}\nFile {}'.format(pd_line1, MATURE_INI)) + mature_dic = mame_load_Mature_ini(MATURE_FN.getPath()) + pDialog.updateProgress(8, '{}\nFile {}'.format(pd_line1, NPLAYERS_INI)) + nplayers_dic = mame_load_nplayers_ini(NPLAYERS_FN.getPath()) + pDialog.updateProgress(9, '{}\nFile {}'.format(pd_line1, SERIES_INI)) + series_dic = mame_load_INI_datfile_simple(SERIES_FN.getPath()) + pDialog.endProgress() + + # --- Load DAT files to include category information --- + num_items = 4 + pd_line1 = 'Processing DAT files...' + pDialog.startProgress(pd_line1, num_items) + pDialog.updateProgress(0, '{}\nFile {}'.format(pd_line1, COMMAND_DAT)) + command_dic = mame_load_Command_DAT(COMMAND_FN.getPath()) + pDialog.updateProgress(1, '{}\nFile {}'.format(pd_line1, GAMEINIT_DAT)) + gameinit_dic = mame_load_GameInit_DAT(GAMEINIT_FN.getPath()) + # First try to load History.xml. If not found, then try History.dat + if HISTORY_XML_FN.exists(): + pDialog.updateProgress(2, '{}\nFile {}'.format(pd_line1, HISTORY_XML)) + history_dic = mame_load_History_xml(HISTORY_XML_FN.getPath()) + else: + pDialog.updateProgress(2, '{}\nFile {}'.format(pd_line1, HISTORY_DAT)) + history_dic = mame_load_History_DAT(HISTORY_DAT_FN.getPath()) + pDialog.updateProgress(3, '{}\nFile {}'.format(pd_line1, MAMEINFO_DAT)) + mameinfo_dic = mame_load_MameInfo_DAT(MAMEINFO_FN.getPath()) + pDialog.endProgress() + + # --- Verify that INIs comply with the data model --- + # In MAME 0.209 only artwork, category and series are lists. Other INIs define + # machine-unique categories (each machine belongs to one category only). + log_info('alltime_dic unique_categories {}'.format(alltime_dic['unique_categories'])) + log_info('artwork_dic unique_categories {}'.format(artwork_dic['unique_categories'])) + log_info('bestgames_dic unique_categories {}'.format(bestgames_dic['unique_categories'])) + log_info('category_dic unique_categories {}'.format(category_dic['unique_categories'])) + log_info('catlist_dic unique_categories {}'.format(catlist_dic['unique_categories'])) + log_info('catver_dic unique_categories {}'.format(catver_dic['unique_categories'])) + log_info('genre_dic unique_categories {}'.format(genre_dic['unique_categories'])) + log_info('mature_dic unique_categories {}'.format(mature_dic['unique_categories'])) + log_info('nplayers_dic unique_categories {}'.format(nplayers_dic['unique_categories'])) + log_info('series_dic unique_categories {}'.format(series_dic['unique_categories'])) + log_info('veradded_dic unique_categories {}'.format(veradded_dic['unique_categories'])) + + # --------------------------------------------------------------------------------------------- + # Incremental Parsing approach B (from [1]) + # --------------------------------------------------------------------------------------------- + # Do not load whole MAME XML into memory! Use an iterative parser to + # grab only the information we want and discard the rest. + # See [1] http://effbot.org/zone/element-iterparse.htm + log_info('Loading XML "{}"'.format(MAME_XML_path.getPath())) + xml_iter = ET.iterparse(MAME_XML_path.getPath(), events = ("start", "end")) + event, root = next(xml_iter) + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + mame_version_str = root.attrib['build'] + mame_version_int = mame_get_numerical_version(mame_version_str) + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + mame_version_str = '0.78 (RA2003Plus)' + mame_version_int = mame_get_numerical_version(mame_version_str) + else: + raise ValueError + log_info('mame_build_MAME_main_database() MAME string version "{}"'.format(mame_version_str)) + log_info('mame_build_MAME_main_database() MAME numerical version {}'.format(mame_version_int)) + + # --- Process MAME XML --- + total_machines = XML_control_dic['total_machines'] + processed_machines = 0 + pDialog.startProgress('Building main MAME database...', total_machines) + stats = _get_stats_dic() + log_info('mame_build_MAME_main_database() total_machines {:,}'.format(total_machines)) + machines, renderdb_dic, machines_roms, machines_devices = {}, {}, {}, {} + roms_sha1_dic = {} + log_info('mame_build_MAME_main_database() Parsing MAME XML file ...') + num_iteration = 0 + for event, elem in xml_iter: + # Debug the elements we are iterating from the XML file + # log_debug('event "{}"'.format(event)) + # log_debug('elem.tag "{}" | elem.text "{}" | elem.attrib "{}"'.format(elem.tag, elem.text, text_type(elem.attrib))) + + # <machine> tag start event includes <machine> attributes + if event == 'start' and (elem.tag == 'machine' or elem.tag == 'game'): + processed_machines += 1 + machine = db_new_machine_dic() + m_render = db_new_machine_render_dic() + m_roms = db_new_roms_object() + device_list = [] + runnable = False + num_displays = 0 + + # --- Process <machine> attributes ---------------------------------------------------- + # name is #REQUIRED attribute + if 'name' not in elem.attrib: + log_error('name attribute not found in <machine> tag.') + raise ValueError('name attribute not found in <machine> tag') + m_name = elem.attrib['name'] + + # In modern MAME sourcefile attribute is always present + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + # sourcefile #IMPLIED attribute + if 'sourcefile' not in elem.attrib: + log_error('sourcefile attribute not found in <machine> tag.') + raise ValueError('sourcefile attribute not found in <machine> tag.') + # Remove trailing '.cpp' from driver name + machine['sourcefile'] = elem.attrib['sourcefile'] + # In MAME 2003 Plus sourcefile attribute does not exists. + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + machine['sourcefile'] = '[ Not set ]' + else: + raise ValueError + + # Optional, default no + if 'isbios' not in elem.attrib: + m_render['isBIOS'] = False + else: + m_render['isBIOS'] = True if elem.attrib['isbios'] == 'yes' else False + if 'isdevice' not in elem.attrib: + m_render['isDevice'] = False + else: + m_render['isDevice'] = True if elem.attrib['isdevice'] == 'yes' else False + if 'ismechanical' not in elem.attrib: + machine['isMechanical'] = False + else: + machine['isMechanical'] = True if elem.attrib['ismechanical'] == 'yes' else False + # Optional, default yes + if 'runnable' not in elem.attrib: + runnable = True + else: + runnable = False if elem.attrib['runnable'] == 'no' else True + + # cloneof is #IMPLIED attribute + if 'cloneof' in elem.attrib: m_render['cloneof'] = elem.attrib['cloneof'] + + # romof is #IMPLIED attribute + if 'romof' in elem.attrib: machine['romof'] = elem.attrib['romof'] + + # sampleof is #IMPLIED attribute + if 'sampleof' in elem.attrib: machine['sampleof'] = elem.attrib['sampleof'] + + # --- Add catver/catlist/genre --- + machine['alltime'] = alltime_dic['data'][m_name] if m_name in alltime_dic['data'] else '[ Not set ]' + machine['artwork'] = artwork_dic['data'][m_name] if m_name in artwork_dic['data'] else [ '[ Not set ]' ] + machine['bestgames'] = bestgames_dic['data'][m_name] if m_name in bestgames_dic['data'] else '[ Not set ]' + machine['category'] = category_dic['data'][m_name] if m_name in category_dic['data'] else [ '[ Not set ]' ] + machine['catlist'] = catlist_dic['data'][m_name] if m_name in catlist_dic['data'] else '[ Not set ]' + machine['catver'] = catver_dic['data'][m_name] if m_name in catver_dic['data'] else '[ Not set ]' + machine['genre'] = genre_dic['data'][m_name] if m_name in genre_dic['data'] else '[ Not set ]' + machine['series'] = series_dic['data'][m_name] if m_name in series_dic['data'] else [ '[ Not set ]' ] + machine['veradded'] = veradded_dic['data'][m_name] if m_name in veradded_dic['data'] else '[ Not set ]' + # Careful, nplayers goes into render database. + m_render['nplayers'] = nplayers_dic['data'][m_name] if m_name in nplayers_dic['data'] else '[ Not set ]' + + elif event == 'start' and elem.tag == 'description': + m_render['description'] = text_type(elem.text) + + elif event == 'start' and elem.tag == 'year': + m_render['year'] = text_type(elem.text) + + elif event == 'start' and elem.tag == 'manufacturer': + m_render['manufacturer'] = text_type(elem.text) + + # Check in machine has BIOS + # <biosset> name and description attributes are mandatory + elif event == 'start' and elem.tag == 'biosset': + # --- Add BIOS to ROMS_DB_PATH --- + bios = db_new_bios_dic() + bios['name'] = text_type(elem.attrib['name']) + bios['description'] = text_type(elem.attrib['description']) + m_roms['bios'].append(bios) + + # Check in machine has ROMs + # A) ROM is considered to be valid if SHA1 has exists. + # Are there ROMs with no sha1? There are a lot, for example + # machine 1941j <rom name="yi22b.1a" size="279" status="nodump" region="bboardplds" /> + # + # B) A ROM is unique to that machine if the <rom> tag does not have the 'merge' attribute. + # For example, snes and snespal both have <rom> tags that point to exactly the same + # BIOS. However, in a split set only snes.zip ROM set exists. + # snes -> <rom name="spc700.rom" size="64" crc="44bb3a40" ... > + # snespal -> <rom name="spc700.rom" merge="spc700.rom" size="64" crc="44bb3a40" ... > + # + # C) In AML, hasROM actually means "machine has it own ROMs not found somewhere else". + elif event == 'start' and elem.tag == 'rom': + # --- Research --- + # if not 'sha1' in elem.attrib: + # raise GeneralError('ROM with no sha1 (machine {})'.format(machine_name)) + + # --- Add BIOS to ROMS_DB_PATH --- + rom = db_new_rom_dic() + rom['name'] = text_type(elem.attrib['name']) + rom['merge'] = text_type(elem.attrib['merge']) if 'merge' in elem.attrib else '' + rom['bios'] = text_type(elem.attrib['bios']) if 'bios' in elem.attrib else '' + rom['size'] = int(elem.attrib['size']) if 'size' in elem.attrib else 0 + rom['crc'] = text_type(elem.attrib['crc']) if 'crc' in elem.attrib else '' + m_roms['roms'].append(rom) + + # --- ROMs SHA1 database --- + sha1 = text_type(elem.attrib['sha1']) if 'sha1' in elem.attrib else '' + # Only add valid ROMs, ignore invalid. + if sha1: + rom_nonmerged_location = m_name + '/' + rom['name'] + roms_sha1_dic[rom_nonmerged_location] = sha1 + + # Check in machine has CHDs + # A) CHD is considered valid if and only if SHA1 hash exists. + # Keep in mind that there can be multiple disks per machine, some valid, some invalid. + # Just one valid CHD is OK. + # B) A CHD is unique to a machine if the <disk> tag does not have the 'merge' attribute. + # See comments for ROMs avobe. + elif event == 'start' and elem.tag == 'disk': + # <!ATTLIST disk name CDATA #REQUIRED> + # if 'sha1' in elem.attrib and 'merge' in elem.attrib: machine['CHDs_merged'].append(elem.attrib['name']) + # if 'sha1' in elem.attrib and 'merge' not in elem.attrib: machine['CHDs'].append(elem.attrib['name']) + + # Add BIOS to ROMS_DB_PATH. + disk = db_new_disk_dic() + disk['name'] = text_type(elem.attrib['name']) + disk['merge'] = text_type(elem.attrib['merge']) if 'merge' in elem.attrib else '' + disk['sha1'] = text_type(elem.attrib['sha1']) if 'sha1' in elem.attrib else '' + m_roms['disks'].append(disk) + + # Machine devices + elif event == 'start' and elem.tag == 'device_ref': + device_list.append(text_type(elem.attrib['name'])) + + # Machine samples + elif event == 'start' and elem.tag == 'sample': + sample = { 'name' : text_type(elem.attrib['name']) } + m_roms['samples'].append(sample) + + # Chips define CPU and audio circuits. + elif event == 'start' and elem.tag == 'chip': + if elem.attrib['type'] == 'cpu': + machine['chip_cpu_name'].append(elem.attrib['name']) + + # Some machines have more than one display tag (for example aquastge has 2). + # Other machines have no display tag (18w) + elif event == 'start' and elem.tag == 'display': + rotate_str = elem.attrib['rotate'] if 'rotate' in elem.attrib else '0' + width_str = elem.attrib['width'] if 'width' in elem.attrib else 'Undefined' + height_str = elem.attrib['height'] if 'height' in elem.attrib else 'Undefined' + # All attribute lists have same length, event if data is empty. + # machine['display_tag'].append(elem.attrib['tag']) + machine['display_type'].append(elem.attrib['type']) + machine['display_rotate'].append(rotate_str) + machine['display_width'].append(width_str) + machine['display_height'].append(height_str) + machine['display_refresh'].append(elem.attrib['refresh']) + num_displays += 1 + + # Some machines have no controls at all. + # 1) <control> reqbuttons attribute, pang uses it (has 2 buttons but only 1 is required + # 2) <control> reqbuttons ways2, bcclimbr uses it. Sometimes ways attribute is a string! + # + # machine['input'] = { + # 'att_players' CDATA #REQUIRED + # 'att_coins' CDATA #IMPLIED + # 'att_service' (yes|no) "no" + # 'att_tilt' (yes|no) "no" + # 'control_list' : [ + # { + # 'type' : string CDATA #REQUIRED + # 'player' : int CDATA #IMPLIED + # 'buttons' : int CDATA #IMPLIED + # 'ways' : [ ways string, ways2 string, ways3 string ] CDATA #IMPLIED + # }, ... + # ] + # } + # + # In MAME 2003 Plus bios machines are not runnable and only have <description>, + # <year>, <manufacturer>, <biosset> and <rom> tags. For example, machine neogeo. + # + elif event == 'start' and elem.tag == 'input': + # In the archaic MAMEs used by Retroarch the control structure is different + # and this code must be adapted. + vanilla_mame_input_mode = True + + # --- <input> attributes --- + # Attribute list in the same order as in the DTD + att_service = False + if 'service' in elem.attrib and elem.attrib['service'] == 'yes': + att_service = True + att_tilt = False + if 'tilt' in elem.attrib and elem.attrib['tilt'] == 'yes': + att_tilt = True + att_players = int(elem.attrib['players']) if 'players' in elem.attrib else 0 + # "control" attribute only in MAME 2003 Plus. + # Note that in some machines with valid controls, for example 88games, <input> control + # attribute is empty and must be given a default value. + att_control = '[ Undefined control type ]' + if 'control' in elem.attrib: + vanilla_mame_input_mode = False + att_control = elem.attrib['control'] + # "buttons" attribute only in MAME 2003 Plus. + att_buttons = 0 + if 'buttons' in elem.attrib: + vanilla_mame_input_mode = False + att_buttons = int(elem.attrib['buttons']) + att_coins = int(elem.attrib['coins']) if 'coins' in elem.attrib else 0 + + # --- Create control_list --- + control_list = [] + if vanilla_mame_input_mode: + # --- Vanilla MAME mode --- + # <input> child tags. + for control_child in elem: + attrib = control_child.attrib + # Skip non <control> tags. Process <control> tags only. + if control_child.tag != 'control': continue + # Error test. "type" is the only required attribute. + if 'type' not in attrib: + raise TypeError('<input> -> <control> has not "type" attribute') + ctrl_dic = {'type' : '', 'player' : -1, 'buttons' : -1, 'ways' : []} + ctrl_dic['type'] = attrib['type'] + ctrl_dic['player'] = int(attrib['player']) if 'player' in attrib else -1 + ctrl_dic['buttons'] = int(attrib['buttons']) if 'buttons' in attrib else -1 + ways_list = [] + if 'ways' in attrib: ways_list.append(attrib['ways']) + if 'ways2' in attrib: ways_list.append(attrib['ways2']) + if 'ways3' in attrib: ways_list.append(attrib['ways3']) + ctrl_dic['ways'] = ways_list + control_list.append(ctrl_dic) + # Fix player field when implied. + if att_players == 1: + for control in control_list: control['player'] = 1 + else: + # --- MAME 2003 Plus mode --- + # Create a simulated control_list. + for i in range(att_players): + control_list.append({ + 'type' : att_control, + 'player' : i + 1, + 'buttons' : att_buttons, + 'ways' : [], + }) + + # Add new input dictionary. + machine['input'] = { + 'att_service' : att_service, + 'att_tilt' : att_tilt, + 'att_players' : att_players, + 'att_coins' : att_coins, + 'control_list' : control_list, + } + + elif event == 'start' and elem.tag == 'driver': + # status is #REQUIRED attribute + m_render['driver_status'] = text_type(elem.attrib['status']) + + elif event == 'start' and elem.tag == 'softwarelist': + # name is #REQUIRED attribute + machine['softwarelists'].append(elem.attrib['name']) + + # Device tag for machines that support loading external files + elif event == 'start' and elem.tag == 'device': + att_type = elem.attrib['type'] # The only mandatory attribute + att_tag = elem.attrib['tag'] if 'tag' in elem.attrib else '' + att_mandatory = elem.attrib['mandatory'] if 'mandatory' in elem.attrib else '' + att_interface = elem.attrib['interface'] if 'interface' in elem.attrib else '' + # Transform device_mandatory into bool + if att_mandatory and att_mandatory == '1': att_mandatory = True + else: att_mandatory = False + + # Iterate children of <device> and search for <instance> tags + instance_tag_found = False + inst_name = '' + inst_briefname = '' + ext_names = [] + for device_child in elem: + if device_child.tag == 'instance': + # Stop if <device> tag has more than one <instance> tag. In MAME 0.190 no + # machines trigger this. + if instance_tag_found: + raise GeneralError('Machine {} has more than one <instance> inside <device>') + inst_name = device_child.attrib['name'] + inst_briefname = device_child.attrib['briefname'] + instance_tag_found = True + elif device_child.tag == 'extension': + ext_names.append(device_child.attrib['name']) + + # NOTE Some machines have no instance inside <device>, for example 2020bb + # I don't know how to launch those machines + # if not instance_tag_found: + # log_warning('<instance> tag not found inside <device> tag (machine {})'.format(m_name)) + # device_type = '{} (NI)'.format(device_type) + + # Add device to database + device_dic = { + 'att_type' : att_type, + 'att_tag' : att_tag, + 'att_mandatory' : att_mandatory, + 'att_interface' : att_interface, + 'instance' : { 'name' : inst_name, 'briefname' : inst_briefname }, + 'ext_names' : ext_names + } + machine['devices'].append(device_dic) + + # --- <machine>/<game> tag closing. Add new machine to database --- + elif event == 'end' and (elem.tag == 'machine' or elem.tag == 'game'): + # Checks in modern MAME + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + # Assumption 1: isdevice = True if and only if runnable = False + if m_render['isDevice'] == runnable: + log_error("Machine {}: machine['isDevice'] == runnable".format(m_name)) + raise ValueError + + # Are there machines with more than 1 <display> tag. Answer: YES + # if num_displays > 1: + # log_error("Machine {}: num_displays = {}".format(m_name, num_displays)) + # raise ValueError + + # All machines with 0 displays are mechanical? NO, 24cdjuke has no screen and + # is not mechanical. However 24cdjuke is a preliminary driver. + # if num_displays == 0 and not machine['ismechanical']: + # log_error("Machine {}: num_displays == 0 and not machine['ismechanical']".format(m_name)) + # raise ValueError + # Checks in Retroarch MAME 2003 Plus + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + # In MAME 2003 Plus XML some <year> tags are empty. + # Set a default value. + if not m_render['year']: m_render['year'] = '[ Not set ]' + else: + raise ValueError + + # Mark dead machines. A machine is dead if Status is preliminary AND have no controls. + if m_render['driver_status'] == 'preliminary' and not machine['input']['control_list']: + machine['isDead'] = True + + # --- Delete XML element once it has been processed to conserve memory --- + elem.clear() + + # --- Compute statistics --- + _update_stats(stats, machine, m_render, runnable) + + # Add new machine + machines[m_name] = machine + renderdb_dic[m_name] = m_render + machines_roms[m_name] = m_roms + machines_devices[m_name] = device_list + + # --- Print something to prove we are doing stuff --- + num_iteration += 1 + if num_iteration % 1000 == 0: + pDialog.updateProgress(processed_machines) + # log_debug('Processed {:10d} events ({:6d} machines so far) ...'.format( + # num_iteration, processed_machines)) + # log_debug('processed_machines = {}'.format(processed_machines)) + # log_debug('total_machines = {}'.format(total_machines)) + # Stop after STOP_AFTER_MACHINES machines have been processed for debug. + if processed_machines >= STOP_AFTER_MACHINES: break + pDialog.endProgress() + log_info('Processed {:,} MAME XML events'.format(num_iteration)) + log_info('Processed machines {:,} ({:,} parents, {:,} clones)'.format( + processed_machines, stats['parents'], stats['clones'])) + log_info('Dead machines {:,} ({:,} parents, {:,} clones)'.format( + stats['dead'], stats['dead_parents'], stats['dead_clones'])) + + # --------------------------------------------------------------------------------------------- + # Main parent-clone list + # --------------------------------------------------------------------------------------------- + # Create a couple of data struct for quickly know the parent of a clone game and + # all clones of a parent. + # main_pclone_dic = { 'parent_name' : ['clone_name', 'clone_name', ... ] , ... } + # main_clone_to_parent_dic = { 'clone_name' : 'parent_name', ... } + log_info('Making PClone list...') + main_pclone_dic = {} + main_clone_to_parent_dic = {} + for machine_name in renderdb_dic: + m_render = renderdb_dic[machine_name] + if m_render['cloneof']: + parent_name = m_render['cloneof'] + # If parent already in main_pclone_dic then add clone to parent list. + # If parent not there, then add parent first and then add clone. + if parent_name not in main_pclone_dic: main_pclone_dic[parent_name] = [] + main_pclone_dic[parent_name].append(machine_name) + # Add clone machine to main_clone_to_parent_dic + main_clone_to_parent_dic[machine_name] = parent_name + continue + # Machine is a parent. Add to main_pclone_dic if not already there. + if machine_name not in main_pclone_dic: main_pclone_dic[machine_name] = [] + + # --------------------------------------------------------------------------------------------- + # Initialise asset list + # --------------------------------------------------------------------------------------------- + log_debug('Initializing MAME asset database...') + log_debug('Option generate_history_infolabel is {}'.format(cfg.settings['generate_history_infolabel'])) + assetdb_dic = {key : db_new_MAME_asset() for key in machines} + if cfg.settings['generate_history_infolabel'] and history_idx_dic: + log_debug('Adding History.DAT to MAME asset database.') + for m_name in assetdb_dic: + asset = assetdb_dic[m_name] + asset['flags'] = db_initial_flags(machines[m_name], renderdb_dic[m_name], machines_roms[m_name]) + if m_name in history_idx_dic['mame']['machines']: + d_name, db_list, db_machine = history_idx_dic['mame']['machines'][m_name].split('|') + asset['history'] = history_dic[db_list][db_machine] + else: + log_debug('Not including History.DAT in MAME asset database.') + for m_name in assetdb_dic: + assetdb_dic[m_name]['flags'] = db_initial_flags(machines[m_name], + renderdb_dic[m_name], machines_roms[m_name]) + + # --------------------------------------------------------------------------------------------- + # Improve information fields in Main Render database + # --------------------------------------------------------------------------------------------- + if mature_dic: + log_info('MAME machine Mature information available.') + for machine_name in renderdb_dic: + renderdb_dic[machine_name]['isMature'] = True if machine_name in mature_dic['data'] else False + else: + log_info('MAME machine Mature flag not available.') + + # Add genre infolabel into render database. + if genre_dic: + log_info('Using genre.ini for MAME genre information.') + for machine_name in renderdb_dic: + renderdb_dic[machine_name]['genre'] = machines[machine_name]['genre'] + elif categories_dic: + log_info('Using catver.ini for MAME genre information.') + for machine_name in renderdb_dic: + renderdb_dic[machine_name]['genre'] = machines[machine_name]['catver'] + elif catlist_dic: + log_info('Using catlist.ini for MAME genre information.') + for machine_name in renderdb_dic: + renderdb_dic[machine_name]['genre'] = machines[machine_name]['catlist'] + + # --------------------------------------------------------------------------------------------- + # Improve name in DAT indices and machine names + # --------------------------------------------------------------------------------------------- + # --- History DAT categories are Software List names --- + if history_dic: + log_debug('Updating History DAT categories and machine names ...') + SL_names_dic = utils_load_JSON_file(cfg.SL_NAMES_PATH.getPath()) + for cat_name in history_dic['index']: + if cat_name == 'mame': + # Improve MAME machine names + history_dic['index'][cat_name]['name'] = 'MAME' + for machine_name in history_dic['index'][cat_name]['machines']: + if machine_name not in renderdb_dic: continue + # Rebuild the CSV string. + m_str = history_dic['index'][cat_name]['machines'][machine_name] + old_display_name, db_list_name, db_machine_name = m_str.split('|') + display_name = renderdb_dic[machine_name]['description'] + m_str = misc_build_db_str_3(display_name, db_list_name, db_machine_name) + history_dic['index'][cat_name]['machines'][machine_name] = m_str + elif cat_name in SL_names_dic: + # Improve SL machine names. This must be done when building the SL databases + # and not here. + history_dic['index'][cat_name]['name'] = SL_names_dic[cat_name] + + # MameInfo DAT machine names. + if mameinfo_dic['index']: + log_debug('Updating Mameinfo DAT machine names ...') + for cat_name in mameinfo_dic['index']: + for machine_key in mameinfo_dic['index'][cat_name]: + if machine_key not in renderdb_dic: continue + mameinfo_dic['index'][cat_name][machine_key] = renderdb_dic[machine_key]['description'] + + # GameInit DAT machine names. + if gameinit_dic['index']: + log_debug('Updating GameInit DAT machine names ...') + for machine_key in gameinit_dic['index']: + if machine_key not in renderdb_dic: continue + gameinit_dic['index'][machine_key] = renderdb_dic[machine_key]['description'] + + # Command DAT machine names. + if command_dic['index']: + log_debug('Updating Command DAT machine names ...') + for machine_key in command_dic['index']: + if machine_key not in renderdb_dic: continue + command_dic['index'][machine_key] = renderdb_dic[machine_key]['description'] + + # --------------------------------------------------------------------------------------------- + # Update/Reset MAME control dictionary + # Create a new control_dic. This effectively resets AML status. + # The XML control file is required to create the new control_dic. + # --------------------------------------------------------------------------------------------- + log_info('Creating new control_dic.') + log_info('AML version string "{}"'.format(cfg.addon.info_version)) + log_info('AML version int {}'.format(cfg.addon_version_int)) + control_dic = db_new_control_dic() + db_safe_edit(control_dic, 'op_mode_raw', cfg.settings['op_mode_raw']) + db_safe_edit(control_dic, 'op_mode', cfg.settings['op_mode']) + + # Information from the XML control file. + db_safe_edit(control_dic, 'stats_total_machines', total_machines) + + # Addon and MAME version strings + db_safe_edit(control_dic, 'ver_AML_str', cfg.addon.info_version) + db_safe_edit(control_dic, 'ver_AML_int', cfg.addon_version_int) + db_safe_edit(control_dic, 'ver_mame_str', mame_version_str) + db_safe_edit(control_dic, 'ver_mame_int', mame_version_int) + # INI files + db_safe_edit(control_dic, 'ver_alltime', alltime_dic['version']) + db_safe_edit(control_dic, 'ver_artwork', artwork_dic['version']) + db_safe_edit(control_dic, 'ver_bestgames', bestgames_dic['version']) + db_safe_edit(control_dic, 'ver_category', category_dic['version']) + db_safe_edit(control_dic, 'ver_catlist', catlist_dic['version']) + db_safe_edit(control_dic, 'ver_catver', catver_dic['version']) + db_safe_edit(control_dic, 'ver_genre', genre_dic['version']) + db_safe_edit(control_dic, 'ver_mature', mature_dic['version']) + db_safe_edit(control_dic, 'ver_nplayers', nplayers_dic['version']) + db_safe_edit(control_dic, 'ver_series', series_dic['version']) + # DAT files + db_safe_edit(control_dic, 'ver_command', command_dic['version']) + db_safe_edit(control_dic, 'ver_gameinit', gameinit_dic['version']) + db_safe_edit(control_dic, 'ver_history', history_dic['version']) + db_safe_edit(control_dic, 'ver_mameinfo', mameinfo_dic['version']) + + # Statistics + db_safe_edit(control_dic, 'stats_processed_machines', processed_machines) + db_safe_edit(control_dic, 'stats_parents', stats['parents']) + db_safe_edit(control_dic, 'stats_clones', stats['clones']) + db_safe_edit(control_dic, 'stats_runnable', stats['runnable']) + db_safe_edit(control_dic, 'stats_runnable_parents', stats['runnable_parents']) + db_safe_edit(control_dic, 'stats_runnable_clones', stats['runnable_clones']) + # Main filters + db_safe_edit(control_dic, 'stats_coin', stats['coin']) + db_safe_edit(control_dic, 'stats_coin_parents', stats['coin_parents']) + db_safe_edit(control_dic, 'stats_coin_clones', stats['coin_clones']) + db_safe_edit(control_dic, 'stats_nocoin', stats['nocoin']) + db_safe_edit(control_dic, 'stats_nocoin_parents', stats['nocoin_parents']) + db_safe_edit(control_dic, 'stats_nocoin_clones', stats['nocoin_clones']) + db_safe_edit(control_dic, 'stats_mechanical', stats['mechanical']) + db_safe_edit(control_dic, 'stats_mechanical_parents', stats['mechanical_parents']) + db_safe_edit(control_dic, 'stats_mechanical_clones', stats['mechanical_clones']) + db_safe_edit(control_dic, 'stats_dead', stats['dead']) + db_safe_edit(control_dic, 'stats_dead_parents', stats['dead_parents']) + db_safe_edit(control_dic, 'stats_dead_clones', stats['dead_clones']) + db_safe_edit(control_dic, 'stats_devices', stats['devices']) + db_safe_edit(control_dic, 'stats_devices_parents', stats['devices_parents']) + db_safe_edit(control_dic, 'stats_devices_clones', stats['devices_clones']) + # Binary filters + db_safe_edit(control_dic, 'stats_BIOS', stats['BIOS']) + db_safe_edit(control_dic, 'stats_BIOS_parents', stats['BIOS_parents']) + db_safe_edit(control_dic, 'stats_BIOS_clones', stats['BIOS_clones']) + db_safe_edit(control_dic, 'stats_samples', stats['samples']) + db_safe_edit(control_dic, 'stats_samples_parents', stats['samples_parents']) + db_safe_edit(control_dic, 'stats_samples_clones', stats['samples_clones']) + + # --- Timestamp --- + db_safe_edit(control_dic, 't_MAME_DB_build', time.time()) + + # --------------------------------------------------------------------------------------------- + # Build main distributed hashed database + # --------------------------------------------------------------------------------------------- + # This saves the hash files in the database directory. + # At this point the main hashed database is complete but the asset hashed DB is empty. + db_build_main_hashed_db(cfg, control_dic, machines, renderdb_dic) + db_build_asset_hashed_db(cfg, control_dic, assetdb_dic) + + # --- Save databases --- + log_info('Saving database JSON files...') + if OPTION_LOWMEM_WRITE_JSON: + json_write_func = utils_write_JSON_file_lowmem + log_debug('Using utils_write_JSON_file_lowmem() JSON writer') + else: + json_write_func = utils_write_JSON_file + log_debug('Using utils_write_JSON_file() JSON writer') + db_files = [ + [machines, 'MAME machines main', cfg.MAIN_DB_PATH.getPath()], + [renderdb_dic, 'MAME render DB', cfg.RENDER_DB_PATH.getPath()], + [assetdb_dic, 'MAME asset DB', cfg.ASSET_DB_PATH.getPath()], + [machines_roms, 'MAME machine ROMs', cfg.ROMS_DB_PATH.getPath()], + [machines_devices, 'MAME machine devices', cfg.DEVICES_DB_PATH.getPath()], + [main_pclone_dic, 'MAME PClone dictionary', cfg.MAIN_PCLONE_DB_PATH.getPath()], + [roms_sha1_dic, 'MAME ROMs SHA1 dictionary', cfg.SHA1_HASH_DB_PATH.getPath()], + # --- DAT files --- + [history_dic['index'], 'History DAT index', cfg.HISTORY_IDX_PATH.getPath()], + [history_dic['data'], 'History DAT database', cfg.HISTORY_DB_PATH.getPath()], + [mameinfo_dic['index'], 'MAMEInfo DAT index', cfg.MAMEINFO_IDX_PATH.getPath()], + [mameinfo_dic['data'], 'MAMEInfo DAT database', cfg.MAMEINFO_DB_PATH.getPath()], + [gameinit_dic['index'], 'Gameinit DAT index', cfg.GAMEINIT_IDX_PATH.getPath()], + [gameinit_dic['data'], 'Gameinit DAT database', cfg.GAMEINIT_DB_PATH.getPath()], + [command_dic['index'], 'Command DAT index', cfg.COMMAND_IDX_PATH.getPath()], + [command_dic['data'], 'Command DAT database', cfg.COMMAND_DB_PATH.getPath()], + # --- Save control_dic after everything is saved --- + [control_dic, 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ] + db_save_files(db_files, json_write_func) + + # Return a dictionary with references to the objects just in case they are needed after + # this function (in "Build everything", for example). This saves time, because databases do not + # need to be reloaded, and apparently memory as well. + return { + 'machines' : machines, + 'renderdb' : renderdb_dic, + 'assetdb' : assetdb_dic, + 'roms' : machines_roms, + 'devices' : machines_devices, + 'main_pclone_dic' : main_pclone_dic, + 'history_idx_dic' : history_dic['index'], + 'mameinfo_idx_dic' : mameinfo_dic['index'], + 'gameinit_idx_dic' : gameinit_dic['index'], + 'command_idx_dic' : command_dic['index'], + 'history_data_dic' : history_dic['data'], + 'control_dic' : control_dic, + } + +# ------------------------------------------------------------------------------------------------- +# Generates the ROM audit database. This database contains invalid ROMs also to display information +# in "View / Audit", "View MAME machine ROMs" context menu. This database also includes +# device ROMs (<device_ref> ROMs). +def _get_ROM_type(rom): + if rom['bios'] and rom['merge']: r_type = ROM_TYPE_BROM + elif rom['bios'] and not rom['merge']: r_type = ROM_TYPE_XROM + elif not rom['bios'] and rom['merge']: r_type = ROM_TYPE_MROM + elif not rom['bios'] and not rom['merge']: r_type = ROM_TYPE_ROM + else: r_type = ROM_TYPE_ERROR + return r_type + +# Finds merged ROM merged_name in the parent ROM set roms (list of dictionaries). +# Returns a dictionary (first item of the returned list) or None if the merged ROM cannot +# be found in the ROMs of the parent. +def _get_merged_rom(roms, merged_name): + merged_rom_list = [r for r in roms if r['name'] == merged_name] + + if len(merged_rom_list) > 0: + return merged_rom_list[0] + else: + return None + +# Traverses the ROM hierarchy and returns the ROM location and name. +def _get_ROM_location(rom_set, rom, m_name, machines, renderdb_dic, machine_roms): + # In the Merged set all Parent and Clone ROMs are in the parent archive. + # What about BIOS and Device ROMs? + # However, according to the Pleasuredome DATs, ROMs are organised like + # this: + # clone_name_a/clone_rom_1 + # clone_name_b/clone_rom_1 + # parent_rom_1 + # parent_rom_2 + if rom_set == 'MERGED': + cloneof = renderdb_dic[m_name]['cloneof'] + if cloneof: + location = cloneof + '/' + m_name + '/' + rom['name'] + else: + location = m_name + '/' + rom['name'] + + elif rom_set == 'SPLIT': + machine = machines[m_name] + cloneof = renderdb_dic[m_name]['cloneof'] + if not cloneof: + # --- Parent machine --- + # 1. In the Split set non-merged ROMs are in the machine archive and merged ROMs + # are in the parent archive. + if rom['merge']: + romof = machine['romof'] + bios_name = romof + bios_roms = machine_roms[bios_name]['roms'] + bios_rom_merged_name = rom['merge'] + bios_merged_rom = _get_merged_rom(bios_roms, bios_rom_merged_name) + if bios_merged_rom['merge']: + bios_romof = machines[bios_name]['romof'] + parent_bios_name = bios_romof + parent_bios_roms = machine_roms[parent_bios_name]['roms'] + parent_bios_rom_merged_name = bios_merged_rom['merge'] + parent_bios_merged_rom = _get_merged_rom(parent_bios_roms, parent_bios_rom_merged_name) + location = parent_bios_name + '/' + parent_bios_merged_rom['name'] + else: + location = bios_name + '/' + bios_merged_rom['name'] + else: + location = m_name + '/' + rom['name'] + else: + # --- Clone machine --- + # 1. In the Split set, non-merged ROMs are in the machine ZIP archive and + # merged ROMs are in the parent archive. + # 2. If ROM is a BIOS it is located in the romof of the parent. BIOS ROMs + # always have the merge attribute. + # 3. Some machines (notably mslugN) also have non-BIOS common ROMs merged in + # neogeo.zip BIOS archive. + # 4. Some machines (notably XXXXX) have all ROMs merged. In other words, do not + # have their own ROMs. + # 5. Special case: there could be duplicate ROMs with different regions. + # For example, in neogeo.zip + # <rom name="sm1.sm1" size="131072" crc="94416d67" sha1="42f..." /> + # <rom name="sm1.sm1" size="131072" crc="94416d67" sha1="42f..." /> + # + # Furthermore, some machines may have more than 2 identical ROMs: + # <machine name="aa3000" sourcefile="aa310.cpp" cloneof="aa310" romof="aa310"> + # <rom name="cmos_riscos3.bin" merge="cmos_riscos3.bin" bios="300" size="256" crc="0da2d31d" /> + # <rom name="cmos_riscos3.bin" merge="cmos_riscos3.bin" bios="310" size="256" crc="0da2d31d" /> + # <rom name="cmos_riscos3.bin" merge="cmos_riscos3.bin" bios="311" size="256" crc="0da2d31d" /> + # <rom name="cmos_riscos3.bin" merge="cmos_riscos3.bin" bios="319" size="256" crc="0da2d31d" /> + # + # 6. In MAME 0.206, clone machine adonisce has a merged ROM 'setchip v4.04.09.u7' + # that is not found on the parent machine adoins ROMs. + # AML WARN : Clone machine "adonisce" + # AML WARN : ROM "setchip v4.04.09.u7" MERGE "setchip v4.04.09.u7" + # AML WARN : Cannot be found on parent "adonis" ROMs + # By looking to the XML, the ROM "setchip v4.04.09.u7" is on the BIOS aristmk5 + # More machines with same issue: adonisu, bootsctnu, bootsctnua, bootsctnub, ... + # and a lot more machines related to BIOS aristmk5. + # + if rom['merge']: + # >> Get merged ROM from parent + parent_name = cloneof + parent_roms = machine_roms[parent_name]['roms'] + clone_rom_merged_name = rom['merge'] + parent_merged_rom = _get_merged_rom(parent_roms, clone_rom_merged_name) + # >> Clone merged ROM cannot be found in parent ROM set. This is likely a MAME + # >> XML bug. In this case, treat the clone marged ROM as a non-merged ROM. + if parent_merged_rom is None: + log_warning('Clone machine "{}" parent_merged_rom is None'.format(m_name)) + log_warning('ROM "{}" MERGE "{}"'.format(rom['name'], rom['merge'])) + log_warning('Cannot be found on parent "{}" ROMs'.format(parent_name)) + # >> Check if merged ROM is in the BIOS machine. + bios_name = machines[parent_name]['romof'] + if bios_name: + log_warning('Parent machine "{}" has BIOS machine "{}"'.format(parent_name, bios_name)) + log_warning('Searching for clone merged ROM "{}" in BIOS ROMs'.format(clone_rom_merged_name)) + bios_roms = machine_roms[bios_name]['roms'] + bios_merged_rom = _get_merged_rom(bios_roms, clone_rom_merged_name) + location = bios_name + '/' + bios_merged_rom['name'] + else: + TypeError + # >> Check if clone merged ROM is also merged in parent (BIOS ROM) + elif parent_merged_rom['merge']: + parent_romof = machines[parent_name]['romof'] + bios_name = parent_romof + bios_roms = machine_roms[bios_name]['roms'] + bios_rom_merged_name = parent_merged_rom['merge'] + bios_merged_rom = _get_merged_rom(bios_roms, bios_rom_merged_name) + # At least in one machine (0.196) BIOS ROMs can be merged in another + # BIOS ROMs (1 level of recursion in BIOS ROM merging). + if bios_merged_rom['merge']: + bios_romof = machines[bios_name]['romof'] + parent_bios_name = bios_romof + parent_bios_roms = machine_roms[parent_bios_name]['roms'] + parent_bios_rom_merged_name = bios_merged_rom['merge'] + parent_bios_merged_rom = _get_merged_rom(parent_bios_roms, parent_bios_rom_merged_name) + location = parent_bios_name + '/' + parent_bios_merged_rom['name'] + else: + location = bios_name + '/' + bios_merged_rom['name'] + else: + location = parent_name + '/' + parent_merged_rom['name'] + else: + location = m_name + '/' + rom['name'] + + # In the Non-Merged set all ROMs are in the machine archive ZIP archive, with + # the exception of BIOS ROMs and device ROMs. + elif rom_set == 'NONMERGED': + location = m_name + '/' + rom['name'] + + # In the Fully Non-Merged sets all ROMs are in the machine ZIP archive, including + # BIOS ROMs and device ROMs. + # Note that PD ROM sets are named Non-Merged but actually they are Fully Non-merged. + elif rom_set == 'FULLYNONMERGED': + location = m_name + '/' + rom['name'] + + else: + raise TypeError + + return location + +def _get_CHD_location(chd_set, disk, m_name, machines, renderdb_dic, machine_roms): + if chd_set == 'MERGED': + machine = machines[m_name] + cloneof = renderdb_dic[m_name]['cloneof'] + romof = machine['romof'] + if not cloneof: + # --- Parent machine --- + if disk['merge']: + location = romof + '/' + disk['merge'] + else: + location = m_name + '/' + disk['name'] + else: + # --- Clone machine --- + if disk['merge']: + # Get merged ROM from parent + parent_name = cloneof + parent_romof = machines[parent_name]['romof'] + parent_disks = machine_roms[parent_name]['disks'] + clone_disk_merged_name = disk['merge'] + # Pick ROMs with same name and choose the first one. + parent_merged_disk_l = [r for r in parent_disks if r['name'] == clone_disk_merged_name] + parent_merged_disk = parent_merged_disk_l[0] + # Check if clone merged ROM is also merged in parent + if parent_merged_disk['merge']: + # ROM is in the 'romof' archive of the parent ROM + super_parent_name = parent_romof + super_parent_disks = machine_roms[super_parent_name]['disks'] + parent_disk_merged_name = parent_merged_disk['merge'] + # Pick ROMs with same name and choose the first one. + super_parent_merged_disk_l = [r for r in super_parent_disks if r['name'] == parent_disk_merged_name] + super_parent_merged_disk = super_parent_merged_disk_l[0] + location = super_parent_name + '/' + super_parent_merged_disk['name'] + else: + location = parent_name + '/' + parent_merged_disk['name'] + else: + location = cloneof + '/' + disk['name'] + + elif chd_set == 'SPLIT': + machine = machines[m_name] + cloneof = renderdb_dic[m_name]['cloneof'] + romof = machine['romof'] + if not cloneof: + # --- Parent machine --- + if disk['merge']: + location = romof + '/' + disk['name'] + else: + location = m_name + '/' + disk['name'] + else: + # --- Clone machine --- + parent_romof = machines[cloneof]['romof'] + if disk['merge']: + location = romof + '/' + disk['name'] + else: + location = m_name + '/' + disk['name'] + + elif chd_set == 'NONMERGED': + location = m_name + '/' + disk['name'] + + else: + raise TypeError + + return location + +# Returns a unique and alphabetically sorted list of ROM ZIP files. +# This list is different depending on the ROM set (Merged, Split or Non-merged). +def mame_get_ROM_ZIP_list(machine_archives_dic): + rom_list = [] + for key, machine in machine_archives_dic.items(): + rom_list.extend(machine['ROMs']) + + return list(sorted(set(rom_list))) + +def mame_get_Sample_ZIP_list(machine_archives_dic): + rom_list = [] + for key, machine in machine_archives_dic.items(): + rom_list.extend(machine['Samples']) + + return list(sorted(set(rom_list))) + +def mame_get_CHD_list(machine_archives_dic): + rom_list = [] + for key, machine in machine_archives_dic.items(): + rom_list.extend(machine['CHDs']) + + return list(sorted(set(rom_list))) + +# +# Checks for errors before scanning for SL ROMs. +# Display a Kodi dialog if an error is found. +# Returns a dictionary of settings: +# options_dic['abort'] is always present. +# +# +def mame_check_before_build_ROM_audit_databases(cfg, st_dic, control_dic): + kodi_reset_status(st_dic) + + # --- Check that MAME database have been built --- + pass + +# +# Builds the ROM/CHD/Samples audit database and more things. +# Updates statistics in control_dic and saves it. +# The audit databases changes for Merged, Split and Non-merged sets (the location of the ROMs changes). +# The audit database is used when auditing MAME machines. +# +# audit_roms_dic = { +# 'machine_name ' : [ +# { +# 'crc' : string, +# 'location' : 'zip_name/rom_name.rom' +# 'name' : string, +# 'size' : int, +# 'type' : 'ROM' or 'BROM' or 'MROM' or 'XROM' +# }, ..., +# { +# 'location' : 'machine_name/chd_name.chd' +# 'name' : string, +# 'sha1' : string, +# 'type' : 'DISK' +# }, ..., +# { +# 'location' : 'machine_name/sample_name' +# 'name' : string, +# 'type' : 'SAM' +# }, ..., +# ], ... +# } +# +# This function also builds the machine files database. +# +# A) For every machine stores the ROM ZIP/CHD/Samples ZIP files required to run the machine. +# B) A ROM ZIP/CHD exists if and only if it has valid ROMs (CRC and SHA1 exist). +# C) Used by the ROM scanner to check how many machines may be run or not depending of the +# ROM ZIPs/CHDs/Sample ZIPs you have. +# D) ROM ZIPs and CHDs are mandatory to run a machine. Samples are not. This function kind of +# thinks that Samples are also mandatory. +# +# machine_archives_dic = { +# 'machine_name ' : { +# 'ROMs' : [name1, name2, ...], +# 'CHDs' : [dir/name1, dir/name2, ...], +# 'Samples' : [name1, name2, ...], +# }, ... +# } +# +# Saved files: +# ROM_AUDIT_DB_PATH +# ROM_SET_MACHINE_FILES_DB_PATH +# MAIN_CONTROL_PATH (control_dic) +# +# Add the following fields to db_dic_in: +# audit_roms +# machine_archives +# +def mame_build_ROM_audit_databases(cfg, st_dic, db_dic_in): + log_info('mame_build_ROM_audit_databases() Initialising ...') + control_dic = db_dic_in['control_dic'] + machines = db_dic_in['machines'] + renderdb_dic = db_dic_in['renderdb'] + devices_db_dic = db_dic_in['devices'] + machine_roms = db_dic_in['roms'] + + # --- Initialize --- + # This must match the values defined in settings.xml, "ROM sets" tab. + rom_set = ['MERGED', 'SPLIT', 'NONMERGED', 'FULLYNONMERGED'][cfg.settings['mame_rom_set']] + rom_set_str = ['Merged', 'Split', 'Non-merged', 'Fully Non-merged'][cfg.settings['mame_rom_set']] + chd_set = ['MERGED', 'SPLIT', 'NONMERGED'][cfg.settings['mame_chd_set']] + chd_set_str = ['Merged', 'Split', 'Non-merged'][cfg.settings['mame_chd_set']] + log_info('mame_build_ROM_audit_databases() ROM set is {}'.format(rom_set)) + log_info('mame_build_ROM_audit_databases() CHD set is {}'.format(chd_set)) + + # --------------------------------------------------------------------------------------------- + # Audit database + # --------------------------------------------------------------------------------------------- + log_info('mame_build_ROM_audit_databases() Starting...') + log_info('Building {} ROM/Sample audit database...'.format(rom_set_str)) + pDialog = KodiProgressDialog() + pDialog.startProgress('Building {} ROM set...'.format(rom_set_str), len(machines)) + stats_audit_MAME_machines_runnable = 0 + audit_roms_dic = {} + for m_name in sorted(machines): + pDialog.updateProgressInc() + + # --- ROMs --- + # Skip device machines. + if renderdb_dic[m_name]['isDevice']: continue + stats_audit_MAME_machines_runnable += 1 + m_roms = machine_roms[m_name]['roms'] + machine_rom_set = [] + for rom in m_roms: + rom['type'] = _get_ROM_type(rom) + rom['location'] = _get_ROM_location(rom_set, rom, m_name, machines, renderdb_dic, machine_roms) + machine_rom_set.append(rom) + + # --- Device ROMs --- + device_roms_list = [] + for device in devices_db_dic[m_name]: + device_roms_dic = machine_roms[device] + for rom in device_roms_dic['roms']: + rom['type'] = ROM_TYPE_DROM + rom['location'] = device + '/' + rom['name'] + device_roms_list.append(rom) + if device_roms_list: machine_rom_set.extend(device_roms_list) + + # --- Samples --- + sampleof = machines[m_name]['sampleof'] + m_samples = machine_roms[m_name]['samples'] + samples_list = [] + for sample in m_samples: + sample['type'] = ROM_TYPE_SAMPLE + sample['location'] = sampleof + '/' + sample['name'] + samples_list.append(sample) + if samples_list: machine_rom_set.extend(samples_list) + + # Add ROMs to main DB + audit_roms_dic[m_name] = machine_rom_set + pDialog.endProgress() + + # --- CHD set (refactored code) --------------------------------------------------------------- + log_info('Building {} CHD audit database...'.format(chd_set_str)) + pDialog.startProgress('Building {} CHD set...'.format(chd_set_str), len(machines)) + for m_name in sorted(machines): + pDialog.updateProgressInc() + # Skip Device Machines + if renderdb_dic[m_name]['isDevice']: continue + m_disks = machine_roms[m_name]['disks'] + machine_chd_set = [] + for disk in m_disks: + disk['type'] = ROM_TYPE_DISK + disk['location'] = _get_CHD_location(chd_set, disk, m_name, machines, renderdb_dic, machine_roms) + machine_chd_set.append(disk) + if m_name in audit_roms_dic: + audit_roms_dic[m_name].extend(machine_chd_set) + else: + audit_roms_dic[m_name] = machine_chd_set + pDialog.endProgress() + + # --------------------------------------------------------------------------------------------- + # Machine files and ROM ZIP/Sample ZIP/CHD lists. + # --------------------------------------------------------------------------------------------- + # NOTE roms_dic and chds_dic may have invalid ROMs/CHDs. However, machine_archives_dic must + # have only valid ROM archives (ZIP/7Z). + # For every machine, it goes ROM by ROM and makes a list of ZIP archive locations. Then, it + # transforms the list into a set to have a list with unique elements. + # roms_dic/chds_dic have invalid ROMs. Skip invalid ROMs. + log_info('Building ROM ZIP/Sample ZIP/CHD file lists...') + pDialog.startProgress('Building ROM, Sample and CHD archive lists...', len(machines)) + machine_archives_dic = {} + full_ROM_archive_set = set() + full_Sample_archive_set = set() + full_CHD_archive_set = set() + machine_archives_ROM = 0 + machine_archives_ROM_parents = 0 + machine_archives_ROM_clones = 0 + machine_archives_Samples = 0 + machine_archives_Samples_parents = 0 + machine_archives_Samples_clones = 0 + machine_archives_CHD = 0 + machine_archives_CHD_parents = 0 + machine_archives_CHD_clones = 0 + archive_less = 0 + archive_less_parents = 0 + archive_less_clones = 0 + ROMs_total = 0 + ROMs_valid = 0 + ROMs_invalid = 0 + CHDs_total = 0 + CHDs_valid = 0 + CHDs_invalid = 0 + for m_name in audit_roms_dic: + pDialog.updateProgressInc() + isClone = True if renderdb_dic[m_name]['cloneof'] else False + rom_list = audit_roms_dic[m_name] + machine_rom_archive_set = set() + machine_sample_archive_set = set() + machine_chd_archive_set = set() + # --- Iterate ROMs/CHDs --- + for rom in rom_list: + if rom['type'] == ROM_TYPE_DISK: + CHDs_total += 1 + # Skip invalid CHDs + if not rom['sha1']: + CHDs_invalid += 1 + continue + CHDs_valid += 1 + chd_name = rom['location'] + machine_chd_archive_set.add(chd_name) + full_CHD_archive_set.add(rom['location']) + elif rom['type'] == ROM_TYPE_SAMPLE: + sample_str_list = rom['location'].split('/') + zip_name = sample_str_list[0] + machine_sample_archive_set.add(zip_name) + archive_str = rom['location'].split('/')[0] + full_Sample_archive_set.add(archive_str) + else: + ROMs_total += 1 + # Skip invalid ROMs + if not rom['crc']: + ROMs_invalid += 1 + continue + ROMs_valid += 1 + rom_str_list = rom['location'].split('/') + zip_name = rom_str_list[0] + machine_rom_archive_set.add(zip_name) + archive_str = rom['location'].split('/')[0] + # if not archive_str: continue + full_ROM_archive_set.add(archive_str) + machine_archives_dic[m_name] = { + 'ROMs' : list(machine_rom_archive_set), + 'Samples' : list(machine_sample_archive_set), + 'CHDs' : list(machine_chd_archive_set), + } + + # --- Statistics --- + if machine_rom_archive_set: + machine_archives_ROM += 1 + if isClone: + machine_archives_ROM_clones += 1 + else: + machine_archives_ROM_parents += 1 + if machine_sample_archive_set: + machine_archives_Samples += 1 + if isClone: + machine_archives_Samples_clones += 1 + else: + machine_archives_Samples_parents += 1 + if machine_chd_archive_set: + machine_archives_CHD += 1 + if isClone: + machine_archives_CHD_clones += 1 + else: + machine_archives_CHD_parents += 1 + if not (machine_rom_archive_set or machine_sample_archive_set or machine_chd_archive_set): + archive_less += 1 + if isClone: + archive_less_clones += 1 + else: + archive_less_parents += 1 + pDialog.endProgress() + + # --------------------------------------------------------------------------------------------- + # machine_roms dictionary is passed as argument and not save in this function. + # It is modified in this function to create audit_roms_dic. + # Remove unused fields to save memory before saving the audit_roms_dic JSON file. + # Do not remove earlier because 'merge' is used in the _get_XXX_location() functions. + # --------------------------------------------------------------------------------------------- + log_info('Cleaning audit database before saving it to disk...') + pDialog.startProgress('Cleaning audit database...', len(machines)) + for m_name in sorted(machines): + pDialog.updateProgressInc() + # --- Skip devices and process ROMs and CHDs --- + if renderdb_dic[m_name]['isDevice']: continue + for rom in machine_roms[m_name]['roms']: + # Remove unused fields to save space in JSON database, but remove from the copy! + rom.pop('merge') + rom.pop('bios') + for disk in machine_roms[m_name]['disks']: + disk.pop('merge') + pDialog.endProgress() + + # --------------------------------------------------------------------------------------------- + # Update control dictionary. + # --------------------------------------------------------------------------------------------- + db_safe_edit(control_dic, 'stats_audit_MAME_machines_runnable', stats_audit_MAME_machines_runnable) + db_safe_edit(control_dic, 'stats_audit_MAME_ROM_ZIP_files', len(full_ROM_archive_set)) + db_safe_edit(control_dic, 'stats_audit_MAME_Sample_ZIP_files', len(full_Sample_archive_set)) + db_safe_edit(control_dic, 'stats_audit_MAME_CHD_files', len(full_CHD_archive_set)) + db_safe_edit(control_dic, 'stats_audit_machine_archives_ROM', machine_archives_ROM) + db_safe_edit(control_dic, 'stats_audit_machine_archives_ROM_parents', machine_archives_ROM_parents) + db_safe_edit(control_dic, 'stats_audit_machine_archives_ROM_clones', machine_archives_ROM_clones) + db_safe_edit(control_dic, 'stats_audit_machine_archives_CHD', machine_archives_CHD) + db_safe_edit(control_dic, 'stats_audit_machine_archives_CHD_parents', machine_archives_CHD_parents) + db_safe_edit(control_dic, 'stats_audit_machine_archives_CHD_clones', machine_archives_CHD_clones) + db_safe_edit(control_dic, 'stats_audit_machine_archives_Samples', machine_archives_Samples) + db_safe_edit(control_dic, 'stats_audit_machine_archives_Samples_parents', machine_archives_Samples_parents) + db_safe_edit(control_dic, 'stats_audit_machine_archives_Samples_clones', machine_archives_Samples_clones) + db_safe_edit(control_dic, 'stats_audit_archive_less', archive_less) + db_safe_edit(control_dic, 'stats_audit_archive_less_parents', archive_less_parents) + db_safe_edit(control_dic, 'stats_audit_archive_less_clones', archive_less_clones) + db_safe_edit(control_dic, 'stats_audit_ROMs_total', ROMs_total) + db_safe_edit(control_dic, 'stats_audit_ROMs_valid', ROMs_valid) + db_safe_edit(control_dic, 'stats_audit_ROMs_invalid', ROMs_invalid) + db_safe_edit(control_dic, 'stats_audit_CHDs_total', CHDs_total) + db_safe_edit(control_dic, 'stats_audit_CHDs_valid', CHDs_valid) + db_safe_edit(control_dic, 'stats_audit_CHDs_invalid', CHDs_invalid) + db_safe_edit(control_dic, 't_MAME_Audit_DB_build', time.time()) + + # --- Save databases --- + if OPTION_LOWMEM_WRITE_JSON: + json_write_func = utils_write_JSON_file_lowmem + log_debug('Using utils_write_JSON_file_lowmem() JSON writer') + else: + json_write_func = utils_write_JSON_file + log_debug('Using utils_write_JSON_file() JSON writer') + db_files = [ + [audit_roms_dic, 'MAME ROM Audit', cfg.ROM_AUDIT_DB_PATH.getPath()], + [machine_archives_dic, 'Machine file list', cfg.ROM_SET_MACHINE_FILES_DB_PATH.getPath()], + # --- Save control_dic after everything is saved --- + [control_dic, 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ] + db_save_files(db_files, json_write_func) + # Add data generated in this function to dictionary for caller code use. + db_dic_in['audit_roms'] = audit_roms_dic + db_dic_in['machine_archives'] = machine_archives_dic + +# +# Checks for errors before scanning for SL ROMs. +# Display a Kodi dialog if an error is found. +# Returns a dictionary of settings: +# options_dic['abort'] is always present. +# +# +def mame_check_before_build_MAME_catalogs(cfg, st_dic, control_dic): + kodi_reset_status(st_dic) + + # --- Check that database exists --- + pass + +# +# Updates db_dic_in and adds cache_index field. +# +# A) Builds the following catalog files +# CATALOG_MAIN_PARENT_PATH +# CATALOG_MAIN_ALL_PATH +# CATALOG_CATVER_PARENT_PATH +# CATALOG_CATVER_ALL_PATH +# ... +# +# main_catalog_parents = { +# 'cat_key' : [ parent1, parent2, ... ] +# } +# +# main_catalog_all = { +# 'cat_key' : [ machine1, machine2, ... ] +# } +# +# B) Cache index: +# CACHE_INDEX_PATH +# +# cache_index_dic = { +# 'catalog_name' : { --> 'Main', 'Binary', ... +# 'cat_key' : { +# 'num_machines' : int, +# 'num_parents' : int, +# 'hash' : text_type +# }, ... +# }, ... +# } +# +def mame_build_MAME_catalogs(cfg, st_dic, db_dic_in): + control_dic = db_dic_in['control_dic'] + machines = db_dic_in['machines'] + renderdb_dic = db_dic_in['renderdb'] + assetdb_dic = db_dic_in['assetdb'] + machine_roms = db_dic_in['roms'] + main_pclone_dic = db_dic_in['main_pclone_dic'] + + # --- Machine count --- + cache_index_dic = { + # Virtual Main filter catalog + 'Main' : {}, + # Virtual Binary filter catalog + 'Binary' : {}, + # INI/DAT based catalogs + 'Catver' : {}, + 'Catlist' : {}, + 'Genre' : {}, + 'Category' : {}, + 'NPlayers' : {}, + 'Bestgames' : {}, + 'Series' : {}, + 'Alltime' : {}, + 'Artwork' : {}, + 'Version' : {}, + # MAME XML extracted catalogs + 'Controls_Expanded' : {}, + 'Controls_Compact' : {}, + 'Devices_Expanded' : {}, + 'Devices_Compact' : {}, + 'Display_Type' : {}, + 'Display_VSync' : {}, + 'Display_Resolution' : {}, + 'CPU' : {}, + 'Driver' : {}, + 'Manufacturer' : {}, + 'ShortName' : {}, + 'LongName' : {}, + 'BySL' : {}, + 'Year' : {}, + } + NUM_CATALOGS = len(cache_index_dic) + + NORMAL_DRIVER_SET = { + '88games.cpp', + 'asteroid.cpp', + 'cball.cpp', + } + UNUSUAL_DRIVER_SET = { + 'aristmk5.cpp', + 'adp.cpp', + 'cubo.cpp', + 'mpu4vid.cpp', + 'peplus.cpp', + 'sfbonus.cpp', + } + + # --- Progress dialog --- + diag_line1 = 'Building catalogs...' + pDialog = KodiProgressDialog() + processed_filters = 0 + + # --------------------------------------------------------------------------------------------- + # Main filters (None catalog) ----------------------------------------------------------------- + # --------------------------------------------------------------------------------------------- + pDialog.startProgress('{}\n{}'.format(diag_line1, 'Main catalog'), NUM_CATALOGS) + main_catalog_parents, main_catalog_all = {}, {} + + # --- Normal and Unusual machine list --- + # Machines with Coin Slot and Non Mechanical and not Dead and not Device + log_info('Making None catalog - Coin index ...') + normal_parent_dic, normal_all_dic, unusual_parent_dic, unusual_all_dic = {}, {}, {}, {} + for parent_name in main_pclone_dic: + machine_main = machines[parent_name] + machine_render = renderdb_dic[parent_name] + n_coins = machine_main['input']['att_coins'] if machine_main['input'] else 0 + if machine_main['isMechanical']: continue + if n_coins == 0: continue + if machine_main['isDead']: continue + if machine_render['isDevice']: continue + + # Make list of machine controls. + if machine_main['input']: + control_list = [ctrl_dic['type'] for ctrl_dic in machine_main['input']['control_list']] + else: + control_list = [] + + # --- Determinte if machine is Normal or Unusual ---- + # Standard machines. + if ('only_buttons' in control_list and len(control_list) > 1) \ + or machine_main['sourcefile'] in NORMAL_DRIVER_SET: + normal_parent_dic[parent_name] = machine_render['description'] + normal_all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, normal_all_dic) + # + # Unusual machines. Most of them you don't wanna play. + # No controls or control_type has "only_buttons" or "gambling" or "hanafuda" or "mahjong" + # + elif not control_list \ + or 'only_buttons' in control_list or 'gambling' in control_list \ + or 'hanafuda' in control_list or 'mahjong' in control_list \ + or machine_main['sourcefile'] in UNUSUAL_DRIVER_SET: + unusual_parent_dic[parent_name] = machine_render['description'] + unusual_all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, unusual_all_dic) + # + # What remains go to the Normal/Standard list. + # + else: + normal_parent_dic[parent_name] = machine_render['description'] + normal_all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, normal_all_dic) + main_catalog_parents['Normal'] = normal_parent_dic + main_catalog_all['Normal'] = normal_all_dic + main_catalog_parents['Unusual'] = unusual_parent_dic + main_catalog_all['Unusual'] = unusual_all_dic + + # --- NoCoin list --- + # A) Machines with No Coin Slot and Non Mechanical and not Dead and not Device + log_info('Making NoCoin index ...') + parent_dic, all_dic = {}, {} + for parent_name in main_pclone_dic: + machine_main = machines[parent_name] + machine_render = renderdb_dic[parent_name] + n_coins = machine_main['input']['att_coins'] if machine_main['input'] else 0 + if machine_main['isMechanical']: continue + if n_coins > 0: continue + if machine_main['isDead']: continue + if machine_render['isDevice']: continue + parent_dic[parent_name] = machine_render['description'] + all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, all_dic) + main_catalog_parents['NoCoin'] = parent_dic + main_catalog_all['NoCoin'] = all_dic + + # --- Mechanical machines --- + # Mechanical machines and not Dead and not Device + log_info('Making Mechanical index ...') + parent_dic, all_dic = {}, {} + for parent_name in main_pclone_dic: + machine_main = machines[parent_name] + machine_render = renderdb_dic[parent_name] + if not machine_main['isMechanical']: continue + if machine_main['isDead']: continue + if machine_render['isDevice']: continue + parent_dic[parent_name] = machine_render['description'] + all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, all_dic) + main_catalog_parents['Mechanical'] = parent_dic + main_catalog_all['Mechanical'] = all_dic + + # --- Dead machines --- + log_info('Making Dead Machines index ...') + parent_dic, all_dic = {}, {} + for parent_name in main_pclone_dic: + machine_main = machines[parent_name] + machine_render = renderdb_dic[parent_name] + if not machine_main['isDead']: continue + parent_dic[parent_name] = machine_render['description'] + all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, all_dic) + main_catalog_parents['Dead'] = parent_dic + main_catalog_all['Dead'] = all_dic + + # --- Device machines --- + log_info('Making Device Machines index ...') + parent_dic, all_dic = {}, {} + for parent_name in main_pclone_dic: + machine_render = renderdb_dic[parent_name] + if not machine_render['isDevice']: continue + parent_dic[parent_name] = machine_render['description'] + all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, all_dic) + main_catalog_parents['Devices'] = parent_dic + main_catalog_all['Devices'] = all_dic + + # --- Build ROM cache index and save Main catalog JSON file --- + mame_cache_index_builder('Main', cache_index_dic, main_catalog_all, main_catalog_parents) + utils_write_JSON_file(cfg.CATALOG_MAIN_ALL_PATH.getPath(), main_catalog_all) + utils_write_JSON_file(cfg.CATALOG_MAIN_PARENT_PATH.getPath(), main_catalog_parents) + processed_filters += 1 + + # --------------------------------------------------------------------------------------------- + # Binary filters ------------------------------------------------------------------------------ + # --------------------------------------------------------------------------------------------- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Binary catalog')) + binary_catalog_parents, binary_catalog_all = {}, {} + + # --- CHD machines --- + log_info('Making CHD Machines index ...') + parent_dic, all_dic = {}, {} + for parent_name in main_pclone_dic: + machine = machines[parent_name] + machine_render = renderdb_dic[parent_name] + if machine_render['isDevice']: continue # >> Skip device machines + if not machine_roms[parent_name]['disks']: continue + parent_dic[parent_name] = machine_render['description'] + all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, all_dic) + binary_catalog_parents['CHD'] = parent_dic + binary_catalog_all['CHD'] = all_dic + + # --- Machines with samples --- + log_info('Making Samples Machines index ...') + parent_dic, all_dic = {}, {} + for parent_name in main_pclone_dic: + machine = machines[parent_name] + machine_render = renderdb_dic[parent_name] + if machine_render['isDevice']: continue # >> Skip device machines + if not machine['sampleof']: continue + parent_dic[parent_name] = machine_render['description'] + all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, all_dic) + binary_catalog_parents['Samples'] = parent_dic + binary_catalog_all['Samples'] = all_dic + + # --- Software List machines --- + log_info('Making Software List Machines index ...') + parent_dic, all_dic = {}, {} + for parent_name in main_pclone_dic: + machine = machines[parent_name] + machine_render = renderdb_dic[parent_name] + if machine_render['isDevice']: continue # >> Skip device machines + if not machine['softwarelists']: continue + parent_dic[parent_name] = machine_render['description'] + all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, all_dic) + binary_catalog_parents['SoftwareLists'] = parent_dic + binary_catalog_all['SoftwareLists'] = all_dic + + # --- BIOS --- + log_info('Making BIOS Machines index ...') + parent_dic, all_dic = {}, {} + for parent_name in main_pclone_dic: + machine_render = renderdb_dic[parent_name] + if machine_render['isDevice']: continue # Skip device machines + if not machine_render['isBIOS']: continue + parent_dic[parent_name] = machine_render['description'] + all_dic[parent_name] = machine_render['description'] + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, all_dic) + binary_catalog_parents['BIOS'] = parent_dic + binary_catalog_all['BIOS'] = all_dic + + # Build cache index and save Binary catalog JSON file + mame_cache_index_builder('Binary', cache_index_dic, binary_catalog_all, binary_catalog_parents) + utils_write_JSON_file(cfg.CATALOG_BINARY_ALL_PATH.getPath(), binary_catalog_all) + utils_write_JSON_file(cfg.CATALOG_BINARY_PARENT_PATH.getPath(), binary_catalog_parents) + processed_filters += 1 + + # --------------------------------------------------------------------------------------------- + # Cataloged machine lists --------------------------------------------------------------------- + # --------------------------------------------------------------------------------------------- + # --- Catver catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Catver catalog')) + log_info('Making Catver catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Catver) + mame_cache_index_builder('Catver', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CATVER_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CATVER_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Catlist catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Catlist catalog')) + log_info('Making Catlist catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Catlist) + mame_cache_index_builder('Catlist', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CATLIST_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CATLIST_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Genre catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Genre catalog')) + log_info('Making Genre catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Genre) + mame_cache_index_builder('Genre', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_GENRE_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_GENRE_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Category catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Category catalog')) + log_info('Making Category catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Category) + mame_cache_index_builder('Category', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CATEGORY_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CATEGORY_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Nplayers catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Nplayers catalog')) + log_info('Making Nplayers catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + renderdb_dic, renderdb_dic, main_pclone_dic, mame_catalog_key_NPlayers) + mame_cache_index_builder('NPlayers', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_NPLAYERS_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_NPLAYERS_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Bestgames catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Bestgames catalog')) + log_info('Making Bestgames catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Bestgames) + mame_cache_index_builder('Bestgames', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_BESTGAMES_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_BESTGAMES_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Series catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Series catalog')) + log_info('Making Series catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Series) + mame_cache_index_builder('Series', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_SERIES_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_SERIES_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Alltime catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Alltime catalog')) + log_info('Making Alltime catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Alltime) + mame_cache_index_builder('Alltime', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_ALLTIME_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_ALLTIME_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Artwork catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Artwork catalog')) + log_info('Making Artwork catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Artwork) + mame_cache_index_builder('Artwork', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_ARTWORK_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_ARTWORK_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Version catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Version catalog')) + log_info('Making Version catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_VerAdded) + mame_cache_index_builder('Version', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_VERADDED_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_VERADDED_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Control catalog (Expanded) --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Control Expanded catalog')) + log_info('Making Control Expanded catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Controls_Expanded) + mame_cache_index_builder('Controls_Expanded', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CONTROL_EXPANDED_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CONTROL_EXPANDED_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Control catalog (Compact) --- + # In this catalog one machine may be in several categories if the machine has more than + # one control. + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Control Compact catalog')) + log_info('Making Control Compact catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Controls_Compact) + mame_cache_index_builder('Controls_Compact', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CONTROL_COMPACT_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CONTROL_COMPACT_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- <device> / Device Expanded catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, '<device> Expanded catalog')) + log_info('Making <device> tag Expanded catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Devices_Expanded) + mame_cache_index_builder('Devices_Expanded', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DEVICE_EXPANDED_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DEVICE_EXPANDED_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- <device> / Device Compact catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, '<device> Compact catalog')) + log_info('Making <device> tag Compact catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Devices_Compact) + mame_cache_index_builder('Devices_Compact', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DEVICE_COMPACT_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DEVICE_COMPACT_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Display Type catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Display Type catalog')) + log_info('Making Display Type catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Display_Type) + mame_cache_index_builder('Display_Type', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DISPLAY_TYPE_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DISPLAY_TYPE_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Display VSync catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Display VSync catalog')) + log_info('Making Display VSync catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Display_VSync) + mame_cache_index_builder('Display_VSync', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DISPLAY_VSYNC_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DISPLAY_VSYNC_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Display Resolution catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Display Resolution catalog')) + log_info('Making Display Resolution catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Display_Resolution) + mame_cache_index_builder('Display_Resolution', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DISPLAY_RES_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DISPLAY_RES_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- CPU catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'CPU catalog')) + log_info('Making CPU catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_CPU) + mame_cache_index_builder('CPU', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CPU_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_CPU_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Driver catalog --- + # This catalog cannot use mame_build_catalog_helper() because of the driver + # name substitution. + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Driver catalog')) + log_info('Making Driver catalog ...') + catalog_parents, catalog_all = {}, {} + # mame_build_catalog_helper(catalog_parents, catalog_all, + # machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Driver) + for parent_name in main_pclone_dic: + render = renderdb_dic[parent_name] + if render['isDevice']: continue # Skip device machines in catalogs. + c_key = machines[parent_name]['sourcefile'] + # Some drivers get a prettier name. + c_key = mame_driver_better_name_dic[c_key] if c_key in mame_driver_better_name_dic else c_key + catalog_key_list = [c_key] + for catalog_key in catalog_key_list: + if catalog_key in catalog_parents: + catalog_parents[catalog_key][parent_name] = render['description'] + catalog_all[catalog_key][parent_name] = render['description'] + else: + catalog_parents[catalog_key] = { parent_name : render['description'] } + catalog_all[catalog_key] = { parent_name : render['description'] } + for clone_name in main_pclone_dic[parent_name]: + catalog_all[catalog_key][clone_name] = renderdb_dic[clone_name]['description'] + mame_cache_index_builder('Driver', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DRIVER_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_DRIVER_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Manufacturer catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Manufacturer catalog')) + log_info('Making Manufacturer catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Manufacturer) + mame_cache_index_builder('Manufacturer', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_MANUFACTURER_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_MANUFACTURER_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- MAME short name catalog --- + # This catalog cannot use mame_build_catalog_helper() because of the special name + # of the catalog (it is not the plain description). + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Short name catalog')) + log_info('Making MAME short name catalog...') + catalog_parents, catalog_all = {}, {} + for parent_name in main_pclone_dic: + render = renderdb_dic[parent_name] + if render['isDevice']: continue + catalog_key = parent_name[0] + t = '{} "{}"'.format(parent_name, render['description']) + if catalog_key in catalog_parents: + catalog_parents[catalog_key][parent_name] = t + catalog_all[catalog_key][parent_name] = t + else: + catalog_parents[catalog_key] = { parent_name : t } + catalog_all[catalog_key] = { parent_name : t } + for clone_name in main_pclone_dic[parent_name]: + t = '{} "{}"'.format(clone_name, renderdb_dic[clone_name]['description']) + catalog_all[catalog_key][clone_name] = t + mame_cache_index_builder('ShortName', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_SHORTNAME_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_SHORTNAME_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- MAME long name catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Long name catalog')) + log_info('Making MAME long name catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_LongName) + mame_cache_index_builder('LongName', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_LONGNAME_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_LONGNAME_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Software List (BySL) catalog --- + # This catalog cannot use mame_build_catalog_helper() because of the name change of the SLs. + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Software List catalog')) + log_info('Making Software List catalog ...') + # Load proper Software List proper names, if available + SL_names_dic = utils_load_JSON_file(cfg.SL_NAMES_PATH.getPath()) + catalog_parents, catalog_all = {}, {} + for parent_name in main_pclone_dic: + machine = machines[parent_name] + render = renderdb_dic[parent_name] + if render['isDevice']: continue + for sl_name in machine['softwarelists']: + catalog_key = sl_name + if catalog_key in SL_names_dic: catalog_key = SL_names_dic[catalog_key] + if catalog_key in catalog_parents: + catalog_parents[catalog_key][parent_name] = render['description'] + catalog_all[catalog_key][parent_name] = render['description'] + else: + catalog_parents[catalog_key] = { parent_name : render['description'] } + catalog_all[catalog_key] = { parent_name : render['description'] } + mame_catalog_add_clones(parent_name, main_pclone_dic, renderdb_dic, catalog_all[catalog_key]) + # Add orphaned Software Lists (SL that do not have an associated machine). + for sl_name in SL_names_dic: + catalog_key = sl_name + if catalog_key in SL_names_dic: catalog_key = SL_names_dic[catalog_key] + if catalog_key in catalog_parents: continue + catalog_parents[catalog_key] = {} + catalog_all[catalog_key] = {} + mame_cache_index_builder('BySL', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_SL_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_SL_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # --- Year catalog --- + pDialog.updateProgress(processed_filters, '{}\n{}'.format(diag_line1, 'Year catalog')) + log_info('Making Year catalog ...') + catalog_parents, catalog_all = {}, {} + mame_build_catalog_helper(catalog_parents, catalog_all, + machines, renderdb_dic, main_pclone_dic, mame_catalog_key_Year) + mame_cache_index_builder('Year', cache_index_dic, catalog_all, catalog_parents) + utils_write_JSON_file(cfg.CATALOG_YEAR_PARENT_PATH.getPath(), catalog_parents) + utils_write_JSON_file(cfg.CATALOG_YEAR_ALL_PATH.getPath(), catalog_all) + processed_filters += 1 + + # Close progress dialog. + pDialog.endProgress() + + # --- Create properties database with default values ------------------------------------------ + # Now overwrites all properties when the catalog is rebuilt. + # New versions must kept user set properties! + # This code is disabled + # mame_properties_dic = {} + # for catalog_name in CATALOG_NAME_LIST: + # catalog_dic = db_get_cataloged_dic_parents(cfg, catalog_name) + # for category_name in sorted(catalog_dic): + # prop_key = '{} - {}'.format(catalog_name, category_name) + # mame_properties_dic[prop_key] = {'vm' : VIEW_MODE_PCLONE} + # utils_write_JSON_file(cfg.MAIN_PROPERTIES_PATH.getPath(), mame_properties_dic) + # log_info('mame_properties_dic has {} entries'.format(len(mame_properties_dic))) + + # --- Compute main filter statistics --- + stats_MF_Normal_Total, stats_MF_Normal_Total_parents = 0, 0 + stats_MF_Normal_Good, stats_MF_Normal_Good_parents = 0, 0 + stats_MF_Normal_Imperfect, stats_MF_Normal_Imperfect_parents = 0, 0 + stats_MF_Normal_Nonworking, stats_MF_Normal_Nonworking_parents = 0, 0 + stats_MF_Unusual_Total, stats_MF_Unusual_Total_parents = 0, 0 + stats_MF_Unusual_Good, stats_MF_Unusual_Good_parents = 0, 0 + stats_MF_Unusual_Imperfect, stats_MF_Unusual_Imperfect_parents = 0, 0 + stats_MF_Unusual_Nonworking, stats_MF_Unusual_Nonworking_parents = 0, 0 + stats_MF_Nocoin_Total, stats_MF_Nocoin_Total_parents = 0, 0 + stats_MF_Nocoin_Good, stats_MF_Nocoin_Good_parents = 0, 0 + stats_MF_Nocoin_Imperfect, stats_MF_Nocoin_Imperfect_parents = 0, 0 + stats_MF_Nocoin_Nonworking, stats_MF_Nocoin_Nonworking_parents = 0, 0 + stats_MF_Mechanical_Total, stats_MF_Mechanical_Total_parents = 0, 0 + stats_MF_Mechanical_Good, stats_MF_Mechanical_Good_parents = 0, 0 + stats_MF_Mechanical_Imperfect, stats_MF_Mechanical_Imperfect_parents = 0, 0 + stats_MF_Mechanical_Nonworking, stats_MF_Mechanical_Nonworking_parents = 0, 0 + stats_MF_Dead_Total, stats_MF_Dead_Total_parents = 0, 0 + stats_MF_Dead_Good, stats_MF_Dead_Good_parents = 0, 0 + stats_MF_Dead_Imperfect, stats_MF_Dead_Imperfect_parents = 0, 0 + stats_MF_Dead_Nonworking, stats_MF_Dead_Nonworking_parents = 0, 0 + NUM_FILTERS = 5 + processed_filters = 0 + pDialog.startProgress('Computing statistics ...', NUM_FILTERS) + for m_name in main_catalog_all['Normal']: + driver_status = renderdb_dic[m_name]['driver_status'] + stats_MF_Normal_Total += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Normal_Total_parents += 1 + if driver_status == 'good': + stats_MF_Normal_Good += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Normal_Good_parents += 1 + elif driver_status == 'imperfect': + stats_MF_Normal_Imperfect += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Normal_Imperfect_parents += 1 + elif driver_status == 'preliminary': + stats_MF_Normal_Nonworking += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Normal_Nonworking_parents += 1 + # Found in mame2003-plus.xml, machine quizf1 and maybe others. + elif driver_status == 'protection': pass + # Are there machines with undefined status? + elif driver_status == '': pass + else: + log_error('Machine {}, unrecognised driver_status {}'.format(m_name, driver_status)) + raise TypeError + processed_filters += 1 + pDialog.updateProgress(processed_filters) + for m_name in main_catalog_all['Unusual']: + driver_status = renderdb_dic[m_name]['driver_status'] + stats_MF_Unusual_Total += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Unusual_Total_parents += 1 + if driver_status == 'good': + stats_MF_Unusual_Good += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Unusual_Good_parents += 1 + elif driver_status == 'imperfect': + stats_MF_Unusual_Imperfect += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Unusual_Imperfect_parents += 1 + elif driver_status == 'preliminary': + stats_MF_Unusual_Nonworking += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Unusual_Nonworking_parents += 1 + elif driver_status == 'protection': pass + elif driver_status == '': pass + else: + log_error('Machine {}, unrecognised driver_status {}'.format(m_name, driver_status)) + raise TypeError + processed_filters += 1 + pDialog.updateProgress(processed_filters) + for m_name in main_catalog_all['NoCoin']: + driver_status = renderdb_dic[m_name]['driver_status'] + stats_MF_Nocoin_Total += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Nocoin_Total_parents += 1 + if driver_status == 'good': + stats_MF_Nocoin_Good += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Nocoin_Good_parents += 1 + elif driver_status == 'imperfect': + stats_MF_Nocoin_Imperfect += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Nocoin_Imperfect_parents += 1 + elif driver_status == 'preliminary': + stats_MF_Nocoin_Nonworking += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Nocoin_Nonworking_parents += 1 + elif driver_status == 'protection': pass + elif driver_status == '': pass + else: + log_error('Machine {}, unrecognised driver_status {}'.format(m_name, driver_status)) + raise TypeError + processed_filters += 1 + pDialog.updateProgress(processed_filters) + for m_name in main_catalog_all['Mechanical']: + driver_status = renderdb_dic[m_name]['driver_status'] + stats_MF_Mechanical_Total += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Mechanical_Total_parents += 1 + if driver_status == 'good': + stats_MF_Mechanical_Good += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Mechanical_Good_parents += 1 + elif driver_status == 'imperfect': + stats_MF_Mechanical_Imperfect += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Mechanical_Imperfect_parents += 1 + elif driver_status == 'preliminary': + stats_MF_Mechanical_Nonworking += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Mechanical_Nonworking_parents += 1 + elif driver_status == 'protection': pass + elif driver_status == '': pass + else: + log_error('Machine {}, unrecognised driver_status {}'.format(m_name, driver_status)) + raise TypeError + processed_filters += 1 + pDialog.updateProgress(processed_filters) + for m_name in main_catalog_all['Dead']: + driver_status = renderdb_dic[m_name]['driver_status'] + stats_MF_Dead_Total += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Dead_Total_parents += 1 + if driver_status == 'good': + stats_MF_Dead_Good += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Dead_Good_parents += 1 + elif driver_status == 'imperfect': + stats_MF_Dead_Imperfect += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Dead_Imperfect_parents += 1 + elif driver_status == 'preliminary': + stats_MF_Dead_Nonworking += 1 + if not renderdb_dic[m_name]['cloneof']: stats_MF_Dead_Nonworking_parents += 1 + elif driver_status == 'protection': pass + elif driver_status == '': pass + else: + log_error('Machine {}, unrecognised driver_status {}'.format(m_name, driver_status)) + raise TypeError + pDialog.endProgress() + + # --- Update statistics --- + db_safe_edit(control_dic, 'stats_MF_Normal_Total', stats_MF_Normal_Total) + db_safe_edit(control_dic, 'stats_MF_Normal_Good', stats_MF_Normal_Good) + db_safe_edit(control_dic, 'stats_MF_Normal_Imperfect', stats_MF_Normal_Imperfect) + db_safe_edit(control_dic, 'stats_MF_Normal_Nonworking', stats_MF_Normal_Nonworking) + db_safe_edit(control_dic, 'stats_MF_Unusual_Total', stats_MF_Unusual_Total) + db_safe_edit(control_dic, 'stats_MF_Unusual_Good', stats_MF_Unusual_Good) + db_safe_edit(control_dic, 'stats_MF_Unusual_Imperfect', stats_MF_Unusual_Imperfect) + db_safe_edit(control_dic, 'stats_MF_Unusual_Nonworking', stats_MF_Unusual_Nonworking) + db_safe_edit(control_dic, 'stats_MF_Nocoin_Total', stats_MF_Nocoin_Total) + db_safe_edit(control_dic, 'stats_MF_Nocoin_Good', stats_MF_Nocoin_Good) + db_safe_edit(control_dic, 'stats_MF_Nocoin_Imperfect', stats_MF_Nocoin_Imperfect) + db_safe_edit(control_dic, 'stats_MF_Nocoin_Nonworking', stats_MF_Nocoin_Nonworking) + db_safe_edit(control_dic, 'stats_MF_Mechanical_Total', stats_MF_Mechanical_Total) + db_safe_edit(control_dic, 'stats_MF_Mechanical_Good', stats_MF_Mechanical_Good) + db_safe_edit(control_dic, 'stats_MF_Mechanical_Imperfect', stats_MF_Mechanical_Imperfect) + db_safe_edit(control_dic, 'stats_MF_Mechanical_Nonworking', stats_MF_Mechanical_Nonworking) + db_safe_edit(control_dic, 'stats_MF_Dead_Total', stats_MF_Dead_Total) + db_safe_edit(control_dic, 'stats_MF_Dead_Good', stats_MF_Dead_Good) + db_safe_edit(control_dic, 'stats_MF_Dead_Imperfect', stats_MF_Dead_Imperfect) + db_safe_edit(control_dic, 'stats_MF_Dead_Nonworking', stats_MF_Dead_Nonworking) + + db_safe_edit(control_dic, 'stats_MF_Normal_Total_parents', stats_MF_Normal_Total_parents) + db_safe_edit(control_dic, 'stats_MF_Normal_Good_parents', stats_MF_Normal_Good_parents) + db_safe_edit(control_dic, 'stats_MF_Normal_Imperfect_parents', stats_MF_Normal_Imperfect_parents) + db_safe_edit(control_dic, 'stats_MF_Normal_Nonworking_parents', stats_MF_Normal_Nonworking_parents) + db_safe_edit(control_dic, 'stats_MF_Unusual_Total_parents', stats_MF_Unusual_Total_parents) + db_safe_edit(control_dic, 'stats_MF_Unusual_Good_parents', stats_MF_Unusual_Good_parents) + db_safe_edit(control_dic, 'stats_MF_Unusual_Imperfect_parents', stats_MF_Unusual_Imperfect_parents) + db_safe_edit(control_dic, 'stats_MF_Unusual_Nonworking_parents', stats_MF_Unusual_Nonworking_parents) + db_safe_edit(control_dic, 'stats_MF_Nocoin_Total_parents', stats_MF_Nocoin_Total_parents) + db_safe_edit(control_dic, 'stats_MF_Nocoin_Good_parents', stats_MF_Nocoin_Good_parents) + db_safe_edit(control_dic, 'stats_MF_Nocoin_Imperfect_parents', stats_MF_Nocoin_Imperfect_parents) + db_safe_edit(control_dic, 'stats_MF_Nocoin_Nonworking_parents', stats_MF_Nocoin_Nonworking_parents) + db_safe_edit(control_dic, 'stats_MF_Mechanical_Total_parents', stats_MF_Mechanical_Total_parents) + db_safe_edit(control_dic, 'stats_MF_Mechanical_Good_parents', stats_MF_Mechanical_Good_parents) + db_safe_edit(control_dic, 'stats_MF_Mechanical_Imperfect_parents', stats_MF_Mechanical_Imperfect_parents) + db_safe_edit(control_dic, 'stats_MF_Mechanical_Nonworking_parents', stats_MF_Mechanical_Nonworking_parents) + db_safe_edit(control_dic, 'stats_MF_Dead_Total_parents', stats_MF_Dead_Total_parents) + db_safe_edit(control_dic, 'stats_MF_Dead_Good_parents', stats_MF_Dead_Good_parents) + db_safe_edit(control_dic, 'stats_MF_Dead_Imperfect_parents', stats_MF_Dead_Imperfect_parents) + db_safe_edit(control_dic, 'stats_MF_Dead_Nonworking_parents', stats_MF_Dead_Nonworking_parents) + + # --- Update timestamp --- + db_safe_edit(control_dic, 't_MAME_Catalog_build', time.time()) + + # --- Save stuff ------------------------------------------------------------------------------ + db_files = [ + [cache_index_dic, 'MAME cache index', cfg.CACHE_INDEX_PATH.getPath()], + [control_dic, 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ] + db_save_files(db_files) + db_dic_in['cache_index'] = cache_index_dic + +# ------------------------------------------------------------------------------------------------- +# Software Lists and ROM audit database building function +# ------------------------------------------------------------------------------------------------- +# +# https://www.mess.org/mess/swlist_format +# The basic idea (which leads basically the whole format) is that each <software> entry should +# correspond to a game box you could have bought in a shop, and that each <part> entry should +# correspond to a piece (i.e. a cart, a disk or a tape) that you would have found in such a box. +# +# --- Example 1: 32x.xml-chaotix --- +# Stored as: SL_ROMS/32x/chaotix.zip +# +# <part name="cart" interface="_32x_cart"> +# <dataarea name="rom" size="3145728"> +# <rom name="knuckles' chaotix (europe).bin" size="3145728" crc="41d63572" sha1="5c1...922" offset="000000" /> +# </dataarea> +# </part> +# +# --- Example 2: 32x.xml-doom --- +# Stored as: SL_ROMS/32x/doom.zip +# +# <part name="cart" interface="_32x_cart"> +# <feature name="pcb" value="171-6885A" /> +# <dataarea name="rom" size="3145728"> +# <rom name="mpr-17351-f.ic1" size="2097152" crc="e0ef6ebc" sha1="302...79d" offset="000000" /> +# <rom name="mpr-17352-f.ic2" size="1048576" crc="c7079709" sha1="0f2...33b" offset="0x200000" /> +# </dataarea> +# </part> +# +# --- Example 3: a800.xml-diamond3 --- +# Stored as: SL_ROMS/a800/diamond3.zip (all ROMs from all parts) +# +# <part name="cart" interface="a8bit_cart"> +# <feature name="slot" value="a800_diamond" /> +# <dataarea name="rom" size="65536"> +# <rom name="diamond gos v3.0.rom" size="65536" crc="0ead07f8" sha1="e92...730" offset="0" /> +# </dataarea> +# </part> +# <part name="flop1" interface="floppy_5_25"> +# <dataarea name="flop" size="92176"> +# <rom name="diamond paint.atr" size="92176" crc="d2994282" sha1="be8...287" offset="0" /> +# </dataarea> +# </part> +# <part name="flop2" interface="floppy_5_25"> +# <dataarea name="flop" size="92176"> +# <rom name="diamond write.atr" size="92176" crc="e1e5b235" sha1="c3c...db5" offset="0" /> +# </dataarea> +# </part> +# <part name="flop3" interface="floppy_5_25"> +# <dataarea name="flop" size="92176"> +# <rom name="diamond utilities.atr" size="92176" crc="bb48082d" sha1="eb7...4e4" offset="0" /> +# </dataarea> +# </part> +# +# --- Example 4: a2600.xml-harmbios --- +# Stored as: SL_ROMS/a2600/harmbios.zip (all ROMs from all dataareas) +# +# <part name="cart" interface="a2600_cart"> +# <feature name="slot" value="a26_harmony" /> +# <dataarea name="rom" size="0x8000"> +# <rom name="bios_updater_NTSC.cu" size="0x8000" crc="03153eb2" sha1="cd9...009" offset="0" /> +# </dataarea> +# <dataarea name="bios" size="0x21400"> +# <rom name="hbios_106_NTSC_official_beta.bin" size="0x21400" crc="1e1d237b" sha1="8fd...1da" offset="0" /> +# <rom name="hbios_106_NTSC_beta_2.bin" size="0x21400" crc="807b86bd" sha1="633...e9d" offset="0" /> +# <rom name="eeloader_104e_PAL60.bin" size="0x36f8" crc="58845532" sha1="255...71c" offset="0" /> +# </dataarea> +# </part> +# +# --- Example 5: psx.xml-traid --- +# Stored as: SL_CHDS/psx/traid/tomb raider (usa) (v1.6).chd +# +# <part name="cdrom" interface="psx_cdrom"> +# <diskarea name="cdrom"> +# <disk name="tomb raider (usa) (v1.6)" sha1="697...3ac"/> +# </diskarea> +# </part> +# +# --- Example 6: psx.xml-traida cloneof=traid --- +# Stored as: SL_CHDS/psx/traid/tomb raider (usa) (v1.5).chd +# +# <part name="cdrom" interface="psx_cdrom"> +# <diskarea name="cdrom"> +# <disk name="tomb raider (usa) (v1.5)" sha1="d48...0a9"/> +# </diskarea> +# </part> +# +# --- Example 7: pico.xml-sanouk5 --- +# Stored as: SL_ROMS/pico/sanouk5.zip (mpr-18458-t.ic1 ROM) +# Stored as: SL_CHDS/pico/sanouk5/imgpico-001.chd +# +# <part name="cart" interface="pico_cart"> +# <dataarea name="rom" size="524288"> +# <rom name="mpr-18458-t.ic1" size="524288" crc="6340c18a" sha1="101..." offset="000000" loadflag="load16_word_swap" /> +# </dataarea> +# <diskarea name="cdrom"> +# <disk name="imgpico-001" sha1="c93...10d" /> +# </diskarea> +# </part> +# +# ------------------------------------------------------------------------------------------------- +# A) One part may have a dataarea, a diskarea, or both. +# +# B) One part may have more than one dataarea with different names. +# +# SL_roms = { +# 'sl_rom_name' : [ +# { +# 'part_name' : string, +# 'part_interface' : string, +# 'dataarea' : [ +# { +# 'name' : string, +# 'roms' : [ +# { +# 'name' : string, 'size' : int, 'crc' : string +# }, +# ] +# } +# ] +# 'diskarea' : [ +# { +# 'name' : string, +# 'disks' : [ +# { +# 'name' : string, 'sha1' : string +# }, +# ] +# } +# ] +# }, ... +# ], ... +# } +# +# ------------------------------------------------------------------------------------------------- +# --- SL List ROM Audit database --- +# +# A) For each SL ROM entry, create a list of the ROM files and CHD files, names, sizes, crc/sha1 +# and location. +# SL_roms = { +# 'sl_rom_name' : [ +# { +# 'type' : string, +# 'name' : string, +# 'size : int, +# 'crc' : sting, +# 'location' : string +# }, ... +# ], ... +# } + +# +# SL_disks = { +# 'sl_rom_name' : [ +# { +# 'type' : string, +# 'name' : string, +# 'sha1' : sting, +# 'location' : string +# }, ... +# ], ... +# } +# +def _new_SL_Data_dic(): + return { + 'items' : {}, + 'SL_roms' : {}, + 'display_name' : '', + 'num_with_ROMs' : 0, + 'num_with_CHDs' : 0, + 'num_items' : 0, + 'num_parents' : 0, + 'num_clones' : 0, + } + +# Get ROMs in dataarea. +def _get_SL_dataarea_ROMs(SL_name, item_name, part_child, dataarea_dic): + __DEBUG_SL_ROM_PROCESSING = False + dataarea_num_roms = 0 + for dataarea_child in part_child: + rom_dic = { 'name' : '', 'size' : '', 'crc' : '', 'sha1' : '' } + # Force Python to guess the base of the conversion looking at 0x prefixes. + size_int = 0 + if 'size' in dataarea_child.attrib: + size_int = int(dataarea_child.attrib['size'], 0) + rom_dic['size'] = size_int + rom_dic['name'] = dataarea_child.attrib['name'] if 'name' in dataarea_child.attrib else '' + rom_dic['crc'] = dataarea_child.attrib['crc'] if 'crc' in dataarea_child.attrib else '' + rom_dic['sha1'] = dataarea_child.attrib['sha1'] if 'sha1' in dataarea_child.attrib else '' + + # In the nes.xml SL some ROM names have a trailing dot '.'. For example (MAME 0.196): + # + # ROM 131072 028bfc44 nes/kingey/0.prg OK + # ROM 131072 1aca7960 nes/kingey/king ver 1.3 vid. ROM not in ZIP + # + # PD torrents do not have the trailing dot because this files cause trouble in Windows. + # To correctly audit PD torrents, remove the trailing dot from filenames. + # Have a look here http://forum.pleasuredome.org.uk/index.php?showtopic=32701&p=284925 + # I will create a PR to MAME repo to fix these names (and then next couple of lines must + # be commented). + if len(rom_dic['name']) > 2 and rom_dic['name'][-1] == '.': + rom_dic['name'] = rom_dic['name'][:-1] + + # Some CRCs are in upper case. Store always lower case in AML DB. + if rom_dic['crc']: rom_dic['crc'] = rom_dic['crc'].lower() + + # Just in case there are SHA1 hashes in upper case (not verified). + if rom_dic['sha1']: rom_dic['sha1'] = rom_dic['sha1'].lower() + + # If ROM has attribute status="nodump" then ignore this ROM. + if 'status' in dataarea_child.attrib: + status = dataarea_child.attrib['status'] + if status == 'nodump': + if __DEBUG_SL_ROM_PROCESSING: + log_debug('SL "{}" item "{}" status="nodump". Skipping ROM.'.format(SL_name, item_name)) + continue + elif status == 'baddump': + if __DEBUG_SL_ROM_PROCESSING: + log_debug('SL "{}" item "{}" status="baddump".'.format(SL_name, item_name)) + pass + else: + log_error('SL "{}" item "{}" Unknown status = {}'.format(SL_name, item_name, status)) + raise CriticalError('DEBUG') + + # Fix "fake" SL ROMs with loadflag="continue". + # For example, SL neogeo, SL item aof + if 'loadflag' in dataarea_child.attrib: + loadflag = dataarea_child.attrib['loadflag'] + if loadflag == 'continue': + # This ROM is not valid (not a valid ROM file). + # Size must be added to previous ROM. + if __DEBUG_SL_ROM_PROCESSING: + log_debug('SL "{}" item "{}" loadflag="continue" case. Adding size {} to previous ROM.'.format( + SL_name, item_name, rom_dic['size'])) + previous_rom = dataarea_dic['roms'][-1] + previous_rom['size'] += rom_dic['size'] + continue + elif loadflag == 'ignore': + if rom_dic['size'] > 0: + if __DEBUG_SL_ROM_PROCESSING: + log_debug('SL "{}" item "{}" loadflag="ignore" case. Adding size {} to previous ROM.'.format( + SL_name, item_name, rom_dic['size'])) + previous_rom = dataarea_dic['roms'][-1] + previous_rom['size'] += rom_dic['size'] + else: + if __DEBUG_SL_ROM_PROCESSING: + log_debug('SL "{}" item "{}" loadflag="ignore" case and size = 0. Skipping ROM.'.format( + SL_name, item_name)) + continue + elif loadflag == 'reload': + if __DEBUG_SL_ROM_PROCESSING: + log_debug('SL "{}" item "{}" loadflag="reload" case. Skipping ROM.'.format( + SL_name, item_name)) + continue + elif loadflag == 'reload_plain': + if __DEBUG_SL_ROM_PROCESSING: + log_debug('SL "{}" item "{}" loadflag="reload_plain" case. Skipping ROM.'.format( + SL_name, item_name)) + continue + elif loadflag == 'fill': + if __DEBUG_SL_ROM_PROCESSING: + log_debug('SL "{}" item "{}" loadflag="fill" case. Skipping ROM.'.format( + SL_name, item_name)) + continue + elif loadflag == 'load16_word_swap': + pass + elif loadflag == 'load16_byte': + pass + elif loadflag == 'load32_word': + pass + elif loadflag == 'load32_byte': + pass + elif loadflag == 'load32_word_swap': + pass + else: + t = 'SL "{}" item "{}" unknown loadflag="{}"'.format(SL_name, item_name, loadflag) + log_error(t) + raise ValueError(t) + + # --- Add ROM to DB --- + dataarea_dic['roms'].append(rom_dic) + dataarea_num_roms += 1 + + # --- DEBUG: Error if rom has merge attribute --- + if 'merge' in dataarea_child.attrib: + log_error('SL {}, Item {}'.format(SL_name, item_name)) + log_error('ROM {} has merge attribute'.format(dataarea_child.attrib['name'])) + raise CriticalError('DEBUG') + + return dataarea_num_roms + +# Get CHDs in diskarea. +def _get_SL_dataarea_CHDs(SL_name, item_name, part_child, diskarea_dic): + da_num_disks = 0 + for diskarea_child in part_child: + disk_dic = { 'name' : '', 'sha1' : '' } + disk_dic['name'] = diskarea_child.attrib['name'] if 'name' in diskarea_child.attrib else '' + disk_dic['sha1'] = diskarea_child.attrib['sha1'] if 'sha1' in diskarea_child.attrib else '' + diskarea_dic['disks'].append(disk_dic) + da_num_disks += 1 + + return da_num_disks + +def _mame_load_SL_XML(xml_filename): + __debug_xml_parser = False + SLData = _new_SL_Data_dic() + + # If file does not exist return empty dictionary. + if not os.path.isfile(xml_filename): return SLData + (head, SL_name) = os.path.split(xml_filename) + + # Parse using ElementTree. + # If XML has errors (invalid characters, etc.) this will rais exception 'err' + # log_debug('_mame_load_SL_XML() Loading XML file "{}"'.format(xml_filename)) + try: + xml_tree = ET.parse(xml_filename) + except: + return SLData + xml_root = xml_tree.getroot() + SL_desc = xml_root.attrib['description'] + # Substitute SL description (long name). + if SL_desc in SL_better_name_dic: + old_SL_desc = SL_desc + SL_desc = SL_better_name_dic[SL_desc] + log_debug('Substitute SL "{}" with "{}"'.format(old_SL_desc, SL_desc)) + SLData['display_name'] = SL_desc + for root_element in xml_root: + if __debug_xml_parser: log_debug('Root child {}'.format(root_element.tag)) + # Only process 'software' elements + if root_element.tag != 'software': + log_warning('In SL {}, unrecognised XML tag <{}>'.format(SL_name, root_element.tag)) + continue + SL_item = db_new_SL_ROM() + SL_rom_list = [] + num_roms = 0 + num_disks = 0 + item_name = root_element.attrib['name'] + if 'cloneof' in root_element.attrib: SL_item['cloneof'] = root_element.attrib['cloneof'] + if 'romof' in root_element.attrib: + raise TypeError('SL {} item {}, "romof" in root_element.attrib'.format(SL_name, item_name)) + + for rom_child in root_element: + # By default read strings + xml_text = rom_child.text if rom_child.text is not None else '' + xml_tag = rom_child.tag + if __debug_xml_parser: log_debug('{} --> {}'.format(xml_tag, xml_text)) + + # --- Only pick tags we want --- + if xml_tag == 'description' or xml_tag == 'year' or xml_tag == 'publisher': + SL_item[xml_tag] = xml_text + + elif xml_tag == 'part': + # <part name="cart" interface="_32x_cart"> + part_dic = db_new_SL_ROM_part() + part_dic['name'] = rom_child.attrib['name'] + part_dic['interface'] = rom_child.attrib['interface'] + SL_item['parts'].append(part_dic) + SL_roms_dic = { + 'part_name' : rom_child.attrib['name'], + 'part_interface' : rom_child.attrib['interface'] + } + + # --- Count number of <dataarea> and <diskarea> tags inside this <part tag> --- + num_dataarea = 0 + num_diskarea = 0 + for part_child in rom_child: + if part_child.tag == 'dataarea': + dataarea_dic = { 'name' : part_child.attrib['name'], 'roms' : [] } + da_num_roms = _get_SL_dataarea_ROMs(SL_name, item_name, part_child, dataarea_dic) + if da_num_roms > 0: + # >> dataarea is valid ONLY if it contains valid ROMs + num_dataarea += 1 + num_roms += da_num_roms + if 'dataarea' not in SL_roms_dic: SL_roms_dic['dataarea'] = [] + SL_roms_dic['dataarea'].append(dataarea_dic) + elif part_child.tag == 'diskarea': + diskarea_dic = { 'name' : part_child.attrib['name'], 'disks' : [] } + da_num_disks = _get_SL_dataarea_CHDs(SL_name, item_name, part_child, diskarea_dic) + if da_num_disks > 0: + # >> diskarea is valid ONLY if it contains valid CHDs + num_diskarea += 1 + num_disks += da_num_disks + if 'diskarea' not in SL_roms_dic: SL_roms_dic['diskarea'] = [] + SL_roms_dic['diskarea'].append(diskarea_dic) + elif part_child.tag == 'feature': + pass + elif part_child.tag == 'dipswitch': + pass + else: + raise TypeError('SL {} item {}, inside <part>, unrecognised tag <{}>'.format( + SL_name, item_name, part_child.tag)) + # --- Add ROMs/disks --- + SL_rom_list.append(SL_roms_dic) + + # --- DEBUG/Research code --- + # if num_dataarea > 1: + # log_error('{} -> num_dataarea = {}'.format(item_name, num_dataarea)) + # raise TypeError('DEBUG') + # if num_diskarea > 1: + # log_error('{} -> num_diskarea = {}'.format(item_name, num_diskarea)) + # raise TypeError('DEBUG') + # if num_dataarea and num_diskarea: + # log_error('{} -> num_dataarea = {}'.format(item_name, num_dataarea)) + # log_error('{} -> num_diskarea = {}'.format(item_name, num_diskarea)) + # raise TypeError('DEBUG') + + # --- Finished processing of <software> element --- + SLData['num_items'] += 1 + if SL_item['cloneof']: SLData['num_clones'] += 1 + else: SLData['num_parents'] += 1 + if num_roms: + SL_item['hasROMs'] = True + SL_item['status_ROM'] = '?' + SLData['num_with_ROMs'] += 1 + else: + SL_item['hasROMs'] = False + SL_item['status_ROM'] = '-' + if num_disks: + SL_item['hasCHDs'] = True + SL_item['status_CHD'] = '?' + SLData['num_with_CHDs'] += 1 + else: + SL_item['hasCHDs'] = False + SL_item['status_CHD'] = '-' + + # Add <software> item (SL_item) to database and software ROM/CHDs to database. + SLData['items'][item_name] = SL_item + SLData['SL_roms'][item_name] = SL_rom_list + + return SLData + +def _get_SL_parent_ROM_dic(parent_name, SL_ROMs): + parent_rom_dic = {} + for part_dic in SL_ROMs[parent_name]: + if not 'dataarea' in part_dic: continue + for dataarea_dic in part_dic['dataarea']: + for rom_dic in dataarea_dic['roms']: + parent_rom_dic[rom_dic['crc']] = rom_dic['name'] + + return parent_rom_dic + +def _get_SL_ROM_location(rom_set, SL_name, SL_item_name, rom_dic, SL_Items, parent_rom_dic): + # Some SL invalid ROMs do not have name attribute (and not CRC and SHA1). + # For those, set the location to empty. + if not rom_dic['name']: return '' + + # In the SL ROM MERGED set all ROMs are stored in the parent ZIP file: + # + # PATH/32x/chaotix.zip/knuckles' chaotix (europe).bin + # PATH/32x/chaotix.zip/chaotixju/chaotix ~ knuckles' chaotix (japan, usa).bin + # PATH/32x/chaotix.zip/chaotixjup/knuckles' chaotix (prototype 214 - feb 14, 1995, 06.46).bin + # + if rom_set == 'MERGED': + cloneof = SL_Items[SL_item_name]['cloneof'] + if cloneof: + location = SL_name + '/' + cloneof + '/' + SL_item_name + '/' + rom_dic['name'] + else: + location = SL_name + '/' + SL_item_name + '/' + rom_dic['name'] + + # In the SL ROM SPLIT set each item ROMs are in their own file: + # + # PATH/32x/chaotix.zip/knuckles' chaotix (europe).bin + # PATH/32x/chaotixju.zip/chaotix ~ knuckles' chaotix (japan, usa).bin + # PATH/32x/chaotixjup.zip/knuckles' chaotix (prototype 214 - feb 14, 1995, 06.46).bin + # + # NOTE that ClrMAME Pro (and hence PD torrents) do implicit ROM merging. SL XMLs do not have + # the merge attribute. However, an implicit ROM merge is done if a ROM with the same + # CRC is found in the parent. Implicit merging only affects clones. A dictionary + # of the parent ROMs with key the CRC hash and value the ROM name is required. + # + elif rom_set == 'SPLIT': + cloneof = SL_Items[SL_item_name]['cloneof'] + if cloneof: + if rom_dic['crc'] in parent_rom_dic: + location = SL_name + '/' + cloneof + '/' + parent_rom_dic[rom_dic['crc']] + else: + location = SL_name + '/' + SL_item_name + '/' + rom_dic['name'] + else: + location = SL_name + '/' + SL_item_name + '/' + rom_dic['name'] + + elif rom_set == 'NONMERGED': + location = SL_name + '/' + SL_item_name + '/' + rom_dic['name'] + + else: + raise TypeError + + return location + +def _get_SL_CHD_location(chd_set, SL_name, SL_item_name, disk_dic, SL_Items): + # In the SL CHD MERGED set all CHDs are in the directory of the parent: + # + # ffant9 --> parent with 4 DISKS (v1.1) + # ffant9a --> parent with 4 DISKS (v1.0) + # + # [parent traid] PATH/psx/traid/tomb raider (usa) (v1.6).chd + # [clone traida] PATH/psx/traid/tomb raider (usa) (v1.5).chd + # [clone traiddm] PATH/psx/traid/tr1.chd + # + if chd_set == 'MERGED': + cloneof = SL_Items[SL_item_name]['cloneof'] + archive_name = cloneof if cloneof else SL_item_name + location = SL_name + '/' + archive_name + '/' + disk_dic['name'] + + # In the SL CHD SPLIT set CHD of each machine are in their own directory. + # This is not confirmed since I do not have the PD DAT file for the SL CHD SPLIT set. + # + # [parent traid] PATH/psx/traid/tomb raider (usa) (v1.6).chd + # [clone traida] PATH/psx/traida/tomb raider (usa) (v1.5).chd + # [clone traiddm] PATH/psx/traiddm/tr1.chd + # + elif chd_set == 'SPLIT': + location = SL_name + '/' + SL_rom + '/' + disk_dic['name'] + + else: + raise TypeError + + return location + +# ------------------------------------------------------------------------------------------------- +# +# Checks for errors before scanning for SL ROMs. +# Display a Kodi dialog if an error is found. +# +def mame_check_before_build_SL_databases(cfg, st_dic, control_dic): + kodi_reset_status(st_dic) + + # --- Error checks --- + if not cfg.settings['SL_hash_path']: + t = ('Software Lists hash path not set. ' + 'Open AML addon settings and configure the location of the MAME hash path in the ' + '"Paths" tab.') + kodi_set_error_status(st_dic, t) + return + + if not cfg.MAIN_DB_PATH.exists(): + t = ('MAME Main database not found. ' + 'Open AML addon settings and configure the location of the MAME executable in the ' + '"Paths" tab.') + kodi_set_error_status(st_dic, t) + return + +# +# Modifies dictionary db_dic_in. +# +# SL_catalog_dic = { 'name' : { +# 'display_name': u'', +# 'num_clones' : int, +# 'num_items' : int, +# 'num_parents' : int, +# 'num_with_CHDs' : int, +# 'num_with_ROMs' : int, +# 'rom_DB_noext' : u'' +# }, +# } +# +# Saves: +# SL_INDEX_PATH, +# SL_MACHINES_PATH, +# SL_PCLONE_DIC_PATH, +# per-SL database (32x.json) +# per-SL database (32x_ROMs.json) +# per-SL ROM audit database (32x_ROM_audit.json) +# per-SL item archives (ROMs and CHDs) (32x_ROM_archives.json) +# +def mame_build_SoftwareLists_databases(cfg, st_dic, db_dic_in): + control_dic = db_dic_in['control_dic'] + machines = db_dic_in['machines'] + renderdb_dic = db_dic_in['renderdb'] + + SL_dir_FN = FileName(cfg.settings['SL_hash_path']) + log_debug('mame_build_SoftwareLists_databases() SL_dir_FN "{}"'.format(SL_dir_FN.getPath())) + + # --- Scan all XML files in Software Lists directory and save SL catalog and SL databases --- + log_info('Processing Software List XML files...') + SL_file_list = SL_dir_FN.scanFilesInPath('*.xml') + # DEBUG code for development, only process first SL file (32x). + # SL_file_list = [ sorted(SL_file_list)[0] ] + total_SL_files = len(SL_file_list) + num_SL_with_ROMs = 0 + num_SL_with_CHDs = 0 + SL_catalog_dic = {} + processed_files = 0 + diag_line = 'Building Sofware Lists item databases...' + pDialog = KodiProgressDialog() + pDialog.startProgress(diag_line, total_SL_files) + for file in sorted(SL_file_list): + # Progress dialog + FN = FileName(file) + pDialog.updateProgress(processed_files, + '{}\nSoftware List [COLOR orange]{}[/COLOR]'.format(diag_line, FN.getBase())) + + # Open software list XML and parse it. Then, save data fields we want in JSON. + # log_debug('mame_build_SoftwareLists_databases() Processing "{}"'.format(file)) + SL_path_FN = FileName(file) + SLData = _mame_load_SL_XML(SL_path_FN.getPath()) + utils_write_JSON_file(cfg.SL_DB_DIR.pjoin(FN.getBaseNoExt() + '_items.json').getPath(), + SLData['items'], verbose = False) + utils_write_JSON_file(cfg.SL_DB_DIR.pjoin(FN.getBaseNoExt() + '_ROMs.json').getPath(), + SLData['SL_roms'], verbose = False) + + # Add software list to catalog + num_SL_with_ROMs += SLData['num_with_ROMs'] + num_SL_with_CHDs += SLData['num_with_CHDs'] + SL = { + 'display_name' : SLData['display_name'], + 'num_with_ROMs' : SLData['num_with_ROMs'], + 'num_with_CHDs' : SLData['num_with_CHDs'], + 'num_items' : SLData['num_items'], + 'num_parents' : SLData['num_parents'], + 'num_clones' : SLData['num_clones'], + 'rom_DB_noext' : FN.getBaseNoExt(), + } + SL_catalog_dic[FN.getBaseNoExt()] = SL + + # Update progress + processed_files += 1 + pDialog.endProgress() + + # --- Make the SL ROM/CHD unified Audit databases --- + log_info('Building Software List ROM Audit database...') + rom_set = ['MERGED', 'SPLIT', 'NONMERGED'][cfg.settings['SL_rom_set']] + chd_set = ['MERGED', 'SPLIT', 'NONMERGED'][cfg.settings['SL_chd_set']] + log_info('mame_build_SoftwareLists_databases() SL ROM set is {}'.format(rom_set)) + log_info('mame_build_SoftwareLists_databases() SL CHD set is {}'.format(chd_set)) + total_files = len(SL_file_list) + processed_files = 0 + stats_audit_SL_items_runnable = 0 + stats_audit_SL_items_with_arch = 0 + stats_audit_SL_items_with_arch_ROM = 0 + stats_audit_SL_items_with_CHD = 0 + diag_line = 'Building Software List ROM audit databases...' + pDialog.startProgress(diag_line, total_files) + for file in sorted(SL_file_list): + # Update progress + FN = FileName(file) + SL_name = FN.getBaseNoExt() + pDialog.updateProgress(processed_files, '{}\nSoftware List [COLOR orange]{}[/COLOR]'.format( + diag_line, FN.getBase())) + + # Filenames of the databases + # log_debug('mame_build_SoftwareLists_databases() Processing "{}"'.format(file)) + SL_Items_DB_FN = cfg.SL_DB_DIR.pjoin(FN.getBaseNoExt() + '_items.json') + SL_ROMs_DB_FN = cfg.SL_DB_DIR.pjoin(FN.getBaseNoExt() + '_ROMs.json') + SL_ROM_Audit_DB_FN = cfg.SL_DB_DIR.pjoin(FN.getBaseNoExt() + '_ROM_audit.json') + SL_Soft_Archives_DB_FN = cfg.SL_DB_DIR.pjoin(FN.getBaseNoExt() + '_ROM_archives.json') + SL_Items = utils_load_JSON_file(SL_Items_DB_FN.getPath(), verbose = False) + SL_ROMs = utils_load_JSON_file(SL_ROMs_DB_FN.getPath(), verbose = False) + + # --- First add the SL item ROMs to the audit database --- + SL_Audit_ROMs_dic = {} + for SL_item_name in SL_ROMs: + # >> If SL item is a clone then create parent_rom_dic. This is only needed in the + # >> SPLIT set, so current code is a bit inefficient for other sets. + # >> key : CRC -> value : rom name + cloneof = SL_Items[SL_item_name]['cloneof'] + if cloneof: + parent_rom_dic = _get_SL_parent_ROM_dic(cloneof, SL_ROMs) + else: + parent_rom_dic = {} + + # >> Iterate Parts in a SL Software item. Then iterate dataareas on each part. + # >> Finally, iterate ROM on each dataarea. + set_roms = [] + for part_dic in SL_ROMs[SL_item_name]: + if not 'dataarea' in part_dic: continue + for dataarea_dic in part_dic['dataarea']: + for rom_dic in dataarea_dic['roms']: + location = _get_SL_ROM_location(rom_set, SL_name, SL_item_name, + rom_dic, SL_Items, parent_rom_dic) + rom_audit_dic = db_new_SL_ROM_audit_dic() + rom_audit_dic['type'] = ROM_TYPE_ROM + rom_audit_dic['name'] = rom_dic['name'] + rom_audit_dic['size'] = rom_dic['size'] + rom_audit_dic['crc'] = rom_dic['crc'] + rom_audit_dic['location'] = location + set_roms.append(rom_audit_dic) + SL_Audit_ROMs_dic[SL_item_name] = set_roms + + # --- Second add the SL item CHDs to the audit database --- + for SL_item_name in SL_ROMs: + set_chds = [] + for part_dic in SL_ROMs[SL_item_name]: + if not 'diskarea' in part_dic: continue + for diskarea_dic in part_dic['diskarea']: + for disk_dic in diskarea_dic['disks']: + location = _get_SL_CHD_location(chd_set, SL_name, SL_item_name, disk_dic, SL_Items) + disk_audit_dic = db_new_SL_DISK_audit_dic() + disk_audit_dic['type'] = ROM_TYPE_DISK + disk_audit_dic['name'] = disk_dic['name'] + disk_audit_dic['sha1'] = disk_dic['sha1'] + disk_audit_dic['location'] = location + set_chds.append(disk_audit_dic) + # >> Extend ROM list with CHDs. + if SL_item_name in SL_Audit_ROMs_dic: + SL_Audit_ROMs_dic[SL_item_name].extend(set_chds) + else: + SL_Audit_ROMs_dic[SL_item_name] = set_chds + + # --- Machine archives --- + # There is not ROMs and CHDs sets for Software List Items (not necessary). + SL_Item_Archives_dic = {} + for SL_item_name in SL_Audit_ROMs_dic: + rom_list = SL_Audit_ROMs_dic[SL_item_name] + machine_rom_archive_set = set() + machine_chd_archive_set = set() + # --- Iterate ROMs/CHDs --- + for rom in rom_list: + if rom['type'] == ROM_TYPE_DISK: + # >> Skip invalid CHDs + if not rom['sha1']: continue + chd_name = rom['location'] + machine_chd_archive_set.add(chd_name) + else: + # >> Skip invalid ROMs + if not rom['crc']: continue + rom_str_list = rom['location'].split('/') + zip_name = rom_str_list[0] + '/' + rom_str_list[1] + machine_rom_archive_set.add(zip_name) + SL_Item_Archives_dic[SL_item_name] = { + 'ROMs' : list(machine_rom_archive_set), + 'CHDs' : list(machine_chd_archive_set) + } + # --- SL Audit database statistics --- + stats_audit_SL_items_runnable += 1 + if SL_Item_Archives_dic[SL_item_name]['ROMs'] or SL_Item_Archives_dic[SL_item_name]['CHDs']: + stats_audit_SL_items_with_arch += 1 + if SL_Item_Archives_dic[SL_item_name]['ROMs']: stats_audit_SL_items_with_arch_ROM += 1 + if SL_Item_Archives_dic[SL_item_name]['CHDs']: stats_audit_SL_items_with_CHD += 1 + + # --- Save databases --- + utils_write_JSON_file(SL_ROM_Audit_DB_FN.getPath(), SL_Audit_ROMs_dic, verbose = False) + utils_write_JSON_file(SL_Soft_Archives_DB_FN.getPath(), SL_Item_Archives_dic, verbose = False) + processed_files += 1 + pDialog.endProgress() + + # --- Make SL Parent/Clone databases --- + log_info('Building Software List PClone list...') + total_files = len(SL_catalog_dic) + processed_files = 0 + SL_PClone_dic = {} + total_SL_XML_files = 0 + total_SL_software_items = 0 + diag_line = 'Building Software List PClone list...' + pDialog.startProgress(diag_line, total_files) + for sl_name in sorted(SL_catalog_dic): + pDialog.updateProgress(processed_files, '{}\nSoftware List [COLOR orange]{}[/COLOR]'.format( + diag_line, sl_name)) + total_SL_XML_files += 1 + pclone_dic = {} + SL_database_FN = cfg.SL_DB_DIR.pjoin(sl_name + '_items.json') + ROMs = utils_load_JSON_file(SL_database_FN.getPath(), verbose = False) + for rom_name in ROMs: + total_SL_software_items += 1 + ROM = ROMs[rom_name] + if ROM['cloneof']: + parent_name = ROM['cloneof'] + if parent_name not in pclone_dic: pclone_dic[parent_name] = [] + pclone_dic[parent_name].append(rom_name) + else: + if rom_name not in pclone_dic: pclone_dic[rom_name] = [] + SL_PClone_dic[sl_name] = pclone_dic + processed_files += 1 + pDialog.endProgress() + + # --- Make a list of machines that can launch each SL --- + log_info('Making Software List machine list...') + total_SL = len(SL_catalog_dic) + processed_SL = 0 + SL_machines_dic = {} + diag_line = 'Building Software List machine list...' + pDialog.startProgress(diag_line, total_SL) + for SL_name in sorted(SL_catalog_dic): + pDialog.updateProgress(processed_SL, '{}\nSoftware List [COLOR orange]{}[/COLOR]'.format( + diag_line, SL_name)) + SL_machine_list = [] + for machine_name in machines: + # if not machines[machine_name]['softwarelists']: continue + for machine_SL_name in machines[machine_name]['softwarelists']: + if machine_SL_name == SL_name: + SL_machine_dic = { + 'machine' : machine_name, + 'description' : renderdb_dic[machine_name]['description'], + 'devices' : machines[machine_name]['devices'] + } + SL_machine_list.append(SL_machine_dic) + SL_machines_dic[SL_name] = SL_machine_list + processed_SL += 1 + pDialog.endProgress() + + # --- Empty SL asset DB --- + log_info('Making Software List (empty) asset databases...') + total_SL = len(SL_catalog_dic) + processed_SL = 0 + diag_line = 'Building Software List (empty) asset databases...' + pDialog.startProgress(diag_line, total_SL) + for SL_name in sorted(SL_catalog_dic): + pDialog.updateProgress(processed_SL, '{}\nSoftware List [COLOR orange]{}[/COLOR]'.format( + diag_line, SL_name)) + + # --- Load SL databases --- + file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_items.json' + SL_DB_FN = cfg.SL_DB_DIR.pjoin(file_name) + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + assets_file_name = SL_catalog_dic[SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + + # --- Second pass: substitute artwork --- + SL_assets_dic = {} + for rom_key in sorted(SL_roms): + SL_assets_dic[rom_key] = db_new_SL_asset() + + # --- Write SL asset JSON --- + utils_write_JSON_file(SL_asset_DB_FN.getPath(), SL_assets_dic, verbose = False) + processed_SL += 1 + pDialog.endProgress() + + # --- Create properties database with default values --- + # --- Make SL properties DB --- + # >> Allows customisation of every SL list window + # >> Not used at the moment -> Global properties + # SL_properties_dic = {} + # for sl_name in SL_catalog_dic: + # # 'vm' : VIEW_MODE_NORMAL or VIEW_MODE_ALL + # prop_dic = {'vm' : VIEW_MODE_NORMAL} + # SL_properties_dic[sl_name] = prop_dic + # utils_write_JSON_file(cfg.SL_MACHINES_PROP_PATH.getPath(), SL_properties_dic) + # log_info('SL_properties_dic has {} items'.format(len(SL_properties_dic))) + + # >> One of the MAME catalogs has changed, and so the property names. + # >> Not used at the moment -> Global properties + # mame_properties_dic = {} + # for catalog_name in CATALOG_NAME_LIST: + # catalog_dic = db_get_cataloged_dic_parents(cfg, catalog_name) + # for category_name in sorted(catalog_dic): + # prop_key = '{} - {}'.format(catalog_name, category_name) + # mame_properties_dic[prop_key] = {'vm' : VIEW_MODE_NORMAL} + # utils_write_JSON_file(cfg.MAIN_PROPERTIES_PATH.getPath(), mame_properties_dic) + # log_info('mame_properties_dic has {} items'.format(len(mame_properties_dic))) + + # ----------------------------------------------------------------------------- + # Update MAME control dictionary + # ----------------------------------------------------------------------------- + # --- SL item database --- + db_safe_edit(control_dic, 'stats_SL_XML_files', total_SL_XML_files) + db_safe_edit(control_dic, 'stats_SL_software_items', total_SL_software_items) + db_safe_edit(control_dic, 'stats_SL_items_with_ROMs', num_SL_with_ROMs) + db_safe_edit(control_dic, 'stats_SL_items_with_CHDs', num_SL_with_CHDs) + + # --- SL audit database statistics --- + db_safe_edit(control_dic, 'stats_audit_SL_items_runnable', stats_audit_SL_items_runnable) + db_safe_edit(control_dic, 'stats_audit_SL_items_with_arch', stats_audit_SL_items_with_arch) + db_safe_edit(control_dic, 'stats_audit_SL_items_with_arch_ROM', stats_audit_SL_items_with_arch_ROM) + db_safe_edit(control_dic, 'stats_audit_SL_items_with_CHD', stats_audit_SL_items_with_CHD) + + # --- SL build timestamp --- + db_safe_edit(control_dic, 't_SL_DB_build', time.time()) + + # --- Save modified/created stuff in this function --- + if OPTION_LOWMEM_WRITE_JSON: + json_write_func = utils_write_JSON_file_lowmem + log_debug('Using utils_write_JSON_file_lowmem() JSON writer') + else: + json_write_func = utils_write_JSON_file + log_debug('Using utils_write_JSON_file() JSON writer') + db_files = [ + # Fix this list of files!!! + [SL_catalog_dic, 'Software Lists index', cfg.SL_INDEX_PATH.getPath()], + [SL_PClone_dic, 'Software Lists P/Clone', cfg.SL_PCLONE_DIC_PATH.getPath()], + [SL_machines_dic, 'Software Lists machines', cfg.SL_MACHINES_PATH.getPath()], + # Save control_dic after everything is saved. + [control_dic, 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + ] + db_save_files(db_files, json_write_func) + db_dic_in['SL_index'] = SL_catalog_dic + db_dic_in['SL_machines'] = SL_machines_dic + db_dic_in['SL_PClone_dic'] = SL_PClone_dic + +# ------------------------------------------------------------------------------------------------- +# ROM/CHD and asset scanner +# ------------------------------------------------------------------------------------------------- +# +# Checks for errors before scanning for SL ROMs. +# Display a Kodi dialog if an error is found. +# +def mame_check_before_scan_MAME_ROMs(cfg, st_dic, options_dic, control_dic): + log_info('mame_check_before_scan_MAME_ROMs() Starting...') + kodi_reset_status(st_dic) + + # ROM scanning is mandatory, even if ROM directory is empty. + # Get paths and check they exist. + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + rom_path = cfg.settings['rom_path_vanilla'] + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + rom_path = cfg.settings['rom_path_2003_plus'] + else: + raise TypeError('Unknown op_mode "{}"'.format(cfg.settings['op_mode'])) + if not rom_path: + kodi_set_error_status(st_dic, 'ROM directory not configured. Aborting scanner.') + return + ROM_path_FN = FileName(rom_path) + if not ROM_path_FN.isdir(): + kodi_set_error_status(st_dic, 'ROM directory does not exist. Aborting scanner.') + return + + # Scanning of CHDs is optional. + if cfg.settings['chd_path']: + CHD_path_FN = FileName(cfg.settings['chd_path']) + if not CHD_path_FN.isdir(): + kodi_dialog_OK('CHD directory does not exist. CHD scanning disabled.') + options_dic['scan_CHDs'] = False + else: + options_dic['scan_CHDs'] = True + else: + kodi_dialog_OK('CHD directory not configured. CHD scanning disabled.') + options_dic['scan_CHDs'] = False + + # Scanning of Samples is optional. + if cfg.settings['samples_path']: + Samples_path_FN = FileName(cfg.settings['samples_path']) + if not Samples_path_FN.isdir(): + kodi_dialog_OK('Samples directory does not exist. Samples scanning disabled.') + options_dic['scan_Samples'] = False + else: + options_dic['scan_Samples'] = True + else: + kodi_dialog_OK('Samples directory not configured. Samples scanning disabled.') + options_dic['scan_Samples'] = False + +# +# Saves control_dic and assets_dic. +# +# PROBLEM with samples scanning. +# Most samples are stored in ZIP files. However, the samples shipped with MAME executable +# are uncompressed: +# MAME_DIR/samples/floppy/35_seek_12ms.wav +# MAME_DIR/samples/floppy/35_seek_20ms.wav +# ... +# MAME_DIR/samples/MM1_keyboard/beep.wav +# MAME_DIR/samples/MM1_keyboard/power_switch.wav +# +def mame_scan_MAME_ROMs(cfg, st_dic, options_dic, db_dic_in): + # --- Convenient variables for databases --- + control_dic = db_dic_in['control_dic'] + machines = db_dic_in['machines'] + renderdb = db_dic_in['renderdb'] + assetdb = db_dic_in['assetdb'] + machine_archives_dic = db_dic_in['machine_archives'] + # ROM_ZIP_list = db_dic_in['ROM_ZIP_list'] + # Sample_ZIP_list = db_dic_in['Sample_ZIP_list'] + # CHD_list = db_dic_in['CHD_archive_list'] + + log_info('mame_scan_MAME_ROMs() Starting...') + kodi_reset_status(st_dic) + + # At this point paths have been verified and exists. + if cfg.settings['op_mode'] == OP_MODE_VANILLA: + rom_path = cfg.settings['rom_path_vanilla'] + elif cfg.settings['op_mode'] == OP_MODE_RETRO_MAME2003PLUS: + rom_path = cfg.settings['rom_path_2003_plus'] + else: + raise TypeError('Unknown op_mode "{}"'.format(cfg.settings['op_mode'])) + ROM_path_FN = FileName(rom_path) + log_info('mame_scan_MAME_ROMs() ROM dir OP {}'.format(ROM_path_FN.getOriginalPath())) + log_info('mame_scan_MAME_ROMs() ROM dir P {}'.format(ROM_path_FN.getPath())) + + if options_dic['scan_CHDs']: + CHD_path_FN = FileName(cfg.settings['chd_path']) + log_info('mame_scan_MAME_ROMs() CHD dir OP {}'.format(CHD_path_FN.getOriginalPath())) + log_info('mame_scan_MAME_ROMs() CHD dir P {}'.format(CHD_path_FN.getPath())) + else: + CHD_path_FN = FileName('') + log_info('Scan of CHDs disabled.') + + if options_dic['scan_Samples']: + Samples_path_FN = FileName(cfg.settings['samples_path']) + log_info('mame_scan_MAME_ROMs() Samples OP {}'.format(Samples_path_FN.getOriginalPath())) + log_info('mame_scan_MAME_ROMs() Samples P {}'.format(Samples_path_FN.getPath())) + else: + Samples_path_FN = FileName('') + log_info('Scan of Samples disabled.') + + # --- Create auxiliary databases --- + pDialog = KodiProgressDialog() + pDialog.startProgress('Creating auxiliary databases...', 3) + ROM_ZIP_list = mame_get_ROM_ZIP_list(machine_archives_dic) + pDialog.updateProgressInc() + Sample_ZIP_list = mame_get_Sample_ZIP_list(machine_archives_dic) + pDialog.updateProgressInc() + CHD_list = mame_get_CHD_list(machine_archives_dic) + pDialog.endProgress() + + # --- Create a cache of files --- + # utils_file_cache_add_dir() creates a set with all files in a given directory. + # That set is stored in a function internal cache associated with the path. + # Files in the cache can be searched with misc_search_file_cache() + # utils_file_cache_add_dir() accepts invalid/empty paths, just do not add them to the cache. + ROM_path_str = ROM_path_FN.getPath() + CHD_path_str = CHD_path_FN.getPath() + Samples_path_str = Samples_path_FN.getPath() + STUFF_PATH_LIST = [ROM_path_str, CHD_path_str, Samples_path_str] + pDialog.startProgress('Listing files in ROM/CHD/Samples directories...', len(STUFF_PATH_LIST)) + utils_file_cache_clear() + for asset_dir in STUFF_PATH_LIST: + pDialog.updateProgressInc() + utils_file_cache_add_dir(asset_dir) + pDialog.endProgress() + + # --- Scan machine archives --- + # Traverses all machines and scans if all required files exist. + scan_march_ROM_total = 0 + scan_march_ROM_have = 0 + scan_march_ROM_missing = 0 + scan_march_SAM_total = 0 + scan_march_SAM_have = 0 + scan_march_SAM_missing = 0 + scan_march_CHD_total = 0 + scan_march_CHD_have = 0 + scan_march_CHD_missing = 0 + r_full_list = [] + r_have_list = [] + r_miss_list = [] + dial_line = 'Scanning MAME machine archives (ROMs, CHDs and Samples)...' + pDialog.startProgress(dial_line, len(renderdb)) + for key in sorted(renderdb): + pDialog.updateProgressInc() + + # --- Initialise machine --- + # log_info('mame_scan_MAME_ROMs() Checking machine {}'.format(key)) + if renderdb[key]['isDevice']: continue # Skip Devices + m_have_str_list = [] + m_miss_str_list = [] + + # --- ROMs --- + rom_list = machine_archives_dic[key]['ROMs'] + if rom_list: + scan_march_ROM_total += 1 + have_rom_list = [False] * len(rom_list) + for i, rom in enumerate(rom_list): + # --- Old code --- + # archive_name = rom + '.zip' + # ROM_FN = ROM_path_FN.pjoin(archive_name) + # if ROM_FN.exists(): + # --- New code using file cache --- + ROM_FN = utils_file_cache_search(ROM_path_str, rom, MAME_ROM_EXTS) + if ROM_FN: + have_rom_list[i] = True + m_have_str_list.append('HAVE ROM {}'.format(rom)) + else: + m_miss_str_list.append('MISS ROM {}'.format(rom)) + if all(have_rom_list): + # --- All ZIP files required to run this machine exist --- + scan_march_ROM_have += 1 + ROM_flag = 'R' + else: + scan_march_ROM_missing += 1 + ROM_flag = 'r' + else: + ROM_flag = '-' + db_set_ROM_flag(assetdb[key], ROM_flag) + + # --- Samples --- + sample_list = machine_archives_dic[key]['Samples'] + if sample_list and options_dic['scan_Samples']: + scan_march_SAM_total += 1 + have_sample_list = [False] * len(sample_list) + for i, sample in enumerate(sample_list): + Sample_FN = utils_file_cache_search(Samples_path_str, sample, MAME_SAMPLE_EXTS) + if ROM_FN: + have_sample_list[i] = True + m_have_str_list.append('HAVE SAM {}'.format(sample)) + else: + m_miss_str_list.append('MISS SAM {}'.format(sample)) + if all(have_sample_list): + scan_march_SAM_have += 1 + Sample_flag = 'S' + else: + scan_march_SAM_missing += 1 + Sample_flag = 's' + elif sample_list and not options_dic['scan_Samples']: + scan_march_SAM_total += 1 + scan_march_SAM_missing += 1 + Sample_flag = 's' + else: + Sample_flag = '-' + db_set_Sample_flag(assetdb[key], Sample_flag) + + # --- Disks --- + # Machines with CHDs: 2spicy, sfiii2 + chd_list = machine_archives_dic[key]['CHDs'] + if chd_list and options_dic['scan_CHDs']: + scan_march_CHD_total += 1 + has_chd_list = [False] * len(chd_list) + for idx, chd_name in enumerate(chd_list): + # --- Old code --- + # CHD_FN = CHD_path_FN.pjoin(chd_name) + # if CHD_FN.exists(): + # --- New code using file cache --- + # log_debug('Testing CHD "{}"'.format(chd_name)) + CHD_FN = utils_file_cache_search(CHD_path_str, chd_name, MAME_CHD_EXTS) + if CHD_FN: + has_chd_list[idx] = True + m_have_str_list.append('HAVE CHD {}'.format(chd_name)) + else: + m_miss_str_list.append('MISS CHD {}'.format(chd_name)) + if all(has_chd_list): + scan_march_CHD_have += 1 + CHD_flag = 'C' + else: + scan_march_CHD_missing += 1 + CHD_flag = 'c' + elif chd_list and not options_dic['scan_CHDs']: + scan_march_CHD_total += 1 + scan_march_CHD_missing += 1 + CHD_flag = 'c' + else: + CHD_flag = '-' + db_set_CHD_flag(assetdb[key], CHD_flag) + + # Build FULL, HAVE and MISSING reports. + r_full_list.append('Machine {} "{}"'.format(key, renderdb[key]['description'])) + if renderdb[key]['cloneof']: + cloneof = renderdb[key]['cloneof'] + r_full_list.append('cloneof {} "{}"'.format(cloneof, renderdb[cloneof]['description'])) + if not rom_list and not sample_list and not chd_list: + r_full_list.append('Machine has no ROMs, Samples and/or CHDs') + else: + r_full_list.extend(m_have_str_list) + r_full_list.extend(m_miss_str_list) + r_full_list.append('') + + # In the HAVE report include machines if and only if every required file is there. + if m_have_str_list and not m_miss_str_list: + r_have_list.append('Machine {} "{}"'.format(key, renderdb[key]['description'])) + if renderdb[key]['cloneof']: + cloneof = renderdb[key]['cloneof'] + r_have_list.append('cloneof {} "{}"'.format(cloneof, renderdb[cloneof]['description'])) + r_have_list.extend(m_have_str_list) + r_have_list.extend(m_miss_str_list) + r_have_list.append('') + + # In the MISSING report include machines if anything is missing. + if m_miss_str_list: + r_miss_list.append('Machine {} "{}"'.format(key, renderdb[key]['description'])) + if renderdb[key]['cloneof']: + cloneof = renderdb[key]['cloneof'] + r_miss_list.append('cloneof {} "{}"'.format(cloneof, renderdb[cloneof]['description'])) + r_miss_list.extend(m_have_str_list) + r_miss_list.extend(m_miss_str_list) + r_miss_list.append('') + pDialog.endProgress() + + # Write MAME scanner reports + reports_total = 3 + pDialog.startProgress('Saving scanner reports...', reports_total) + log_info('Writing report "{}"'.format(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_FULL_PATH.getPath())) + report_slist = [ + '*** Advanced MAME Launcher MAME machines scanner report ***', + 'This report shows all the scanned MAME machines.', + '', + 'MAME ROM path "{}"'.format(ROM_path_str), + 'MAME Samples path "{}"'.format(Samples_path_str), + 'MAME CHD path "{}"'.format(CHD_path_str), + '', + ] + report_slist.extend(r_full_list) + utils_write_slist_to_file(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_FULL_PATH.getPath(), report_slist) + + pDialog.updateProgress(1) + log_info('Writing report "{}"'.format(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_HAVE_PATH.getPath())) + report_slist = [ + '*** Advanced MAME Launcher MAME machines scanner report ***', + 'This reports shows MAME machines that have all the required', + 'ROM ZIP files, Sample ZIP files and CHD files.', + 'Machines that no require files are not listed.', + '', + 'MAME ROM path "{}"'.format(ROM_path_str), + 'MAME Samples path "{}"'.format(Samples_path_str), + 'MAME CHD path "{}"'.format(CHD_path_str), + '', + ] + if not r_have_list: + r_have_list.append('Ouch!!! You do not have any ROM ZIP files and/or CHDs.') + report_slist.extend(r_have_list) + utils_write_slist_to_file(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_HAVE_PATH.getPath(), report_slist) + + pDialog.updateProgress(2) + log_info('Writing report "{}"'.format(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_MISS_PATH.getPath())) + report_slist = [ + '*** Advanced MAME Launcher MAME machines scanner report ***', + 'This reports shows MAME machines that miss all or some of the required', + 'ROM ZIP files, Sample ZIP files or CHD files.', + 'Machines that no require files are not listed.', + '', + 'MAME ROM path "{}"'.format(ROM_path_str), + 'MAME Samples path "{}"'.format(Samples_path_str), + 'MAME CHD path "{}"'.format(CHD_path_str), + '', + ] + if not r_miss_list: + r_miss_list.append('Congratulations!!! You have no missing ROM ZIP and/or CHDs files.') + report_slist.extend(r_miss_list) + utils_write_slist_to_file(cfg.REPORT_MAME_SCAN_MACHINE_ARCH_MISS_PATH.getPath(), report_slist) + pDialog.endProgress() + + # --- ROM ZIP file list --- + scan_ROM_ZIP_files_total = 0 + scan_ROM_ZIP_files_have = 0 + scan_ROM_ZIP_files_missing = 0 + r_list = [ + '*** Advanced MAME Launcher MAME machines scanner report ***', + 'This report shows all missing MAME machine ROM ZIP files.', + 'Each missing ROM ZIP appears only once, but more than one machine may be affected.', + '', + 'MAME ROM path "{}"'.format(ROM_path_str), + 'MAME Samples path "{}"'.format(Samples_path_str), + 'MAME CHD path "{}"'.format(CHD_path_str), + '', + ] + pDialog.startProgress('Scanning MAME ROM ZIPs...', len(ROM_ZIP_list)) + for rom_name in ROM_ZIP_list: + pDialog.updateProgressInc() + scan_ROM_ZIP_files_total += 1 + ROM_FN = utils_file_cache_search(ROM_path_str, rom_name, MAME_ROM_EXTS) + if ROM_FN: + scan_ROM_ZIP_files_have += 1 + else: + scan_ROM_ZIP_files_missing += 1 + r_list.append('Missing ROM {}'.format(rom_name)) + pDialog.endProgress() + log_info('Writing report "{}"'.format(cfg.REPORT_MAME_SCAN_ROM_LIST_MISS_PATH.getPath())) + if scan_ROM_ZIP_files_missing == 0: + r_list.append('Congratulations!!! You have no missing ROM ZIP files.') + utils_write_slist_to_file(cfg.REPORT_MAME_SCAN_ROM_LIST_MISS_PATH.getPath(), r_list) + + # --- Sample ZIP file list --- + scan_Samples_ZIP_total = 0 + scan_Samples_ZIP_have = 0 + scan_Samples_ZIP_missing = 0 + r_list = [ + '*** Advanced MAME Launcher MAME machines scanner report ***', + 'This report shows all missing MAME machine Sample ZIP files.', + 'Each missing Sample ZIP appears only once, but more than one machine may be affected.', + '', + 'MAME ROM path "{}"'.format(ROM_path_str), + 'MAME Samples path "{}"'.format(Samples_path_str), + 'MAME CHD path "{}"'.format(CHD_path_str), + '', + ] + pDialog.startProgress('Scanning MAME Sample ZIPs...', len(Sample_ZIP_list)) + for sample_name in Sample_ZIP_list: + pDialog.updateProgressInc() + scan_Samples_ZIP_total += 1 + Sample_FN = utils_file_cache_search(Samples_path_str, sample_name, MAME_SAMPLE_EXTS) + if Sample_FN: + scan_Samples_ZIP_have += 1 + else: + scan_Samples_ZIP_missing += 1 + r_list.append('Missing Sample {}'.format(sample_name)) + pDialog.endProgress() + log_info('Writing report "{}"'.format(cfg.REPORT_MAME_SCAN_SAM_LIST_MISS_PATH.getPath())) + if scan_Samples_ZIP_missing == 0: + r_list.append('Congratulations!!! You have no missing Sample ZIP files.') + utils_write_slist_to_file(cfg.REPORT_MAME_SCAN_SAM_LIST_MISS_PATH.getPath(), r_list) + + # --- CHD file list --- + scan_CHD_files_total = 0 + scan_CHD_files_have = 0 + scan_CHD_files_missing = 0 + r_list = [ + '*** Advanced MAME Launcher MAME machines scanner report ***', + 'This report shows all missing MAME machine CHDs', + 'Each missing CHD appears only once, but more than one machine may be affected.', + '', + 'MAME ROM path "{}"'.format(ROM_path_str), + 'MAME Samples path "{}"'.format(Samples_path_str), + 'MAME CHD path "{}"'.format(CHD_path_str), + '', + ] + pDialog.startProgress('Scanning MAME CHDs...', len(CHD_list)) + for chd_name in CHD_list: + pDialog.updateProgressInc() + scan_CHD_files_total += 1 + CHD_FN = utils_file_cache_search(CHD_path_str, chd_name, MAME_CHD_EXTS) + if CHD_FN: + scan_CHD_files_have += 1 + else: + scan_CHD_files_missing += 1 + r_list.append('Missing CHD {}'.format(chd_name)) + pDialog.endProgress() + log_info('Writing report "{}"'.format(cfg.REPORT_MAME_SCAN_CHD_LIST_MISS_PATH.getPath())) + if scan_CHD_files_missing == 0: + r_list.append('Congratulations!!! You have no missing CHD files.') + utils_write_slist_to_file(cfg.REPORT_MAME_SCAN_CHD_LIST_MISS_PATH.getPath(), r_list) + + # --- Update statistics --- + db_safe_edit(control_dic, 'scan_machine_archives_ROM_total', scan_march_ROM_total) + db_safe_edit(control_dic, 'scan_machine_archives_ROM_have', scan_march_ROM_have) + db_safe_edit(control_dic, 'scan_machine_archives_ROM_missing', scan_march_ROM_missing) + db_safe_edit(control_dic, 'scan_machine_archives_Samples_total', scan_march_SAM_total) + db_safe_edit(control_dic, 'scan_machine_archives_Samples_have', scan_march_SAM_have) + db_safe_edit(control_dic, 'scan_machine_archives_Samples_missing', scan_march_SAM_missing) + db_safe_edit(control_dic, 'scan_machine_archives_CHD_total', scan_march_CHD_total) + db_safe_edit(control_dic, 'scan_machine_archives_CHD_have', scan_march_CHD_have) + db_safe_edit(control_dic, 'scan_machine_archives_CHD_missing', scan_march_CHD_missing) + + db_safe_edit(control_dic, 'scan_ROM_ZIP_files_total', scan_ROM_ZIP_files_total) + db_safe_edit(control_dic, 'scan_ROM_ZIP_files_have', scan_ROM_ZIP_files_have) + db_safe_edit(control_dic, 'scan_ROM_ZIP_files_missing', scan_ROM_ZIP_files_missing) + db_safe_edit(control_dic, 'scan_Samples_ZIP_total', scan_Samples_ZIP_total) + db_safe_edit(control_dic, 'scan_Samples_ZIP_have', scan_Samples_ZIP_have) + db_safe_edit(control_dic, 'scan_Samples_ZIP_missing', scan_Samples_ZIP_missing) + db_safe_edit(control_dic, 'scan_CHD_files_total', scan_CHD_files_total) + db_safe_edit(control_dic, 'scan_CHD_files_have', scan_CHD_files_have) + db_safe_edit(control_dic, 'scan_CHD_files_missing', scan_CHD_files_missing) + + # --- Scanner timestamp --- + db_safe_edit(control_dic, 't_MAME_ROMs_scan', time.time()) + + # --- Save databases --- + db_files = [ + [control_dic, 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + [assetdb, 'MAME machine assets', cfg.ASSET_DB_PATH.getPath()], + ] + db_save_files(db_files) + +# +# Checks for errors before scanning for SL assets. +# Caller function displays a Kodi dialog if an error is found and scanning must be aborted. +# +def mame_check_before_scan_MAME_assets(cfg, st_dic, control_dic): + kodi_reset_status(st_dic) + + # Get assets directory. Abort if not configured/found. + if not cfg.settings['assets_path']: + kodi_set_error_status(st_dic, 'MAME asset directory not configured. Aborting.') + return + Asset_path_FN = FileName(cfg.settings['assets_path']) + if not Asset_path_FN.isdir(): + kodi_set_error_status(st_dic, 'MAME asset directory does not exist. Aborting.') + return + +# +# Note that MAME is able to use clone artwork from parent machines. Mr. Do's Artwork ZIP files +# are provided only for parents. +# First pass: search for on-disk assets. +# Second pass: do artwork substitution +# A) A clone may use assets from parent. +# B) A parent may use assets from a clone. +# +def mame_scan_MAME_assets(cfg, st_dic, db_dic_in): + control_dic = db_dic_in['control_dic'] + renderdb_dic = db_dic_in['renderdb'] + assetdb_dic = db_dic_in['assetdb'] + main_pclone_dic = db_dic_in['main_pclone_dic'] + + Asset_path_FN = FileName(cfg.settings['assets_path']) + log_info('mame_scan_MAME_assets() Asset path {}'.format(Asset_path_FN.getPath())) + + # Iterate machines, check if assets/artwork exist. + table_str = [] + table_str.append([ + 'left', + 'left', 'left', 'left', 'left', 'left', 'left', 'left', + 'left', 'left', 'left', 'left', 'left', 'left', 'left']) + table_str.append([ + 'Name', + '3DB', 'Apr', 'Art', 'Cab', 'Clr', 'CPa', 'Fan', + 'Fly', 'Man', 'Mar', 'PCB', 'Snp', 'Tit', 'Tra']) + + # --- Create a cache of assets --- + asset_dirs = [''] * len(ASSET_MAME_T_LIST) + pDialog = KodiProgressDialog() + pDialog.startProgress('Listing files in asset directories...', len(ASSET_MAME_T_LIST)) + utils_file_cache_clear() + for i, asset_tuple in enumerate(ASSET_MAME_T_LIST): + pDialog.updateProgressInc() + asset_dir = asset_tuple[1] + full_asset_dir_FN = Asset_path_FN.pjoin(asset_dir) + asset_dir_str = full_asset_dir_FN.getPath() + asset_dirs[i] = asset_dir_str + utils_file_cache_add_dir(asset_dir_str) + pDialog.endProgress() + + # --- First pass: search for on-disk assets --- + ondisk_assets_dic = {} + pDialog.startProgress('Scanning MAME assets/artwork (first pass)...', len(renderdb_dic)) + for m_name in sorted(renderdb_dic): + pDialog.updateProgressInc() + machine_assets = db_new_MAME_asset() + for idx, asset_tuple in enumerate(ASSET_MAME_T_LIST): + asset_key = asset_tuple[0] + asset_dir = asset_tuple[1] + if asset_key == 'artwork': + asset_FN = utils_file_cache_search(asset_dirs[idx], m_name, ASSET_ARTWORK_EXTS) + elif asset_key == 'manual': + asset_FN = utils_file_cache_search(asset_dirs[idx], m_name, ASSET_MANUAL_EXTS) + elif asset_key == 'trailer': + asset_FN = utils_file_cache_search(asset_dirs[idx], m_name, ASSET_TRAILER_EXTS) + else: + asset_FN = utils_file_cache_search(asset_dirs[idx], m_name, ASSET_IMAGE_EXTS) + # Low level debug. + # if m_name == '005': + # log_debug('asset_key "{}"'.format(asset_key)) + # log_debug('asset_dir "{}"'.format(asset_dir)) + # log_debug('asset_dirs[idx] "{}"'.format(asset_dirs[idx])) + # log_debug('asset_FN "{}"'.format(asset_FN)) + machine_assets[asset_key] = asset_FN.getOriginalPath() if asset_FN else '' + ondisk_assets_dic[m_name] = machine_assets + pDialog.endProgress() + + # --- Second pass: substitute artwork --- + have_count_list = [0] * len(ASSET_MAME_T_LIST) + alternate_count_list = [0] * len(ASSET_MAME_T_LIST) + pDialog.startProgress('Scanning MAME assets/artwork (second pass)...', len(renderdb_dic)) + for m_name in sorted(renderdb_dic): + pDialog.updateProgressInc() + asset_row = ['---'] * len(ASSET_MAME_T_LIST) + for idx, asset_tuple in enumerate(ASSET_MAME_T_LIST): + asset_key = asset_tuple[0] + asset_dir = asset_tuple[1] + # Reset asset + assetdb_dic[m_name][asset_key] = '' + # If artwork exists on disk set it on database + if ondisk_assets_dic[m_name][asset_key]: + assetdb_dic[m_name][asset_key] = ondisk_assets_dic[m_name][asset_key] + have_count_list[idx] += 1 + asset_row[idx] = 'YES' + # If artwork does not exist on disk ... + else: + # if machine is a parent search in the clone list + if m_name in main_pclone_dic: + for clone_key in main_pclone_dic[m_name]: + if ondisk_assets_dic[clone_key][asset_key]: + assetdb_dic[m_name][asset_key] = ondisk_assets_dic[clone_key][asset_key] + have_count_list[idx] += 1 + alternate_count_list[idx] += 1 + asset_row[idx] = 'CLO' + break + # if machine is a clone search in the parent first, then search in the clones + else: + # Search parent + parent_name = renderdb_dic[m_name]['cloneof'] + if ondisk_assets_dic[parent_name][asset_key]: + assetdb_dic[m_name][asset_key] = ondisk_assets_dic[parent_name][asset_key] + have_count_list[idx] += 1 + alternate_count_list[idx] += 1 + asset_row[idx] = 'PAR' + # Search clones + else: + for clone_key in main_pclone_dic[parent_name]: + if clone_key == m_name: continue + if ondisk_assets_dic[clone_key][asset_key]: + assetdb_dic[m_name][asset_key] = ondisk_assets_dic[clone_key][asset_key] + have_count_list[idx] += 1 + alternate_count_list[idx] += 1 + asset_row[idx] = 'CLX' + break + table_row = [m_name] + asset_row + table_str.append(table_row) + pDialog.endProgress() + + # --- Asset statistics and report --- + total_machines = len(renderdb_dic) + # This must match the order of ASSET_MAME_T_LIST defined in disk_IO.py + box3D = (have_count_list[0], total_machines - have_count_list[0], alternate_count_list[0]) + Artp = (have_count_list[1], total_machines - have_count_list[1], alternate_count_list[1]) + Art = (have_count_list[2], total_machines - have_count_list[2], alternate_count_list[2]) + Cab = (have_count_list[3], total_machines - have_count_list[3], alternate_count_list[3]) + Clr = (have_count_list[4], total_machines - have_count_list[4], alternate_count_list[4]) + CPan = (have_count_list[5], total_machines - have_count_list[5], alternate_count_list[5]) + Fan = (have_count_list[6], total_machines - have_count_list[6], alternate_count_list[6]) + Fly = (have_count_list[7], total_machines - have_count_list[7], alternate_count_list[7]) + Man = (have_count_list[8], total_machines - have_count_list[8], alternate_count_list[8]) + Mar = (have_count_list[9], total_machines - have_count_list[9], alternate_count_list[9]) + PCB = (have_count_list[10], total_machines - have_count_list[10], alternate_count_list[10]) + Snap = (have_count_list[11], total_machines - have_count_list[11], alternate_count_list[11]) + Tit = (have_count_list[12], total_machines - have_count_list[12], alternate_count_list[12]) + Tra = (have_count_list[13], total_machines - have_count_list[13], alternate_count_list[13]) + pDialog.startProgress('Creating MAME asset report...') + report_slist = [] + report_slist.append('*** Advanced MAME Launcher MAME machines asset scanner report ***') + report_slist.append('Total MAME machines {}'.format(total_machines)) + report_slist.append('Have 3D Boxes {:5d} (Missing {:5d}, Alternate {:5d})'.format(*box3D)) + report_slist.append('Have Artpreview {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Artp)) + report_slist.append('Have Artwork {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Art)) + report_slist.append('Have Cabinets {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Cab)) + report_slist.append('Have Clearlogos {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Clr)) + report_slist.append('Have CPanels {:5d} (Missing {:5d}, Alternate {:5d})'.format(*CPan)) + report_slist.append('Have Fanarts {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Fan)) + report_slist.append('Have Flyers {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Fly)) + report_slist.append('Have Manuals {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Man)) + report_slist.append('Have Marquees {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Mar)) + report_slist.append('Have PCBs {:5d} (Missing {:5d}, Alternate {:5d})'.format(*PCB)) + report_slist.append('Have Snaps {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Snap)) + report_slist.append('Have Titles {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Tit)) + report_slist.append('Have Trailers {:5d} (Missing {:5d}, Alternate {:5d})'.format(*Tra)) + report_slist.append('') + table_str_list = text_render_table(table_str) + report_slist.extend(table_str_list) + log_info('Writing MAME asset report file "{}"'.format(cfg.REPORT_MAME_ASSETS_PATH.getPath())) + utils_write_slist_to_file(cfg.REPORT_MAME_ASSETS_PATH.getPath(), report_slist) + pDialog.endProgress() + + # Update control_dic by assigment (will be saved in caller) + db_safe_edit(control_dic, 'assets_num_MAME_machines', total_machines) + db_safe_edit(control_dic, 'assets_3dbox_have', box3D[0]) + db_safe_edit(control_dic, 'assets_3dbox_missing', box3D[1]) + db_safe_edit(control_dic, 'assets_3dbox_alternate', box3D[2]) + db_safe_edit(control_dic, 'assets_artpreview_have', Artp[0]) + db_safe_edit(control_dic, 'assets_artpreview_missing', Artp[1]) + db_safe_edit(control_dic, 'assets_artpreview_alternate', Artp[2]) + db_safe_edit(control_dic, 'assets_artwork_have', Art[0]) + db_safe_edit(control_dic, 'assets_artwork_missing', Art[1]) + db_safe_edit(control_dic, 'assets_artwork_alternate', Art[2]) + db_safe_edit(control_dic, 'assets_cabinets_have', Cab[0]) + db_safe_edit(control_dic, 'assets_cabinets_missing', Cab[1]) + db_safe_edit(control_dic, 'assets_cabinets_alternate', Cab[2]) + db_safe_edit(control_dic, 'assets_clearlogos_have', Clr[0]) + db_safe_edit(control_dic, 'assets_clearlogos_missing', Clr[1]) + db_safe_edit(control_dic, 'assets_clearlogos_alternate', Clr[2]) + db_safe_edit(control_dic, 'assets_cpanels_have', CPan[0]) + db_safe_edit(control_dic, 'assets_cpanels_missing', CPan[1]) + db_safe_edit(control_dic, 'assets_cpanels_alternate', CPan[2]) + db_safe_edit(control_dic, 'assets_fanarts_have', Fan[0]) + db_safe_edit(control_dic, 'assets_fanarts_missing', Fan[1]) + db_safe_edit(control_dic, 'assets_fanarts_alternate', Fan[2]) + db_safe_edit(control_dic, 'assets_flyers_have', Fly[0]) + db_safe_edit(control_dic, 'assets_flyers_missing', Fly[1]) + db_safe_edit(control_dic, 'assets_flyers_alternate', Fly[2]) + db_safe_edit(control_dic, 'assets_manuals_have', Man[0]) + db_safe_edit(control_dic, 'assets_manuals_missing', Man[1]) + db_safe_edit(control_dic, 'assets_manuals_alternate', Man[2]) + db_safe_edit(control_dic, 'assets_marquees_have', Mar[0]) + db_safe_edit(control_dic, 'assets_marquees_missing', Mar[1]) + db_safe_edit(control_dic, 'assets_marquees_alternate', Mar[2]) + db_safe_edit(control_dic, 'assets_PCBs_have', PCB[0]) + db_safe_edit(control_dic, 'assets_PCBs_missing', PCB[1]) + db_safe_edit(control_dic, 'assets_PCBs_alternate', PCB[2]) + db_safe_edit(control_dic, 'assets_snaps_have', Snap[0]) + db_safe_edit(control_dic, 'assets_snaps_missing', Snap[1]) + db_safe_edit(control_dic, 'assets_snaps_alternate', Snap[2]) + db_safe_edit(control_dic, 'assets_titles_have', Tit[0]) + db_safe_edit(control_dic, 'assets_titles_missing', Tit[1]) + db_safe_edit(control_dic, 'assets_titles_alternate', Tit[2]) + db_safe_edit(control_dic, 'assets_trailers_have', Tra[0]) + db_safe_edit(control_dic, 'assets_trailers_missing', Tra[1]) + db_safe_edit(control_dic, 'assets_trailers_alternate', Tra[2]) + db_safe_edit(control_dic, 't_MAME_assets_scan', time.time()) + + # --- Save databases --- + db_files = [ + [control_dic, 'Control dictionary', cfg.MAIN_CONTROL_PATH.getPath()], + [assetdb_dic, 'MAME machine assets', cfg.ASSET_DB_PATH.getPath()], + ] + db_save_files(db_files) + +# ------------------------------------------------------------------------------------------------- +# +# Checks for errors before scanning for SL ROMs. +# Display a Kodi dialog if an error is found. +# Returns a dictionary of settings: +# options_dic['abort'] is always present. +# options_dic['scan_SL_CHDs'] scanning of CHDs is optional. +# +def mame_check_before_scan_SL_ROMs(cfg, st_dic, options_dic, control_dic): + kodi_reset_status(st_dic) + + # Abort if SL are globally disabled. + if not cfg.settings['global_enable_SL']: + kodi_set_error_status(st_dic, 'Software Lists globally disabled. SL ROM scanning aborted.') + return + + # Abort if SL hash path not configured. + if not cfg.settings['SL_hash_path']: + kodi_set_error_status(st_dic, 'Software Lists hash path not set. SL ROM scanning aborted.') + return + + # Abort if SL ROM dir not configured. + if not cfg.settings['SL_rom_path']: + kodi_set_error_status(st_dic, 'Software Lists ROM path not set. SL ROM scanning aborted.') + return + + # SL CHDs scanning is optional + if cfg.settings['SL_chd_path']: + SL_CHD_path_FN = FileName(cfg.settings['SL_chd_path']) + if not SL_CHD_path_FN.isdir(): + kodi_dialog_OK('SL CHD directory does not exist. SL CHD scanning disabled.') + options_dic['scan_SL_CHDs'] = False + else: + options_dic['scan_SL_CHDs'] = True + else: + kodi_dialog_OK('SL CHD directory not configured. SL CHD scanning disabled.') + options_dic['scan_SL_CHDs'] = False + +# Saves SL JSON databases, MAIN_CONTROL_PATH. +def mame_scan_SL_ROMs(cfg, st_dic, options_dic, SL_dic): + log_info('mame_scan_SL_ROMs() Starting...') + control_dic = SL_dic['control_dic'] + SL_index_dic = SL_dic['SL_index'] + + # Paths have been verified at this point + SL_hash_dir_FN = cfg.SL_DB_DIR + log_info('mame_scan_SL_ROMs() SL hash dir OP {}'.format(SL_hash_dir_FN.getOriginalPath())) + log_info('mame_scan_SL_ROMs() SL hash dir P {}'.format(SL_hash_dir_FN.getPath())) + + SL_ROM_dir_FN = FileName(cfg.settings['SL_rom_path']) + log_info('mame_scan_SL_ROMs() SL ROM dir OP {}'.format(SL_ROM_dir_FN.getOriginalPath())) + log_info('mame_scan_SL_ROMs() SL ROM dir P {}'.format(SL_ROM_dir_FN.getPath())) + + if options_dic['scan_SL_CHDs']: + SL_CHD_path_FN = FileName(cfg.settings['SL_chd_path']) + log_info('mame_scan_SL_ROMs() SL CHD dir OP {}'.format(SL_CHD_path_FN.getOriginalPath())) + log_info('mame_scan_SL_ROMs() SL CHD dir P {}'.format(SL_CHD_path_FN.getPath())) + else: + SL_CHD_path_FN = FileName('') + log_info('Scan of SL CHDs disabled.') + + # --- Add files to cache --- + SL_ROM_path_str = SL_ROM_dir_FN.getPath() + SL_CHD_path_str = SL_CHD_path_FN.getPath() + pDialog = KodiProgressDialog() + d_text = 'Listing Sofware Lists ROM ZIPs and CHDs...' + pDialog.startProgress('{}\n{}'.format(d_text, 'Listing SL ROM ZIP path'), 2) + utils_file_cache_clear() + utils_file_cache_add_dir(SL_ROM_path_str, verbose = True) + pDialog.updateProgress(1, '{}\n{}'.format(d_text, 'Listing SL CHD path')) + utils_file_cache_add_dir(SL_CHD_path_str, verbose = True) + pDialog.endProgress() + + # --- SL ROM ZIP archives and CHDs --- + # Traverse the Software Lists, check if ROMs ZIPs and CHDs exists for every SL item, + # update and save database. + SL_ROMs_have = 0 + SL_ROMs_missing = 0 + SL_ROMs_total = 0 + SL_CHDs_have = 0 + SL_CHDs_missing = 0 + SL_CHDs_total = 0 + r_all_list = [] + r_have_list = [] + r_miss_list = [] + d_text = 'Scanning Sofware Lists ROM ZIPs and CHDs...' + pDialog.startProgress(d_text, len(SL_index_dic)) + for SL_name in sorted(SL_index_dic): + pDialog.updateProgressInc('{}\nSoftware List [COLOR orange]{}[/COLOR]'.format(d_text, SL_name)) + + # Load SL databases + SL_DB_FN = SL_hash_dir_FN.pjoin(SL_name + '_items.json') + SL_SOFT_ARCHIVES_DB_FN = SL_hash_dir_FN.pjoin(SL_name + '_ROM_archives.json') + sl_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + soft_archives = utils_load_JSON_file(SL_SOFT_ARCHIVES_DB_FN.getPath(), verbose = False) + + # Scan + for rom_key in sorted(sl_roms): + m_have_str_list = [] + m_miss_str_list = [] + rom = sl_roms[rom_key] + + # --- ROMs --- + rom_list = soft_archives[rom_key]['ROMs'] + if rom_list: + have_rom_list = [False] * len(rom_list) + for i, rom_file in enumerate(rom_list): + SL_ROMs_total += 1 + SL_ROM_FN = utils_file_cache_search(SL_ROM_path_str, rom_file, SL_ROM_EXTS) + if SL_ROM_FN: + have_rom_list[i] = True + m_have_str_list.append('HAVE ROM {}'.format(rom_file)) + else: + m_miss_str_list.append('MISS ROM {}'.format(rom_file)) + if all(have_rom_list): + rom['status_ROM'] = 'R' + SL_ROMs_have += 1 + else: + rom['status_ROM'] = 'r' + SL_ROMs_missing += 1 + else: + rom['status_ROM'] = '-' + + # --- Disks --- + chd_list = soft_archives[rom_key]['CHDs'] + if chd_list: + if options_dic['scan_SL_CHDs']: + SL_CHDs_total += 1 + has_chd_list = [False] * len(chd_list) + for idx, chd_file in enumerate(chd_list): + SL_CHD_FN = utils_file_cache_search(SL_CHD_path_str, chd_file, SL_CHD_EXTS) + # CHD_path = SL_CHD_path_str + '/' + chd_file + if SL_CHD_FN: + has_chd_list[idx] = True + m_have_str_list.append('HAVE CHD {}'.format(chd_file)) + else: + m_miss_str_list.append('MISS CHD {}'.format(chd_file)) + if all(has_chd_list): + rom['status_CHD'] = 'C' + SL_CHDs_have += 1 + else: + rom['status_CHD'] = 'c' + SL_CHDs_missing += 1 + else: + rom['status_CHD'] = 'c' + SL_CHDs_missing += 1 + else: + rom['status_CHD'] = '-' + + # --- Build report --- + description = sl_roms[rom_key]['description'] + clone_name = sl_roms[rom_key]['cloneof'] + r_all_list.append('SL {} item {} "{}"'.format(SL_name, rom_key, description)) + if clone_name: + clone_description = sl_roms[clone_name]['description'] + r_all_list.append('cloneof {} "{}"'.format(clone_name, clone_description)) + if m_have_str_list: + r_all_list.extend(m_have_str_list) + if m_miss_str_list: + r_all_list.extend(m_miss_str_list) + r_all_list.append('') + + if m_have_str_list: + r_have_list.append('SL {} item {} "{}"'.format(SL_name, rom_key, description)) + if clone_name: + r_have_list.append('cloneof {} "{}"'.format(clone_name, clone_description)) + r_have_list.extend(m_have_str_list) + if m_miss_str_list: r_have_list.extend(m_miss_str_list) + r_have_list.append('') + + if m_miss_str_list: + r_miss_list.append('SL {} item {} "{}"'.format(SL_name, rom_key, description)) + if clone_name: + r_miss_list.append('cloneof {} "{}"'.format(clone_name, clone_description)) + r_miss_list.extend(m_miss_str_list) + if m_have_str_list: r_miss_list.extend(m_have_str_list) + r_miss_list.append('') + # Save SL database to update flags and update progress. + utils_write_JSON_file(SL_DB_FN.getPath(), sl_roms, verbose = False) + pDialog.endProgress() + + # Write SL scanner reports + reports_total = 3 + pDialog.startProgress('Writing scanner reports...', reports_total) + log_info('Writing SL ROM ZIPs/CHDs FULL report') + log_info('Report file "{}"'.format(cfg.REPORT_SL_SCAN_MACHINE_ARCH_FULL_PATH.getPath())) + sl = [ + '*** Advanced MAME Launcher Software Lists scanner report ***', + 'This report shows all the scanned SL items', + '', + ] + sl.extend(r_all_list) + utils_write_slist_to_file(cfg.REPORT_SL_SCAN_MACHINE_ARCH_FULL_PATH.getPath(), sl) + + pDialog.updateProgressInc() + log_info('Writing SL ROM ZIPs and/or CHDs HAVE report') + log_info('Report file "{}"'.format(cfg.REPORT_SL_SCAN_MACHINE_ARCH_HAVE_PATH.getPath())) + sl = [ + '*** Advanced MAME Launcher Software Lists scanner report ***', + 'This reports shows the SL items with ROM ZIPs and/or CHDs with HAVE status', + '', + ] + if r_have_list: + sl.extend(r_have_list) + else: + sl.append('You do not have any ROM ZIP or CHD files!') + utils_write_slist_to_file(cfg.REPORT_SL_SCAN_MACHINE_ARCH_HAVE_PATH.getPath(), sl) + + pDialog.updateProgressInc() + log_info('Writing SL ROM ZIPs/CHDs MISS report') + log_info('Report file "{}"'.format(cfg.REPORT_SL_SCAN_MACHINE_ARCH_MISS_PATH.getPath())) + sl = [ + '*** Advanced MAME Launcher Software Lists scanner report ***', + 'This reports shows the SL items with ROM ZIPs and/or CHDs with MISSING status', + '', + ] + if r_miss_list: + sl.extend(r_miss_list) + else: + sl.append('Congratulations! No missing SL ROM ZIP or CHD files.') + utils_write_slist_to_file(cfg.REPORT_SL_SCAN_MACHINE_ARCH_MISS_PATH.getPath(), sl) + pDialog.endProgress() + + # Update statistics, timestamp and save control_dic. + db_safe_edit(control_dic, 'scan_SL_archives_ROM_total', SL_ROMs_total) + db_safe_edit(control_dic, 'scan_SL_archives_ROM_have', SL_ROMs_have) + db_safe_edit(control_dic, 'scan_SL_archives_ROM_missing', SL_ROMs_missing) + db_safe_edit(control_dic, 'scan_SL_archives_CHD_total', SL_CHDs_total) + db_safe_edit(control_dic, 'scan_SL_archives_CHD_have', SL_CHDs_have) + db_safe_edit(control_dic, 'scan_SL_archives_CHD_missing', SL_CHDs_missing) + db_safe_edit(control_dic, 't_SL_ROMs_scan', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) + +# +# Checks for errors before scanning for SL assets. +# Display a Kodi dialog if an error is found and returns True if scanning must be aborted. +# Returns False if no errors. +# +def mame_check_before_scan_SL_assets(cfg, st_dic, control_dic): + kodi_reset_status(st_dic) + + # Abort if SL are globally disabled. + if not cfg.settings['global_enable_SL']: + kodi_set_error_status(st_dic, 'Software Lists globally disabled. SL ROM scanning aborted.') + return + + # Get assets directory. Abort if not configured/found. + if not cfg.settings['assets_path']: + kodi_set_error_status(st_dic, 'Asset directory not configured. Aborting.') + return + Asset_path_FN = FileName(cfg.settings['assets_path']) + if not Asset_path_FN.isdir(): + kodi_set_error_status(st_dic, 'Asset directory does not exist. Aborting.') + return + +def mame_scan_SL_assets(cfg, st_dic, SL_dic): + log_debug('mame_scan_SL_assets() Starting...') + control_dic = SL_dic['control_dic'] + SL_index_dic = SL_dic['SL_index'] + SL_pclone_dic = SL_dic['SL_PClone_dic'] + + # At this point assets_path is configured and the directory exists. + Asset_path_FN = FileName(cfg.settings['assets_path']) + log_info('mame_scan_SL_assets() SL asset path {}'.format(Asset_path_FN.getPath())) + + # --- Traverse Software List, check if ROM exists, update and save database --- + table_str = [] + table_str.append(['left', 'left', 'left', 'left', 'left', 'left', 'left', 'left', 'left']) + table_str.append(['Soft', 'Name', '3DB', 'Tit', 'Snap', 'Bft', 'Fan', 'Tra', 'Man']) + have_count_list = [0] * len(ASSET_SL_T_LIST) + alternate_count_list = [0] * len(ASSET_SL_T_LIST) + SL_item_count = 0 + # DEBUG code + # SL_index_dic = { + # "32x" : + # { "display_name" : "Sega 32X cartridges", "num_with_CHDs" : 0, "num_with_ROMs" : 203, "rom_DB_noext" : "32x" } + # } + d_text = 'Scanning Sofware Lists assets/artwork...' + pDialog = KodiProgressDialog() + pDialog.startProgress(d_text, len(SL_index_dic)) + for SL_name in sorted(SL_index_dic): + pDialog.updateProgressInc('{}\nSoftware List [COLOR orange]{}[/COLOR]'.format(d_text, SL_name)) + + # --- Load SL databases --- + file_name = SL_index_dic[SL_name]['rom_DB_noext'] + '_items.json' + SL_DB_FN = cfg.SL_DB_DIR.pjoin(file_name) + SL_roms = utils_load_JSON_file(SL_DB_FN.getPath(), verbose = False) + + # --- Cache files --- + utils_file_cache_clear(verbose = False) + num_assets = len(ASSET_SL_T_LIST) + asset_dirs = [''] * num_assets + for i, asset_tuple in enumerate(ASSET_SL_T_LIST): + asset_dir = asset_tuple[1] + full_asset_dir_FN = Asset_path_FN.pjoin(asset_dir).pjoin(SL_name) + asset_dir_str = full_asset_dir_FN.getPath() + asset_dirs[i] = asset_dir_str + utils_file_cache_add_dir(asset_dir_str, verbose = False) + + # --- First pass: scan for on-disk assets --- + assets_file_name = SL_index_dic[SL_name]['rom_DB_noext'] + '_assets.json' + SL_asset_DB_FN = cfg.SL_DB_DIR.pjoin(assets_file_name) + # log_info('Assets JSON "{}"'.format(SL_asset_DB_FN.getPath())) + ondisk_assets_dic = {} + for rom_key in sorted(SL_roms): + SL_assets = db_new_SL_asset() + for idx, asset_tuple in enumerate(ASSET_SL_T_LIST): + asset_key = asset_tuple[0] + asset_dir = asset_tuple[1] + full_asset_dir_FN = Asset_path_FN.pjoin(asset_dir).pjoin(SL_name) + if asset_key == 'manual': + asset_FN = utils_file_cache_search(asset_dirs[idx], rom_key, ASSET_MANUAL_EXTS) + elif asset_key == 'trailer': + asset_FN = utils_file_cache_search(asset_dirs[idx], rom_key, ASSET_TRAILER_EXTS) + else: + asset_FN = utils_file_cache_search(asset_dirs[idx], rom_key, ASSET_IMAGE_EXTS) + # log_info('Testing P "{}"'.format(asset_FN.getPath())) + SL_assets[asset_key] = asset_FN.getOriginalPath() if asset_FN else '' + ondisk_assets_dic[rom_key] = SL_assets + + # --- Second pass: substitute artwork --- + main_pclone_dic = SL_pclone_dic[SL_name] + SL_assets_dic = {} + for rom_key in sorted(SL_roms): + SL_item_count += 1 + SL_assets_dic[rom_key] = db_new_SL_asset() + asset_row = ['---'] * len(ASSET_SL_T_LIST) + for idx, asset_tuple in enumerate(ASSET_SL_T_LIST): + asset_key = asset_tuple[0] + asset_dir = asset_tuple[1] + # >> Reset asset + SL_assets_dic[rom_key][asset_key] = '' + # TODO Refactor this to reduce indentation. + # >> If artwork exists on disk set it on database + if ondisk_assets_dic[rom_key][asset_key]: + SL_assets_dic[rom_key][asset_key] = ondisk_assets_dic[rom_key][asset_key] + have_count_list[idx] += 1 + asset_row[idx] = 'YES' + # >> If artwork does not exist on disk ... + else: + # >> if machine is a parent search in the clone list + if rom_key in main_pclone_dic: + for clone_key in main_pclone_dic[rom_key]: + if ondisk_assets_dic[clone_key][asset_key]: + SL_assets_dic[rom_key][asset_key] = ondisk_assets_dic[clone_key][asset_key] + have_count_list[idx] += 1 + alternate_count_list[idx] += 1 + asset_row[idx] = 'CLO' + break + # >> if machine is a clone search in the parent first, then search in the clones + else: + # >> Search parent + parent_name = SL_roms[rom_key]['cloneof'] + if ondisk_assets_dic[parent_name][asset_key]: + SL_assets_dic[rom_key][asset_key] = ondisk_assets_dic[parent_name][asset_key] + have_count_list[idx] += 1 + alternate_count_list[idx] += 1 + asset_row[idx] = 'PAR' + # >> Search clones + else: + for clone_key in main_pclone_dic[parent_name]: + if clone_key == rom_key: continue + if ondisk_assets_dic[clone_key][asset_key]: + SL_assets_dic[rom_key][asset_key] = ondisk_assets_dic[clone_key][asset_key] + have_count_list[idx] += 1 + alternate_count_list[idx] += 1 + asset_row[idx] = 'CLX' + break + table_row = [SL_name, rom_key] + asset_row + table_str.append(table_row) + # --- Write SL asset JSON --- + utils_write_JSON_file(SL_asset_DB_FN.getPath(), SL_assets_dic, verbose = False) + pDialog.endProgress() + + # Asset statistics and report. + # This must match the order of ASSET_SL_T_LIST defined in disk_IO.py + _3db = (have_count_list[0], SL_item_count - have_count_list[0], alternate_count_list[0]) + Tit = (have_count_list[1], SL_item_count - have_count_list[1], alternate_count_list[1]) + Snap = (have_count_list[2], SL_item_count - have_count_list[2], alternate_count_list[2]) + Boxf = (have_count_list[3], SL_item_count - have_count_list[3], alternate_count_list[3]) + Fan = (have_count_list[4], SL_item_count - have_count_list[4], alternate_count_list[4]) + Tra = (have_count_list[5], SL_item_count - have_count_list[5], alternate_count_list[5]) + Man = (have_count_list[6], SL_item_count - have_count_list[6], alternate_count_list[6]) + pDialog.startProgress('Creating SL asset report...') + report_slist = [] + report_slist.append('*** Advanced MAME Launcher Software List asset scanner report ***') + report_slist.append('Total SL items {}'.format(SL_item_count)) + report_slist.append('Have 3D Boxes {:6d} (Missing {:6d}, Alternate {:6d})'.format(*_3db)) + report_slist.append('Have Titles {:6d} (Missing {:6d}, Alternate {:6d})'.format(*Tit)) + report_slist.append('Have Snaps {:6d} (Missing {:6d}, Alternate {:6d})'.format(*Snap)) + report_slist.append('Have Boxfronts {:6d} (Missing {:6d}, Alternate {:6d})'.format(*Boxf)) + report_slist.append('Have Fanarts {:6d} (Missing {:6d}, Alternate {:6d})'.format(*Fan)) + report_slist.append('Have Trailers {:6d} (Missing {:6d}, Alternate {:6d})'.format(*Tra)) + report_slist.append('Have Manuals {:6d} (Missing {:6d}, Alternate {:6d})'.format(*Man)) + report_slist.append('') + table_str_list = text_render_table(table_str) + report_slist.extend(table_str_list) + log_info('Writing SL asset report file "{}"'.format(cfg.REPORT_SL_ASSETS_PATH.getPath())) + utils_write_slist_to_file(cfg.REPORT_SL_ASSETS_PATH.getPath(), report_slist) + pDialog.endProgress() + + # Update control_dic by assigment (will be saved in caller) and save JSON. + db_safe_edit(control_dic, 'assets_SL_num_items', SL_item_count) + db_safe_edit(control_dic, 'assets_SL_3dbox_have', _3db[0]) + db_safe_edit(control_dic, 'assets_SL_3dbox_missing', _3db[1]) + db_safe_edit(control_dic, 'assets_SL_3dbox_alternate', _3db[2]) + db_safe_edit(control_dic, 'assets_SL_titles_have', Tit[0]) + db_safe_edit(control_dic, 'assets_SL_titles_missing', Tit[1]) + db_safe_edit(control_dic, 'assets_SL_titles_alternate', Tit[2]) + db_safe_edit(control_dic, 'assets_SL_snaps_have', Snap[0]) + db_safe_edit(control_dic, 'assets_SL_snaps_missing', Snap[1]) + db_safe_edit(control_dic, 'assets_SL_snaps_alternate', Snap[2]) + db_safe_edit(control_dic, 'assets_SL_boxfronts_have', Boxf[0]) + db_safe_edit(control_dic, 'assets_SL_boxfronts_missing', Boxf[1]) + db_safe_edit(control_dic, 'assets_SL_boxfronts_alternate', Boxf[2]) + db_safe_edit(control_dic, 'assets_SL_fanarts_have', Fan[0]) + db_safe_edit(control_dic, 'assets_SL_fanarts_missing', Fan[1]) + db_safe_edit(control_dic, 'assets_SL_fanarts_alternate', Fan[2]) + db_safe_edit(control_dic, 'assets_SL_trailers_have', Tra[0]) + db_safe_edit(control_dic, 'assets_SL_trailers_missing', Tra[1]) + db_safe_edit(control_dic, 'assets_SL_trailers_alternate', Tra[2]) + db_safe_edit(control_dic, 'assets_SL_manuals_have', Man[0]) + db_safe_edit(control_dic, 'assets_SL_manuals_missing', Man[1]) + db_safe_edit(control_dic, 'assets_SL_manuals_alternate', Man[2]) + db_safe_edit(control_dic, 't_SL_assets_scan', time.time()) + utils_write_JSON_file(cfg.MAIN_CONTROL_PATH.getPath(), control_dic) diff --git a/plugin.program.AML/resources/mame_misc.py b/plugin.program.AML/resources/mame_misc.py new file mode 100644 index 0000000000..ea4c4641ac --- /dev/null +++ b/plugin.program.AML/resources/mame_misc.py @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher miscellaneous MAME functions. +# +# Functions in this module only depend on the Python standard library. +# This module can be loaded anywhere without creating circular dependencies. +# Optionally this module can include utils.py to use the log_*() functions. + +# --- AEL packages --- + +# --- Python standard library --- +import hashlib + +# ------------------------------------------------------------------------------------------------- +# Functions +# ------------------------------------------------------------------------------------------------- +# Builds a string separated by a | character. Replaces | occurrences with _ +# The string can be separated with text_type.split('|') +def misc_build_db_str_3(str1, str2, str3): + if str1.find('|') >= 0: str1 = str1.replace('|', '_') + if str2.find('|') >= 0: str2 = str2.replace('|', '_') + if str3.find('|') >= 0: str3 = str3.replace('|', '_') + + return '{}|{}|{}'.format(str1, str2, str3) + +# Used in mame_build_MAME_plots() +def misc_get_mame_control_str(control_type_list): + control_set = set() + improved_c_type_list = misc_improve_mame_control_type_list(control_type_list) + for control in improved_c_type_list: control_set.add(control) + control_str = ', '.join(list(sorted(control_set))) + + return control_str + +# Used here in misc_get_mame_screen_str() +def misc_get_mame_screen_rotation_str(display_rotate): + if display_rotate == '0' or display_rotate == '180': + screen_str = 'horizontal' + elif display_rotate == '90' or display_rotate == '270': + screen_str = 'vertical' + else: + raise TypeError + + return screen_str + +# Used in mame_build_MAME_plots() +def misc_get_mame_screen_str(machine_name, machine): + d_list = machine['display_type'] + if d_list: + if len(d_list) == 1: + rotation_str = misc_get_mame_screen_rotation_str(machine['display_rotate'][0]) + screen_str = 'One {} {} screen'.format(d_list[0], rotation_str) + elif len(d_list) == 2: + if d_list[0] == 'lcd' and d_list[1] == 'raster': + r_str_1 = misc_get_mame_screen_rotation_str(machine['display_rotate'][0]) + r_str_2 = misc_get_mame_screen_rotation_str(machine['display_rotate'][1]) + screen_str = 'One LCD {} screen and one raster {} screen'.format(r_str_1, r_str_2) + elif d_list[0] == 'raster' and d_list[1] == 'raster': + r_str = misc_get_mame_screen_rotation_str(machine['display_rotate'][0]) + screen_str = 'Two raster {} screens'.format(r_str) + elif d_list[0] == 'svg' and d_list[1] == 'svg': + r_str = misc_get_mame_screen_rotation_str(machine['display_rotate'][0]) + screen_str = 'Two SVG {} screens'.format(r_str) + elif d_list[0] == 'unknown' and d_list[1] == 'unknown': + screen_str = 'Two unknown screens' + else: + screen_str = 'Two unrecognised screens' + elif len(d_list) == 3: + if d_list[0] == 'raster' and d_list[1] == 'raster' and d_list[2] == 'raster': + r_str = misc_get_mame_screen_rotation_str(machine['display_rotate'][0]) + screen_str = 'Three raster {} screens'.format(r_str) + elif d_list[0] == 'raster' and d_list[1] == 'lcd' and d_list[2] == 'lcd': + screen_str = 'Three screens special case' + else: + screen_str = 'Three unrecognised screens' + elif len(d_list) == 4: + if d_list[0] == 'raster' and d_list[1] == 'raster' and d_list[2] == 'raster' and d_list[3] == 'raster': + r_str = misc_get_mame_screen_rotation_str(machine['display_rotate'][0]) + screen_str = 'Four raster {} screens'.format(r_str) + else: + screen_str = 'Four unrecognised screens' + elif len(d_list) == 5: + screen_str = 'Five unrecognised screens' + elif len(d_list) == 6: + screen_str = 'Six unrecognised screens' + else: + log_error('mame_get_screen_str() d_list = {}'.format(text_type(d_list))) + raise TypeError + else: + screen_str = 'No screen' + + return screen_str + +def misc_get_mame_display_type(display_str): + if display_str == 'lcd': display_name = 'LCD' + elif display_str == 'raster': display_name = 'Raster' + elif display_str == 'svg': display_name = 'SVG' + elif display_str == 'vector': display_name = 'Vector' + else: display_name = display_str + + return display_name + +def misc_get_mame_display_rotation(d_str): + if d_str == '0' or d_str == '180': + rotation_letter = 'Hor' + elif d_str == '90' or d_str == '270': + rotation_letter = 'Ver' + else: + raise TypeError('Wrong display rotate "{}"'.format(d_str)) + + return rotation_letter + +def misc_get_display_type_catalog_key(display_type_list, display_rotate_list): + if len(display_type_list) == 0: + catalog_key = '[ No display ]' + else: + display_list = [] + for dis_index in range(len(display_type_list)): + display_name = misc_get_mame_display_type(display_type_list[dis_index]) + display_rotation = misc_get_mame_display_rotation(display_rotate_list[dis_index]) + display_list.append('{} {}'.format(display_name, display_rotation)) + catalog_key = " / ".join(display_list) + + return catalog_key + +def misc_get_display_resolution_catalog_key(display_width, display_height): + if len(display_width) > 1 or len(display_height) > 1: + catalog_key = '{} displays'.format(len(display_width)) + elif len(display_width) == 0 and len(display_height) == 1: + catalog_key = 'Empty x {}'.format(display_height[0]) + elif len(display_width) == 1 and len(display_height) == 0: + catalog_key = '{} x Empty'.format(display_width[0]) + elif len(display_width) == 0 and len(display_height) == 0: + catalog_key = 'Empty x Empty' + else: + catalog_key = '{} x {}'.format(display_width[0], display_height[0]) + + return catalog_key + +# +# A) Capitalise every list item +# B) Substitute Only_buttons -> Only buttons +# +def misc_improve_mame_control_type_list(control_type_list): + out_list = [] + for control_str in control_type_list: + capital_str = control_str.title() + if capital_str == 'Only_Buttons': capital_str = 'Only Buttons' + out_list.append(capital_str) + + return out_list + +# +# A) Capitalise every list item +# +def misc_improve_mame_device_list(control_type_list): + out_list = [] + for control_str in control_type_list: out_list.append(control_str.title()) + + return out_list + +# +# A) Substitute well know display types with fancier names. +# +def misc_improve_mame_display_type_list(display_type_list): + out_list = [] + for dt in display_type_list: + if dt == 'lcd': out_list.append('LCD') + elif dt == 'raster': out_list.append('Raster') + elif dt == 'svg': out_list.append('SVG') + elif dt == 'vector': out_list.append('Vector') + else: out_list.append(dt) + + return out_list + +# +# See tools/test_compress_item_list.py for reference +# Input/Output examples: +# 1) ['dial'] -> ['dial'] +# 2) ['dial', 'dial'] -> ['2 x dial'] +# 3) ['dial', 'dial', 'joy'] -> ['2 x dial', 'joy'] +# 4) ['joy', 'dial', 'dial'] -> ['joy', '2 x dial'] +# +def misc_compress_mame_item_list(item_list): + reduced_list = [] + num_items = len(item_list) + if num_items == 0 or num_items == 1: return item_list + previous_item = item_list[0] + item_count = 1 + for i in range(1, num_items): + current_item = item_list[i] + # log_debug('{} | item_count {} | previous_item "{2:>8}" | current_item "{3:>8}"'.format( + # i, item_count, previous_item, current_item)) + if current_item == previous_item: + item_count += 1 + else: + if item_count == 1: reduced_list.append('{}'.format(previous_item)) + else: reduced_list.append('{} {}'.format(item_count, previous_item)) + item_count = 1 + previous_item = current_item + # >> Last elemnt of the list + if i == num_items - 1: + if current_item == previous_item: + if item_count == 1: reduced_list.append('{}'.format(current_item)) + else: reduced_list.append('{} {}'.format(item_count, current_item)) + else: + reduced_list.append('{}'.format(current_item)) + + return reduced_list + +# +# See tools/test_compress_item_list.py for reference +# Output is sorted alphabetically +# Input/Output examples: +# 1) ['dial'] -> ['dial'] +# 2) ['dial', 'dial'] -> ['dial'] +# 3) ['dial', 'dial', 'joy'] -> ['dial', 'joy'] +# 4) ['joy', 'dial', 'dial'] -> ['dial', 'joy'] +# +def misc_compress_mame_item_list_compact(item_list): + num_items = len(item_list) + if num_items == 0 or num_items == 1: return item_list + item_set = set(item_list) + reduced_list = list(item_set) + reduced_list_sorted = sorted(reduced_list) + + return reduced_list_sorted + +# ------------------------------------------------------------------------------------------------- +# Helper functions to build catalogs. +# ------------------------------------------------------------------------------------------------- +# Add clones to the all catalog dictionary catalog_all_dic. +# catalog_all_dic is modified by refence. +def mame_catalog_add_clones(parent_name, main_pclone_dic, machines_render, catalog_all_dic): + for clone_name in main_pclone_dic[parent_name]: + catalog_all_dic[clone_name] = machines_render[clone_name]['description'] + +# Do not store the number of categories in a catalog. If necessary, calculate it on the fly. +# I think Python len() on dictionaries is very fast. +# [August 2020] Why??? +def mame_cache_index_builder(cat_name, cache_index_dic, catalog_all, catalog_parents): + for cat_key in catalog_all: + key_str = '{} - {}'.format(cat_name, cat_key) + cache_index_dic[cat_name][cat_key] = { + 'num_parents' : len(catalog_parents[cat_key]), + 'num_machines' : len(catalog_all[cat_key]), + # Make sure this key is the same as db_render_cache_get_hash() + 'hash' : hashlib.md5(key_str.encode('utf-8')).hexdigest(), + } + +# Helper functions to get the catalog key. +def mame_catalog_key_Catver(parent_name, machines, machines_render): + return [ machines[parent_name]['catver'] ] + +def mame_catalog_key_Catlist(parent_name, machines, machines_render): + return [ machines[parent_name]['catlist'] ] + +def mame_catalog_key_Genre(parent_name, machines, machines_render): + return [ machines[parent_name]['genre'] ] + +def mame_catalog_key_Category(parent_name, machines, machines_render): + # Already a list. + return machines[parent_name]['category'] + +def mame_catalog_key_NPlayers(parent_name, machines, machines_render): + return [ machines[parent_name]['nplayers'] ] + +def mame_catalog_key_Bestgames(parent_name, machines, machines_render): + return [ machines[parent_name]['bestgames'] ] + +def mame_catalog_key_Series(parent_name, machines, machines_render): + # Already a list. + return machines[parent_name]['series'] + +def mame_catalog_key_Alltime(parent_name, machines, machines_render): + # log_debug('Machine {}, key {}'.format(parent_name, machines[parent_name]['alltime'])) + return [ machines[parent_name]['alltime'] ] + +def mame_catalog_key_Artwork(parent_name, machines, machines_render): + # Already a list. + return machines[parent_name]['artwork'] + +def mame_catalog_key_VerAdded(parent_name, machines, machines_render): + return [ machines[parent_name]['veradded'] ] + +def mame_catalog_key_Controls_Expanded(parent_name, machines, machines_render): + machine = machines[parent_name] + machine_render = machines_render[parent_name] + # Order alphabetically the list + # In MAME 2003 Plus machine['input'] may be an empty dictionary. + if machine['input'] and machine['input']['control_list']: + control_list = [ctrl_dic['type'] for ctrl_dic in machine['input']['control_list']] + pretty_control_type_list = misc_improve_mame_control_type_list(control_list) + sorted_control_type_list = sorted(pretty_control_type_list) + # Maybe a setting should be added for compact or non-compact control list. + # sorted_control_type_list = misc_compress_mame_item_list(sorted_control_type_list) + sorted_control_type_list = misc_compress_mame_item_list_compact(sorted_control_type_list) + catalog_key_list = [ " / ".join(sorted_control_type_list) ] + else: + catalog_key_list = [ '[ No controls ]' ] + + return catalog_key_list + +def mame_catalog_key_Controls_Compact(parent_name, machines, machines_render): + machine = machines[parent_name] + machine_render = machines_render[parent_name] + # Order alphabetically the list + # In MAME 2003 Plus machine['input'] may be an empty dictionary. + if machine['input'] and machine['input']['control_list']: + control_list = [ctrl_dic['type'] for ctrl_dic in machine['input']['control_list']] + pretty_control_type_list = misc_improve_mame_control_type_list(control_list) + sorted_control_type_list = sorted(pretty_control_type_list) + catalog_key_list = misc_compress_mame_item_list_compact(sorted_control_type_list) + else: + catalog_key_list = [ '[ No controls ]' ] + + return catalog_key_list + +def mame_catalog_key_Devices_Expanded(parent_name, machines, machines_render): + machine = machines[parent_name] + machine_render = machines_render[parent_name] + # Order alphabetically the list + device_list = [device['att_type'] for device in machine['devices']] + pretty_device_list = misc_improve_mame_device_list(device_list) + sorted_device_list = sorted(pretty_device_list) + # Maybe a setting should be added for compact or non-compact control list + # sorted_device_list = misc_compress_mame_item_list(sorted_device_list) + sorted_device_list = misc_compress_mame_item_list_compact(sorted_device_list) + catalog_key = " / ".join(sorted_device_list) + # Change category name for machines with no devices + if catalog_key == '': + catalog_key = '[ No devices ]' + + return [catalog_key] + +def mame_catalog_key_Devices_Compact(parent_name, machines, machines_render): + machine = machines[parent_name] + machine_render = machines_render[parent_name] + # Order alphabetically the list + device_list = [ device['att_type'] for device in machine['devices'] ] + pretty_device_list = misc_improve_mame_device_list(device_list) + sorted_device_list = sorted(pretty_device_list) + compressed_device_list = misc_compress_mame_item_list_compact(sorted_device_list) + if not compressed_device_list: + compressed_device_list = [ '[ No devices ]' ] + + return compressed_device_list + +def mame_catalog_key_Display_Type(parent_name, machines, machines_render): + # Compute the catalog_key. display_type and display_rotate main DB entries used. + catalog_key = misc_get_display_type_catalog_key( + machines[parent_name]['display_type'], machines[parent_name]['display_rotate']) + + return [catalog_key] + +def mame_catalog_key_Display_VSync(parent_name, machines, machines_render): + catalog_list = [] + # machine['display_refresh'] is a list. + # Change string '60.12346' to 1 decimal only to avoid having a log of keys in this catalog. + # An empty machine['display_refresh'] means no display. + if len(machines[parent_name]['display_refresh']) == 0: + catalog_list.append('No display') + else: + for display_str in machines[parent_name]['display_refresh']: + vsync = float(display_str) + catalog_list.append('{0:.1f} Hz'.format(vsync)) + + return catalog_list + +def mame_catalog_key_Display_Resolution(parent_name, machines, machines_render): + # Move code of this function here??? + catalog_key = misc_get_display_resolution_catalog_key( + machines[parent_name]['display_width'], machines[parent_name]['display_height']) + + return [catalog_key] + +def mame_catalog_key_CPU(parent_name, machines, machines_render): + # machine['chip_cpu_name'] is a list. + return machines[parent_name]['chip_cpu_name'] + +# Some drivers get a prettier name. +# def mame_catalog_key_Driver(parent_name, machines, machines_render): +# c_key = machines[parent_name]['sourcefile'] +# c_key = mame_driver_name_dic[c_key] if c_key in mame_driver_name_dic else c_key +# +# return [c_key] + +def mame_catalog_key_Manufacturer(parent_name, machines, machines_render): + return [machines_render[parent_name]['manufacturer']] + +# This is a special catalog, not easy to automatise. +# def mame_catalog_key_ShortName(parent_name, machines, machines_render): +# return [machines_render[parent_name]['year']] + +def mame_catalog_key_LongName(parent_name, machines, machines_render): + return [machines_render[parent_name]['description'][0]] + +# This is a special catalog, not easy to automatise. +# def mame_catalog_key_BySL(parent_name, machines, machines_render): +# return [machines_render[parent_name]['year']] + +def mame_catalog_key_Year(parent_name, machines, machines_render): + return [machines_render[parent_name]['year']] + +# Uses a "function pointer" to obtain the catalog_key. +# catalog_key is a list that has one element for most catalogs. +# In some catalogs (Controls_Compact) this list has sometimes more than one item, for example +# one parent machine may have more than one control. +def mame_build_catalog_helper(catalog_parents, catalog_all, + machines, machines_render, main_pclone_dic, catalog_key_function): + for parent_name in main_pclone_dic: + render = machines_render[parent_name] + if render['isDevice']: continue # Skip device machines in catalogs. + catalog_key_list = catalog_key_function(parent_name, machines, machines_render) + for catalog_key in catalog_key_list: + if catalog_key in catalog_parents: + catalog_parents[catalog_key][parent_name] = render['description'] + catalog_all[catalog_key][parent_name] = render['description'] + else: + catalog_parents[catalog_key] = { parent_name : render['description'] } + catalog_all[catalog_key] = { parent_name : render['description'] } + for clone_name in main_pclone_dic[parent_name]: + catalog_all[catalog_key][clone_name] = machines_render[clone_name]['description'] diff --git a/plugin.program.AML/resources/manuals.py b/plugin.program.AML/resources/manuals.py new file mode 100644 index 0000000000..884f8c0dee --- /dev/null +++ b/plugin.program.AML/resources/manuals.py @@ -0,0 +1,513 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced MAME Launcher PDF/CBZ/CBR manual image extraction. + +# --- Modules/packages in this plugin --- +from .constants import * +from .utils import * +from .db import * + +# --- Kodi stuff --- +import xbmcaddon + +# --- Load pdfrw module --- +import sys +addon_id = xbmcaddon.Addon().getAddonInfo('id') +sys.path.insert(0, FileName('special://home/addons').pjoin(addon_id).pjoin('pdfrw').getPath()) +from pdfrw import PdfReader +from pdfrw.objects.pdfarray import PdfArray +from pdfrw.objects.pdfname import BasePdfName + +# --- Python standard library --- +# NOTE String literals are needed in this file so unicode_literals cannot be defined. +# Is this because of the PDF library??? +import io +import pprint +import struct +import time +import types +import zlib +if ADDON_RUNNING_PYTHON_2: + import StringIO +try: + from PIL import Image + PYTHON_PIL_AVAILABLE = True +except: + PYTHON_PIL_AVAILABLE = False + +# ------------------------------------------------------------------------------------------------- +# pdfrw module code +# ------------------------------------------------------------------------------------------------- +""" +See https://stackoverflow.com/questions/2693820/extract-images-from-pdf-without-resampling-in-python/34116472#34116472 + +Links: + PDF format: http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf + CCITT Group 4: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.6-198811-I!!PDF-E&type=items + Extract images from pdf: http://stackoverflow.com/questions/2693820/extract-images-from-pdf-without-resampling-in-python + Extract images coded with CCITTFaxDecode in .net: http://stackoverflow.com/questions/2641770/extracting-image-from-pdf-with-ccittfaxdecode-filter + TIFF format and tags: http://www.awaresystems.be/imaging/tiff/faq.html +""" +def _tiff_header_for_CCITT(width, height, img_size, CCITT_group = 4): + tiff_header_struct = '<' + '2s' + 'H' + 'L' + 'H' + 'HHLL' * 8 + 'H' + + return struct.pack( + tiff_header_struct, + 'II', # Byte order indication: Little endian + 42, # Version number (always 42) + 8, # Offset to first IFD + 8, # Number of tags in IFD + 256, 4, 1, width, # ImageWidth, LONG, 1, width + 257, 4, 1, height, # ImageLength, LONG, 1, lenght + 258, 3, 1, 1, # BitsPerSample, SHORT, 1, 1 + 259, 3, 1, CCITT_group, # Compression, SHORT, 1, 4 = CCITT Group 4 fax encoding + 262, 3, 1, 0, # Threshholding, SHORT, 1, 0 = WhiteIsZero + 273, 4, 1, struct.calcsize(tiff_header_struct), # StripOffsets, LONG, 1, len of header + 278, 4, 1, height, # RowsPerStrip, LONG, 1, lenght + 279, 4, 1, img_size, # StripByteCounts, LONG, 1, size of image + 0 # last IFD + ) + +# Extracs an image from an xobj_dic object. +# Returns a PIL image object or None +def _extract_image_from_XObject(xobj_dic): + log_debug('extract_image_from_XObject() Initialising ...') + + # --- Get image type and parameters --- + num_filters = 0 + filter_list = [] + if type(xobj_dic['/Filter']) is PdfArray: + # log_info('Filter list = "{}"'.format(text_type(xobj_dic['/Filter']))) + for filter_name in xobj_dic['/Filter']: + filter_list.append(filter_name) + num_filters = len(xobj_dic['/Filter']) + elif type(xobj_dic['/Filter']) is BasePdfName: + num_filters = 1 + filter_list.append(xobj_dic['/Filter']) + elif type(xobj_dic['/Filter']) is type(None): + log_info('type(xobj_dic[\'/Filter\']) is type(None). Skipping.') + log_info('--- xobj_dic ---') + log_info(pprint.pformat(xobj_dic)) + log_info('----------------') + return None + else: + log_info('Unknown type(xobj_dic[\'/Filter\']) = "{}"'.format(type(xobj_dic['/Filter']))) + raise TypeError + color_space = xobj_dic['/ColorSpace'] + bits_per_component = xobj_dic['/BitsPerComponent'] + height = int(xobj_dic['/Height']) + width = int(xobj_dic['/Width']) + + # --- Print info --- + log_debug('num_filters {}'.format(num_filters)) + for i, filter_name in enumerate(filter_list): + log_debug('/Filter[{}] {}'.format(i, filter_name)) + log_debug('/ColorSpace {}'.format(color_space)) + log_debug('/BitsPerComponent {}'.format(bits_per_component)) + log_debug('/Height {}'.format(height)) + log_debug('/Width {}'.format(width)) + + # NOTE /Filter = /FlateDecode may be PNG images. Check for magic number. + jpg_magic_number = '\xff\xd8' + jp2_magic_number = '\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A' + png_magic_number = '\x89\x50\x4E\x47' + gif87_magic_number = '\x47\x49\x46\x38\x37\x61' + gif89_magic_number = '\x47\x49\x46\x38\x39\x61' + tiff_LE_magic_number = '\x49\x49\x2A\x00' + tiff_BE_magic_number = '\x4D\x4D\x00\x2A' + + # --- Check for magic numbers --- + # See https://en.wikipedia.org/wiki/Magic_number_(programming) + # + if xobj_dic.stream[0:2] == jpg_magic_number: + log_debug('JPEG magic number detected!') + elif xobj_dic.stream[0:12] == jp2_magic_number: + log_debug('JPEG 2000 magic number detected!') + elif xobj_dic.stream[0:4] == png_magic_number: + log_debug('PNG magic number detected!') + elif xobj_dic.stream[0:6] == gif87_magic_number: + log_debug('GIF87a magic number detected!') + elif xobj_dic.stream[0:6] == gif89_magic_number: + log_debug('GIF89a magic number detected!') + elif xobj_dic.stream[0:4] == tiff_LE_magic_number: + log_debug('TIFF little endian magic number detected!') + elif xobj_dic.stream[0:4] == tiff_BE_magic_number: + log_debug('TIFF big endian magic number detected!') + else: + log_debug('Not known image magic number') + + # --- JPEG and JPEG 2000 embedded images --- + img = None + if num_filters == 1 and filter_list[0] == '/DCTDecode': + log_debug('extract_image_from_XObject() Converting JPG into PIL IMG (/DCTDecode)') + memory_f = StringIO.StringIO(xobj_dic.stream) + img = Image.open(memory_f) + + elif num_filters == 2 and filter_list[0] == '/FlateDecode' and filter_list[1] == '/DCTDecode': + log_debug('extract_image_from_XObject() Converting JPG into PIL IMG (/DCTDecode)') + # First decompress /FlateDecode + contents_plain = zlib.decompress(xobj_dic.stream) + memory_f = StringIO.StringIO(contents_plain) + img = Image.open(memory_f) + + elif num_filters == 1 and filter_list[0] == '/JPXDecode': + log_debug('extract_image_from_XObject() Converting JPEG 2000 into PIL IMG (/JPXDecode)') + memory_f = StringIO.StringIO(xobj_dic.stream) + img = Image.open(memory_f) + + # --- RGB images with FlateDecode --- + elif num_filters == 1 and color_space == '/DeviceRGB' and filter_list[0] == '/FlateDecode': + log_debug('extract_image_from_XObject() Saving /DeviceRGB /FlateDecode image') + contents_plain = zlib.decompress(xobj_dic.stream) + img = Image.frombytes('RGB', (width, height), contents_plain) + + # --- Monochrome images, 1 bit per pixel, /Filter /FlateDecode --- + elif num_filters == 1 and color_space == '/DeviceGray' and filter_list[0] == '/FlateDecode': + log_debug('extract_image_from_XObject() Saving monochrome /FlateDecode') + contents_plain = zlib.decompress(xobj_dic.stream) + img = Image.frombytes('1', (width, height), contents_plain) + + # --- Monochrome images, 1 bit per pixel, /Filter /CCITTFaxDecode (TIFF) --- + # WARNING FOR SOME REASON THIS CODEC DOES NOT WORK OK YET. + # For example, avsp.pdf from ArcadeDB images do not decompress correctly + elif num_filters == 1 and color_space == '/DeviceGray' and filter_list[0] == '/CCITTFaxDecode': + """ + The CCITTFaxDecode filter decodes image data that has been encoded using + either Group 3 or Group 4 CCITT facsimile (fax) encoding. CCITT encoding is + designed to achieve efficient compression of monochrome (1 bit per pixel) image + data at relatively low resolutions, and so is useful only for bitmap image data, not + for color images, grayscale images, or general data. + + K < 0 --- Pure two-dimensional encoding (Group 4) + K = 0 --- Pure one-dimensional encoding (Group 3, 1-D) + K > 0 --- Mixed one- and two-dimensional encoding (Group 3, 2-D) + """ + log_debug('extract_image_from_XObject() Saving monochrome /CCITTFaxDecode') + log_debug('/DecodeParms =') + log_debug('{}'.format(pprint.pformat(xobj_dic['/DecodeParms']))) + K = int(xobj_dic['/DecodeParms']['/K']) + if K == -1: CCITT_group = 4 + else: CCITT_group = 3 + img_size = len(xobj_dic.stream) + tiff_header = _tiff_header_for_CCITT(width, height, img_size, CCITT_group) + log_debug('img_size = {0:d}'.format(img_size)) + log_debug('CCITT_group = {0:d}'.format(CCITT_group)) + log_debug('type(tiff_header) = {}'.format(type(tiff_header))) + log_debug('type(xobj_dic.stream) = {}'.format(type(xobj_dic.stream))) + + # DEBUG file write + # with io.open('test.tiff', 'wb') as img_file: + # img_file.write(tiff_header + xobj_dic.stream) + + # Open memory file with PIL + # img = Image.open(StringIO.StringIO(tiff_header + xobj_dic.stream)) + + else: + log_debug('Unrecognised image type/filter. It cannot be extracted. Skipping.') + + return img + +# OLD CODE (extracts all pages). +# Keep this function for future reference. +def manuals_extract_pages_pdfrw(status_dic, PDF_file_FN, img_dir_FN): + # --- Load and parse PDF --- + reader = PdfReader(PDF_file_FN.getPath()) + log_info('PDF file "{}"'.format(PDF_file_FN.getPath())) + log_info('PDF has {} pages'.format(reader.numPages)) + + # --- Iterate page by page --- + image_counter = 0 + for page_index, page in enumerate(reader.pages): + # --- Iterate /Resources in page --- + # log_debug('###### Processing page {} ######'.format(page_index)) + resource_dic = page['/Resources'] + for r_name in resource_dic: + resource = resource_dic[r_name] + # Skip non /XObject keys in /Resources + if r_name != '/XObject': continue + + # DEBUG dump /XObjects dictionary + # print('--- resource ---') + # pprint(resource) + # print('----------------') + + # Traverse /XObject dictionary data. Each page may have 0, 1 or more /XObjects + # If there is more than 1 image in the page there could be more than 1 /XObject. + # Some /XObjects are not images, for example, /Subtype = /Form. + # NOTE Also, images may be inside the /Resources of a /From /XObject. + img_index = 0 + for xobj_name in resource: + xobj_dic = resource[xobj_name] + xobj_type = xobj_dic['/Type'] + xobj_subtype = xobj_dic['/Subtype'] + # >> Skip XObject forms + if xobj_subtype == '/Form': + # >> NOTE There could be an image /XObject in the /From : /Resources dictionary. + log_info('Skipping /Form /XObject') + log_info('--- xobj_dic ---') + log_info(pprint.pformat(xobj_dic)) + log_info('----------------') + continue + + # --- Print info --- + log_debug('------ Page {0:02d} Image {1:02d} ------'.format(page_index, img_index)) + log_debug('xobj_name {}'.format(xobj_name)) + log_debug('xobj_type {}'.format(xobj_type)) + log_debug('xobj_subtype {}'.format(xobj_subtype)) + # log_debug('--- xobj_dic ---') + # log_debug(pprint.pformat(xobj_dic)) + # log_debug('----------------') + + # --- Extract image --- + # Returns a PIL image object or None + img = _extract_image_from_XObject(xobj_dic) + + # --- Save image --- + if img: + img_basename_str = 'Image_page{0:02d}_img{1:02d}.png'.format(i, img_index) + img_path_str = img_dir_FN.pjoin(img_basename_str).getPath() + log_debug('Saving IMG "{}"'.format(img_path_str)) + img.save(img_path_str, 'PNG') + image_counter += 1 + img_index += 1 + else: + log_warning('Error extracting image from /XObject') + log_info('Extracted {} images from PDF'.format(image_counter)) + + # --- Initialise status_dic --- + status_dic['manFormat'] = 'PDF' + status_dic['numImages'] = image_counter + +# ------------------------------------------------------------------------------------------------- +# Main function to extract images +# ------------------------------------------------------------------------------------------------- +# +# Creates status_dic. +# If JSON INFO files does not exists then rendering is needed. +# If JSON INFO file exists, then compare manual mtime with image extraction time. +# Returns True if extraction is needed, False otherwise. +def manuals_check_img_extraction_needed(PDF_file_FN, img_dir_FN): + log_debug('manuals_check_img_extraction_needed() Starting ...') + status_dic = { + 'extraction_needed' : True, + 'manFormat' : 'PDF', + 'numPages' : 0, + 'numImages' : 0, + # This is a list of lists because each image can have more than 1 filter. + 'imgFilterList' : [], + } + + # Does the JSON INFO file exists? + rom_name = PDF_file_FN.getBaseNoExt() + info_FN = img_dir_FN.pjoin(rom_name + '.json') + log_info('manuals_check_img_extraction_needed() Info file "{}"'.format(info_FN.getPath())) + if not info_FN.exists(): + status_dic['extraction_needed'] = True + return status_dic + + # JSON INFO file exists. Open JSON file and check timestamps. + info_dic = utils_load_JSON_file(info_FN.getPath()) + man_file_mtime = PDF_file_FN.getmtime() + if man_file_mtime > info_dic['IMG_timestamp']: + status_dic['extraction_needed'] = True + status_dic['manFormat'] = info_dic['manFormat'] + status_dic['numPages'] = info_dic['numPages'] + status_dic['numImages'] = info_dic['numImages'] + status_dic['imgFilterList'] = info_dic['imgFilterList'] + else: + status_dic['extraction_needed'] = False + + return status_dic + +# Global variables for the PDF manual reader. +PDF_reader = None + +# This function receives a PDF file in man_file_FN. +# Directory img_dir_FN must exist when calling this function. +# +# Mutates status_dic: +# status_dic['abort_extraction'] = bool True is extraction should be aborted +# status_dic['manFormat'] = string ['PDF', 'CBZ', 'CBR'] +# status_dic['numPages'] = int +# status_dic['numImages'] = int +# +# Creates a JSON file with the timestamp of the extraction and timestamp of the manual file. +def manuals_open_PDF_file(status_dic, PDF_file_FN, img_dir_FN): + global PDF_reader + log_debug('manuals_open_PDF_file() Starting ...') + + # --- Load and parse PDF --- + log_info('PDF file "{}"'.format(PDF_file_FN.getPath())) + PDF_reader = PdfReader(PDF_file_FN.getPath()) + log_info('PDF has {} pages'.format(PDF_reader.numPages)) + + # --- Update status_dic --- + status_dic['abort_extraction'] = False + status_dic['manFormat'] = 'PDF' + status_dic['numPages'] = PDF_reader.numPages + status_dic['numImages'] = 0 + +def manuals_close_PDF_file(): + global PDF_reader + + PDF_reader = None + +# Create JSON INFO file. Call this function after the PDF images have been extracted. +def manuals_create_INFO_file(status_dic, PDF_file_FN, img_dir_FN): + rom_name = PDF_file_FN.getBaseNoExt() + info_FN = img_dir_FN.pjoin(rom_name + '.json') + log_info('manuals_create_INFO_file() Info file "{}"'.format(info_FN.getPath())) + PDF_timestamp = PDF_file_FN.getmtime() + IMG_timestamp = time.time() + + info_dic = { + # Fields copied from status_dic + 'manFormat' : status_dic['manFormat'], + 'numPages' : status_dic['numPages'], + 'numImages' : status_dic['numImages'], + 'imgFilterList' : status_dic['imgFilterList'], + # Fields only in JSON INFO file + 'PDF_path' : PDF_file_FN.getPath(), + 'IMG_path' : img_dir_FN.getPath(), + 'PDF_timestamp' : PDF_timestamp, + 'PDF_time' : time.strftime('%a %d %b %Y %H:%M:%S', time.localtime(PDF_timestamp)), + 'IMG_timestamp' : IMG_timestamp, + 'IMG_time' : time.strftime('%a %d %b %Y %H:%M:%S', time.localtime(IMG_timestamp)), + } + utils_write_JSON_file_pprint(info_FN.getPath(), info_dic) + +# Gets a list of filters (codecs) used in the PDF file. +# manuals_extract_PDF_page() is based on this function. In other words, this function has +# debug code and manuals_extract_PDF_page() is a cleaner version. +def manuals_get_PDF_filter_list(status_dic, man_file_FN, img_dir_FN): + # List of lists because each image may have more than 1 filter. + img_filter_list_list = [] + + # --- Iterate PDF pages --- + for page_index, page in enumerate(PDF_reader.pages): + # log_debug('###### Processing page {} ######'.format(page_index)) + resource_dic = page['/Resources'] + # --- Iterate /Resources in page --- + for resource_name in resource_dic: + resource = resource_dic[resource_name] + # Skip non /XObject keys in /Resources + if resource_name != '/XObject': continue + + # DEBUG dump /XObjects dictionary + # print('--- resource ---') + # pprint(resource) + # print('----------------') + + # Traverse /XObject dictionary data. Each page may have 0, 1 or more /XObjects + # If there is more than 1 image in the page there could be more than 1 /XObject. + # Some /XObjects are not images, for example, /Subtype = /Form. + # Also, images may be inside the /Resources of a /From /XObject. + img_index = 0 + for xobj_name in resource: + xobj_dic = resource[xobj_name] + xobj_type = xobj_dic['/Type'] + xobj_subtype = xobj_dic['/Subtype'] + # Skip XObject forms + if xobj_subtype == '/Form': + # NOTE There could be an image /XObject in the /From : /Resources dictionary. + log_info('Skipping /Form /XObject') + log_info('--- xobj_dic ---') + log_info(pprint.pformat(xobj_dic)) + log_info('----------------') + continue + + # --- Print info --- + log_debug('------ Page {0:02d} Image {1:02d} ------'.format(page_index, img_index)) + log_debug('xobj_name {}'.format(xobj_name)) + log_debug('xobj_type {}'.format(xobj_type)) + log_debug('xobj_subtype {}'.format(xobj_subtype)) + # log_debug('--- xobj_dic ---') + # log_debug(pprint.pformat(xobj_dic)) + # log_debug('----------------') + + # --- Extract image filters --- + num_filters = 0 + filter_list = [] + if type(xobj_dic['/Filter']) is PdfArray: + # log_info('Filter list = "{}"'.format(text_type(xobj_dic['/Filter']))) + for filter_name in xobj_dic['/Filter']: + filter_list.append(filter_name) + num_filters = len(xobj_dic['/Filter']) + elif type(xobj_dic['/Filter']) is BasePdfName: + num_filters = 1 + filter_list.append(xobj_dic['/Filter']) + elif type(xobj_dic['/Filter']) is type(None): + log_info('type(xobj_dic[\'/Filter\']) is type(None). Skipping.') + log_info('--- xobj_dic ---') + log_info(pprint.pformat(xobj_dic)) + log_info('----------------') + else: + log_info('Unknown type(xobj_dic[\'/Filter\']) = "{}"'.format(type(xobj_dic['/Filter']))) + raise TypeError + img_filter_list_list.append(' and '.join(filter_list)) + img_index += 1 + status_dic['imgFilterList'] = img_filter_list_list + +# Extracts images in a PDF page. +def manuals_extract_PDF_page(status_dic, man_file_FN, img_dir_FN, page_index): + # --- Get page object --- + page = PDF_reader.pages[page_index] + + # --- Iterate /Resources in page --- + image_counter = 0 + log_debug('###### Processing page {} ######'.format(page_index)) + resource_dic = page['/Resources'] + for resource_name in resource_dic: + resource = resource_dic[resource_name] + # Skip non /XObject keys in /Resources + if resource_name != '/XObject': continue + + # Traverse /XObject dictionary data. + img_index = 0 + for xobj_name in resource: + xobj_dic = resource[xobj_name] + xobj_type = xobj_dic['/Type'] + xobj_subtype = xobj_dic['/Subtype'] + # Skip XObject forms + if xobj_subtype == '/Form': + # NOTE There could be an image /XObject in the /From : /Resources dictionary. + log_info('Skipping /Form /XObject') + log_info('--- xobj_dic ---') + log_info(pprint.pformat(xobj_dic)) + log_info('----------------') + continue + + # --- Print info --- + log_debug('------ Page {0:02d} Image {1:02d} ------'.format(page_index, img_index)) + log_debug('xobj_name {}'.format(xobj_name)) + log_debug('xobj_type {}'.format(xobj_type)) + log_debug('xobj_subtype {}'.format(xobj_subtype)) + + # --- Extract image --- + # Returns a PIL image object or None + img = _extract_image_from_XObject(xobj_dic) + + # --- Save image --- + if img: + img_basename_str = 'Image_page{0:02d}_img{1:02d}.png'.format(page_index, img_index) + img_path_str = img_dir_FN.pjoin(img_basename_str).getPath() + log_debug('Saving IMG "{}"'.format(img_path_str)) + img.save(img_path_str, 'PNG') + image_counter += 1 + img_index += 1 + else: + log_warning('Error extracting image from /XObject') + # --- Update info --- + status_dic['numImages'] += image_counter + log_info('PDF page {} extracted {} images'.format(page_index, image_counter)) diff --git a/plugin.program.AML/resources/misc.py b/plugin.program.AML/resources/misc.py new file mode 100644 index 0000000000..9542a64873 --- /dev/null +++ b/plugin.program.AML/resources/misc.py @@ -0,0 +1,795 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2020 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced Emulator/MAME Launcher miscellaneous functions. +# +# The idea if this module is to share it between AEL and AML. +# Functions in this module only depend on the Python standard library. +# This module can be loaded anywhere without creating circular dependencies. +# Optionally this module can include utils.py to use the log_*() functions but better avoid it. + +# --- Addon modules --- +from .constants import * +from .utils import * + +# --- Python standard library --- +import collections +import hashlib +import os +import random +import re +import string +import time +import zlib +if ADDON_RUNNING_PYTHON_2: + import HTMLParser + import urlparse +elif ADDON_RUNNING_PYTHON_3: + import html.parser + import urllib.parse +else: + raise TypeError('Undefined Python runtime version.') + +# ------------------------------------------------------------------------------------------------- +# Strings and text functions. +# ------------------------------------------------------------------------------------------------- +# Limits the length of a string for printing. If max_length == -1 do nothing (string has no +# length limit). The string is trimmed by cutting it and adding three dots ... at the end. +# Including these three dots the length of the returned string is max_length or less. +# Example: 'asdfasdfdasf' -> 'asdfsda...' +def text_limit_string(string, max_length): + if max_length > 5 and len(string) > max_length: + string = string[0:max_length-3] + '...' + return string + +# Given a Category/Launcher name clean it so the cleaned srt can be used as a filename. +# 1) Convert any non-printable character into '_' +# 2) Convert spaces ' ' into '_' +def text_title_to_filename_str(title_str): + cleaned_str_1 = ''.join([i if i in string.printable else '_' for i in title_str]) + cleaned_str_2 = cleaned_str_1.replace(' ', '_') + return cleaned_str_2 + +# Writes a XML text tag line, indented 2 spaces by default. +# Both tag_name and tag_text must be Unicode strings. +# Returns an Unicode string. +def text_XML(tag_name, tag_text, num_spaces = 2): + if tag_text: + tag_text = text_escape_XML(tag_text) + line = '{}<{}>{}</{}>'.format(' ' * num_spaces, tag_name, tag_text, tag_name) + else: + # Empty tag. + line = '{}<{} />'.format(' ' * num_spaces, tag_name) + + return line + +def text_remove_Kodi_color_tags(s): + s = re.sub('\[COLOR \S+?\]', '', s) + s = re.sub('\[color \S+?\]', '', s) + s = s.replace('[/color]', '') + s = s.replace('[/COLOR]', '') + + return s + +# Have a look at this https://beautifultable.readthedocs.io/en/latest/quickstart.html +# It may be used to improve the current functions. +# +# >>> from beautifultable import BeautifulTable +# >>> table = BeautifulTable() +# >>> table.rows.append(["Jacob", 1, "boy"]) +# >>> table.rows.append(["Isabella", 1, "girl"]) +# >>> table.columns.header = ["name", "rank", "gender"] +# >>> table.rows.header = ["S1", "S2", "S3", "S4", "S5"] +# >>> print(table) +# +----+----------+------+--------+ +# | | name | rank | gender | +# +----+----------+------+--------+ +# | S1 | Jacob | 1 | boy | +# +----+----------+------+--------+ + +# Renders a list of list of strings table into a CSV list of strings. +# The list of strings must be joined with '\n'.join() +def text_render_table_CSV(table_str): + rows = len(table_str) + cols = len(table_str[0]) + table_str_list = [] + for i in range(1, rows): + row_str = '' + for j in range(cols): + if j < cols - 1: + row_str += '{},'.format(table_str[i][j]) + else: + row_str += '{}'.format(table_str[i][j]) + table_str_list.append(row_str) + + return table_str_list + +# Returns a list of strings that must be joined with '\n'.join() +# +# First row column aligment 'right' or 'left' +# Second row column titles +# Third and next rows table data +# +# Input: +# table_str = [ +# ['left', 'left', 'left'], +# ['Platform', 'Parents', 'Clones'], +# ['a', 'b', 'c'], +# ] +# +# Output: +# +def text_render_table(table_str, trim_Kodi_colours = False): + rows = len(table_str) + cols = len(table_str[0]) + + # Remove Kodi tags [COLOR string] and [/COLOR] + if trim_Kodi_colours: + new_table_str = [] + for i in range(rows): + new_table_str.append([]) + for j in range(cols): + s = text_remove_Kodi_color_tags(table_str[i][j]) + new_table_str[i].append(s) + table_str = new_table_str + + # Determine sizes and padding. + # Ignore row 0 when computing sizes. + table_str_list = [] + col_sizes = text_get_table_str_col_sizes(table_str, rows, cols) + col_padding = table_str[0] + + # --- Table header --- + row_str = '' + for j in range(cols): + if j < cols - 1: + row_str += text_print_padded_left(table_str[1][j], col_sizes[j]) + ' ' + else: + row_str += text_print_padded_left(table_str[1][j], col_sizes[j]) + table_str_list.append(row_str) + # Table line ----- + total_size = sum(col_sizes) + 2*(cols-1) + table_str_list.append('{}'.format('-' * total_size)) + + # --- Data rows --- + for i in range(2, rows): + row_str = '' + for j in range(cols): + if j < cols - 1: + if col_padding[j] == 'right': + row_str += text_print_padded_right(table_str[i][j], col_sizes[j]) + ' ' + else: + row_str += text_print_padded_left(table_str[i][j], col_sizes[j]) + ' ' + else: + if col_padding[j] == 'right': + row_str += text_print_padded_right(table_str[i][j], col_sizes[j]) + else: + row_str += text_print_padded_left(table_str[i][j], col_sizes[j]) + table_str_list.append(row_str) + + return table_str_list + +# First row column aligment 'right' or 'left' +# Second and next rows table data +# Input: +# table_str = [ +# ['left', 'left', 'left'], +# ['Platform', 'Parents', 'Clones'], +# ['', '', ''], +# ] +# +# Output: +def text_render_table_NO_HEADER(table_str, trim_Kodi_colours = False): + rows = len(table_str) + cols = len(table_str[0]) + + # Remove Kodi tags [COLOR string] and [/COLOR] + # BUG Currently this code removes all the colour tags so the table is rendered + # with no colours. + # NOTE To render tables with colours is more difficult than this... + # All the paddings changed. I will left this for the future. + if trim_Kodi_colours: + new_table_str = [] + for i in range(rows): + new_table_str.append([]) + for j in range(cols): + s = text_remove_Kodi_color_tags(table_str[i][j]) + new_table_str[i].append(s) + table_str = new_table_str + + # Ignore row 0 when computing sizes. + table_str_list = [] + col_sizes = text_get_table_str_col_sizes(table_str, rows, cols) + col_padding = table_str[0] + + # --- Data rows --- + for i in range(1, rows): + row_str = '' + for j in range(cols): + if j < cols - 1: + if col_padding[j] == 'right': + row_str += text_print_padded_right(table_str[i][j], col_sizes[j]) + ' ' + else: + row_str += text_print_padded_left(table_str[i][j], col_sizes[j]) + ' ' + else: + if col_padding[j] == 'right': + row_str += text_print_padded_right(table_str[i][j], col_sizes[j]) + else: + row_str += text_print_padded_left(table_str[i][j], col_sizes[j]) + table_str_list.append(row_str) + + return table_str_list + +# Removed Kodi colour tags before computing size (substitute by ''): +# A) [COLOR skyblue] +# B) [/COLOR] +def text_get_table_str_col_sizes(table_str, rows, cols): + col_sizes = [0] * cols + for j in range(cols): + col_max_size = 0 + for i in range(1, rows): + cell_str = re.sub(r'\[COLOR \w+?\]', '', table_str[i][j]) + cell_str = re.sub(r'\[/COLOR\]', '', cell_str) + str_size = len('{}'.format(cell_str)) + if str_size > col_max_size: col_max_size = str_size + col_sizes[j] = col_max_size + + return col_sizes + +def text_str_list_size(str_list): + max_str_size = 0 + for str_item in str_list: + str_size = len('{}'.format(str_item)) + if str_size > max_str_size: max_str_size = str_size + + return max_str_size + +def text_str_dic_max_size(dictionary_list, dic_key, title_str = ''): + max_str_size = 0 + for item in dictionary_list: + str_size = len('{}'.format(item[dic_key])) + if str_size > max_str_size: max_str_size = str_size + if title_str: + str_size = len(title_str) + if str_size > max_str_size: max_str_size = str_size + + return max_str_size + +def text_print_padded_left(text_line, text_max_size): + formatted_str = '{}'.format(text_line) + padded_str = formatted_str + ' ' * (text_max_size - len(formatted_str)) + + return padded_str + +def text_print_padded_right(text_line, text_max_size): + formatted_str = '{}'.format(text_line) + padded_str = ' ' * (text_max_size - len(formatted_str)) + formatted_str + + return padded_str + +# --- BEGIN code in dev-misc/test_color_tag_remove.py --------------------------------------------- +def text_remove_color_tags_slist(slist): + # Iterate list of strings and remove the following tags + # 1) [COLOR colorname] + # 2) [/COLOR] + # + # Modifying a list is OK when iterating the list. However, do not change the size of the + # list when iterating. + for i, s in enumerate(slist): + s_temp, modified = s, False + + # Remove all [COLOR colorname] tags. + fa_list = re.findall('(\[COLOR \w+?\])', s_temp) + fa_set = set(fa_list) + if len(fa_set) > 0: + modified = True + for m in fa_set: + s_temp = s_temp.replace(m, '') + + # Remove all [/COLOR] + if s_temp.find('[/COLOR]') >= 0: + s_temp = s_temp.replace('[/COLOR]', '') + modified = True + + # Update list + if modified: + slist[i] = s_temp +# --- END code in dev-misc/test_color_tag_remove.py ----------------------------------------------- + +# Some XML encoding of special characters: +# {'\n': ' ', '\r': ' ', '\t':' '} +# +# See http://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents +# See https://wiki.python.org/moin/EscapingXml +# See https://github.com/python/cpython/blob/master/Lib/xml/sax/saxutils.py +# See http://stackoverflow.com/questions/2265966/xml-carriage-return-encoding +def text_escape_XML(data_str): + # Ampersand MUST BE replaced FIRST + data_str = data_str.replace('&', '&') + data_str = data_str.replace('>', '>') + data_str = data_str.replace('<', '<') + + data_str = data_str.replace("'", ''') + data_str = data_str.replace('"', '"') + + # --- Unprintable characters --- + data_str = data_str.replace('\n', ' ') + data_str = data_str.replace('\r', ' ') + data_str = data_str.replace('\t', ' ') + + return data_str + +def text_unescape_XML(data_str): + data_str = data_str.replace('"', '"') + data_str = data_str.replace(''', "'") + + data_str = data_str.replace('<', '<') + data_str = data_str.replace('>', '>') + # Ampersand MUST BE replaced LAST + data_str = data_str.replace('&', '&') + + # --- Unprintable characters --- + data_str = data_str.replace(' ', '\n') + data_str = data_str.replace(' ', '\r') + data_str = data_str.replace(' ', '\t') + + return data_str + +# Unquote an HTML string. Replaces %xx with Unicode characters. +# http://www.w3schools.com/tags/ref_urlencode.asp +def text_decode_HTML(s): + s = s.replace('%25', '%') # Must be done first + s = s.replace('%20', ' ') + s = s.replace('%23', '#') + s = s.replace('%26', '&') + s = s.replace('%28', '(') + s = s.replace('%29', ')') + s = s.replace('%2C', ',') + s = s.replace('%2F', '/') + s = s.replace('%3B', ';') + s = s.replace('%3A', ':') + s = s.replace('%3D', '=') + s = s.replace('%3F', '?') + + return s + +# Decodes HTML <br> tags and HTML entities (&xxx;) into Unicode characters. +# See https://stackoverflow.com/questions/2087370/decode-html-entities-in-python-string +def text_unescape_HTML(s): + __debug_text_unescape_HTML = False + if __debug_text_unescape_HTML: + log_debug('text_unescape_HTML() input "{}"'.format(s)) + + # --- Replace HTML tag characters by their Unicode equivalent --- + s = s.replace('<br>', '\n') + s = s.replace('<br/>', '\n') + s = s.replace('<br />', '\n') + + # --- HTML entities --- + # s = s.replace('<', '<') + # s = s.replace('>', '>') + # s = s.replace('"', '"') + # s = s.replace(' ', ' ') + # s = s.replace('©', '©') + # s = s.replace('&', '&') # >> Must be done last + + # --- HTML Unicode entities --- + # s = s.replace(''', "'") + # s = s.replace('•', "•") + # s = s.replace('"', '"') + # s = s.replace('&', '&') + # s = s.replace(''', "'") + + # s = s.replace('ā', "ā") + # s = s.replace('ē', "ē") + # s = s.replace('ī', "ī") + # s = s.replace('ī', "ī") + # s = s.replace('ō', "ō") + # s = s.replace('ō', "ō") + # s = s.replace('ū', "ū") + # s = s.replace('ū', "ū") + + # Use HTMLParser module to decode HTML entities. + if ADDON_RUNNING_PYTHON_2: + s = HTMLParser.HTMLParser().unescape(s) + elif ADDON_RUNNING_PYTHON_3: + s = html.parser.HTMLParser().unescape(s) + else: + raise TypeError('Undefined Python runtime version.') + + if __debug_text_unescape_HTML: + log_debug('text_unescape_HTML() output "{}"'.format(s)) + + return s + +# Remove HTML tags from string. +def text_remove_HTML_tags(s): + p = re.compile('<.*?>') + s = p.sub('', s) + + return s + +def text_unescape_and_untag_HTML(s): + s = text_unescape_HTML(s) + s = text_remove_HTML_tags(s) + + return s + +# ------------------------------------------------------------------------------------------------- +# ROM name cleaning and formatting +# ------------------------------------------------------------------------------------------------- +# This function is used to clean the ROM name to be used as search string for the scraper. +# +# 1) Cleans ROM tags: [BIOS], (Europe), (Rev A), ... +# 2) Substitutes some characters by spaces +def text_format_ROM_name_for_scraping(title): + title = re.sub('\[.*?\]', '', title) + title = re.sub('\(.*?\)', '', title) + title = re.sub('\{.*?\}', '', title) + + title = title.replace('_', '') + title = title.replace('-', '') + title = title.replace(':', '') + title = title.replace('.', '') + title = title.strip() + + return title + +# Format ROM file name when scraping is disabled. +# 1) Remove No-Intro/TOSEC tags (), [], {} at the end of the file +# +# title -> Unicode string +# clean_tags -> bool +# +# Returns a Unicode string. +def text_format_ROM_title(title, clean_tags): + # + # Regexp to decompose a string in tokens + # + if clean_tags: + reg_exp = '\[.+?\]\s?|\(.+?\)\s?|\{.+?\}|[^\[\(\{]+' + tokens = re.findall(reg_exp, title) + str_list = [] + for token in tokens: + stripped_token = token.strip() + if (stripped_token[0] == '[' or stripped_token[0] == '(' or stripped_token[0] == '{') and \ + stripped_token != '[BIOS]': + continue + str_list.append(stripped_token) + cleaned_title = ' '.join(str_list) + else: + cleaned_title = title + + # if format_title: + # if (title.startswith("The ")): new_title = title.replace("The ","", 1)+", The" + # if (title.startswith("A ")): new_title = title.replace("A ","", 1)+", A" + # if (title.startswith("An ")): new_title = title.replace("An ","", 1)+", An" + # else: + # if (title.endswith(", The")): new_title = "The "+"".join(title.rsplit(", The", 1)) + # if (title.endswith(", A")): new_title = "A "+"".join(title.rsplit(", A", 1)) + # if (title.endswith(", An")): new_title = "An "+"".join(title.rsplit(", An", 1)) + + return cleaned_title + +# ------------------------------------------------------------------------------------------------- +# URLs +# ------------------------------------------------------------------------------------------------- +# Get extension of URL. Returns '' if not found. Examples: 'png', 'jpg', 'gif'. +def text_get_URL_extension(url): + if ADDON_RUNNING_PYTHON_2: + path = urlparse.urlparse(url).path + elif ADDON_RUNNING_PYTHON_3: + path = urllib.parse.urlparse(url).path + else: + raise TypeError('Undefined Python runtime version.') + ext = os.path.splitext(path)[1] + if ext[0] == '.': ext = ext[1:] # Remove initial dot + return ext + +# Defaults to 'jpg' if URL extension cannot be determined +def text_get_image_URL_extension(url): + if ADDON_RUNNING_PYTHON_2: + path = urlparse.urlparse(url).path + elif ADDON_RUNNING_PYTHON_3: + path = urllib.parse.urlparse(url).path + else: + raise TypeError('Undefined Python runtime version.') + ext = os.path.splitext(path)[1] + if ext[0] == '.': ext = ext[1:] # Remove initial dot + ret = 'jpg' if ext == '' else ext + return ret + +# ------------------------------------------------------------------------------------------------- +# Misc stuff +# +# TODO Filesystem IO functions must be moved to utils.py +# ------------------------------------------------------------------------------------------------- +# Generates a random an unique MD5 hash and returns a string with the hash +def misc_generate_random_SID(): + t1 = time.time() + t2 = t1 + random.getrandbits(32) + if ADDON_RUNNING_PYTHON_2: + base = hashlib.md5(text_type(t1 + t2)) + elif ADDON_RUNNING_PYTHON_3: + base = hashlib.md5(text_type(t1 + t2).encode('utf-8')) + else: + raise TypeError('Undefined Python runtime version.') + sid = base.hexdigest() + return sid + +# See https://docs.python.org/3.8/library/time.html#time.gmtime +def misc_time_to_str(secs): + return time.strftime('%a %d %b %Y %H:%M:%S', time.localtime(secs)) + +def misc_escape_regex_special_chars(s): + s = s.replace('(', '\(') + s = s.replace(')', '\)') + s = s.replace('+', '\+') + + return s + +# Search for a No-Intro DAT filename. +def misc_look_for_NoIntro_DAT(platform, DAT_list): + # log_debug('Testing No-Intro platform "{}"'.format(platform.long_name)) + if not platform.DAT_prefix: + # log_debug('Empty DAT_prefix. Return empty string.') + return '' + # Traverse all files and make a list of DAT matches. + DAT_str = misc_escape_regex_special_chars(platform.DAT_prefix) + patt = '.*' + DAT_str + ' \(Parent-Clone\) \((\d\d\d\d\d\d\d\d)-(\d\d\d\d\d\d)\)\.dat' + # log_variable('patt', patt) + fname_list = [] + for fname in DAT_list: + m = re.match(patt, fname) + if m: fname_list.append(fname) + # log_variable('fname_list', fname_list) + if fname_list: + # If more than one DAT found sort alphabetically and pick the first. + # Because the fname include the date the most recent must be first. + return sorted(fname_list, reverse = True)[0] + else: + return '' + +# Atari - Jaguar CD Interactive Multimedia System - Datfile (10) (2019-08-27 00-06-32) +# Commodore - Amiga CD - Datfile (350) (2019-06-28 13-05-34) +# Commodore - Amiga CD32 - Datfile (157) (2019-09-24 21-03-02) +def misc_look_for_Redump_DAT(platform, DAT_list): + # log_debug('Testing Redump platform "{}"'.format(platform.long_name)) + if not platform.DAT_prefix: + # log_debug('Empty DAT_prefix. Return empty string.') + return '' + DAT_str = misc_escape_regex_special_chars(platform.DAT_prefix) + patt = '.*' + DAT_str + ' \(\d+\) \((\d\d\d\d-\d\d-\d\d) (\d\d-\d\d-\d\d)\)\.dat' + # log_variable('patt', patt) + fname_list = [] + for fname in DAT_list: + m = re.match(patt, fname) + if m: fname_list.append(fname) + # log_variable('fname_list', fname_list) + if fname_list: + return sorted(fname_list, reverse = True)[0] + else: + return '' + +# Lazy function (generator) to read a file piece by piece. Default chunk size: 8k. +# Default return value in Python is None. +# Usage example: +# f = open() +# for chunk in misc_read_file_in_chunks(f): +# do_something() +def misc_read_file_in_chunks(file_object, chunk_size = 8192): + while True: + data = file_object.read(chunk_size) + if not data: break + yield data + +# Calculates CRC, MD5 and SHA1 of a file in an efficient way. +# Returns a dictionary with the checksums or None in case of error. +# +# https://stackoverflow.com/questions/519633/lazy-method-for-reading-big-file-in-python +# https://stackoverflow.com/questions/1742866/compute-crc-of-file-in-python +def misc_calculate_file_checksums(full_file_path): + log_debug('Computing checksums "{}"'.format(full_file_path)) + try: + f = open(full_file_path, 'rb') + crc_prev = 0 + md5 = hashlib.md5() + sha1 = hashlib.sha1() + for piece in misc_read_file_in_chunks(f): + crc_prev = zlib.crc32(piece, crc_prev) + md5.update(piece) + sha1.update(piece) + crc_digest = '{:08X}'.format(crc_prev & 0xFFFFFFFF) + md5_digest = md5.hexdigest() + sha1_digest = sha1.hexdigest() + size = os.path.getsize(full_file_path) + except: + log_debug('(Exception) In misc_calculate_file_checksums()') + log_debug('Returning None') + return None + checksums = { + 'crc' : crc_digest.upper(), + 'md5' : md5_digest.upper(), + 'sha1' : sha1_digest.upper(), + 'size' : size, + } + + return checksums + +# This function not finished yet. +def misc_read_bytes_in_chunks(file_bytes, chunk_size = 8192): + file_length = len(file_bytes) + block_number = 0 + while True: + start_index = None + end_index = None + data = file_bytes[start_index:end_index] + yield data + +def misc_calculate_stream_checksums(file_bytes): + # log_debug('Computing checksums of bytes stream...'.format(len(file_bytes))) + crc_prev = 0 + md5 = hashlib.md5() + sha1 = hashlib.sha1() + # Process bytes stream block by block + # for piece in misc_read_bytes_in_chunks(file_bytes): + # crc_prev = zlib.crc32(piece, crc_prev) + # md5.update(piece) + # sha1.update(piece) + # Process bytes in one go + crc_prev = zlib.crc32(file_bytes, crc_prev) + md5.update(file_bytes) + sha1.update(file_bytes) + # Output data. + crc_digest = '{:08X}'.format(crc_prev & 0xFFFFFFFF) + md5_digest = md5.hexdigest() + sha1_digest = sha1.hexdigest() + size = len(file_bytes) + checksums = { + 'crc' : crc_digest.upper(), + 'md5' : md5_digest.upper(), + 'sha1' : sha1_digest.upper(), + 'size' : size, + } + return checksums + +# Replace an item in dictionary. If dict_in is an OrderedDict then keep original order. +# Returns a dict or OrderedDict +def misc_replace_fav(dict_in, old_item_key, new_item_key, new_value): + if type(dict_in) is dict: + dict_in.pop(old_item_key) + dict_in[new_item_key] = new_value + return dict_in + elif type(dict_in) is collections.OrderedDict: + # In this case create a new OrderedDict to respect original order. + # This implementation is slow and naive but I don't care, OrderedDict are only use + # when editing ROM Collections. + dict_out = collections.OrderedDict() + for key in dict_in: + if key == old_item_key: + dict_out[new_item_key] = new_value + else: + dict_out[key] = dict_in[key] + return dict_out + else: + raise TypeError + +# Inspects an image file and determine its type by using the magic numbers, +# Returns an image id defined in list IMAGE_IDS or IMAGE_UKNOWN_ID. +def misc_identify_image_id_by_contents(asset_fname): + # If file size is 0 or less than 64 bytes it is corrupt. + statinfo = os.stat(asset_fname) + if statinfo.st_size < 64: return IMAGE_CORRUPT_ID + + # Read first 64 bytes of file. + # Search for the magic number of the beginning of the file. + with open(asset_fname, "rb") as f: + file_bytes = f.read(64) + for img_id in IMAGE_MAGIC_DIC: + for magic_bytes in IMAGE_MAGIC_DIC[img_id]: + magic_bytes_len = len(magic_bytes) + file_chunk = file_bytes[0:magic_bytes_len] + if len(file_chunk) != magic_bytes_len: raise TypeError + if file_chunk == magic_bytes: return img_id + + return IMAGE_UKNOWN_ID + +# Returns an image id defined in list IMAGE_IDS or IMAGE_UKNOWN_ID. +def misc_identify_image_id_by_ext(asset_fname): + asset_root, asset_ext = os.path.splitext(asset_fname) + # log_debug('asset_ext {}'.format(asset_ext)) + if not asset_ext: return IMAGE_UKNOWN_ID + asset_ext = asset_ext[1:] # Remove leading dot '.png' -> 'png' + for img_id in IMAGE_EXTENSIONS: + for img_ext in IMAGE_EXTENSIONS[img_id]: + if asset_ext.lower() == img_ext: return img_id + return IMAGE_UKNOWN_ID + +# Remove initial and trailing quotation characters " or ' +def misc_strip_quotes(my_str): + my_str = my_str[1:] if my_str[0] == '"' or my_str[0] == "'" else my_str + my_str = my_str[:-1] if my_str[-1] == '"' or my_str[-1] == "'" else my_str + return my_str + +# All version numbers must be less than 100, except the major version. +# AML version is like this: aa.bb.cc[-|~][alpha[dd]|beta[dd]] +# It gets converted to: aa.bb.cc Rdd -> int aab,bcc,Rdd +# The number 2,147,483,647 is the maximum positive value for a 32-bit signed binary integer. +# +# aa.bb.cc.Xdd formatted aab,bcc,Xdd +# | | | | |--> Beta/Alpha flag 0, 1, ..., 99 +# | | | |----> Release kind flag +# | | | 5 for non-beta, non-alpha, non RC versions. +# | | | 2 for RC versions +# | | | 1 for beta versions +# | | | 0 for alpha versions +# | | |------> Build version 0, 1, ..., 99 +# | |---------> Minor version 0, 1, ..., 99 +# |------------> Major version 0, ..., infinity +def misc_addon_version_str_to_int(AML_version_str): + # log_debug('misc_addon_version_str_to_int() AML_version_str = "{}"'.format(AML_version_str)) + version_int = 0 + # Parse versions like "0.9.8[-|~]alpha[jj]" + m_obj_alpha_n = re.search('^(\d+?)\.(\d+?)\.(\d+?)[\-\~](alpha|beta)(\d+?)$', AML_version_str) + # Parse versions like "0.9.8[-|~]alpha" + m_obj_alpha = re.search('^(\d+?)\.(\d+?)\.(\d+?)[\-\~](alpha|beta)$', AML_version_str) + # Parse versions like "0.9.8" + m_obj_standard = re.search('^(\d+?)\.(\d+?)\.(\d+?)$', AML_version_str) + + if m_obj_alpha_n: + major = int(m_obj_alpha_n.group(1)) + minor = int(m_obj_alpha_n.group(2)) + build = int(m_obj_alpha_n.group(3)) + kind_str = m_obj_alpha_n.group(4) + beta = int(m_obj_alpha_n.group(5)) + if kind_str == 'alpha': + release_flag = 0 + elif kind_str == 'beta': + release_flag = 1 + # log_debug('misc_addon_version_str_to_int() major {}'.format(major)) + # log_debug('misc_addon_version_str_to_int() minor {}'.format(minor)) + # log_debug('misc_addon_version_str_to_int() build {}'.format(build)) + # log_debug('misc_addon_version_str_to_int() kind_str {}'.format(kind_str)) + # log_debug('misc_addon_version_str_to_int() release_flag {}'.format(release_flag)) + # log_debug('misc_addon_version_str_to_int() beta {}'.format(beta)) + version_int = major * 10000000 + minor * 100000 + build * 1000 + release_flag * 100 + beta + elif m_obj_alpha: + major = int(m_obj_alpha.group(1)) + minor = int(m_obj_alpha.group(2)) + build = int(m_obj_alpha.group(3)) + kind_str = m_obj_alpha.group(4) + if kind_str == 'alpha': + release_flag = 0 + elif kind_str == 'beta': + release_flag = 1 + # log_debug('misc_addon_version_str_to_int() major {}'.format(major)) + # log_debug('misc_addon_version_str_to_int() minor {}'.format(minor)) + # log_debug('misc_addon_version_str_to_int() build {}'.format(build)) + # log_debug('misc_addon_version_str_to_int() kind_str {}'.format(kind_str)) + # log_debug('misc_addon_version_str_to_int() release_flag {}'.format(release_flag)) + version_int = major * 10000000 + minor * 100000 + build * 1000 + release_flag * 100 + elif m_obj_standard: + major = int(m_obj_standard.group(1)) + minor = int(m_obj_standard.group(2)) + build = int(m_obj_standard.group(3)) + release_flag = 5 + # log_debug('misc_addon_version_str_to_int() major {}'.format(major)) + # log_debug('misc_addon_version_str_to_int() minor {}'.format(minor)) + # log_debug('misc_addon_version_str_to_int() build {}'.format(build)) + version_int = major * 10000000 + minor * 100000 + build * 1000 + release_flag * 100 + else: + # log_debug('AML addon version "{}" cannot be parsed.'.format(AML_version_str)) + raise TypeError('misc_addon_version_str_to_int() failure') + # log_debug('misc_addon_version_str_to_int() version_int = {}'.format(version_int)) + + return version_int diff --git a/plugin.program.AML/resources/settings-new-format.xml b/plugin.program.AML/resources/settings-new-format.xml new file mode 100644 index 0000000000..8a1fbb11d5 --- /dev/null +++ b/plugin.program.AML/resources/settings-new-format.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<settings version="1"> +<section id="plugin.program.AML.dev"> + <!-- + For Kodi Matrix use the old settings format and not the new one, unless the textual label + attribute regression present in Matrix alpha 1 is fixed. + + Always use type="xxx" id="xxx" label="xxx" help="xxx" in <setting> + + See [Kodi Matrix alpha 1, addon settings do not show](https://forum.kodi.tv/showthread.php?tid=356245) + --> + <category id="main-operation" label="Main operation" help="Main operation help"> + <group id="1" label="Group label"> + <setting type="integer" id="op_mode_raw" label="Mode" help="Test help"> + <level>0</level> + <default>0</default> + <constraints> + <options> + <option label="Vanilla MAME">0</option> + <option label="Retroarch MAME 2003 Plus">1</option> + </options> + </constraints> + <control type="spinner" format="string" /> + </setting> + </group> + <group id="2" label="Vanilla MAME"> + <setting type="boolean" id="enable_SL" label="Enable Software Lists" help="Test help"> + <level>0</level> + <default>true</default> + <control type="toggle"/> + </setting> + <setting type="path" id="mame_prog" label="MAME executable path" help="Test help"> + <level>0</level> + <default/> + <constraints> + <writable>false</writable> + <allowempty>true</allowempty> + </constraints> + <control type="button" format="file"> + <heading>Heading test</heading> + </control> + </setting> + <setting type="path" id="SL_hash_path" label="Software Lists hash path" help="Test help"> + <level>0</level> + <default/> + <constraints> + <writable>false</writable> + <allowempty>true</allowempty> + </constraints> + <control type="button" format="path"> + <heading>Heading test</heading> + </control> + </setting> + </group> + <group id="3" label="Retroarch MAME 2003 Plus"> + <setting type="path" id="retroarch_prog" label="Retroarch executable path" help="Test help"> + <level>0</level> + <default/> + <constraints> + <writable>false</writable> + <allowempty>true</allowempty> + </constraints> + <control type="button" format="file"> + <heading>Heading test</heading> + </control> + </setting> + <setting type="path" id="libretro_dir" label="Libretro path" help="Test help"> + <level>0</level> + <default/> + <constraints> + <writable>false</writable> + <allowempty>true</allowempty> + </constraints> + <control type="button" format="file"> + <heading>Heading test</heading> + </control> + </setting> + <setting type="path" id="xml_2003_path" label="MAME XML path" help="Test help"> + <level>0</level> + <default/> + <constraints> + <writable>false</writable> + <allowempty>true</allowempty> + </constraints> + <control type="button" format="file"> + <heading>Heading test</heading> + </control> + </setting> + </group> + </category> +</section> +</settings> diff --git a/plugin.program.AML/resources/settings.xml b/plugin.program.AML/resources/settings.xml new file mode 100644 index 0000000000..085aac9c3b --- /dev/null +++ b/plugin.program.AML/resources/settings.xml @@ -0,0 +1,95 @@ +<settings> +<category label="Main operation"> + <setting label="Operation mode" type="enum" id="op_mode_raw" default="0" values="Vanilla MAME|Retroarch MAME 2003 Plus" /> + + <setting id="separator" type="lsep" label="Vanilla MAME settings" /> + <setting label="MAME ROMs path" type="folder" id="rom_path_vanilla" source="" /> + <setting label="Enable Software Lists" type="bool" id="enable_SL" default="true" /> + <setting label="MAME executable path" type="file" id="mame_prog" /> + <setting label="Software Lists hash path" type="folder" id="SL_hash_path" source="" /> + + <setting id="separator" type="lsep" label="Retroarch MAME 2003 Plus settings" /> + <setting label="MAME ROMs path" type="folder" id="rom_path_2003_plus" source="" /> + <setting label="Retroarch executable path" type="file" id="retroarch_prog" /> + <setting label="Libretro path" type="folder" id="libretro_dir" source="" /> + <setting label="MAME XML path" type="file" id="xml_2003_path" /> +</category> +<category label="Optional paths"> + <setting label="MAME Assets path" type="folder" id="assets_path" source="" /> + <setting label="MAME INI/DAT path" type="folder" id="dats_path" source="" /> + <setting label="MAME CHDs path" type="folder" id="chd_path" source="" /> + <setting label="MAME Samples path" type="folder" id="samples_path" source="" /> + <setting label="Software Lists ROMs path" type="folder" id="SL_rom_path" source="" /> + <setting label="Software Lists CHDs path" type="folder" id="SL_chd_path" source="" /> +</category> +<category label="ROM sets"> + <setting label="MAME ROM set" type="enum" id="mame_rom_set" default="1" values="Merged|Split|Non-merged|Fully non-merged" /> + <setting label="MAME CHD set" type="enum" id="mame_chd_set" default="0" values="Merged|Split|Non-merged" /> + <setting label="Software Lists ROM set" type="enum" id="SL_rom_set" default="1" values="Merged|Split|Non-merged" /> + <setting label="Software Lists CHD set" type="enum" id="SL_chd_set" default="0" values="Merged|Split|Non-merged" /> + + <setting id="separator" type="lsep" label="Misc" /> + <setting label="Custom filter XML path" type="file" id="filter_XML" default="" /> + <setting label="Generate History DAT infolabel" type="bool" id="generate_history_infolabel" default="false" /> +</category> +<category label="Display I"> + <!-- <setting id="separator" type="lsep" label="General"/> --> + <setting label="Launching application notification" type="bool" id="display_launcher_notify" default="true" /> + <setting label="MAME view mode" type="enum" id="mame_view_mode" default="1" values="Flat|Parent/Clone" /> + <setting label="Software Lists view mode" type="enum" id="sl_view_mode" default="1" values="Flat|Parent/Clone" /> + <setting label="Hide Mature machines" type="bool" default="false" id="display_hide_Mature" /> + <setting label="Hide BIOSes" type="bool" default="false" id="display_hide_BIOS" /> + <setting label="Hide imperfect machines" type="bool" default="false" id="display_hide_imperfect" /> + <setting label="Hide non-working machines" type="bool" default="false" id="display_hide_nonworking" /> + <setting label="Display machines with available ROMs only" type="bool" default="false" id="display_rom_available" /> + <setting label="Display machines with available CHDs only" type="bool" default="false" id="display_chd_available" /> + <setting label="Display SL items with available ROMS/CHDs only" type="bool" default="false" id="display_SL_items_available" /> + <setting label="Display MAME ROM flags" type="bool" default="true" id="display_MAME_flags" /> + <setting label="Display SL ROM flags" type="bool" default="true" id="display_SL_flags" /> +</category> +<category label="Display II"> + <!-- <setting id="separator" type="lsep" label="Addon filters" /> --> + <setting label="Display Main filters" type="bool" default="true" id="display_main_filters" /> + <setting label="Display Binary filters" type="bool" default="true" id="display_binary_filters" /> + <setting label="Display Catalog filters" type="bool" default="true" id="display_catalog_filters" /> + <setting label="Display DAT browser" type="bool" default="false" id="display_DAT_browser" /> + <setting label="Display Software List browser" type="bool" default="true" id="display_SL_browser" /> + <setting label="Display Custom filters" type="bool" default="true" id="display_custom_filters" /> + <setting label="Display Read-only Launchers" type="bool" default="false" id="display_ROLs" /> + <setting label="Display MAME Favourites" type="bool" default="true" id="display_MAME_favs" /> + <setting label="Display MAME Most Played" type="bool" default="true" id="display_MAME_most" /> + <setting label="Display MAME Recently Played" type="bool" default="true" id="display_MAME_recent" /> + <setting label="Display SL Favourites" type="bool" default="true" id="display_SL_favs" /> + <setting label="Display SL Most Played" type="bool" default="true" id="display_SL_most" /> + <setting label="Display SL Recently Played" type="bool" default="true" id="display_SL_recent" /> + <setting label="Display Utilities" type="bool" default="true" id="display_utilities" /> + <setting label="Display Global Reports" type="bool" default="true" id="display_global_reports" /> +</category> +<category label="Artwork / Assets"> + <setting label="Do not render trailers" type="bool" default="false" id="display_hide_trailers" /> + <setting label="MAME Icon" type="enum" id="artwork_mame_icon" default="0" values="Title|Snap|Flyer|Cabinet|PCB" /> + <setting label="MAME Fanart" type="enum" id="artwork_mame_fanart" default="0" values="Fanart|Snap|Title|Flyer|CPanel" /> + <setting label="Software Lists Icon" type="enum" id="artwork_SL_icon" default="0" values="Boxfront|Title|Snap" /> + <setting label="Software Lists Fanart" type="enum" id="artwork_SL_fanart" default="0" values="Fanart|Snap|Title" /> +</category> +<category label="Advanced"> + <setting label="Action on Kodi playing media" type="enum" id="media_state_action" default="0" values="Stop|Pause|Keep playing" /> + <setting label="After/before launch delay (ms)" type="slider" id="delay_tempo" default="100" range="0,100,15000" option="int" /> + <setting label="Suspend/resume Kodi audio engine" type="bool" id="suspend_audio_engine" default="false" /> + <setting label="Suspend/resume Kodi screensaver" type="bool" id="suspend_screensaver" default="false" /> + <setting label="Toggle Kodi into windowed mode" type="bool" id="toggle_window" default="false" /> + <setting label="Log level" type="enum" id="log_level" default="2" values="ERROR|WARNING|INFO|VERBOSE|DEBUG" /> + + <setting id="separator" type="lsep" label="MAME database cache" /> + <setting label="Enable MAME render cache" type="bool" default="false" id="debug_enable_MAME_render_cache" /> + <setting label="Enable MAME asset cache" type="bool" default="false" id="debug_enable_MAME_asset_cache" /> + + <setting id="separator" type="lsep" label="Information dump" /> + <setting label="Write MAME machine data" type="bool" default="false" id="debug_MAME_machine_data" /> + <setting label="Write MAME ROMs DB data" type="bool" default="false" id="debug_MAME_ROM_DB_data" /> + <setting label="Write MAME Audit DB data" type="bool" default="false" id="debug_MAME_Audit_DB_data" /> + <setting label="Write SL item data" type="bool" default="false" id="debug_SL_item_data" /> + <setting label="Write SL ROMs DB data" type="bool" default="false" id="debug_SL_ROM_DB_data" /> + <setting label="Write SL Audit DB data" type="bool" default="false" id="debug_SL_Audit_DB_data" /> +</category> +</settings> diff --git a/plugin.program.AML/resources/utils.py b/plugin.program.AML/resources/utils.py new file mode 100644 index 0000000000..7fa595d47b --- /dev/null +++ b/plugin.program.AML/resources/utils.py @@ -0,0 +1,1280 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2021 Wintermute0110 <wintermute0110@gmail.com> +# +# 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; version 2 of the License. +# +# 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. + +# Advanced Emulator/MAME Launcher utility functions. +# +# The idea if this module is to share it between AEL and AML. +# +# All functions that depends on Kodi modules are here. This includes IO functions +# and logging functions. Misc function that do not depend on Kodi modules are +# located in the misc module. +# +# Low-level filesystem and IO functions are here (FileName class). +# db module (formaer disk_IO module) contains high level IO functions. +# +# When Kodi modules are not available replacements can be provided. This is useful +# to use addon modules with CPython for testing or debugging. +# +# This module must NOT include any other addon modules to avoid circular dependencies. The +# only exception to this rule is the module .constants. This module is virtually included +# by every other addon module. +# +# How to report errors on the low-level filesystem functions??? See the end of the file. + +# --- Addon modules --- +from .constants import * + +# --- Kodi modules --- +try: + import xbmc + import xbmcaddon + import xbmcgui + import xbmcplugin + import xbmcvfs +except: + KODI_RUNTIME_AVAILABLE_UTILS = False +else: + KODI_RUNTIME_AVAILABLE_UTILS = True + +# --- Python standard library --- +# Check what modules are really used and remove not used ones. +import collections +import errno +import fnmatch +import io +import json +import math +import os +import re +import shutil +import string +import sys +import threading +import time +import xml.etree.ElementTree +import zlib + +# --- Determine interpreter running platform --- +# Cache all possible platform values in global variables for maximum speed. +# See http://stackoverflow.com/questions/446209/possible-values-from-sys-platform +cached_sys_platform = sys.platform +def _aux_is_android(): + if not cached_sys_platform.startswith('linux'): return False + return 'ANDROID_ROOT' in os.environ or 'ANDROID_DATA' in os.environ or 'XBMC_ANDROID_APK' in os.environ + +is_windows_bool = cached_sys_platform == 'win32' or cached_sys_platform == 'win64' or cached_sys_platform == 'cygwin' +is_osx_bool = cached_sys_platform.startswith('darwin') +is_android_bool = _aux_is_android() +is_linux_bool = cached_sys_platform.startswith('linux') and not is_android_bool + +def is_windows(): return is_windows_bool + +def is_osx(): return is_osx_bool + +def is_android(): return is_android_bool + +def is_linux(): return is_linux_bool + +# ------------------------------------------------------------------------------------------------- +# Filesystem helper class. +# The addon must not use any Python IO functions, only this class. This class can be changed +# to use Kodi IO functions or Python IO functions. +# +# This class always takes and returns Unicode string paths. Decoding to UTF-8 must be done in +# caller code. +# +# A) Transform paths like smb://server/directory/ into \\server\directory\ +# B) Use xbmcvfs.translatePath() for paths starting with special:// +# +# Decomposes a file name path or directory into its constituents +# FileName.getOriginalPath() Full path /home/Wintermute/Sonic.zip +# FileName.getPath() Full path /home/Wintermute/Sonic.zip +# FileName.getPathNoExt() Full path with no extension /home/Wintermute/Sonic +# FileName.getDir() Directory name of file. Does not end in '/' /home/Wintermute/ +# FileName.getBase() File name with no path Sonic.zip +# FileName.getBaseNoExt() File name with no path and no extension Sonic +# FileName.getExt() File extension .zip +# ------------------------------------------------------------------------------------------------- +class FileName: + # pathString must be a Unicode string object + def __init__(self, pathString): + self.originalPath = pathString + self.path = pathString + + # --- Path transformation --- + if self.originalPath.lower().startswith('smb:'): + self.path = self.path.replace('smb:', '') + self.path = self.path.replace('SMB:', '') + self.path = self.path.replace('//', '\\\\') + self.path = self.path.replace('/', '\\') + + elif self.originalPath.lower().startswith('special:'): + self.path = xbmcvfs.translatePath(self.path) + + def _join_raw(self, arg): + self.path = os.path.join(self.path, arg) + self.originalPath = os.path.join(self.originalPath, arg) + return self + + # Appends a string to path. Returns self FileName object + # Instead of append() use pappend(). This will avoid using string.append() instead of FileName.append() + def pappend(self, arg): + self.path = self.path + arg + self.originalPath = self.originalPath + arg + return self + + # Behaves like os.path.join(). Returns a FileName object + # Instead of join() use pjoin(). This will avoid using string.join() instead of FileName.join() + def pjoin(self, *args): + child = FileName(self.originalPath) + for arg in args: + child._join_raw(arg) + return child + + # Behaves like os.path.join() + # + # See http://blog.teamtreehouse.com/operator-overloading-python + # other is a FileName object. other originalPath is expected to be a subdirectory (path + # transformation not required) + # def __add__(self, other): + # current_path = self.originalPath + # if type(other) is FileName: other_path = other.originalPath + # elif type(other) is text_type: other_path = other + # elif type(other) is binary_type: other_path = other + # else: raise NameError('Unknown type for overloaded + in FileName object') + # new_path = os.path.join(current_path, other_path) + # child = FileName(new_path) + # return child + + def escapeQuotes(self): + self.path = self.path.replace("'", "\\'") + self.path = self.path.replace('"', '\\"') + + # --------------------------------------------------------------------------------------------- + # Filename decomposition. + # --------------------------------------------------------------------------------------------- + def getOriginalPath(self): + return self.originalPath + + def getPath(self): + return self.path + + def getPathNoExt(self): + root, ext = os.path.splitext(self.path) + return root + + def getDir(self): + return os.path.dirname(self.path) + + def getBase(self): + return os.path.basename(self.path) + + def getBaseNoExt(self): + basename = os.path.basename(self.path) + root, ext = os.path.splitext(basename) + return root + + def getExt(self): + root, ext = os.path.splitext(self.path) + return ext + + # --------------------------------------------------------------------------------------------- + # Scanner functions + # --------------------------------------------------------------------------------------------- + def scanFilesInPath(self, mask): + files = [] + filenames = os.listdir(self.path) + for filename in fnmatch.filter(filenames, mask): + files.append(os.path.join(self.path, filename)) + return files + + def scanFilesInPathAsPaths(self, mask): + files = [] + filenames = os.listdir(self.path) + for filename in fnmatch.filter(filenames, mask): + files.append(FileName(os.path.join(self.path, filename))) + return files + + def recursiveScanFilesInPath(self, mask): + files = [] + for root, dirs, foundfiles in os.walk(self.path): + for filename in fnmatch.filter(foundfiles, mask): + files.append(os.path.join(root, filename)) + return files + + # --------------------------------------------------------------------------------------------- + # Filesystem functions + # --------------------------------------------------------------------------------------------- + # + # mtime (Modification time) is a floating point number giving the number of seconds since + # the epoch (see the time module). + # + def getmtime(self): + return os.path.getmtime(self.path) + + def stat(self): + return os.stat(self.path) + + def fileSize(self): + stat_output = os.stat(self.path) + return stat_output.st_size + + def exists(self): + return os.path.exists(self.path) + + def isdir(self): + return os.path.isdir(self.path) + + def isfile(self): + return os.path.isfile(self.path) + + def makedirs(self): + if not os.path.exists(self.path): + os.makedirs(self.path) + + # os.remove() and os.unlink() are exactly the same. + def unlink(self): + os.unlink(self.path) + + def rename(self, to): + os.rename(self.path, to.getPath()) + +# How to report errors in these IO functions? That's the eternal question. +# 1) Raise an exception and make the addon crash? Crashes are always reported in the GUI. +# 2) Use AEL approach and report status in a control dictionary? Caller code is responsible +# to report the error in the GUI. +# +# A convention must be chosen. +# A) Low-level, basic IO functions raise KodiAddonError exception. However, any other function +# uses st_dic to report errors. +# B) All functions use st_dic. IO functions are responsible to catch exceptions and fill st_dic. +# C) Do not use st_dic at all, only use KodiAddonError. + +# ------------------------------------------------------------------------------------------------- +# Low level filesystem functions. +# ------------------------------------------------------------------------------------------------- +def utils_get_fs_encoding(): + fs_encoding = sys.getfilesystemencoding() + return fs_encoding + +def utils_copy_file(source_str, dest_str): + if ADDON_RUNNING_PYTHON_2: + source_bytes = source_str.decode(utils_get_fs_encoding(), 'ignore') + dest_bytes = dest_str.decode(utils_get_fs_encoding(), 'ignore') + shutil.copy(source_bytes, dest_bytes) + elif ADDON_RUNNING_PYTHON_3: + shutil.copy(source_str, dest_str) + else: + raise TypeError('Undefined Python runtime version.') + +# Always write UNIX end of lines regarding of the operating system. +def utils_write_str_to_file(filename, full_string): + log_debug('utils_write_str_to_file() File "{}"'.format(filename)) + with io.open(filename, 'wt', encoding = 'utf-8', newline = '\n') as f: + f.write(full_string) + +def utils_load_file_to_str(filename): + log_debug('utils_load_file_to_str() File "{}"'.format(filename)) + with io.open(filename, 'rt', encoding = 'utf-8') as f: + string = f.read() + return string + +# ------------------------------------------------------------------------------------------------- +# Generic text file writer. +# slist is a list of Unicode strings that will be joined and written to a file encoded in UTF-8. +# Joining command is '\n'.join() +# ------------------------------------------------------------------------------------------------- +def utils_write_slist_to_file(filename, slist): + log_debug('utils_write_slist_to_file() File "{}"'.format(filename)) + try: + file_obj = io.open(filename, 'wt', encoding = 'utf-8') + file_obj.write('\n'.join(slist)) + file_obj.close() + except OSError: + log_error('(OSError) exception in utils_write_slist_to_file()') + log_error('Cannot write {} file'.format(filename)) + raise AEL_Error('(OSError) Cannot write {} file'.format(filename)) + except IOError: + log_error('(IOError) exception in utils_write_slist_to_file()') + log_error('Cannot write {} file'.format(filename)) + raise AEL_Error('(IOError) Cannot write {} file'.format(filename)) + +def utils_load_file_to_slist(filename): + log_debug('utils_load_file_to_slist() File "{}"'.format(filename)) + with io.open(filename, 'rt', encoding = 'utf-8') as f: + slist = f.readlines() + return slist + +# If there are issues in the XML file (for example, invalid XML chars) ET.parse will fail. +# Returns None if error. +# Returns xml_tree = xml.etree.ElementTree.parse() if success. +def utils_load_XML_to_ET(filename): + log_debug('utils_load_XML_to_ET() Loading {}'.format(filename)) + xml_tree = None + try: + xml_tree = xml.etree.ElementTree.parse(filename) + except IOError as ex: + log_debug('utils_load_XML_to_ET() (IOError) errno = {}'.format(ex.errno)) + # log_debug(text_type(ex.errno.errorcode)) + # No such file or directory + if ex.errno == errno.ENOENT: + log_error('utils_load_XML_to_ET() (IOError) ENOENT No such file or directory.') + else: + log_error('utils_load_XML_to_ET() (IOError) Unhandled errno value.') + except xml.etree.ElementTree.ParseError as ex: + log_error('utils_load_XML_to_ET() (ParseError) Exception parsing {}'.format(filename)) + log_error('utils_load_XML_to_ET() (ParseError) {}'.format(text_type(ex))) + return xml_tree + +# ------------------------------------------------------------------------------------------------- +# JSON write/load +# ------------------------------------------------------------------------------------------------- +# Replace fs_load_JSON_file with this. +def utils_load_JSON_file(json_filename, default_obj = {}, verbose = True): + # If file does not exist return default object (usually empty object) + json_data = default_obj + if not os.path.isfile(json_filename): + log_warning('utils_load_JSON_file() Not found "{}"'.format(json_filename)) + return json_data + # Load and parse JSON file. + if verbose: log_debug('utils_load_JSON_file() "{}"'.format(json_filename)) + with io.open(json_filename, 'rt', encoding = 'utf-8') as file: + try: + json_data = json.load(file) + except ValueError as ex: + log_error('utils_load_JSON_file() ValueError exception in json.load() function') + + return json_data + +# This consumes a lot of memory but it is fast. +# See https://stackoverflow.com/questions/24239613/memoryerror-using-json-dumps +# +# Note that there is a bug in the json module where the ensure_ascii=False flag can produce +# a mix of unicode and str objects. +# See http://stackoverflow.com/questions/18337407/saving-utf-8-texts-in-json-dumps-as-utf8-not-as-u-escape-sequence +def utils_write_JSON_file(json_filename, json_data, verbose = True, pprint = False, lowmem = False): + l_start = time.time() + if verbose: log_debug('utils_write_JSON_file() "{}"'.format(json_filename)) + + # Choose JSON iterative encoder or normal encoder. + if lowmem: + if verbose: log_debug('utils_write_JSON_file() Using lowmem option') + if pprint: + jobj = json.JSONEncoder(ensure_ascii = False, sort_keys = True, + indent = JSON_INDENT, separators = JSON_SEP) + else: + if OPTION_COMPACT_JSON: + jobj = json.JSONEncoder(ensure_ascii = False, sort_keys = True) + else: + jobj = json.JSONEncoder(ensure_ascii = False, sort_keys = True, + indent = JSON_INDENT, separators = JSON_SEP) + else: + # Parameter pprint == True overrides option OPTION_COMPACT_JSON. + if pprint: + f_data = json.dumps(json_data, ensure_ascii = False, sort_keys = True, + indent = JSON_INDENT, separators = JSON_SEP) + else: + if OPTION_COMPACT_JSON: + f_data = json.dumps(json_data, ensure_ascii = False, sort_keys = True) + else: + f_data = json.dumps(json_data, ensure_ascii = False, sort_keys = True, + indent = JSON_INDENT, separators = JSON_SEP) + + # Write JSON to disk + try: + with io.open(json_filename, 'wt', encoding = 'utf-8') as file: + if lowmem: + # Chunk by chunk JSON writer, uses less memory but takes longer. + for chunk in jobj.iterencode(json_data): + file.write(chunk) + else: + file.write(f_data) + except OSError: + kodi_notify(ADDON_LONG_NAME, 'Cannot write {} file (OSError)'.format(json_filename)) + except IOError: + kodi_notify(ADDON_LONG_NAME, 'Cannot write {} file (IOError)'.format(json_filename)) + l_end = time.time() + if verbose: + write_time_s = l_end - l_start + log_debug('utils_write_JSON_file() Writing time {:f} s'.format(write_time_s)) + +# ------------------------------------------------------------------------------------------------- +# Threaded JSON loader +# ------------------------------------------------------------------------------------------------- +# How to use this code: +# render_thread = Threaded_Load_JSON(cfg.RENDER_DB_PATH.getPath()) +# assets_thread = Threaded_Load_JSON(cfg.MAIN_ASSETS_DB_PATH.getPath()) +# render_thread.start() +# assets_thread.start() +# render_thread.join() +# assets_thread.join() +# MAME_db_dic = render_thread.output_dic +# MAME_assets_dic = assets_thread.output_dic +class Threaded_Load_JSON(threading.Thread): + def __init__(self, json_filename): + threading.Thread.__init__(self) + self.json_filename = json_filename + + def run(self): + self.output_dic = utils_load_JSON_file(self.json_filename) + +# ------------------------------------------------------------------------------------------------- +# File cache functions. +# Depends on the FileName class. +# ------------------------------------------------------------------------------------------------- +file_cache = {} + +def utils_file_cache_clear(verbose = True): + global file_cache + if verbose: log_debug('utils_file_cache_clear() Clearing file cache') + file_cache = {} + +def utils_file_cache_add_dir(dir_str, verbose = True): + global file_cache + + # Create a set with all the files in the directory + if not dir_str: + log_warning('utils_file_cache_add_dir() Empty dir_str. Exiting') + return + dir_FN = FileName(dir_str) + if not dir_FN.exists(): + log_debug('utils_file_cache_add_dir() Does not exist "{}"'.format(dir_str)) + file_cache[dir_str] = set() + return + if not dir_FN.isdir(): + log_warning('utils_file_cache_add_dir() Not a directory "{}"'.format(dir_str)) + return + if verbose: + # log_debug('utils_file_cache_add_dir() Scanning OP "{}"'.format(dir_FN.getOriginalPath())) + log_debug('utils_file_cache_add_dir() Scanning P "{}"'.format(dir_FN.getPath())) + # A recursive scanning function is needed. os.listdir() is not. os.walk() is recursive + # file_list = os.listdir(dir_FN.getPath()) + file_list = [] + root_dir_str = dir_FN.getPath() + # For Unicode errors in os.walk() see + # https://stackoverflow.com/questions/21772271/unicodedecodeerror-when-performing-os-walk + for root, dirs, files in os.walk(text_type(root_dir_str)): + # log_debug('----------') + # log_debug('root = {}'.format(root)) + # log_debug('dirs = {}'.format(text_type(dirs))) + # log_debug('files = {}'.format(text_type(files))) + # log_debug('\n') + for f in files: + my_file = os.path.join(root, f) + cache_file = my_file.replace(root_dir_str, '') + # In the cache always store paths as '/' and not as '\' + cache_file = cache_file.replace('\\', '/') + # Remove '/' character at the beginning of the file. If the directory dir_str + # is like '/example/dir/' then the slash at the beginning will be removed. However, + # if dir_str is like '/example/dir' it will be present. + if cache_file.startswith('/'): cache_file = cache_file[1:] + file_list.append(cache_file) + file_set = set(file_list) + if verbose: + # for file in file_set: log_debug('File "{}"'.format(file)) + log_debug('utils_file_cache_add_dir() Adding {} files to cache'.format(len(file_set))) + file_cache[dir_str] = file_set + +# See utils_look_for_file() documentation below. +def utils_file_cache_search(dir_str, filename_noext, file_exts): + # Check for empty, unconfigured dirs + if not dir_str: return None + current_cache_set = file_cache[dir_str] + # if filename_noext == '005': + # log_debug('utils_file_cache_search() Searching in "{}"'.format(dir_str)) + # log_debug('utils_file_cache_search() current_cache_set "{}"'.format(text_type(current_cache_set))) + for ext in file_exts: + file_base = filename_noext + '.' + ext + # log_debug('utils_file_cache_search() file_Base = "{}"'.format(file_base)) + if file_base in current_cache_set: + # log_debug('utils_file_cache_search() Found in cache') + return FileName(dir_str).pjoin(file_base) + return None + +# Given the image path, image filename with no extension and a list of file +# extensions search for a file. +# +# rootPath -> FileName object +# filename_noext -> Unicode string +# file_exts -> list of extensions with no dot ['zip', 'rar'] +# +# Returns a FileName object if a valid filename is found. +# Returns None if no file was found. +def utils_look_for_file(rootPath, filename_noext, file_exts): + for ext in file_exts: + file_path = rootPath.pjoin(filename_noext + '.' + ext) + if file_path.exists(): return file_path + return None + +# ------------------------------------------------------------------------------------------------- +# Logging functions. +# AEL never uses LOG_FATAL. Fatal error in my addons use LOG_ERROR. When an ERROR message is +# printed the addon must stop execution and exit. +# Kodi Matrix has changed the log levels. +# Valid set of log levels should now be: DEBUG, INFO, WARNING, ERROR and FATAL +# +# @python_v17 Default level changed from LOGNOTICE to LOGDEBUG +# @python_v19 Removed LOGNOTICE (use LOGINFO) and LOGSEVERE (use LOGFATAL) +# +# https://forum.kodi.tv/showthread.php?tid=344263&pid=2943703#pid2943703 +# https://github.com/xbmc/xbmc/pull/17730 +# ------------------------------------------------------------------------------------------------- +# Constants +LOG_ERROR = 0 +LOG_WARNING = 1 +LOG_INFO = 2 +LOG_DEBUG = 3 + +# Internal globals +current_log_level = LOG_INFO + +def set_log_level(level): + global current_log_level + current_log_level = level + +def log_variable(var_name, var): + if current_log_level < LOG_DEBUG: return + log_text = '{} DUMP : Dumping variable "{}"\n{}'.format(ADDON_SHORT_NAME, + var_name, pprint.pformat(var)) + xbmc.log(log_text, level = xbmc.LOGERROR) + +# For Unicode stuff in Kodi log see https://github.com/romanvm/kodi.six +def log_debug_KR(text_line): + if current_log_level < LOG_DEBUG: return + + # If it is bytes we assume it's "utf-8" encoded. + # will fail if called with other encodings (latin, etc). + if isinstance(text_line, binary_type): text_line = text_line.decode('utf-8') + + # At this point we are sure text_line is a Unicode string. + # Kodi functions (Python 3) require Unicode strings as arguments. + # Kodi functions (Python 2) require UTF-8 encoded bytes as arguments. + log_text = ADDON_SHORT_NAME + ' DEBUG: ' + text_line + xbmc.log(log_text, level = xbmc.LOGINFO) + +def log_info_KR(text_line): + if current_log_level < LOG_INFO: return + if isinstance(text_line, binary_type): text_line = text_line.decode('utf-8') + log_text = ADDON_SHORT_NAME + ' INFO : ' + text_line + xbmc.log(log_text, level = xbmc.LOGINFO) + +def log_warning_KR(text_line): + if current_log_level < LOG_WARNING: return + if isinstance(text_line, binary_type): text_line = text_line.decode('utf-8') + log_text = ADDON_SHORT_NAME + ' WARN : ' + text_line + xbmc.log(log_text, level = xbmc.LOGWARNING) + +def log_error_KR(text_line): + if current_log_level < LOG_ERROR: return + if isinstance(text_line, binary_type): text_line = text_line.decode('utf-8') + log_text = ADDON_SHORT_NAME + ' ERROR: ' + text_line + xbmc.log(log_text, level = xbmc.LOGERROR) + +# Replacement functions when running outside Kodi with the standard Python interpreter. +def log_debug_Python(text_line): print(text_line) + +def log_info_Python(text_line): print(text_line) + +def log_warning_Python(text_line): print(text_line) + +def log_error_Python(text_line): print(text_line) + +# ------------------------------------------------------------------------------------------------- +# Kodi notifications and dialogs +# ------------------------------------------------------------------------------------------------- +# Displays a modal dialog with an OK button. Dialog can have up to 3 rows of text, however first +# row is multiline. +# Call examples: +# 1) ret = kodi_dialog_OK('Launch ROM?') +# 2) ret = kodi_dialog_OK('Launch ROM?', title = 'AML - Launcher') +def kodi_dialog_OK(text, title = ADDON_LONG_NAME): + xbmcgui.Dialog().ok(title, text) + +# Returns True is YES was pressed, returns False if NO was pressed or dialog canceled. +def kodi_dialog_yesno(text, title = ADDON_LONG_NAME): + return xbmcgui.Dialog().yesno(title, text) + +# Returns True is YES was pressed, returns False if NO was pressed or dialog canceled. +def kodi_dialog_yesno_custom(text, yeslabel_str, nolabel_str, title = ADDON_LONG_NAME): + return xbmcgui.Dialog().yesno(title, text, yeslabel = yeslabel_str, nolabel = nolabel_str) + +def kodi_dialog_yesno_timer(text, timer_ms = 30000, title = ADDON_LONG_NAME): + return xbmcgui.Dialog().yesno(title, text, autoclose = timer_ms) + +# Returns a directory. See https://codedocs.xyz/AlwinEsch/kodi +# +# This supports directories, files, images and writable directories. +# xbmcgui.Dialog().browse(type, heading, shares[, mask, useThumbs, treatAsFolder, defaultt, enableMultiple]) +# +# This supports files and images only. +# xbmcgui.Dialog().browseMultiple(type, heading, shares[, mask, useThumbs, treatAsFolder, defaultt]) +# +# This supports directories, files, images and writable directories. +# xbmcgui.Dialog().browseSingle(type, heading, shares[, mask, useThumbs, treatAsFolder, defaultt]) +# +# shares string or unicode - from sources.xml +# "files" list file sources (added through filemanager) +# "local" list local drives +# "" list local drives and network shares + +# Returns a directory. +def kodi_dialog_get_directory(d_heading, d_dir = ''): + if d_dir: + ret = xbmcgui.Dialog().browse(0, d_heading, '', defaultt = d_dir) + else: + ret = xbmcgui.Dialog().browse(0, d_heading, '') + + return ret + +# Mask is supported only for files. +# mask [opt] string or unicode - '|' separated file mask. (i.e. '.jpg|.png') +# +# KODI BUG For some reason *.dat files are not shown on the dialog, but XML files are OK!!! +# Fixed in Krypton Beta 6 http://forum.kodi.tv/showthread.php?tid=298161 +def kodi_dialog_get_file(d_heading, mask = '', default_file = ''): + if mask and default_file: + ret = xbmcgui.Dialog().browse(1, d_heading, '', mask = mask, defaultt = default_file) + elif default_file: + ret = xbmcgui.Dialog().browse(1, d_heading, '', defaultt = default_file) + elif mask: + ret = xbmcgui.Dialog().browse(1, d_heading, '', mask = mask) + else: + ret = xbmcgui.Dialog().browse(1, d_heading, '') + + return ret + +def kodi_dialog_get_image(d_heading, mask = '', default_file = ''): + if mask and default_file: + ret = xbmcgui.Dialog().browse(2, d_heading, '', mask = mask, defaultt = default_file) + elif default_file: + ret = xbmcgui.Dialog().browse(2, d_heading, '', defaultt = default_file) + elif mask: + ret = xbmcgui.Dialog().browse(2, d_heading, '', mask = mask) + else: + ret = xbmcgui.Dialog().browse(2, d_heading, '') + + return ret + +def kodi_dialog_get_wdirectory(d_heading): + return xbmcgui.Dialog().browse(3, d_heading, '') + +# Select multiple versions of the avobe functions. +def kodi_dialog_get_file_multiple(d_heading, mask = '', d_file = ''): + if mask and d_file: + ret = xbmcgui.Dialog().browse(1, d_heading, '', mask = mask, defaultt = d_file, enableMultiple = True) + elif d_file: + ret = xbmcgui.Dialog().browse(1, d_heading, '', defaultt = d_file, enableMultiple = True) + elif mask: + ret = xbmcgui.Dialog().browse(1, d_heading, '', mask = mask, enableMultiple = True) + else: + ret = xbmcgui.Dialog().browse(1, d_heading, '', enableMultiple = True) + + return ret + +# Displays a small box in the bottom right corner +def kodi_notify(text, title = ADDON_LONG_NAME, time = 5000): + xbmcgui.Dialog().notification(title, text, xbmcgui.NOTIFICATION_INFO, time) + +def kodi_notify_warn(text, title = ADDON_LONG_NAME, time = 7000): + xbmcgui.Dialog().notification(title, text, xbmcgui.NOTIFICATION_WARNING, time) + +# Do not use this function much because it is the same icon displayed when Python fails +# with an exception and that may confuse the user. +def kodi_notify_error(text, title = ADDON_LONG_NAME, time = 7000): + xbmcgui.Dialog().notification(title, text, xbmcgui.NOTIFICATION_ERROR, time) + +def kodi_refresh_container(): + log_debug('kodi_refresh_container()') + xbmc.executebuiltin('Container.Refresh') + +# Progress dialog that can be closed and reopened. +# Messages and progress in the dialog are always remembered, even if closed and reopened. +# If the dialog is canceled this class remembers it forever. +# +# Kodi Matrix change: Renamed option line1 to message. Removed option line2. Removed option line3. +# See https://forum.kodi.tv/showthread.php?tid=344263&pid=2933596#pid2933596 +# +# --- Example 1 --- +# pDialog = KodiProgressDialog() +# pDialog.startProgress('Doing something...', step_total) +# for i in range(): +# pDialog.updateProgressInc() +# # Do stuff... +# pDialog.endProgress() +class KodiProgressDialog(object): + def __init__(self): + self.heading = ADDON_LONG_NAME + self.progress = 0 + self.flag_dialog_canceled = False + self.dialog_active = False + self.progressDialog = xbmcgui.DialogProgress() + + # Creates a new progress dialog. + def startProgress(self, message, step_total = 100, step_counter = 0): + if self.dialog_active: raise TypeError + self.step_total = step_total + self.step_counter = step_counter + try: + self.progress = math.floor((self.step_counter * 100) / self.step_total) + except ZeroDivisionError: + # Fix case when step_total is 0. + self.step_total = 0.001 + self.progress = math.floor((self.step_counter * 100) / self.step_total) + self.dialog_active = True + self.message = message + # In Leia and lower xbmcgui.DialogProgress().update() requires an int. + if kodi_running_version >= KODI_VERSION_MATRIX: + self.progressDialog.create(self.heading, self.message) + self.progressDialog.update(self.progress) + else: + self.progressDialog.create(self.heading, self.message, ' ', ' ') + self.progressDialog.update(int(self.progress)) + + # Changes message and resets progress. + def resetProgress(self, message, step_total = 100, step_counter = 0): + if not self.dialog_active: raise TypeError + self.step_total = step_total + self.step_counter = step_counter + try: + self.progress = math.floor((self.step_counter * 100) / self.step_total) + except ZeroDivisionError: + # Fix case when step_total is 0. + self.step_total = 0.001 + self.progress = math.floor((self.step_counter * 100) / self.step_total) + self.message = message + if kodi_running_version >= KODI_VERSION_MATRIX: + self.progressDialog.update(self.progress, self.message) + else: + self.progressDialog.update(int(self.progress), self.message, ' ', ' ') + + # Update progress and optionally update message as well. + def updateProgress(self, step_counter, message = None): + if not self.dialog_active: raise TypeError + self.step_counter = step_counter + self.progress = math.floor((self.step_counter * 100) / self.step_total) + if message is None: + if kodi_running_version >= KODI_VERSION_MATRIX: + self.progressDialog.update(self.progress) + else: + self.progressDialog.update(int(self.progress)) + else: + if type(message) is not text_type: raise TypeError + self.message = message + if kodi_running_version >= KODI_VERSION_MATRIX: + self.progressDialog.update(self.progress, self.message) + else: + self.progressDialog.update(int(self.progress), self.message, ' ', ' ') + # DEBUG code + # time.sleep(1) + + # Update progress, optionally update message as well, and autoincrements. + # Progress is incremented AFTER dialog is updated. + def updateProgressInc(self, message = None): + if not self.dialog_active: raise TypeError + self.progress = math.floor((self.step_counter * 100) / self.step_total) + self.step_counter += 1 + if message is None: + if kodi_running_version >= KODI_VERSION_MATRIX: + self.progressDialog.update(self.progress) + else: + self.progressDialog.update(int(self.progress)) + else: + if type(message) is not text_type: raise TypeError + self.message = message + if kodi_running_version >= KODI_VERSION_MATRIX: + self.progressDialog.update(self.progress, self.message) + else: + self.progressDialog.update(int(self.progress), self.message, ' ', ' ') + + # Update dialog message but keep same progress. + def updateMessage(self, message): + if not self.dialog_active: raise TypeError + if type(message) is not text_type: raise TypeError + self.message = message + if kodi_running_version >= KODI_VERSION_MATRIX: + self.progressDialog.update(self.progress, self.message) + else: + self.progressDialog.update(int(self.progress), self.message, ' ', ' ') + + def isCanceled(self): + # If the user pressed the cancel button before then return it now. + if self.flag_dialog_canceled: return True + # If not check and set the flag. + if not self.dialog_active: raise TypeError + self.flag_dialog_canceled = self.progressDialog.iscanceled() + return self.flag_dialog_canceled + + # Before closing the dialog check if the user pressed the Cancel button and remember + # the user decision. + def endProgress(self): + if not self.dialog_active: raise TypeError + if self.progressDialog.iscanceled(): self.flag_dialog_canceled = True + self.progressDialog.update(100) + self.progressDialog.close() + self.dialog_active = False + + # Like endProgress() but do not completely fills the progress bar. + def close(self): + if not self.dialog_active: raise TypeError + if self.progressDialog.iscanceled(): self.flag_dialog_canceled = True + self.progressDialog.close() + self.dialog_active = False + + # Reopens a previously closed dialog with close(), remembering the messages + # and the progress it had when it was closed. + def reopen(self): + if self.dialog_active: raise TypeError + if kodi_running_version >= KODI_VERSION_MATRIX: + self.progressDialog.create(self.heading, self.message) + self.progressDialog.update(self.progress) + else: + self.progressDialog.create(self.heading, self.message, ' ', ' ') + self.progressDialog.update(int(self.progress)) + self.dialog_active = True + +# Wrapper class for xbmcgui.Dialog().select(). Takes care of Kodi bugs. +# v17 (Krypton) Python API changes: +# Preselect option added. +# Added new option useDetails. +# Allow listitems for parameter list +class KodiSelectDialog(object): + def __init__(self, heading = ADDON_LONG_NAME, rows = [], preselect = -1, useDetails = False): + self.heading = heading + self.rows = rows + self.preselect = preselect + self.useDetails = useDetails + self.dialog = xbmcgui.Dialog() + + def setHeading(self, heading): self.heading = heading + + def setRows(self, row_list): self.rows = row_list + + def setPreselect(self, preselect): self.preselect = preselect + + def setUseDetails(self, useDetails): self.useDetails = useDetails + + def executeDialog(self): + # Kodi Krypton bug: if preselect is used then dialog never returns < 0 even if cancel + # button is pressed. This bug has been solved in Leia. + # See https://forum.kodi.tv/showthread.php?tid=337011 + if self.preselect >= 0 and kodi_running_version >= KODI_VERSION_LEIA: + selection = self.dialog.select(self.heading, self.rows, useDetails = self.useDetails, + preselect = self.preselect) + else: + selection = self.dialog.select(self.heading, self.rows, useDetails = self.useDetails) + selection = None if selection < 0 else selection + return selection + +# Wrapper class for xbmc.Keyboard() +class KodiKeyboardDialog(object): + def __init__(self, heading = 'Kodi keyboard', default_text = ''): + self.heading = heading + self.default_text = default_text + self.keyboard = xbmc.Keyboard() + + def setHeading(self, heading): self.heading = heading + + def setDefaultText(self, default_text): self.default_text = default_text + + def executeDialog(self): + self.keyboard.setHeading(self.heading) + self.keyboard.setDefault(self.default_text) + self.keyboard.doModal() + + def isConfirmed(self): return self.keyboard.isConfirmed() + + # Use a different name from getText() to avoid coding errors. + def getData(self): return self.keyboard.getText() + +# Wrapper function to get a text from the keyboard or None if the keyboard +# modal dialog was canceled. +def kodi_get_keyboard_text(heading = 'Kodi keyboard', default_text = ''): + keyboard = KodiKeyboardDialog(heading, default_text) + keyboard.executeDialog() + if not keyboard.isConfirmed(): return None + new_value_str = keyboard.getData().strip() + return new_value_str + +def kodi_toogle_fullscreen(): + kodi_jsonrpc_dict('Input.ExecuteAction', {'action' : 'togglefullscreen'}) + +def kodi_get_screensaver_mode(): + r_dic = kodi_jsonrpc_dict('Settings.getSettingValue', {'setting' : 'screensaver.mode'}) + screensaver_mode = r_dic['value'] + return screensaver_mode + +g_screensaver_mode = None # Global variable to store screensaver status. +def kodi_disable_screensaver(): + global g_screensaver_mode + g_screensaver_mode = kodi_get_screensaver_mode() + log_debug('kodi_disable_screensaver() g_screensaver_mode "{}"'.format(g_screensaver_mode)) + p_dic = { + 'setting' : 'screensaver.mode', + 'value' : '', + } + kodi_jsonrpc_dict('Settings.setSettingValue', p_dic) + log_debug('kodi_disable_screensaver() Screensaver disabled.') + +# kodi_disable_screensaver() must be called before this function or bad things will happen. +def kodi_restore_screensaver(): + if g_screensaver_mode is None: + log_error('kodi_disable_screensaver() must be called before kodi_restore_screensaver()') + raise RuntimeError + log_debug('kodi_restore_screensaver() Screensaver mode "{}"'.format(g_screensaver_mode)) + p_dic = { + 'setting' : 'screensaver.mode', + 'value' : g_screensaver_mode, + } + kodi_jsonrpc_dict('Settings.setSettingValue', p_dic) + log_debug('kodi_restore_screensaver() Restored previous screensaver status.') + +# Access Kodi JSON-RPC interface in an easy way. +# Returns a dictionary with the parsed response 'result' field. +# +# Query input: +# +# { +# "id" : 1, +# "jsonrpc" : "2.0", +# "method" : "Application.GetProperties", +# "params" : { "properties" : ["name", "version"] } +# } +# +# Query response: +# +# { +# "id" : 1, +# "jsonrpc" : "2.0", +# "result" : { +# "name" : "Kodi", +# "version" : {"major":17,"minor":6,"revision":"20171114-a9a7a20","tag":"stable"} +# } +# } +# +# Query response ERROR: +# { +# "id" : null, +# "jsonrpc" : "2.0", +# "error" : { "code":-32700, "message" : "Parse error."} +# } +# +def kodi_jsonrpc_dict(method_str, params_dic, verbose = False): + params_str = json.dumps(params_dic) + if verbose: + log_debug('kodi_jsonrpc_dict() method_str "{}"'.format(method_str)) + log_debug('kodi_jsonrpc_dict() params_dic = \n{}'.format(pprint.pformat(params_dic))) + log_debug('kodi_jsonrpc_dict() params_str "{}"'.format(params_str)) + + # --- Do query --- + header = '"id" : 1, "jsonrpc" : "2.0"' + query_str = '{{{}, "method" : "{}", "params" : {} }}'.format(header, method_str, params_str) + response_json_str = xbmc.executeJSONRPC(query_str) + + # --- Parse JSON response --- + response_dic = json.loads(response_json_str) + if 'error' in response_dic: + result_dic = response_dic['error'] + log_warning('kodi_jsonrpc_dict() JSONRPC ERROR {}'.format(result_dic['message'])) + else: + result_dic = response_dic['result'] + if verbose: + log_debug('kodi_jsonrpc_dict() result_dic = \n{}'.format(pprint.pformat(result_dic))) + + return result_dic + +# Displays a text window and requests a monospaced font. +# v18 Leia change: New optional param added usemono. +def kodi_display_text_window_mono(window_title, info_text): + xbmcgui.Dialog().textviewer(window_title, info_text.encode('utf-8'), True) + +# Displays a text window with a proportional font (default). +def kodi_display_text_window(window_title, info_text): + xbmcgui.Dialog().textviewer(window_title, info_text.encode('utf-8')) + +# Displays a text window and requests a monospaced font. +# def kodi_display_text_window_mono(window_title, info_text): +# log_debug('Setting Window(10000) Property "FontWidth" = "monospaced"') +# xbmcgui.Window(10000).setProperty('FontWidth', 'monospaced') +# xbmcgui.Dialog().textviewer(window_title, info_text) +# log_debug('Setting Window(10000) Property "FontWidth" = "proportional"') +# xbmcgui.Window(10000).setProperty('FontWidth', 'proportional') + +# Displays a text window with a proportional font (default). +# def kodi_display_text_window(window_title, info_text): +# xbmcgui.Dialog().textviewer(window_title, info_text) + +# ------------------------------------------------------------------------------------------------- +# Kodi addon functions +# ------------------------------------------------------------------------------------------------- +class KodiAddon: pass + +def kodi_addon_obj(): + addon = KodiAddon() + + # Get an instance of the Addon object and keep it. + addon.addon = xbmcaddon.Addon() + + # Cache useful addon information. + addon.info_id = addon.addon.getAddonInfo('id') + addon.info_name = addon.addon.getAddonInfo('name') + addon.info_version = addon.addon.getAddonInfo('version') + addon.info_author = addon.addon.getAddonInfo('author') + addon.info_profile = addon.addon.getAddonInfo('profile') + addon.info_type = addon.addon.getAddonInfo('type') + + return addon + +# ------------------------------------------------------------------------------------------------- +# Abstraction layer for settings to easy the Leia-Matrix transition. +# Settings are only read once on every execution and they are not performance critical. +# ------------------------------------------------------------------------------------------------- +def kodi_get_int_setting(cfg, setting_str): + return cfg.addon.addon.getSettingInt(setting_str) + +def kodi_get_float_setting_as_int(cfg, setting_str): + return int(round(cfg.addon.addon.getSettingNumber(setting_str))) + +def kodi_get_bool_setting(cfg, setting_str): + return cfg.addon.addon.getSettingBool(setting_str) + +def kodi_get_str_setting(cfg, setting_str): + return cfg.addon.addon.getSettingString(setting_str) + +# ------------------------------------------------------------------------------------------------- +# Determine Kodi version and create some constants to allow version-dependent code. +# This if useful to work around bugs in Kodi core. +# ------------------------------------------------------------------------------------------------- +# Version constants. Minimum required version is Kodi Krypton. +KODI_VERSION_ISENGARD = 15 +KODI_VERSION_JARVIS = 16 +KODI_VERSION_KRYPTON = 17 +KODI_VERSION_LEIA = 18 +KODI_VERSION_MATRIX = 19 + +def kodi_get_Kodi_major_version(): + r_dic = kodi_jsonrpc_dict('Application.GetProperties', {'properties' : ['version']}) + return int(r_dic['version']['major']) + +# ------------------------------------------------------------------------------------------------- +# If running with Kodi Python interpreter use Kodi proper functions. +# If running with the standard Python interpreter use replacement functions. +# +# Functions here in the same order as in the Function List browser. +# ------------------------------------------------------------------------------------------------- +if KODI_RUNTIME_AVAILABLE_UTILS: + log_debug = log_debug_KR + log_info = log_info_KR + log_warning = log_warning_KR + log_error = log_error_KR + + # Execute the Kodi version query when module is loaded and store results in global variable. + kodi_running_version = kodi_get_Kodi_major_version() +else: + log_debug = log_debug_Python + log_info = log_info_Python + log_warning = log_warning_Python + log_error = log_error_Python + + # We are using this module with the Python interpreter outside Kodi. + # Simulate we are running a recent Kodi version. + kodi_running_version = KODI_VERSION_MATRIX + +# ------------------------------------------------------------------------------------------------- +# Kodi useful definition +# ------------------------------------------------------------------------------------------------- +# https://codedocs.xyz/AlwinEsch/kodi/group__kodi__guilib__listitem__iconoverlay.html +KODI_ICON_OVERLAY_NONE = 0 +KODI_ICON_OVERLAY_RAR = 1 +KODI_ICON_OVERLAY_ZIP = 2 +KODI_ICON_OVERLAY_LOCKED = 3 +KODI_ICON_OVERLAY_UNWATCHED = 4 +KODI_ICON_OVERLAY_WATCHED = 5 +KODI_ICON_OVERLAY_HD = 6 + +# ------------------------------------------------------------------------------------------------- +# Kodi GUI error reporting. +# * Errors can be reported up in the function backtrace with `if not st_dic['status']: return` after +# every function call. +# * Warnings and non-fatal messages are printed in the callee function. +# * If st_dic['status'] is True but st_dic['dialog'] is not KODI_MESSAGE_NONE then display +# the message but do not abort execution (success information message). +# * When kodi_display_status_message() is used to display the last message on a chaing of +# function calls it is irrelevant its return value because addon always finishes. +# +# How to use: +# def high_level_function(): +# st_dic = kodi_new_status_dic() +# function_that_does_something_that_may_fail(..., st_dic) +# if kodi_display_status_message(st_dic): return # Display error message and abort addon execution. +# if not st_dic['status']: return # Alternative code to return to caller function. +# +# def function_that_does_something_that_may_fail(..., st_dic): +# code_that_fails +# kodi_set_error_status(st_dic, 'Message') # Or change st_dic manually. +# return +# ------------------------------------------------------------------------------------------------- +KODI_MESSAGE_NONE = 100 +# Kodi notifications must be short. +KODI_MESSAGE_NOTIFY = 200 +KODI_MESSAGE_NOTIFY_WARN = 300 +# Kodi OK dialog to display a message. +KODI_MESSAGE_DIALOG = 400 + +# If st_dic['abort'] is False then everything is OK. +# If st_dic['abort'] is True then execution must be aborted and error displayed. +# Success message can also be displayed (st_dic['abort'] False and +# st_dic['dialog'] is different from KODI_MESSAGE_NONE). +def kodi_new_status_dic(): + return { + 'abort' : False, + 'dialog' : KODI_MESSAGE_NONE, + 'msg' : '', + } + +# Display an status/error message in the GUI. +# Note that it is perfectly OK to display an error message and not abort execution. +# Returns True in case of error and addon must abort/exit immediately. +# Returns False if no error. +# +# Example of use: if kodi_display_user_message(st_dic): return +def kodi_display_status_message(st_dic): + # Display (error) message and return status. + if st_dic['dialog'] == KODI_MESSAGE_NONE: + pass + elif st_dic['dialog'] == KODI_MESSAGE_NOTIFY: + kodi_notify(st_dic['msg']) + elif st_dic['dialog'] == KODI_MESSAGE_NOTIFY_WARN: + kodi_notify(st_dic['msg']) + elif st_dic['dialog'] == KODI_MESSAGE_DIALOG: + kodi_dialog_OK(st_dic['msg']) + else: + raise TypeError('st_dic["dialog"] = {}'.format(st_dic['dialog'])) + + return st_dic['abort'] + +def kodi_is_error_status(st_dic): return st_dic['abort'] + +# Utility function to write more compact code. +# By default error messages are shown in modal OK dialogs. +def kodi_set_error_status(st_dic, msg, dialog = KODI_MESSAGE_DIALOG): + st_dic['abort'] = True + st_dic['msg'] = msg + st_dic['dialog'] = dialog + +def kodi_reset_status(st_dic): + st_dic['abort'] = False + st_dic['msg'] = '' + st_dic['dialog'] = KODI_MESSAGE_NONE + +# ------------------------------------------------------------------------------------------------- +# Alternative Kodi GUI error reporting. +# This is a more phytonic way of reporting errors than using st_dic. +# ------------------------------------------------------------------------------------------------- +# Create a Exception-derived class and use that for reporting. +# +# Example code: +# try: +# function_that_may_fail() +# except KodiAddonError as ex: +# kodi_display_status_message(ex) +# else: +# kodi_notify('Operation completed') +# +# def function_that_may_fail(): +# raise KodiAddonError(msg, dialog) +class KodiAddonError(Exception): + def __init__(self, msg, dialog = KODI_MESSAGE_DIALOG): + self.dialog = dialog + self.msg = msg + + def __str__(self): + return self.msg + +def kodi_display_exception(ex): + st_dic = kodi_new_status_dic() + st_dic['abort'] = True + st_dic['dialog'] = ex.dialog + st_dic['msg'] = ex.msg + kodi_display_status_message(st_dic) + +# ------------------------------------------------------------------------------------------------- +# Kodi specific stuff +# ------------------------------------------------------------------------------------------------- +# About Kodi image cache +# +# See http://kodi.wiki/view/Caches_explained +# See http://kodi.wiki/view/Artwork +# See http://kodi.wiki/view/HOW-TO:Reduce_disk_space_usage +# See http://forum.kodi.tv/showthread.php?tid=139568 (What are .tbn files for?) +# +# Whenever Kodi downloads images from the internet, or even loads local images saved along +# side your media, it caches these images inside of ~/.kodi/userdata/Thumbnails/. By default, +# large images are scaled down to the default values shown below, but they can be sized +# even smaller to save additional space. + +# Gets where in Kodi image cache an image is located. +# image_path is a Unicode string. +# cache_file_path is a Unicode string. +def kodi_get_cached_image_FN(image_path): + THUMBS_CACHE_PATH = os.path.join(xbmc.translatePath('special://profile/' ), 'Thumbnails') + + # --- Get the Kodi cached image --- + # This function return the cache file base name + base_name = xbmc.getCacheThumbName(image_path) + cache_file_path = os.path.join(THUMBS_CACHE_PATH, base_name[0], base_name) + + return cache_file_path + +# Updates Kodi image cache for the image provided in img_path. +# In other words, copies the image img_path into Kodi cache entry. +# Needles to say, only update image cache if image already was on the cache. +# img_path is a Unicode string +def kodi_update_image_cache(img_path): + # What if image is not cached? + cached_thumb = kodi_get_cached_image_FN(img_path) + log_debug('kodi_update_image_cache() img_path {}'.format(img_path)) + log_debug('kodi_update_image_cache() cached_thumb {}'.format(cached_thumb)) + + # For some reason Kodi xbmc.getCacheThumbName() returns a filename ending in TBN. + # However, images in the cache have the original extension. Replace TBN extension + # with that of the original image. + cached_thumb_root, cached_thumb_ext = os.path.splitext(cached_thumb) + if cached_thumb_ext == '.tbn': + img_path_root, img_path_ext = os.path.splitext(img_path) + cached_thumb = cached_thumb.replace('.tbn', img_path_ext) + log_debug('kodi_update_image_cache() U cached_thumb {}'.format(cached_thumb)) + + # --- Check if file exists in the cache --- + # xbmc.getCacheThumbName() seems to return a filename even if the local file does not exist! + if not os.path.isfile(cached_thumb): + log_debug('kodi_update_image_cache() Cached image not found. Doing nothing') + return + + # --- Copy local image into Kodi image cache --- + # See https://docs.python.org/2/library/sys.html#sys.getfilesystemencoding + log_debug('kodi_update_image_cache() Image found in cache. Updating Kodi image cache') + log_debug('kodi_update_image_cache() copying {}'.format(img_path)) + log_debug('kodi_update_image_cache() into {}'.format(cached_thumb)) + fs_encoding = sys.getfilesystemencoding() + log_debug('kodi_update_image_cache() fs_encoding = "{}"'.format(fs_encoding)) + encoded_img_path = img_path.encode(fs_encoding, 'ignore') + encoded_cached_thumb = cached_thumb.encode(fs_encoding, 'ignore') + try: + shutil.copy2(encoded_img_path, encoded_cached_thumb) + except OSError: + log_kodi_notify_warn('AEL warning', 'Cannot update cached image (OSError)') + lod_error('Exception in kodi_update_image_cache()') + lod_error('(OSError) Cannot update cached image') + + # Is this really needed? + # xbmc.executebuiltin('XBMC.ReloadSkin()') diff --git a/plugin.program.AML/templates/3dbox_angleY_56.json b/plugin.program.AML/templates/3dbox_angleY_56.json new file mode 100644 index 0000000000..ebf46f2eb4 --- /dev/null +++ b/plugin.program.AML/templates/3dbox_angleY_56.json @@ -0,0 +1,121 @@ +{ + "data": { + "angleX": -90, + "angleY": -56, + "angleZ": 0, + "fov": 1250, + "viewer_distance": 3000, + "spine_width": 300, + "spine_length": 1500, + "box_width": 1000, + "box_length": 1500 + }, + "Spine": [ + [ + 62.00359192493244, + 90.89324310885877 + ], + [ + 181.59307732728723, + 27.74830693319086 + ], + [ + 181.59307732728735, + 1472.2516930668091 + ], + [ + 62.00359192493255, + 1409.1067568911412 + ] + ], + "Frontbox": [ + [ + 181.59307732728723, + 27.74830693319086 + ], + [ + 894.8967375914328, + 155.750587853742 + ], + [ + 894.896737591433, + 1344.249412146258 + ], + [ + 181.59307732728735, + 1472.2516930668091 + ] + ], + "Flyer": [ + [ + 215.97485840814795, + 138.9434578022889 + ], + [ + 871.2536416959471, + 239.28668698823833 + ], + [ + 871.2536416959472, + 1316.572581622423 + ], + [ + 215.97485840814807, + 1427.8908515005858 + ] + ], + "Clearlogo": [ + [ + 76.72642068014738, + 936.7265731160364 + ], + [ + 164.34057625487276, + 949.6797961197688 + ], + [ + 164.34057625487281, + 1425.1078821192182 + ], + [ + 76.72642068014744, + 1381.313651963742 + ] + ], + "Clearlogo_MAME": [ + [ + 76.72642068014733, + 118.68634803625798 + ], + [ + 164.3405762548727, + 74.89211788078194 + ], + [ + 164.34057625487276, + 550.3202038802312 + ], + [ + 76.72642068014738, + 563.2734268839637 + ] + ], + "Front_Title": [ + [ + 215.97485840814795, + 72.10914849941423 + ], + [ + 871.2536416959471, + 183.42741837757694 + ], + [ + 871.2536416959471, + 231.3067914724296 + ], + [ + 215.97485840814795, + 129.3956993304497 + ] + ] +} \ No newline at end of file diff --git a/plugin.program.AML/templates/3dbox_angleY_60.json b/plugin.program.AML/templates/3dbox_angleY_60.json new file mode 100644 index 0000000000..f2b9669b62 --- /dev/null +++ b/plugin.program.AML/templates/3dbox_angleY_60.json @@ -0,0 +1,121 @@ +{ + "data": { + "angleX": -90, + "angleY": -60, + "angleZ": 0, + "fov": 1250, + "viewer_distance": 3000, + "spine_width": 300, + "spine_length": 1500, + "box_width": 1000, + "box_length": 1500 + }, + "Spine": [ + [ + 59.00202289040726, + 98.9365883958418 + ], + [ + 158.39736787508377, + 34.37741806485565 + ], + [ + 158.3973678750839, + 1465.6225819351444 + ], + [ + 59.002022890407375, + 1401.0634116041583 + ] + ], + "Frontbox": [ + [ + 158.39736787508377, + 34.37741806485565 + ], + [ + 907.0489105534967, + 149.05698857920947 + ], + [ + 907.0489105534969, + 1350.9430114207905 + ], + [ + 158.3973678750839, + 1465.6225819351444 + ] + ], + "Flyer": [ + [ + 193.7879697004787, + 143.96147519002807 + ], + [ + 881.7395145306136, + 233.88698213490602 + ], + [ + 881.7395145306136, + 1322.5628791940885 + ], + [ + 193.7879697004788, + 1422.3239884610625 + ] + ], + "Clearlogo": [ + [ + 71.20595077375259, + 934.5172266794797 + ], + [ + 144.0206010813945, + 947.7596874356403 + ], + [ + 144.02060108139455, + 1418.6160860919267 + ], + [ + 71.20595077375259, + 1373.843956868717 + ] + ], + "Clearlogo_MAME": [ + [ + 71.20595077375253, + 126.15604313128301 + ], + [ + 144.02060108139443, + 81.38391390807328 + ], + [ + 144.0206010813945, + 552.2403125643598 + ], + [ + 71.20595077375259, + 565.4827733205203 + ] + ], + "Front_Title": [ + [ + 193.7879697004787, + 77.67601153893736 + ], + [ + 881.7395145306136, + 177.43712080591138 + ], + [ + 881.7395145306136, + 225.82271623076394 + ], + [ + 193.7879697004787, + 134.49212323987217 + ] + ] +} \ No newline at end of file diff --git a/plugin.program.AML/templates/AML-MAME-Fanart-template.xml b/plugin.program.AML/templates/AML-MAME-Fanart-template.xml new file mode 100644 index 0000000000..40e8365d91 --- /dev/null +++ b/plugin.program.AML/templates/AML-MAME-Fanart-template.xml @@ -0,0 +1,69 @@ +<!-- + AML MAME Fanart template. + Images/assets are drawn in the order they appear in the XML. + Fanart size is 1920 x 1080. +--> +<Advanced_MAME_Launcher_MAME_Fanart> + <!-- Artwork images --> + <Title> + <width>450</width> + <height>450</height> + <left>50</left> + <top>50</top> + + + 450 + 450 + 50 + 550 + + + 450 + 450 + 1420 + 50 + + + 300 + 425 + 1050 + 625 + + + 450 + 550 + 550 + 500 + + + 300 + 300 + 1500 + 525 + + + 450 + 200 + 1400 + 850 + + + 300 + 100 + 1050 + 500 + + + 800 + 275 + 550 + 175 + + + + + 550 + 50 + 72 + + diff --git a/plugin.program.AML/templates/AML-SL-Fanart-template.xml b/plugin.program.AML/templates/AML-SL-Fanart-template.xml new file mode 100644 index 0000000000..5c8b059035 --- /dev/null +++ b/plugin.program.AML/templates/AML-SL-Fanart-template.xml @@ -0,0 +1,39 @@ + + + + + 650 + 980 + 100 + 50 + + + + 950 + 710 + 840 + 320 + + + <width>350</width> + <height>262</height> + <left>1460</left> + <top>50</top> + + + + + 840 + 50 + 76 + + + 840 + 130 + 76 + + diff --git a/plugin.program.AML/templates/datafile.dtd b/plugin.program.AML/templates/datafile.dtd new file mode 100644 index 0000000000..ab86446ee4 --- /dev/null +++ b/plugin.program.AML/templates/datafile.dtd @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin.program.AML/version.sha b/plugin.program.AML/version.sha new file mode 100644 index 0000000000..ac34a29c40 --- /dev/null +++ b/plugin.program.AML/version.sha @@ -0,0 +1 @@ +7bead1a50beea31e1fbee35b9ff137a0dd97b491 diff --git a/plugin.program.autocompletion/LICENSE.txt b/plugin.program.autocompletion/LICENSE.txt new file mode 100644 index 0000000000..602bfc9463 --- /dev/null +++ b/plugin.program.autocompletion/LICENSE.txt @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/plugin.program.autocompletion/addon.xml b/plugin.program.autocompletion/addon.xml new file mode 100644 index 0000000000..da8caa2d4c --- /dev/null +++ b/plugin.program.autocompletion/addon.xml @@ -0,0 +1,27 @@ + + + + + + + + executable + + + + + AutoCompletion for the virtual keyboard (needs skin support) + AutoCompletion for the virtual keyboard (needs skin support) + Autocompletar para el teclado virtual (necesita soporte de skin) + Autocompletar para el teclado virtual (necesita soporte de skin) + all + GPL-2.0-or-later + https://github.com/finkleandeinhorn/plugin.program.autocompletion + + resources/icon.png + resources/screenshot-01.jpg + resources/screenshot-02.jpg + resources/screenshot-03.jpg + + + diff --git a/plugin.program.autocompletion/changelog.txt b/plugin.program.autocompletion/changelog.txt new file mode 100644 index 0000000000..086becec6b --- /dev/null +++ b/plugin.program.autocompletion/changelog.txt @@ -0,0 +1,7 @@ +1.0.1 + +- cleanup, take over some listitem work from AutoCompletion lib + +1.0.0 + +- Initial release diff --git a/plugin.program.autocompletion/default.py b/plugin.program.autocompletion/default.py new file mode 100644 index 0000000000..0428a15277 --- /dev/null +++ b/plugin.program.autocompletion/default.py @@ -0,0 +1,16 @@ +# -*- coding: utf8 -*- + +# Copyright (C) 2015 - Philipp Temminghoff +# This program is Free Software see LICENSE file for details + +import xbmc +import xbmcaddon + +ADDON = xbmcaddon.Addon() +ADDON_VERSION = ADDON.getAddonInfo('version') + + +xbmc.log("version %s started" % ADDON_VERSION) +ADDON.openSettings() +xbmc.log('finished') + diff --git a/plugin.program.autocompletion/plugin.py b/plugin.program.autocompletion/plugin.py new file mode 100644 index 0000000000..2cad226152 --- /dev/null +++ b/plugin.program.autocompletion/plugin.py @@ -0,0 +1,99 @@ +# -*- coding: utf8 -*- + +# Copyright (C) 2015 - Philipp Temminghoff +# This program is Free Software see LICENSE file for details + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin + +import json +import sys +from urllib.parse import parse_qsl + +import AutoCompletion + +ADDON = xbmcaddon.Addon() +ADDON_VERSION = ADDON.getAddonInfo('version') + + +def get_kodi_json(method, params): + query_params = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params} + json_query = xbmc.executeJSONRPC(json.dumps(query_params)) + + return json.loads(json_query) + + +def start_info_actions(infos, params): + listitems = [] + for info in infos: + if info == 'autocomplete': + listitems = AutoCompletion.get_autocomplete_items(params["id"], params.get("limit", 10)) + elif info == 'selectautocomplete': + xbmc.executebuiltin('Dialog.Close(busydialog)') + xbmc.sleep(500) + get_kodi_json( + method="Input.SendText", + params={"text": params.get("id"), "done": False}, + ) + xbmc.executebuiltin('ActivateWindowAndFocus(virtualkeyboard,300,0)') + + pass_list_to_skin( + data=listitems, + handle=params.get("handle", ""), + limit=params.get("limit", 20), + ) + + +def pass_list_to_skin(data=[], handle=None, limit=False): + if data and limit and int(limit) < len(data): + data = data[: int(limit)] + + if handle and data: + items = create_listitems(data) + xbmcplugin.addDirectoryItems( + handle=handle, + items=[(i.getProperty("path"), i, False) for i in items], + totalItems=len(items), + ) + xbmc.executebuiltin('Dialog.Close(busydialog)') + xbmcplugin.endOfDirectory(handle) + + +def create_listitems(data=None): + if not data: + return [] + itemlist = [] + for count, result in enumerate(data): + listitem = xbmcgui.ListItem(str(count)) + for key, value in result.items(): + if not value: + continue + if key.lower() in ["label"]: + listitem.setLabel(value) + elif key.lower() in ["search_string"]: + path = f"plugin://plugin.program.autocompletion/?info=selectautocomplete&id={value}" + listitem.setPath(path=path) + listitem.setProperty('path', path) + listitem.setProperty("index", str(count)) + listitem.setProperty("isPlayable", "false") + itemlist.append(listitem) + return itemlist + + +if __name__ == "__main__": + xbmc.log(f"version {ADDON_VERSION} started") + args = sys.argv[2][1:] + handle = int(sys.argv[1]) + infos = [] + params = {"handle": handle} + params.update(dict(parse_qsl(args, keep_blank_values=True))) + + if "info" in params: + infos.append(params['info']) + + if infos: + start_info_actions(infos, params) + +xbmc.log('finished') diff --git a/plugin.program.autocompletion/resources/icon.png b/plugin.program.autocompletion/resources/icon.png new file mode 100644 index 0000000000..a30fc6930a Binary files /dev/null and b/plugin.program.autocompletion/resources/icon.png differ diff --git a/plugin.program.autocompletion/resources/language/resource.language.en_gb/strings.po b/plugin.program.autocompletion/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..a2786a7a24 --- /dev/null +++ b/plugin.program.autocompletion/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,40 @@ +# Kodi language file +# Addon Name: AutoCompletion for virtual keyboard +# Addon id: plugin.program.autocompletion +# Addon version: 1.0.0 +# Addon Provider: phil65 +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "AutoCompletion provider" +msgstr "" + +msgctxt "Addon Description" +msgid "" +"This script offers various possibilities to display online content inside " +"Needs skin support for most functions." +msgstr "" + +msgctxt "#32001" +msgid "Autocompletion" +msgstr "" + +msgctxt "#32002" +msgid "Autocompletion language" +msgstr "" + +msgctxt "#32003" +msgid "Autocompletion provider" +msgstr "" diff --git a/plugin.program.autocompletion/resources/language/resource.language.es_es/strings.po b/plugin.program.autocompletion/resources/language/resource.language.es_es/strings.po new file mode 100644 index 0000000000..48306ae9bb --- /dev/null +++ b/plugin.program.autocompletion/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,40 @@ +# Kodi language file +# Addon Name: AutoCompletion for virtual keyboard +# Addon id: plugin.program.autocompletion +# Addon version: 1.0.0 +# Addon Provider: phil65 +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "AutoCompletion provider" +msgstr "Proveedor de autocompletado" + +msgctxt "Addon Description" +msgid "" +"This script offers various possibilities to display online content inside " +"Needs skin support for most functions." +msgstr "" + +msgctxt "#32001" +msgid "Autocompletion" +msgstr "Autocompletado" + +msgctxt "#32002" +msgid "Autocompletion language" +msgstr "Idioma de autocompletado" + +msgctxt "#32003" +msgid "Autocompletion provider" +msgstr "Proveedor para autocompletado" diff --git a/plugin.program.autocompletion/resources/language/resource.language.fr_fr/strings.po b/plugin.program.autocompletion/resources/language/resource.language.fr_fr/strings.po new file mode 100644 index 0000000000..cadbe28752 --- /dev/null +++ b/plugin.program.autocompletion/resources/language/resource.language.fr_fr/strings.po @@ -0,0 +1,40 @@ +# Kodi language file +# Addon Name: AutoCompletion for virtual keyboard +# Addon id: plugin.program.autocompletion +# Addon version: 1.0.0 +# Addon Provider: phil65 +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: 2016-02-18 03:49+0100\n" +"Language-Team: Guilouz\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Last-Translator: \n" + +msgctxt "Addon Summary" +msgid "AutoCompletion provider" +msgstr "Fournisseur de la saisie automatique" + +msgctxt "Addon Description" +msgid "" +"This script offers various possibilities to display online content inside " +"Needs skin support for most functions." +msgstr "Ce script vous propose différentes possibilités pour afficher le contenu en ligne. La plupart des fonctions nécessitent d'être supportées par le thème." + +msgctxt "#32001" +msgid "Autocompletion" +msgstr "Saisie automatique" + +msgctxt "#32002" +msgid "Autocompletion language" +msgstr "Langue de saisie semi-automatique" + +msgctxt "#32003" +msgid "Autocompletion provider" +msgstr "Fournisseur de la saisie automatique" diff --git a/plugin.program.autocompletion/resources/language/resource.language.he_il/strings.po b/plugin.program.autocompletion/resources/language/resource.language.he_il/strings.po new file mode 100644 index 0000000000..5eeb44e7a5 --- /dev/null +++ b/plugin.program.autocompletion/resources/language/resource.language.he_il/strings.po @@ -0,0 +1,43 @@ +# Kodi language file +# Addon Name: AutoCompletion for virtual keyboard +# Addon id: plugin.program.autocompletion +# Addon version: 1.0.0 +# Addon Provider: phil65 +msgid "" +msgstr "" +"Project-Id-Version: plugin.program.autocompletion\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: 2017-09-25 09:00+0300\n" +"Last-Translator: A. Dambledore\n" +"Language-Team: Eng2Heb\n" +"Language: he_IL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.0.4\n" + +msgctxt "Addon Summary" +msgid "AutoCompletion provider" +msgstr "ספק השלמה אוטומטית" + +msgctxt "Addon Description" +msgid "" +"This script offers various possibilities to display online content inside " +"Needs skin support for most functions." +msgstr "" +"קובץ זה מציע אפשרויות שונות להצגת תוכן מקוון. הסקריפט דורש תמיכה " +"של המעטפת עבור רוב הפונקציות." + +msgctxt "#32001" +msgid "Autocompletion" +msgstr "השלמה אוטומטית" + +msgctxt "#32002" +msgid "Autocompletion language" +msgstr "שפת השלמה אוטומטית" + +msgctxt "#32003" +msgid "Autocompletion provider" +msgstr "ספק השלמה אוטומטית" diff --git a/plugin.program.autocompletion/resources/language/resource.language.it_it/strings.po b/plugin.program.autocompletion/resources/language/resource.language.it_it/strings.po new file mode 100644 index 0000000000..a7f0b122d3 --- /dev/null +++ b/plugin.program.autocompletion/resources/language/resource.language.it_it/strings.po @@ -0,0 +1,40 @@ +# Kodi language file +# Addon Name: AutoCompletion for virtual keyboard +# Addon id: plugin.program.autocompletion +# Addon version: 1.0.0 +# Addon Provider: phil65 +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2014-10-26 17:05+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "AutoCompletion provider" +msgstr "Provider di AutoCompletamento" + +msgctxt "Addon Description" +msgid "" +"This script offers various possibilities to display online content inside " +"Needs skin support for most functions." +msgstr "" + +msgctxt "#32001" +msgid "Autocompletion" +msgstr "Autocompletamento" + +msgctxt "#32002" +msgid "Autocompletion language" +msgstr "Linguaggio di autocompletamento" + +msgctxt "#32003" +msgid "Autocompletion provider" +msgstr "Provider di autocompletamento" \ No newline at end of file diff --git a/plugin.program.autocompletion/resources/screenshot-01.jpg b/plugin.program.autocompletion/resources/screenshot-01.jpg new file mode 100644 index 0000000000..9dc48a1d7e Binary files /dev/null and b/plugin.program.autocompletion/resources/screenshot-01.jpg differ diff --git a/plugin.program.autocompletion/resources/screenshot-02.jpg b/plugin.program.autocompletion/resources/screenshot-02.jpg new file mode 100644 index 0000000000..5e41ec46cd Binary files /dev/null and b/plugin.program.autocompletion/resources/screenshot-02.jpg differ diff --git a/plugin.program.autocompletion/resources/screenshot-03.jpg b/plugin.program.autocompletion/resources/screenshot-03.jpg new file mode 100644 index 0000000000..c44b522df6 Binary files /dev/null and b/plugin.program.autocompletion/resources/screenshot-03.jpg differ diff --git a/plugin.program.autocompletion/resources/settings.xml b/plugin.program.autocompletion/resources/settings.xml new file mode 100644 index 0000000000..6024ece656 --- /dev/null +++ b/plugin.program.autocompletion/resources/settings.xml @@ -0,0 +1,91 @@ + + +
    + + + + 0 + Google + + + + + + + + + false + + + 32003 + + + + + 0 + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + 32002 + + + + + local + bing + + + + + + + 0 + en + + + + + + + + false + + + 32002 + + + local + + + + +
    +
    diff --git a/plugin.program.mceremote/LICENSE.txt b/plugin.program.mceremote/LICENSE.txt new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/plugin.program.mceremote/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugin.program.mceremote/ReadMeFirst.txt b/plugin.program.mceremote/ReadMeFirst.txt new file mode 100644 index 0000000000..6184b4c16a --- /dev/null +++ b/plugin.program.mceremote/ReadMeFirst.txt @@ -0,0 +1,130 @@ +MCERemote Addon for Kodi +======================== + +Introduction +------------ +This plugin will customise the Microsoft remote (and compatibles like +the Asrock and HP remotes) for use with Kodi. If you don't have an MS +or compatible remote the plugin will warn you of this. If your remote +isn't MS compatible the addon won't do anything useful, but won't do +any harm either. + +The Microsoft remote works pretty well in Kodi without any +customisation needed. Just a few buttons won't work. If you're happy +with this you don't need to use this addon and you need read no +further. + +Quick start +----------- +Most people will just want to configure the remote to use the standard +Windows Media Center keyboard shortcuts. In this case just select the +second item in the list "Apply current settings to remote". Exit Kodi +and reboot your PC, and all buttons on the remote will now work with +Kodi. The remote will still work with Windows Media Center as well. If +for any reason you want to back out the changes select the third +option "Apply Windows default settings to remote", exit Kodi and +reboot. This will set the remote back to the way it was before you used +the MCERemote addon. + +After you've configured the remote you don't need the addon again and +you can uninstall it. The only reason to leave the addon installed is +if you want to tweak the remote config further. + +A note on Windows 10, 7 and Vista: these versions of Windows have a +feature called User Access Control that (very wisely) stops +applications overwriting key bits of the registry. If UAC is on you may +get an error message from the plugin to say it cannot write to the +registry. If this happens try right-clicking on the Kodi shortcut then +choose "Run as administrator", and the plugin should now be able to +write to the registry. You only need to do this once. After you've run +Kodi as administrator and used the plugin you can go back to using Kodi +normally. + +Advanced config +--------------- + +The fourth option in the list "Configure MCERemote settings" allows you +to tweak the config and make any button on the remote send any +keypress. + +When you select "Configure MCERemote settings" you see a list of all +the buttons on the remote (NB not all Microsoft remotes have all +buttons). To modify a button mapping select that button and type in the +keypress you want the button to send. + +NB if you want the button to use the default Microsoft setting type the +text "mce" (without the quotes). If you don't want the button to be +configured leave the text blank. + +The keypress is described as follows: + +- use "ctrl-", "shift-", "alt-" and "win-" to set the modifier key. You + can use any combination of these, for example: + "ctrl-alt-X" configures the button to send an X keypress with the + control and alt keys held down. The "win-" modifier is the Windows + key between the control and alt keys. + +- for keypresses that send characters, e.g. "3", "A" or "?", just type + the character, for example: + "ctrl-shift-?" sends a ? keystroke with the control and shift keys + held down. + + Note that Kodi doesn't distinguish between shifted keys, e.g. "/" and + "?" are the same keystroke because "?" is just "/" with shift down. + The key text "ctrl-?" and "ctrl-/" send the same keypress and you can + use either. + +- for keypresses that don't send characters, e.g. return and backspace, + you can use any of the following: + + f1 to f12 - for the function keys + right, left, down and up - for the arrow keys + pageup + pagedown + return + backspace + escape + tab + space + insert + home + delete + end + +When you close the Settings dialog be sure to click OK or Kodi will +discard all your changes without warning you! + +When you close the Settings dialog you'll be asked if you want to +apply the settings to the remote. You can do this now, or you can +answer "No" then at a later time use the second option "Apply current +settings to remote" to write your settings to the remote. + +Finally, if you make a pig's ear of the settings you can use the fifth +option "Restore default MCERemote settings" to restore all the settings +to their default values. + +Edit keyboard.xml +----------------- + +If you tweak the button settings you might want to write a keyboard.xml +file to make Kodi take whatever action you want on your customised +keypress. The sixth option "Edit keyboard.xml" will edit your +keyboard.xml. + +By default your keyboard.xml will be opened in Notepad. If you don't +already have a keyboard.xml you'll be asked if you want to create one. +Answer yes to create a template keyboard.xml that you can modify to your +requirements. + +I have written a keymap editor applet but it can't be included in this +addon as the Kodi team (very sensibly) don't allow random executables to +be included in addons. You can download it from: + +http://swarchive.ratsauce.co.uk/XBMC/KeyMapEdit.exe + +If you download this and place it in any directory that is on the path +the MCERemote addon will use it instead of Notepad. There isn't a manual +for the keymap editor since it hould be pretty obvious how to use it. + +John Rennie +16th March 2021 diff --git a/plugin.program.mceremote/addon.py b/plugin.program.mceremote/addon.py new file mode 100644 index 0000000000..135436e642 --- /dev/null +++ b/plugin.program.mceremote/addon.py @@ -0,0 +1,33 @@ +""" +************************************************************************ +MCERemote Addon +Author: John Rennie +v2.0.5 16th March 2014 + +This addon allows you to configure a Microsoft MCE remote, or any +compatible remote using the eHome driver. + +The addon modifies the ReportMappingTable registry entry to configure +the remote to send the standard MCE keyboard shortcuts. See +http://msdn.microsoft.com/en-us/library/bb189249.aspx for details. + +The addon can also reset the ReportMappingTable registry entry to the +default for a freshly installed MCE remote. +************************************************************************ +""" + +import xbmcgui +import mceremote + +# ********************************************************************** +# Start execution +# --------------- +# ********************************************************************** + +# Check if we are running on Windows and display an error if not +if sys.platform != "win32": + xbmcgui.Dialog().ok(mceremote.local_string(30300), mceremote.local_string(30301)) + +# We're on Windows +else: + mceremote.Main() diff --git a/plugin.program.mceremote/addon.xml b/plugin.program.mceremote/addon.xml new file mode 100644 index 0000000000..fcba21abc4 --- /dev/null +++ b/plugin.program.mceremote/addon.xml @@ -0,0 +1,23 @@ + + + + + + + executable + + + windx + MCE Remote configuration + This addon allows you to configure a Microsoft MCE remote, or any compatible remote using the eHome driver. The addon appears under the Programs heading on the Home page as "MCERemote". + GPL-2.0-or-later, GPL-3.0-or-later, Apache-2.0 + https://forum.kodi.tv/showthread.php?tid=81687 + https://github.com/jhsrennie/MCERemote + + resources/icon.png + + + diff --git a/plugin.program.mceremote/changelog.txt b/plugin.program.mceremote/changelog.txt new file mode 100644 index 0000000000..25fa4486ee --- /dev/null +++ b/plugin.program.mceremote/changelog.txt @@ -0,0 +1,34 @@ +[B]Version 3.0.0 16th March 2021[/B] +- Change Python version to 3.0.0 for Matrix + +[B]Version 2.0.5 16th March 2014[/B] +- Change Python version to 2.1.0 for Gotham + +[B]Version 2.0.4 20th Sep 2012[/B] +- Remove dependance on xbmc.gui from addon.xml + +[B]Version 2.0.3 20th Sep 2012[/B] +- Use xbmc.gui 3.0 + +[B]Version 2.0.2 18th Sep 2012[/B] +- Add new language tag + +[B]Version 2.0.1 20th Nov 2011[/B] +- Cosmetic change to the add-on description + +[B]Version 2.0.0 14th Nov 2011[/B] +- New version for Eden +- Add support for extra buttons on XBox universal, Mediagate and HP remotes +- Fix error if the Windows button is set to "mce" + +[B]Version 1.1.15 20th Nov 2010[/B] +- Fix minor bug in keymap editor download. + +[B]Version 1.1.0 16th Nov 2010[/B] +- Now uses KeyMapEdit to edit the keyboard.xml. + +[B]Version 1.0.1 9th Nov 2010[/B] +- Updated icon - no other changes. + +[B]Version 1.0.0 17th Oct 2010[/B] +- First official release. diff --git a/plugin.program.mceremote/mceremote.py b/plugin.program.mceremote/mceremote.py new file mode 100644 index 0000000000..cfe081e3ac --- /dev/null +++ b/plugin.program.mceremote/mceremote.py @@ -0,0 +1,683 @@ +""" +************************************************************************ +MCERemote Addon +Author: John Rennie +v2.0.5 16th March 2014 + +This addon allows you to configure a Microsoft MCE remote, or any +compatible remote using the eHome driver. + +The addon modifies the ReportMappingTable registry entry to configure +the remote to send the standard MCE keyboard shortcuts. See +http://msdn.microsoft.com/en-us/library/bb189249.aspx for details. + +The addon can also reset the ReportMappingTable registry entry to the +default for a freshly installed MCE remote. +************************************************************************ +""" + +import sys +import winreg +import platform +import xbmcvfs +import xbmcplugin +import xbmcgui +import xbmcaddon + +# Save the ID of this plugin - cast to integer +_thisPlugin = int(sys.argv[1]) + +# Name of the plugin +_thisPluginName = "plugin.program.mceremote" + +# Addon object for reading settings +_settings = xbmcaddon.Addon(id=_thisPluginName) + +# Registry key where the ReportMappingTable is stored +_ReportMappingTable = "SYSTEM\\CurrentControlSet\\Services\\HidIr\\Remotes\\745a17a0-74d3-11d0-b6fe-00a0c90f57da" + +# Path to our data directory +_AddonDir = xbmcvfs.validatePath(_settings.getAddonInfo("path") + "/") +_DataDir = xbmcvfs.validatePath(_AddonDir + "resources/data/") + +# Data for the remote control buttons +# The array contains tuples of (button_name, button_number, default_setting, mce_default) +_buttonData = ( + ("button_0", 0x00, "0", [0x04,0x00,0x27]), + ("button_1", 0x01, "1", [0x04,0x00,0x1e]), + ("button_2", 0x02, "2", [0x04,0x00,0x1f]), + ("button_3", 0x03, "3", [0x04,0x00,0x20]), + ("button_4", 0x04, "4", [0x04,0x00,0x21]), + ("button_5", 0x05, "5", [0x04,0x00,0x22]), + ("button_6", 0x06, "6", [0x04,0x00,0x23]), + ("button_7", 0x07, "7", [0x04,0x00,0x24]), + ("button_8", 0x08, "8", [0x04,0x00,0x25]), + ("button_9", 0x09, "9", [0x04,0x00,0x26]), + ("button_clear", 0x0a, "escape", [0x04,0x00,0x29]), + ("button_enter", 0x0b, "return", [0x04,0x00,0x28]), + ("button_power", 0x0c, "", [0x03,0x82,0x00]), + ("button_windows", 0x0d, "ctrl-shift-w", []), + ("button_mute", 0x0e, "f8", [0x01,0xe2,0x00]), + ("button_info", 0x0f, "ctrl-d", [0x01,0x09,0x02]), + ("button_volup", 0x10, "f10", [0x01,0xe9,0x00]), + ("button_voldown", 0x11, "f9", [0x01,0xea,0x00]), + ("button_chanup", 0x12, "pageup", [0x01,0x9c,0x00]), + ("button_chandown", 0x13, "pagedown", [0x01,0x9d,0x00]), + ("button_ff", 0x14, "ctrl-shift-f", [0x01,0xb3,0x00]), + ("button_rew", 0x15, "ctrl-shift-b", [0x01,0xb4,0x00]), + ("button_play", 0x16, "ctrl-shift-p", [0x01,0xb0,0x00]), + ("button_record", 0x17, "ctrl-r", [0x01,0xb2,0x00]), + ("button_pause", 0x18, "ctrl-p", [0x01,0xb1,0x00]), + ("button_stop", 0x19, "ctrl-shift-s", [0x01,0xb7,0x00]), + ("button_next", 0x1a, "ctrl-f", [0x01,0xb5,0x00]), + ("button_prev", 0x1b, "ctrl-b", [0x01,0xb6,0x00]), + ("button_hash", 0x1c, "ctrl-shift-3", [0x04,0x02,0x20]), + ("button_star", 0x1d, "ctrl-shift-8", [0x04,0x02,0x25]), + ("button_up", 0x1e, "up", [0x04,0x00,0x52]), + ("button_down", 0x1f, "down", [0x04,0x00,0x51]), + ("button_left", 0x20, "left", [0x04,0x00,0x50]), + ("button_right", 0x21, "right", [0x04,0x00,0x4f]), + ("button_ok", 0x22, "return", [0x04,0x00,0x28]), + ("button_back", 0x23, "backspace", [0x01,0x24,0x02]), + ("button_dvdmenu", 0x24, "ctrl-shift-m", []), + ("button_livetv", 0x25, "ctrl-shift-t", []), + ("button_guide", 0x26, "ctrl-g", [0x01,0x8d,0x00]), + ("button_asrock", 0x27, "ctrl-alt-t", []), + + ("button_xbopen", 0x28, "", []), # Open/Close on XBox universal remote + ("button_hpwron", 0x29, "", [0x03,0x83,0x00]), # Harmony discrete power on + ("button_hpwroff", 0x2a, "", [0x03,0x82,0x00]), # Harmony discrete power off + + ("button_unkwn", 0x3b, "", [0x01,0x04,0x02]), # Unknown button + + ("button_music", 0x47, "ctrl-m", []), + ("button_recordedtv", 0x48, "ctrl-o", []), + ("button_pictures", 0x49, "ctrl-i", []), + ("button_movies", 0x4A, "ctrl-e", []), + + ("button_mgangle", 0x4b, "", []), # Mediagate DVD Angle + ("button_mgaudio", 0x4c, "", []), # Mediagate DVD Audio + ("button_mgsubtitle", 0x4d, "", []), # Mediagate Subtitles + ("button_hpprint", 0x4e, "ctrl-alt-p", [0x01,0x08,0x02]), # Print on HP remote + ("button_xbdisplay", 0x4f, "", []), # Display on XBox universal remote + + ("button_radio", 0x50, "ctrl-a", []), + ("button_teletext", 0x5a, "ctrl-t", []), + ("button_red", 0x5b, "ctrl-alt-1", []), + ("button_green", 0x5c, "ctrl-alt-2", []), + ("button_yellow", 0x5d, "ctrl-alt-3", []), + ("button_blue", 0x5e, "ctrl-alt-4", []), + + ("button_xblargex", 0x64, "", []), # Large X on XBox universal remote + ("button_xbgreena", 0x66, "", []), # Green A on XBox universal remote + ("button_xbbluex", 0x68, "", []), # Blue X on XBox universal remote + ("button_xbchanup", 0x6c, "", []), # Chan up on XBox universal remote + ("button_xbchandn", 0x6d, "", []), # Chan dn on XBox universal remote + ("button_playpause", 0x6e, "", [0x01,0xcd,0x00]), # Play/pause on HP remote +) + +# Key to eHome character code mapping table +_KeyToeHomeCode = ( + ("a",0x04), + ("b",0x05), + ("c",0x06), + ("d",0x07), + ("e",0x08), + ("f",0x09), + ("g",0x0A), + ("h",0x0B), + ("i",0x0C), + ("j",0x0D), + ("k",0x0E), + ("l",0x0F), + ("m",0x10), + ("n",0x11), + ("o",0x12), + ("p",0x13), + ("q",0x14), + ("r",0x15), + ("s",0x16), + ("t",0x17), + ("u",0x18), + ("v",0x19), + ("w",0x1A), + ("x",0x1B), + ("y",0x1C), + ("z",0x1D), + ("1",0x1E), + ("!",0x1E), + ("2",0x1F), + ("@",0x1F), + ("3",0x20), + ("#",0x20), + ("4",0x21), + ("$",0x21), + ("5",0x22), + ("%",0x22), + ("6",0x23), + ("^",0x23), + ("7",0x24), + ("&",0x24), + ("8",0x25), + ("*",0x25), + ("9",0x26), + ("(",0x26), + ("0",0x27), + (")",0x27), + ("return",0x28), + ("escape",0x29), + ("esc",0x29), + ("backspace",0x2A), + ("tab",0x2B), + ("space",0x2C), + ("-",0x2D), + ("_",0x2D), + ("=",0x2E), + ("+",0x2E), + ("[",0x2F), + ("{",0x2F), + ("]",0x30), + ("}",0x30), + ("\\",0x31), + ("|",0x31), + (";",0x33), + (":",0x33), + ("'",0x34), + ("\"",0x34), + ("`",0x35), + ("~",0x35), + (",",0x36), + ("<",0x36), + (".",0x37), + (">",0x37), + ("/",0x38), + ("?",0x38), + ("f1",0x3A), + ("f2",0x3B), + ("f3",0x3C), + ("f4",0x3D), + ("f5",0x3E), + ("f6",0x3F), + ("f7",0x40), + ("f8",0x41), + ("f9",0x42), + ("f10",0x43), + ("f11",0x44), + ("f12",0x45), + ("insert", 0x49), + ("ins", 0x49), + ("home", 0x4A), + ("pageup", 0x4B), + ("pgup", 0x4B), + ("delete", 0x4C), + ("del", 0x4C), + ("end", 0x4D), + ("pagedown", 0x4E), + ("pgdown", 0x4E), + ("pgdn", 0x4E), + ("right", 0x4F), + ("left", 0x50), + ("down", 0x51), + ("up", 0x52) +) + + +# ********************************************************************** +# local_string +# ------------ +# Retrieve a string from strings.po +# ********************************************************************** +def local_string(string_id): + return _settings.getLocalizedString(string_id) + + +# ********************************************************************** +# CheckRegKey +# ----------- +# Check for the presence of the ReportMappingTable registry value. +# Return true if the registry value is found or false if the registry +# value is not present. +# ********************************************************************** +def CheckRegKey(): + + try: + hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, _ReportMappingTable) + rmp = winreg.QueryValueEx(hkey, "ReportMappingTable") + foundkey = True + winreg.CloseKey(hkey) + + except: + foundkey = False + + return foundkey + + +# ********************************************************************** +# ConvertKeyText +# -------------- +# Convert the text representation of a key e.g. ctr;-shift-P into the +# three character representation required by the eHome driver. +# ********************************************************************** +def ConvertKeyText(KeyText, Default): + + keymod = 0 + keycode = 0 + ehomecode = [] + + keytext = KeyText.replace(" ", "").lower() + if keytext == "": + return ehomecode + +# If the key text is "mce" return the default + + if keytext == "mce": + return Default + +# Check for key modifiers + + i = keytext.find("ctrl-") + if i != -1: + keymod = keymod | 1 + keytext = keytext[:i] + keytext[i+5:] + + i = keytext.find("control-") + if i != -1: + keymod = keymod | 1 + keytext = keytext[:i] + keytext[i+8:] + + i = keytext.find("shift-") + if i != -1: + keymod = keymod | 2 + keytext = keytext[:i] + keytext[i+6:] + + i = keytext.find("alt-") + if i != -1: + keymod = keymod | 4 + keytext = keytext[:i] + keytext[i+4:] + + i = keytext.find("win-") + if i != -1: + keymod = keymod | 8 + keytext = keytext[:i] + keytext[i+4:] + + i = keytext.find("super-") + if i != -1: + keymod = keymod | 8 + keytext = keytext[:i] + keytext[i+6:] + +# There should be just the character remaining + + for ehomekey in _KeyToeHomeCode: + if ehomekey[0] == keytext: + keycode = ehomekey[1] + break + +# Return the converted key text + + ehomecode = [0x04, keymod, keycode] + + return ehomecode + + +# ********************************************************************** +# ApplyCurrentSettings +# -------------------- +# Read the addon settings and use them to configure the remote +# ********************************************************************** +def ApplyCurrentSettings(Prompt): + +# Require confirmation from the user + + dialog = xbmcgui.Dialog() + + if Prompt: + if not dialog.yesno(local_string(30300), local_string(30303)): + return + +# Build the settings string from the _buttonData + + reportmappingtable = [] + + for button in _buttonData: + # keytext is the text entered by the user + keytext = _settings.getSetting(button[0]) + + # if the user has entered "mce" but the button table contains no + # default, set the keytext to "" so it will be ignored. + if keytext == "mce" and len(button[3]) == 0: + keytext = "" + + if keytext != "": + thisbutton = ConvertKeyText(keytext, button[3]) + if len(thisbutton) != 3: + dialog.ok(local_string(30300), local_string(30304).format(button[0], keytext)) + return + + reportmappingtable.append(button[1]) + reportmappingtable.append(0) + reportmappingtable.append(0) + reportmappingtable.append(0) + for i in thisbutton: + reportmappingtable.append(i) + +# Open the registry key + + try: + hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, _ReportMappingTable, 0, winreg.KEY_SET_VALUE) + except WindowsError as e: + dialog.ok(local_string(30300), local_string(30305).format(e.args[1])) + return + except: + dialog.ok(local_string(30300), local_string(30306)) + return + +# Write the ReportMappingTable data + + try: + winreg.SetValueEx(hkey, "ReportMappingTable", 0, winreg.REG_BINARY, bytes(reportmappingtable)) + except Exception as e: + dialog.ok(local_string(30300), local_string(30307).format(e.args[0])) + except: + dialog.ok(local_string(30300), local_string(30308)) + +# Finished! + + winreg.CloseKey(hkey) + dialog.ok(local_string(30300), local_string(30309)) + + +# ********************************************************************** +# ApplyDefaultSettings +# -------------------- +# Restore the default Windows ReportMappingTable. +# ********************************************************************** +def ApplyDefaultSettings(Prompt): + +# Require confirmation from the user + + dialog = xbmcgui.Dialog() + + if Prompt: + if not dialog.yesno(local_string(30300), local_string(30310)): + return + +# Open the registry key + + try: + hkey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, _ReportMappingTable, 0, winreg.KEY_SET_VALUE) + except WindowsError as e: + dialog.ok(local_string(30300), local_string(30305).format(e.args[1])) + return + except: + dialog.ok(local_string(30300), local_string(30306)) + return + +# Build the settings list from the _buttonData + + reportmappingtable = [] + + for button in _buttonData: + if len(button[3]) == 3: + reportmappingtable.append(button[1]) + reportmappingtable.append(0) + reportmappingtable.append(0) + reportmappingtable.append(0) + for i in button[3]: + reportmappingtable.append(i) + +# Write the ReportMappingTable data + + try: + winreg.SetValueEx(hkey, "ReportMappingTable", 0, winreg.REG_BINARY, bytes(reportmappingtable)) + except Exception as e: + dialog.ok(local_string(30300), local_string(30307).format(e.args[0])) + except: + dialog.ok(local_string(30300), local_string(30308)) + +# Finished! + + winreg.CloseKey(hkey) + dialog.ok(local_string(30300), local_string(30309)) + + +# ********************************************************************** +# ConfigureButtonSettings +# ----------------------- +# Open the dialog to configure the settings +# ********************************************************************** +def ConfigureButtonSettings(): + +# Open the settings dialog + + _settings.openSettings() + +# Ask the user if they want to apply these settings + + if xbmcgui.Dialog().yesno(local_string(30300), local_string(30311)): + ApplyCurrentSettings(False) + + +# ********************************************************************** +# RestoreDefaultButtonSettings +# ---------------------------- +# Restore the default settings +# ********************************************************************** +def RestoreDefaultButtonSettings(): + +# Require confirmation from the user + + dialog = xbmcgui.Dialog() + if not dialog.yesno(local_string(30300), local_string(30312)): + return + +# Restore the default settings from the info in _buttonData + + for nextbutton in _buttonData: + _settings.setSetting(nextbutton[0], nextbutton[2]) + +# Ask the user if they want to apply these settings + + if xbmcgui.Dialog().yesno(local_string(30300), local_string(30311)): + ApplyCurrentSettings(False) + + +# ********************************************************************** +# EditKeyboardDotXML +# -------------------- +# Edit the keyboard.xml in the user's userdata\keymaps folder. +# ********************************************************************** +def EditKeyboardDotXML(): + + import os + import shutil + import subprocess + + dialog = xbmcgui.Dialog() + +# Check whether keyboard.xml exists + + dstpath = xbmcvfs.translatePath("special://home/userdata/keymaps/keyboard.xml") + if not os.path.isfile(dstpath): + if dialog.yesno(local_string(30300), local_string(30313)): + CreateKeyboardDotXML() + else: + return + +# Select the keymap editor: if KeyMapEdit.exe exists use it, otherwise +# use Notepad. + + srcpath = shutil.which("keymapedit.exe") + if srcpath == None: + srcpath = "notepad.exe" + +# Edit the keyboard.xml in Notepad + + child = subprocess.Popen(srcpath + ' "' + dstpath + '"') + rc = child.wait() + ourpath = xbmcvfs.translatePath("special://xbmcbin/") + child = subprocess.Popen(ourpath + "kodi.exe") + +# Now parse the file to check for any obvious errors + + import xml.dom.minidom + + try: + doc = xml.dom.minidom.parse(dstpath) + + except xml.parsers.expat.ExpatError as e: + dialog.ok(local_string(30300), local_string(30314).format(e.args[0])) + + except: + dialog.ok(local_string(30300), local_string(30315)) + + +# ********************************************************************** +# CreateKeyboardDotXML +# -------------------- +# Copy the template keyboard.xml from resources\data to the user's +# userdata\keymaps folder. +# ********************************************************************** +def CreateKeyboardDotXML(): + """ + Create a template keyboard.xml + @return void + """ + + import os + import shutil + import subprocess + + dialog = xbmcgui.Dialog() + +# Find the template file: try the profile first + + srcpath = _DataDir + "keyboard.xml" + if not os.path.isfile(srcpath): + srcpath = xbmcvfs.translatePath("special://xbmc/addons/" + _thisPluginName + "/resources/data/keyboard.xml") + + if not os.path.isfile(srcpath): + dialog.ok(local_string(30300), local_string(30316)) + return + +# Check whether a keyboard.xml already exists + + dstpath = xbmcvfs.translatePath("special://home/userdata/keymaps/keyboard.xml") + if os.path.isfile(dstpath): + if not dialog.yesno(local_string(30300), local_string(30317).format(dstpath)): + return + + elif not dialog.yesno(local_string(30300), local_string(30318).format(dstpath)): + return + +# Copy the template keyboard.xml + + try: + shutil.copyfile(srcpath, dstpath) + except: + dialog.ok(local_string(30300), local_string(30319)) + + +# ********************************************************************** +# ReadInstructions +# ---------------- +# Display the instructions in Notepad +# ********************************************************************** +def ReadInstructions(): + + import os + import subprocess + +# Find the Readme file: try the profile first + + ourpath = _AddonDir + "ReadMeFirst.txt" + if not os.path.isfile(ourpath): + ourpath = xbmcvfs.translatePath("special://xbmc/addons/" + _thisPluginName + "/ReadMeFirst.txt") + + if not os.path.isfile(ourpath): + dialog = xbmcgui.Dialog() + dialog.ok(local_string(30300), local_string(30320)) + return + +# Open the readme file in Notepad + + child = subprocess.Popen('notepad.exe "' + ourpath + '"') + rc = child.wait() + +# Run XBMC: this will simply set the focus back to the current instance + + ourpath = xbmcvfs.translatePath("special://xbmcbin/") + child = subprocess.Popen(ourpath + "kodi.exe") + + +# ********************************************************************** +# ListOptions +# ----------- +# List the MCERemote options +# ********************************************************************** +def ListOptions(): + + #Add the options + listItem = xbmcgui.ListItem(local_string(30321)) + xbmcplugin.addDirectoryItem(_thisPlugin, "plugin://" + _thisPluginName + "?instructions", listItem) + + listItem = xbmcgui.ListItem(local_string(30322)) + xbmcplugin.addDirectoryItem(_thisPlugin,"plugin://" + _thisPluginName + "?applycurrentsettings", listItem) + + listItem = xbmcgui.ListItem(local_string(30323)) + xbmcplugin.addDirectoryItem(_thisPlugin, "plugin://" + _thisPluginName + "?applydefaultsettings", listItem) + + listItem = xbmcgui.ListItem(local_string(30324)) + xbmcplugin.addDirectoryItem(_thisPlugin, "plugin://" + _thisPluginName + "?configurebuttons", listItem) + + listItem = xbmcgui.ListItem(local_string(30325)) + xbmcplugin.addDirectoryItem(_thisPlugin, "plugin://" + _thisPluginName + "?defaultbuttons", listItem) + + listItem = xbmcgui.ListItem(local_string(30326)) + xbmcplugin.addDirectoryItem(_thisPlugin, "plugin://" + _thisPluginName + "?editkeyboarddotxml", listItem) + + xbmcplugin.endOfDirectory(_thisPlugin) + + +# ********************************************************************** +# Start execution +# --------------- +# ********************************************************************** + +def Main(): + +# Get the selected option if any + cmd = sys.argv[2].replace("?", "") + +# The "applycurrentsettings" command applies the current button settings to the remote + if cmd == "applycurrentsettings": + ApplyCurrentSettings(True) + +# The "applydefaultsettings" command resets the remote to the ehome driver defaults + elif cmd == "applydefaultsettings": + ApplyDefaultSettings(True) + +# The "configurebuttons" command opens the settings dialog + elif cmd == "configurebuttons": + ConfigureButtonSettings() + +# The "defaultbuttons" command restores the default button settings + elif cmd == "defaultbuttons": + RestoreDefaultButtonSettings() + +# The "editkeyboarddotxml" command edits keyboard.xml + elif cmd == "editkeyboarddotxml": + EditKeyboardDotXML() + +# The "instructions" command displays the readme file in Notepad + elif cmd == "instructions": + ReadInstructions() + +# If no command was specified check if the ReportMappingTable registry +# key is not present and warn the user + else: + if not CheckRegKey(): + xbmcgui.Dialog().ok(local_string(30300), local_string(30302)) + +# Finally list the options + ListOptions() + diff --git a/plugin.program.mceremote/resources/data/CopyKeyboardDotXML.bat b/plugin.program.mceremote/resources/data/CopyKeyboardDotXML.bat new file mode 100644 index 0000000000..a451470565 --- /dev/null +++ b/plugin.program.mceremote/resources/data/CopyKeyboardDotXML.bat @@ -0,0 +1,62 @@ +@echo off +rem ******************************************************************** +rem CopyKeyboardDotXML.bat +rem ====================== +rem Batch file to copy the sample keyboard.xml to userdata +rem +rem 14/08/2010 J. Rennie +rem ******************************************************************** + +rem *** Get the date to backup the old keyboard.xml + +for /f "tokens=1" %%i in ('date /t') do set THEDATE=%%i +set THEDAY=%THEDATE:~0,2% +set THEDATE=%THEDATE:~6,4%%THEDATE:~3,2%%THEDATE:~0,2% + +rem *** Warn the user what we're about to do + +echo This script will copy the sample keyboard.xml to: +echo %APPDATA%\Kodi\userdata\keymaps\keyboard.xml +echo. +echo If there is already a keyboard.xml it will be renamed keyboard.xml.%THEDATE% +echo. +echo Press control-C to abort or any key to continue +echo. +pause + +rem *** Check we can find the keyboard.xml + +set NEWKB=%0\..\keyboard.xml +set NEWKB=%NEWKB:"=% + +if exist "%NEWKB%" goto foundnewkb +echo ERROR: Cannot find the new keyboard.xml to copy +echo Check keyboard.xml is in the same directory as %0 +pause +exit +:foundnewkb + +rem *** Check if we need to backup the old keyboard.xml + +set OLDKB=%APPDATA%\Kodi\userdata\keymaps\keyboard.xml +if not exist "%OLDKB%" goto nooldkb + +set OLDKBBACK=keyboard.xml.%THEDATE% +if not exist "%APPDATA%\Kodi\userdata\keymaps\%OLDKBBACK%" goto nooldkbback +del /F /Q "%APPDATA%\Kodi\userdata\keymaps\%OLDKBBACK%" +:nooldkbback + +echo Renaming %OLDKB% to %OLDKBBACK% +rename "%OLDKB%" "%OLDKBBACK%" + +pause +:nooldkb + +rem *** Copy the new keyboard.xml + +md "%APPDATA%\Kodi\userdata\keymaps" 1>NUL 2>&1 +copy "%NEWKB%" "%OLDKB%" + +echo Keyboard.xml copied to %OLDKB% + +pause diff --git a/plugin.program.mceremote/resources/data/CreateTestConfig.reg b/plugin.program.mceremote/resources/data/CreateTestConfig.reg new file mode 100644 index 0000000000..d8e9682966 --- /dev/null +++ b/plugin.program.mceremote/resources/data/CreateTestConfig.reg @@ -0,0 +1,260 @@ +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\HidIr\Remotes\745a17a0-74d3-11d0-b6fe-00a0c90f57da] +"ReportMappingTable"=hex:\ + 00,00,00,00,04,00,04,\ ; A + 01,00,00,00,04,00,05,\ ; B + 02,00,00,00,04,00,06,\ ; C + 03,00,00,00,04,00,07,\ ; D + 04,00,00,00,04,00,08,\ ; E + 05,00,00,00,04,00,09,\ ; F + 06,00,00,00,04,00,0A,\ ; G + 07,00,00,00,04,00,0B,\ ; H + 08,00,00,00,04,00,0C,\ ; I + 09,00,00,00,04,00,0D,\ ; J + 0A,00,00,00,04,00,0E,\ ; K + 0B,00,00,00,04,00,0F,\ ; L + 0C,00,00,00,04,00,10,\ ; M + 0D,00,00,00,04,00,11,\ ; N + 0E,00,00,00,04,00,12,\ ; O + 0F,00,00,00,04,00,13,\ ; P + 10,00,00,00,04,00,14,\ ; Q + 11,00,00,00,04,00,15,\ ; R + 12,00,00,00,04,00,16,\ ; S + 13,00,00,00,04,00,17,\ ; T + 14,00,00,00,04,00,18,\ ; U + 15,00,00,00,04,00,19,\ ; V + 16,00,00,00,04,00,1A,\ ; W + 17,00,00,00,04,00,1B,\ ; X + 18,00,00,00,04,00,1C,\ ; Y + 19,00,00,00,04,00,1D,\ ; Z + 1A,00,00,00,04,00,1E,\ ; 1 + 1B,00,00,00,04,00,1F,\ ; 2 + 1C,00,00,00,04,00,20,\ ; 3 + 1D,00,00,00,04,00,21,\ ; 4 + 1E,00,00,00,04,00,22,\ ; 5 + 1F,00,00,00,04,00,23,\ ; 6 + 20,00,00,00,04,00,24,\ ; 7 + 21,00,00,00,04,00,25,\ ; 8 + 22,00,00,00,04,00,26,\ ; 9 + 23,00,00,00,04,00,27,\ ; 0 + 24,00,00,00,04,01,04,\ ; ctrl-A + 25,00,00,00,04,01,05,\ ; ctrl-B + 26,00,00,00,04,01,06,\ ; ctrl-C + 27,00,00,00,04,01,07,\ ; ctrl-D + 28,00,00,00,04,01,08,\ ; ctrl-E + 29,00,00,00,04,01,09,\ ; ctrl-F + 2A,00,00,00,04,01,0A,\ ; ctrl-G + 2B,00,00,00,04,01,0B,\ ; ctrl-H + 2C,00,00,00,04,01,0C,\ ; ctrl-I + 2D,00,00,00,04,01,0D,\ ; ctrl-J + 2E,00,00,00,04,01,0E,\ ; ctrl-K + 2F,00,00,00,04,01,0F,\ ; ctrl-L + 30,00,00,00,04,01,10,\ ; ctrl-M + 31,00,00,00,04,01,11,\ ; ctrl-N + 32,00,00,00,04,01,12,\ ; ctrl-O + 33,00,00,00,04,01,13,\ ; ctrl-P + 34,00,00,00,04,01,14,\ ; ctrl-Q + 35,00,00,00,04,01,15,\ ; ctrl-R + 36,00,00,00,04,01,16,\ ; ctrl-S + 37,00,00,00,04,01,17,\ ; ctrl-T + 38,00,00,00,04,01,18,\ ; ctrl-U + 39,00,00,00,04,01,19,\ ; ctrl-V + 3A,00,00,00,04,01,1A,\ ; ctrl-W + 3B,00,00,00,04,01,1B,\ ; ctrl-X + 3C,00,00,00,04,01,1C,\ ; ctrl-Y + 3D,00,00,00,04,01,1D,\ ; ctrl-Z + 3E,00,00,00,04,01,1E,\ ; ctrl-1 + 3F,00,00,00,04,01,1F,\ ; ctrl-2 + 40,00,00,00,04,01,20,\ ; ctrl-3 + 41,00,00,00,04,01,21,\ ; ctrl-4 + 42,00,00,00,04,01,22,\ ; ctrl-5 + 43,00,00,00,04,01,23,\ ; ctrl-6 + 44,00,00,00,04,01,24,\ ; ctrl-7 + 45,00,00,00,04,01,25,\ ; ctrl-8 + 46,00,00,00,04,01,26,\ ; ctrl-9 + 47,00,00,00,04,01,27,\ ; ctrl-0 + 48,00,00,00,04,02,04,\ ; shift-A + 49,00,00,00,04,02,05,\ ; shift-B + 4A,00,00,00,04,02,06,\ ; shift-C + 4B,00,00,00,04,02,07,\ ; shift-D + 4C,00,00,00,04,02,08,\ ; shift-E + 4D,00,00,00,04,02,09,\ ; shift-F + 4E,00,00,00,04,02,0A,\ ; shift-G + 4F,00,00,00,04,02,0B,\ ; shift-H + 50,00,00,00,04,02,0C,\ ; shift-I + 51,00,00,00,04,02,0D,\ ; shift-J + 52,00,00,00,04,02,0E,\ ; shift-K + 53,00,00,00,04,02,0F,\ ; shift-L + 54,00,00,00,04,02,10,\ ; shift-M + 55,00,00,00,04,02,11,\ ; shift-N + 56,00,00,00,04,02,12,\ ; shift-O + 57,00,00,00,04,02,13,\ ; shift-P + 58,00,00,00,04,02,14,\ ; shift-Q + 59,00,00,00,04,02,15,\ ; shift-R + 5A,00,00,00,04,02,16,\ ; shift-S + 5B,00,00,00,04,02,17,\ ; shift-T + 5C,00,00,00,04,02,18,\ ; shift-U + 5D,00,00,00,04,02,19,\ ; shift-V + 5E,00,00,00,04,02,1A,\ ; shift-W + 5F,00,00,00,04,02,1B,\ ; shift-X + 60,00,00,00,04,02,1C,\ ; shift-Y + 61,00,00,00,04,02,1D,\ ; shift-Z + 62,00,00,00,04,02,1E,\ ; shift-1 + 63,00,00,00,04,02,1F,\ ; shift-2 + 64,00,00,00,04,02,20,\ ; shift-3 + 65,00,00,00,04,02,21,\ ; shift-4 + 66,00,00,00,04,02,22,\ ; shift-5 + 67,00,00,00,04,02,23,\ ; shift-6 + 68,00,00,00,04,02,24,\ ; shift-7 + 69,00,00,00,04,02,25,\ ; shift-8 + 6A,00,00,00,04,02,26,\ ; shift-9 + 6B,00,00,00,04,02,27,\ ; shift-0 + 6C,00,00,00,04,03,04,\ ; ctrl-shift-A + 6D,00,00,00,04,03,05,\ ; ctrl-shift-B + 6E,00,00,00,04,03,06,\ ; ctrl-shift-C + 6F,00,00,00,04,03,07,\ ; ctrl-shift-D + 70,00,00,00,04,03,08,\ ; ctrl-shift-E + 71,00,00,00,04,03,09,\ ; ctrl-shift-F + 72,00,00,00,04,03,0A,\ ; ctrl-shift-G + 73,00,00,00,04,03,0B,\ ; ctrl-shift-H + 74,00,00,00,04,03,0C,\ ; ctrl-shift-I + 75,00,00,00,04,03,0D,\ ; ctrl-shift-J + 76,00,00,00,04,03,0E,\ ; ctrl-shift-K + 77,00,00,00,04,03,0F,\ ; ctrl-shift-L + 78,00,00,00,04,03,10,\ ; ctrl-shift-M + 79,00,00,00,04,03,11,\ ; ctrl-shift-N + 7A,00,00,00,04,03,12,\ ; ctrl-shift-O + 7B,00,00,00,04,03,13,\ ; ctrl-shift-P + 7C,00,00,00,04,03,14,\ ; ctrl-shift-Q + 7D,00,00,00,04,03,15,\ ; ctrl-shift-R + 7E,00,00,00,04,03,16,\ ; ctrl-shift-S + 7F,00,00,00,04,03,17,\ ; ctrl-shift-T + 80,00,00,00,04,03,18,\ ; ctrl-shift-U + 81,00,00,00,04,03,19,\ ; ctrl-shift-V + 82,00,00,00,04,03,1A,\ ; ctrl-shift-W + 83,00,00,00,04,03,1B,\ ; ctrl-shift-X + 84,00,00,00,04,03,1C,\ ; ctrl-shift-Y + 85,00,00,00,04,03,1D,\ ; ctrl-shift-Z + 86,00,00,00,04,03,1E,\ ; ctrl-shift-1 + 87,00,00,00,04,03,1F,\ ; ctrl-shift-2 + 88,00,00,00,04,03,20,\ ; ctrl-shift-3 + 89,00,00,00,04,03,21,\ ; ctrl-shift-4 + 8A,00,00,00,04,03,22,\ ; ctrl-shift-5 + 8B,00,00,00,04,03,23,\ ; ctrl-shift-6 + 8C,00,00,00,04,03,24,\ ; ctrl-shift-7 + 8D,00,00,00,04,03,25,\ ; ctrl-shift-8 + 8E,00,00,00,04,03,26,\ ; ctrl-shift-9 + 8F,00,00,00,04,03,27,\ ; ctrl-shift-0 + 90,00,00,00,04,04,04,\ ; alt-A + 91,00,00,00,04,04,05,\ ; alt-B + 92,00,00,00,04,04,06,\ ; alt-C + 93,00,00,00,04,04,07,\ ; alt-D + 94,00,00,00,04,04,08,\ ; alt-E + 95,00,00,00,04,04,09,\ ; alt-F + 96,00,00,00,04,04,0A,\ ; alt-G + 97,00,00,00,04,04,0B,\ ; alt-H + 98,00,00,00,04,04,0C,\ ; alt-I + 99,00,00,00,04,04,0D,\ ; alt-J + 9A,00,00,00,04,04,0E,\ ; alt-K + 9B,00,00,00,04,04,0F,\ ; alt-L + 9C,00,00,00,04,04,10,\ ; alt-M + 9D,00,00,00,04,04,11,\ ; alt-N + 9E,00,00,00,04,04,12,\ ; alt-O + 9F,00,00,00,04,04,13,\ ; alt-P + A0,00,00,00,04,04,14,\ ; alt-Q + A1,00,00,00,04,04,15,\ ; alt-R + A2,00,00,00,04,04,16,\ ; alt-S + A3,00,00,00,04,04,17,\ ; alt-T + A4,00,00,00,04,04,18,\ ; alt-U + A5,00,00,00,04,04,19,\ ; alt-V + A6,00,00,00,04,04,1A,\ ; alt-W + A7,00,00,00,04,04,1B,\ ; alt-X + A8,00,00,00,04,04,1C,\ ; alt-Y + A9,00,00,00,04,04,1D,\ ; alt-Z + AA,00,00,00,04,04,1E,\ ; alt-1 + AB,00,00,00,04,04,1F,\ ; alt-2 + AC,00,00,00,04,04,20,\ ; alt-3 + AD,00,00,00,04,04,21,\ ; alt-4 + AE,00,00,00,04,04,22,\ ; alt-5 + AF,00,00,00,04,04,23,\ ; alt-6 + B0,00,00,00,04,04,24,\ ; alt-7 + B1,00,00,00,04,04,25,\ ; alt-8 + B2,00,00,00,04,04,26,\ ; alt-9 + B3,00,00,00,04,04,27,\ ; alt-0 + B4,00,00,00,04,05,04,\ ; ctrl-alt-A + B5,00,00,00,04,05,05,\ ; ctrl-alt-B + B6,00,00,00,04,05,06,\ ; ctrl-alt-C + B7,00,00,00,04,05,07,\ ; ctrl-alt-D + B8,00,00,00,04,05,08,\ ; ctrl-alt-E + B9,00,00,00,04,05,09,\ ; ctrl-alt-F + BA,00,00,00,04,05,0A,\ ; ctrl-alt-G + BB,00,00,00,04,05,0B,\ ; ctrl-alt-H + BC,00,00,00,04,05,0C,\ ; ctrl-alt-I + BD,00,00,00,04,05,0D,\ ; ctrl-alt-J + BE,00,00,00,04,05,0E,\ ; ctrl-alt-K + BF,00,00,00,04,05,0F,\ ; ctrl-alt-L + C0,00,00,00,04,05,10,\ ; ctrl-alt-M + C1,00,00,00,04,05,11,\ ; ctrl-alt-N + C2,00,00,00,04,05,12,\ ; ctrl-alt-O + C3,00,00,00,04,05,13,\ ; ctrl-alt-P + C4,00,00,00,04,05,14,\ ; ctrl-alt-Q + C5,00,00,00,04,05,15,\ ; ctrl-alt-R + C6,00,00,00,04,05,16,\ ; ctrl-alt-S + C7,00,00,00,04,05,17,\ ; ctrl-alt-T + C8,00,00,00,04,05,18,\ ; ctrl-alt-U + C9,00,00,00,04,05,19,\ ; ctrl-alt-V + CA,00,00,00,04,05,1A,\ ; ctrl-alt-W + CB,00,00,00,04,05,1B,\ ; ctrl-alt-X + CC,00,00,00,04,05,1C,\ ; ctrl-alt-Y + CD,00,00,00,04,05,1D,\ ; ctrl-alt-Z + CE,00,00,00,04,05,1E,\ ; ctrl-alt-1 + CF,00,00,00,04,05,1F,\ ; ctrl-alt-2 + D0,00,00,00,04,05,20,\ ; ctrl-alt-3 + D1,00,00,00,04,05,21,\ ; ctrl-alt-4 + D2,00,00,00,04,05,22,\ ; ctrl-alt-5 + D3,00,00,00,04,05,23,\ ; ctrl-alt-6 + D4,00,00,00,04,05,24,\ ; ctrl-alt-7 + D5,00,00,00,04,05,25,\ ; ctrl-alt-8 + D6,00,00,00,04,05,26,\ ; ctrl-alt-9 + D7,00,00,00,04,05,27,\ ; ctrl-alt-0 + D8,00,00,00,04,06,04,\ ; shift-alt-A + D9,00,00,00,04,06,05,\ ; shift-alt-B + DA,00,00,00,04,06,06,\ ; shift-alt-C + DB,00,00,00,04,06,07,\ ; shift-alt-D + DC,00,00,00,04,06,08,\ ; shift-alt-E + DD,00,00,00,04,06,09,\ ; shift-alt-F + DE,00,00,00,04,06,0A,\ ; shift-alt-G + DF,00,00,00,04,06,0B,\ ; shift-alt-H + E0,00,00,00,04,06,0C,\ ; shift-alt-I + E1,00,00,00,04,06,0D,\ ; shift-alt-J + E2,00,00,00,04,06,0E,\ ; shift-alt-K + E3,00,00,00,04,06,0F,\ ; shift-alt-L + E4,00,00,00,04,06,10,\ ; shift-alt-M + E5,00,00,00,04,06,11,\ ; shift-alt-N + E6,00,00,00,04,06,12,\ ; shift-alt-O + E7,00,00,00,04,06,13,\ ; shift-alt-P + E8,00,00,00,04,06,14,\ ; shift-alt-Q + E9,00,00,00,04,06,15,\ ; shift-alt-R + EA,00,00,00,04,06,16,\ ; shift-alt-S + EB,00,00,00,04,06,17,\ ; shift-alt-T + EC,00,00,00,04,06,18,\ ; shift-alt-U + ED,00,00,00,04,06,19,\ ; shift-alt-V + EE,00,00,00,04,06,1A,\ ; shift-alt-W + EF,00,00,00,04,06,1B,\ ; shift-alt-X + F0,00,00,00,04,06,1C,\ ; shift-alt-Y + F1,00,00,00,04,06,1D,\ ; shift-alt-Z + F2,00,00,00,04,06,1E,\ ; shift-alt-1 + F3,00,00,00,04,06,1F,\ ; shift-alt-2 + F4,00,00,00,04,06,20,\ ; shift-alt-3 + F5,00,00,00,04,06,21,\ ; shift-alt-4 + F6,00,00,00,04,06,22,\ ; shift-alt-5 + F7,00,00,00,04,06,23,\ ; shift-alt-6 + F8,00,00,00,04,06,24,\ ; shift-alt-7 + F9,00,00,00,04,06,25,\ ; shift-alt-8 + FA,00,00,00,04,06,26,\ ; shift-alt-9 + FB,00,00,00,04,06,27,\ ; shift-alt-0 + FC,00,00,00,04,07,04,\ ; ctrl-shift-alt-A + FD,00,00,00,04,07,05,\ ; ctrl-shift-alt-B + FE,00,00,00,04,07,06,\ ; ctrl-shift-alt-C + FF,00,00,00,04,07,07 ; ctrl-shift-alt-D diff --git a/plugin.program.mceremote/resources/data/MSDefault.reg b/plugin.program.mceremote/resources/data/MSDefault.reg new file mode 100644 index 0000000000..5224f6a778 --- /dev/null +++ b/plugin.program.mceremote/resources/data/MSDefault.reg @@ -0,0 +1,46 @@ +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\HidIr\Remotes\745a17a0-74d3-11d0-b6fe-00a0c90f57da] +"ReportMappingTable"=hex:\ +01,00,00,00,04,00,1e,\ ; 1 +02,00,00,00,04,00,1f,\ ; 2 +03,00,00,00,04,00,20,\ ; 3 +04,00,00,00,04,00,21,\ ; 4 +05,00,00,00,04,00,22,\ ; 5 +06,00,00,00,04,00,23,\ ; 6 +07,00,00,00,04,00,24,\ ; 7 +08,00,00,00,04,00,25,\ ; 8 +09,00,00,00,04,00,26,\ ; 9 +00,00,00,00,04,00,27,\ ; 0 +0b,00,00,00,04,00,28,\ ; Enter +0a,00,00,00,04,00,29,\ ; Clear +1d,00,00,00,04,02,25,\ ; * +1c,00,00,00,04,02,20,\ ; # +1f,00,00,00,04,00,51,\ ; down +1e,00,00,00,04,00,52,\ ; up +21,00,00,00,04,00,4f,\ ; right +20,00,00,00,04,00,50,\ ; left +22,00,00,00,04,00,28,\ ; OK (return) +4e,00,00,00,01,08,02,\ ; HP remote: Print button +0f,00,00,00,01,09,02,\ ; Info +23,00,00,00,01,24,02,\ ; back +3b,00,00,00,01,04,02,\ +16,00,00,00,01,b0,00,\ ; play +18,00,00,00,01,b1,00,\ ; pause +17,00,00,00,01,b2,00,\ ; record +14,00,00,00,01,b3,00,\ ; ff +15,00,00,00,01,b4,00,\ ; rew +1a,00,00,00,01,b5,00,\ ; next +1b,00,00,00,01,b6,00,\ ; prev +19,00,00,00,01,b7,00,\ ; stop +6e,00,00,00,01,cd,00,\ +10,00,00,00,01,e9,00,\ ; vol up +11,00,00,00,01,ea,00,\ ; vol down +0e,00,00,00,01,e2,00,\ ; Mute +26,00,00,00,01,8d,00,\ ; Guide +12,00,00,00,01,9c,00,\ ; chan up +13,00,00,00,01,9d,00,\ ; chan down +0c,00,00,00,03,82,00,\ ; Power +29,00,00,00,03,83,00,\ +2a,00,00,00,03,82,00 + diff --git a/plugin.program.mceremote/resources/data/MSRemote.reg b/plugin.program.mceremote/resources/data/MSRemote.reg new file mode 100644 index 0000000000..170b6670ab --- /dev/null +++ b/plugin.program.mceremote/resources/data/MSRemote.reg @@ -0,0 +1,55 @@ +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\HidIr\Remotes\745a17a0-74d3-11d0-b6fe-00a0c90f57da] +"ReportMappingTable"=hex:\ + 00,00,00,00,04,00,27,\ ; 0 + 01,00,00,00,04,00,1e,\ ; 1 + 02,00,00,00,04,00,1f,\ ; 2 + 03,00,00,00,04,00,20,\ ; 3 + 04,00,00,00,04,00,21,\ ; 4 + 05,00,00,00,04,00,22,\ ; 5 + 06,00,00,00,04,00,23,\ ; 6 + 07,00,00,00,04,00,24,\ ; 7 + 08,00,00,00,04,00,25,\ ; 8 + 09,00,00,00,04,00,26,\ ; 9 + 0a,00,00,00,04,00,29,\ ; Clear - sends escape + 0b,00,00,00,04,00,28,\ ; Enter - sends return + 0c,00,00,00,03,82,00,\ ; Power - left at Windows default, use "04,04,3d" to send alt-F4 + 0d,00,00,00,04,03,1a,\ ; Windows - sends ctrl-shift-W + 0e,00,00,00,04,00,41,\ ; Mute - was "01,e2,00" sends F8 + 0f,00,00,00,04,01,07,\ ; Info - was "01,09,02" sends ctrl-D + 10,00,00,00,04,00,43,\ ; vol up - was "01,e9,00" sends F10 + 11,00,00,00,04,00,42,\ ; vol down - was "01,ea,00" sends F9 + 12,00,00,00,04,00,4B,\ ; chan up - was "01,9c,00" sends PgUp + 13,00,00,00,04,00,4E,\ ; chan down - was "01,9d,00" sends PgDn + 14,00,00,00,04,03,09,\ ; ff - was "01,b3,00" sends ctrl-shift-F + 15,00,00,00,04,03,05,\ ; rew - was "01,b4,00" sends ctrl-shift-B + 16,00,00,00,04,03,13,\ ; play - was "01,b0,00" sends ctrl-shift-P + 17,00,00,00,04,01,15,\ ; record - was "01,b2,00" sends ctrl-R + 18,00,00,00,04,01,13,\ ; pause - was "01,b1,00" sends ctrl-P + 19,00,00,00,04,03,16,\ ; stop - was "01,b7,00" sends ctrl-shift-S + 1a,00,00,00,04,01,09,\ ; next - was "01,b5,00" sends ctrl-F + 1b,00,00,00,04,01,05,\ ; prev - was "01,b6,00" sends ctrl-B + 1c,00,00,00,04,03,20,\ ; # - sends ctrl-shift-3 + 1d,00,00,00,04,03,25,\ ; * - sends strl-shift-8 + 1e,00,00,00,04,00,52,\ ; up + 1f,00,00,00,04,00,51,\ ; down + 20,00,00,00,04,00,50,\ ; left + 21,00,00,00,04,00,4f,\ ; right + 22,00,00,00,04,00,28,\ ; OK (return) + 23,00,00,00,04,00,2a,\ ; back - was "01,24,02" sends Backspace + 24,00,00,00,04,03,10,\ ; DVD menu - sends ctrl-shift-M + 25,00,00,00,04,03,17,\ ; Live TV - sends ctrl-shift-T + 26,00,00,00,04,01,0a,\ ; Guide - was "01,8d,00" sends ctrl-G + 27,00,00,00,04,05,17,\ ; Unidentified top left button on Asrock remote - configured as ctrl-alt-T + 47,00,00,00,04,01,10,\ ; Music - sends ctrl-M + 48,00,00,00,04,01,12,\ ; Recorded TV - sends ctrl-O + 49,00,00,00,04,01,0C,\ ; Pictures - sends ctrl-I + 4A,00,00,00,04,01,08,\ ; Movies - sends ctrl-E + 4E,00,00,00,04,05,13,\ ; HP remote: Print button - sends ctrl-alt-P + 50,00,00,00,04,01,04,\ ; Radio - sends ctrl-A + 5A,00,00,00,04,01,17,\ ; Teletext - sends ctrl-T + 5B,00,00,00,04,05,1e,\ ; Red - sends ctrl-alt-1 + 5C,00,00,00,04,05,1f,\ ; Green - sends ctrl-alt-2 + 5D,00,00,00,04,05,20,\ ; Yellow - sends ctrl-alt-3 + 5E,00,00,00,04,05,21 ; Blue - sends ctrl-alt-4 diff --git a/plugin.program.mceremote/resources/data/ReadMe.txt b/plugin.program.mceremote/resources/data/ReadMe.txt new file mode 100644 index 0000000000..a6694334c2 --- /dev/null +++ b/plugin.program.mceremote/resources/data/ReadMe.txt @@ -0,0 +1,279 @@ +Introduction +------------ +The MCERemote addon for Kodi Dharma (and later versions) is intended to +make it very easy for users to get a Microsoft remote working with +Kodi. However to keep things simple it offers little flexibility. This +document and the associated files are intended for users who want more +control over the customisation, or who are just curious to find out +how it all works. + +Note that this document applies exclusively to Windows. On Linux and +OSX remote controllers work through the Lirc interface. + +The files included here are (in no particular order): + + MSRemote.reg + - a registry file to configure the MS remote for use with Kodi + + CreateTestConfig.reg + - a registry file to install a test configuration. This won't work + with Kodi. The only purpose of this configuration is to identify any + additional buttons on MS compatible remotes + + MSDefault.reg + - a registry file to restore the default configuration set up when a + Microsoft remote is first connected. + + keyboard.xml + - an Kodi keymap for buttons that don't have a standard Media Center + keyboard shortcut, e.g. the four coloured buttons. + + CopyKeyboardDotXML.bat + - run this script to copy the sample keyboard.xml to your + userdata\keymaps folder. + + translate.pdf + - Microsoft document listing all the key codes that can be used in the + remote registry configuration file. + + ShowKey.exe + - an applet to identify keystrokes sent by an MCE remote. + - NB this applet isn't included in the addon because the Kodi addon + repository doesn't allow binaries. You can get the addon from + http://Kodimce.sourceforge.net/. + + +Using a remote with Kodi +------------------------ +The big problem with using remote controls on Kodi is that Windows +provides no standard interface for remote ontrols. The way we get round +this is to make the remote control emulate a keyboard i.e. when you +press a button on the remote handset Windows thinks a key has been +pressed on the keyboard. + +This may seem a strange way to use a remote control, but in fact many +of the cheaper MCE remotes already work this way. Microsoft have +documented a standard set of keytrokes that remote controls can send. +See http://msdn.microsoft.com/en-us/library/bb189249.aspx if you're +really interested. + +The MCERemote addon, and the registry files here, configure the +Microsoft remote so it sends these standard keystrokes. The registry +files will also work with any Microsoft compatible remote that uses the +Microsoft eHome device driver. This includes the HP remote and the +remote included with the Asrock 330HT HTPCs, as well as various other +remotes. See http://wiki.Kodi.org/index.php?title=Remote_Control_Reviews +for some remotes that are known to be compatible with the MS remote. + + +How it works +------------ +On Windows MS compatible remotes are automatically detected as soon as +you connect them, and they use a driver called eHome. If you look in +Device Manager under the Human Interface Devices heading you should see +your remote listed as "Microsoft eHome Infrared Transceiver". + +The eHome driver keeps the configuration for the buttons on the remote +in the registry key: + +HKEY_LOCAL_MACHINE + \SYSTEM + \CurrentControlSet + \Services + \HidIr + \Remotes + \745a17a0-74d3-11d0-b6fe-00a0c90f57da + +The configuration is in a REG_BINARY value called ReportMappingTable. +The MCERemote addon and registry files here work by modifying this +value. + +You can install any of the configurations included here just by +double-clicking the .reg file (or by using the addon from within Kodi). + +This is probably as far as you need to read if you're just curious. The +following sections go into detail on the format of the +ReportMappingTable value and how to modify it. + + +The ReportMappingTable value +---------------------------- +The ReportMappingTable value is a binary array consisting of rows of 7 +bytes. Each row defines one button. The seven bytes in the row are: + +Byte Action + 0 button number (see below) + 1 always 0 + 2 always 0 + 3 always 0 + 4 04 sends a keystroke + 5 key modifier (see below) + 6 keystroke (see below) + +Byte 4 can be 01 or 03. These send IR codes not keystrokes and I'm +ignoring these values in this article. I'm only interested in setting +byte 4 to "04" to indicate a keystroke. + +Byte 5, the key modifier, specifies if control, shift etc are down when +the key is sent. The value can be: + +Byte Action + 0 No modifier + 1 Control + 2 Shift + 3 Control-Shift + 4 Alt + 5 Control-Alt + 6 Shift-Alt + 7 Control-Shift-Alt + 8 Windows + 9 Control-Windows + a Shift-Windows + b Control-Shift-Windows + c Alt-Windows + d Control-Alt-Windows + e Shift-Alt-Windows + f Control-Shift-Alt-Windows + +If you're happy with binary numbers you've probably spotted that bit 0 +specifies Control, bit 1 specifies Shift, bit 2 specifies Alt and bit 3 +specifies the Windows key. + +The keystroke is not an ACSII code or a scan code. It's an arbitrary +code selected by MS. You can find a list of the codes in + +http://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/translate.pdf + +and I've included a copy with this article in translate.pdf in case the +above link breaks. + +So the MCERemote addon and the .reg files work by setting byte 4 to 04 +to send a keystroke, and then setting appropriate values for bytes 5 +and 6 to specify the keystroke. For example the MCE shortcut for "Play" +is ctrl-shift-P. To specify this keystroke use: + +Byte 4 - 04 to specify a keystroke +Byte 5 - 03 to specify ctrl-shift +Byte 6 - 13 to specify the "P" key + +and the full line in MSRemote.reg is: + + 16,00,00,00,04,03,13,\ ; play - sends ctrl-shift-P + +You need to know the button number to put in byte 0 (16 in this +example). The known button numbers are listed in Appendix A. To find +the number for a button you can use the CreateTestConfig.reg config. +See the next section for the details. + +Feel free to inspect the .reg files using Notepad; they are all well +commented. Note the files are UNICODE so they may not display properly +in editors that don't support UNICODE. + +Finally note that the eHome driver only reads the ReportMappingTable +when it starts i.e. when Windows starts. If you modify +ReportMappingTable you need to restart Windows for your changes to take +effect. + + +CreateTestConfig.reg +-------------------- +The CreateTestConfig.reg config assigns a different keystroke to every +possible button number from 00 to FF. If you need to identify a button +number install this config (and reboot) then run the ShowKey applet, +press the button and see what keystroke ShowKey reports. + +For example: + +Suppose after installing the CreateTestConfig.reg config you press a +button and Showkey reports: + + KeyID 68 (0x44) - VK_D + Mod Ctrl + +This is telling you that the button generated a ctrl-D keypress. Open +CreateTestConfig.reg in Notepad and scroll down until you find: + + 27,00,00,00,04,01,07,\ ; ctrl-D + +This tells you that button number 27 is configured as ctrl-D, and +therefore that the button you pressed is number 27. This is actually +the top left button on the Asrock 330HT remote, which isn't on a +standard MS remote, and this is exactly how I figured out what number +this button was. + + +Showkey.exe +----------- +When playing around with remote controllers it's useful to see exactly +what the remote controller is sending to Windows. ShowKey is an applet +that records any keystrokes it receives and tells you what they are. It +also gives you the XML you need for using that keystroke in an Kodi +keyboard mapping file. Finally it records any WM_APPCOMMAND +Windows messages it receives. This is likely to be of interest only to +us Windows programmers! + + +Appendix A: Button numbers +-------------------------- +The button numbers I know about are: + + 00 0 + 01 1 + 02 2 + 03 3 + 04 4 + 05 5 + 06 6 + 07 7 + 08 8 + 09 9 + 0a Clear + 0b Enter + 0c Power + 0d Windows + 0e Mute + 0f Info + 10 vol up + 11 vol down + 12 chan up + 13 chan down + 14 ff + 15 rew + 16 play + 17 record + 18 pause + 19 stop + 1a next + 1b prev + 1c # + 1d * + 1e up + 1f down + 20 left + 21 right + 22 OK (return) + 23 back + 24 DVD menu + 25 Live TV + 26 Guide + 27 Unidentified top left button on Asrock 330HT remote + 47 My Music + 48 Recorded TV + 49 My Pictures + 4A My Movies + 4E Print button on HP remote + 50 Radio + 5A Teletext button + 5B Red + 5C Green + 5D Yellow + 5E Blue + +Every now and then a new MS compatible remote turns up with a new button +not listed above. In that case you can use the CreateTestConfig.reg +configuration to determine the button number. + + +John Rennie +john.rennie@ratsauce.co.uk +17th October 2010 diff --git a/plugin.program.mceremote/resources/data/keyboard.xml b/plugin.program.mceremote/resources/data/keyboard.xml new file mode 100644 index 0000000000..458e26a5f7 --- /dev/null +++ b/plugin.program.mceremote/resources/data/keyboard.xml @@ -0,0 +1,22 @@ + + + + + + + Notification(Key, Windows button, 3) + + + + + + Notification(Key, Red button, 3) + + Notification(Key, Green button, 3) + + Notification(Key, Yellow button, 3) + + Notification(Key, Blue button, 3) + + + diff --git a/plugin.program.mceremote/resources/data/translate.pdf b/plugin.program.mceremote/resources/data/translate.pdf new file mode 100644 index 0000000000..f780963a43 Binary files /dev/null and b/plugin.program.mceremote/resources/data/translate.pdf differ diff --git a/plugin.program.mceremote/resources/icon.png b/plugin.program.mceremote/resources/icon.png new file mode 100644 index 0000000000..4668fb96a0 Binary files /dev/null and b/plugin.program.mceremote/resources/icon.png differ diff --git a/plugin.program.mceremote/resources/language/resource.language.en_gb/strings.po b/plugin.program.mceremote/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..105a221f28 --- /dev/null +++ b/plugin.program.mceremote/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,391 @@ +# Kodi Media Center language file +# Addon Name: MCERemote +# Addon id: plugin.program.mceremote3 +# Addon Provider: John Rennie +msgid "" +msgstr "" + +msgctxt "#30000" +msgid "Number 0" +msgstr "" + +msgctxt "#30001" +msgid "Number 1" +msgstr "" + +msgctxt "#30002" +msgid "Number 2" +msgstr "" + +msgctxt "#30003" +msgid "Number 3" +msgstr "" + +msgctxt "#30004" +msgid "Number 4" +msgstr "" + +msgctxt "#30005" +msgid "Number 5" +msgstr "" + +msgctxt "#30006" +msgid "Number 6" +msgstr "" + +msgctxt "#30007" +msgid "Number 7" +msgstr "" + +msgctxt "#30008" +msgid "Number 8" +msgstr "" + +msgctxt "#30009" +msgid "Number 9" +msgstr "" + +msgctxt "#30010" +msgid "Clear" +msgstr "" + +msgctxt "#30011" +msgid "Enter" +msgstr "" + +msgctxt "#30012" +msgid "Power" +msgstr "" + +msgctxt "#30013" +msgid "Windows" +msgstr "" + +msgctxt "#30014" +msgid "Volume mute" +msgstr "" + +msgctxt "#30015" +msgid "Info/More/Details" +msgstr "" + +msgctxt "#30016" +msgid "Volume up" +msgstr "" + +msgctxt "#30017" +msgid "Volume down" +msgstr "" + +msgctxt "#30018" +msgid "Page up" +msgstr "" + +msgctxt "#30019" +msgid "Page down" +msgstr "" + +msgctxt "#30020" +msgid "FF" +msgstr "" + +msgctxt "#30021" +msgid "Rewind" +msgstr "" + +msgctxt "#30022" +msgid "Play" +msgstr "" + +msgctxt "#30023" +msgid "Record" +msgstr "" + +msgctxt "#30024" +msgid "Pause" +msgstr "" + +msgctxt "#30025" +msgid "Stop" +msgstr "" + +msgctxt "#30026" +msgid "Next" +msgstr "" + +msgctxt "#30027" +msgid "Prev" +msgstr "" + +msgctxt "#30028" +msgid "Hash (#)" +msgstr "" + +msgctxt "#30029" +msgid "Asterisk (*)" +msgstr "" + +msgctxt "#30030" +msgid "Up arrow" +msgstr "" + +msgctxt "#30031" +msgid "Down arrow" +msgstr "" + +msgctxt "#30032" +msgid "Left arrow" +msgstr "" + +msgctxt "#30033" +msgid "Right arrow" +msgstr "" + +msgctxt "#30034" +msgid "OK" +msgstr "" + +msgctxt "#30035" +msgid "Back" +msgstr "" + +msgctxt "#30036" +msgid "DVD Menu" +msgstr "" + +msgctxt "#30037" +msgid "Live TV" +msgstr "" + +msgctxt "#30038" +msgid "Guide" +msgstr "" + +msgctxt "#30039" +msgid "Asrock top left button" +msgstr "" + +msgctxt "#30071" +msgid "My Music" +msgstr "" + +msgctxt "#30072" +msgid "Recorded TV" +msgstr "" + +msgctxt "#30073" +msgid "My Pictures" +msgstr "" + +msgctxt "#30074" +msgid "My Movies" +msgstr "" + +msgctxt "#30080" +msgid "Radio" +msgstr "" + +msgctxt "#30090" +msgid "Teletext" +msgstr "" + +msgctxt "#30091" +msgid "Red button" +msgstr "" + +msgctxt "#30092" +msgid "Green button" +msgstr "" + +msgctxt "#30093" +msgid "Yellow button" +msgstr "" + +msgctxt "#30094" +msgid "Blue button" +msgstr "" + +msgctxt "#30041" +msgid "Harmony discrete power on" +msgstr "" + +msgctxt "#30042" +msgid "Harmony discrete power off" +msgstr "" + +msgctxt "#30075" +msgid "Mediagate DVD Angle" +msgstr "" + +msgctxt "#30076" +msgid "Mediagate DVD Audio" +msgstr "" + +msgctxt "#30077" +msgid "Mediagate Subtitles" +msgstr "" + +msgctxt "#30078" +msgid "HP print" +msgstr "" + +msgctxt "#30110" +msgid "HP play/pause" +msgstr "" + +msgctxt "#30040" +msgid "XBox Open/Close" +msgstr "" + +msgctxt "#30079" +msgid "XBox Display" +msgstr "" + +msgctxt "#30100" +msgid "XBox Large X" +msgstr "" + +msgctxt "#30102" +msgid "XBox Green A" +msgstr "" + +msgctxt "#30104" +msgid "XBox Blue X" +msgstr "" + +msgctxt "#30108" +msgid "XBox Chan up" +msgstr "" + +msgctxt "#30109" +msgid "XBox Chan dn" +msgstr "" + +msgctxt "#30200" +msgid "Button mappings" +msgstr "" + +msgctxt "#30300" +msgid "MCERemote" +msgstr "" + +msgctxt "#30301" +msgid "This plugin only runs on Windows" +msgstr "" + +msgctxt "#30302" +msgid "Warning: The ReportMappingTable registry key is not present. No Microsoft remote is installed." +msgstr "" + +#ApplyCurrentSettings + +msgctxt "#30303" +msgid "This will configure the remote using the currently configured settings. Do you want to proceed?" +msgstr "" + +msgctxt "#30304" +msgid "The setting for {} is invalid: {}" +msgstr "" + +msgctxt "#30305" +msgid "Error opening the IR registry key: {}. Try running XBMC as administrator." +msgstr "" + +msgctxt "#30306" +msgid "Unexpected error opening the IR registry key." +msgstr "" + +msgctxt "#30307" +msgid "Unexpected error writing the ReportMappingTable data: {}" +msgstr "" + +msgctxt "#30308" +msgid "Unexpected error writing the ReportMappingTable data" +msgstr "" + +msgctxt "#30309" +msgid "Done! Please restart Windows to activate the changes." +msgstr "" + +#ApplyDefaultSettings + +msgctxt "#30310" +msgid "This will reset the remote to the default MS config. Do you want to proceed?" +msgstr "" + +#ConfigureButtonSettings + +msgctxt "#30311" +msgid "Do you want to apply these settings to the remote now?" +msgstr "" + +#RestoreDefaultButtonSettings + +msgctxt "#30312" +msgid "This will reset the remote button settings to the Media Center defaults. Do you want to proceed?" +msgstr "" + +#EditKeyboardDotXML + +msgctxt "#30313" +msgid "You don't currently have a keyboard.xml file. Do you want to create one?" +msgstr "" + +msgctxt "#30314" +msgid "Warning: your file has an error: {}" +msgstr "" + +msgctxt "#30315" +msgid "There was an unidentified error in your keyboard.xml file." +msgstr "" + +#CreateKeyboardDotXML + +msgctxt "#30316" +msgid "Cannot find the template keyboard.xml. The MCERemote addon is not properly installed." +msgstr "" + +msgctxt "#30317" +msgid "A keyboard.xml already exists in: {}. Do you want to overwrite it?" +msgstr "" + +msgctxt "#30318" +msgid "Create the template keyboard.xml in: {}?" +msgstr "" + +msgctxt "#30319" +msgid "Unexpected error copying the keyboard.xml file." +msgstr "" + +#ReadInstructions + +msgctxt "#30320" +msgid "Cannot find ReadMeFirst.txt. The MCERemote addon is not properly installed." +msgstr "" + +#ListOptions + +msgctxt "#30321" +msgid "Read the instructions" +msgstr "" + +msgctxt "#30322" +msgid "Apply current settings to remote" +msgstr "" + +msgctxt "#30323" +msgid "Apply Windows default settings to remote" +msgstr "" + +msgctxt "#30324" +msgid "Configure MCERemote settings" +msgstr "" + +msgctxt "#30325" +msgid "Restore default MCERemote settings" +msgstr "" + +msgctxt "#30326" +msgid "Edit keyboard.xml" +msgstr "" + diff --git a/plugin.program.mceremote/resources/settings.xml b/plugin.program.mceremote/resources/settings.xml new file mode 100644 index 0000000000..1c9ffd19fc --- /dev/null +++ b/plugin.program.mceremote/resources/settings.xml @@ -0,0 +1,374 @@ + + +
    + + + + 0 + 0 + + + + 0 + 1 + + + + 0 + 2 + + + + 0 + 3 + + + + 0 + 4 + + + + 0 + 5 + + + + 0 + 6 + + + + 0 + 7 + + + + 0 + 8 + + + + 0 + 9 + + + + 0 + escape + + + + 0 + return + + + + 0 + mce + + + + 0 + ctrl-shift-w + + + + 0 + f8 + + + + 0 + ctrl-d + + + + 0 + f10 + + + + 0 + f9 + + + + 0 + pageup + + + + 0 + pagedown + + + + 0 + ctrl-shift-f + + + + 0 + ctrl-shift-b + + + + 0 + ctrl-shift-p + + + + 0 + ctrl-r + + + + 0 + ctrl-p + + + + 0 + ctrl-shift-s + + + + 0 + ctrl-f + + + + 0 + ctrl-b + + + + 0 + ctrl-shift-3 + + + + 0 + ctrl-shift-8 + + + + 0 + up + + + + 0 + down + + + + 0 + left + + + + 0 + right + + + + 0 + return + + + + 0 + backspace + + + + 0 + ctrl-shift-m + + + + 0 + ctrl-shift-t + + + + 0 + ctrl-g + + + + 0 + ctrl-m + + + + 0 + ctrl-o + + + + 0 + ctrl-i + + + + 0 + ctrl-e + + + + 0 + ctrl-a + + + + 0 + ctrl-t + + + + 0 + ctrl-alt-1 + + + + 0 + ctrl-alt-2 + + + + 0 + ctrl-alt-3 + + + + 0 + ctrl-alt-4 + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + 0 + + + true + + + + + +
    +
    diff --git a/plugin.video.3satmediathek/LICENSE.txt b/plugin.video.3satmediathek/LICENSE.txt new file mode 100644 index 0000000000..23cb790338 --- /dev/null +++ b/plugin.video.3satmediathek/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.video.3satmediathek/addon.xml b/plugin.video.3satmediathek/addon.xml new file mode 100644 index 0000000000..b497ffa24a --- /dev/null +++ b/plugin.video.3satmediathek/addon.xml @@ -0,0 +1,27 @@ + + + + + + + + video + + + all + de + GPL-2.0-only + https://github.com/sarbes/plugin.video.3satmediathek + https://forum.kodi.tv/showthread.php?tid=353903 + https://www.3sat.de/ + Videos from the 3sat Mediathek. + This add-on provides access to the 3sat Mediathek. Videos may be geolocked to Germany. + Sendungen und Beiträge aus der 3sat Mediathek. + Dieses Add-on bietet Zugriff auf die Mediathek von 3sat. Hier gibt es Sendungen und Dokumentationen zum Abruf. + Dieses Add-on wird von keiner Sendeanstalt unterstützt und ist daher inoffiziell. + + resources/icon.png + resources/fanart.jpg + + + diff --git a/plugin.video.3satmediathek/default.py b/plugin.video.3satmediathek/default.py new file mode 100644 index 0000000000..3575e3bb2c --- /dev/null +++ b/plugin.video.3satmediathek/default.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +import libzdf + +class tsat(libzdf.libzdf): + def __init__(self): + self.apiVersion = 1 + self.baseApi = 'https://api.3sat.de' + self.userAgent = '3Sat-App/3.53.800' + self.channels = ['3sat'] + self.tokenUrl = False + self.API_CLIENT_ID = '8764caa8' + self.API_CLIENT_KEY = '60a8d9c60feb563dd70001e46e147d1b' + libzdf.libzdf.__init__(self) + + + def libZdfListMain(self): + l = [] + #l.append({'metadata':{'name':self.translation(32031)}, 'params':{'mode':'libZdfListPage', 'url':f'{self.baseApi}/content/documents/meist-gesehen-100.json?profile=default'}, 'type':'dir'})#endpoint out of service atm + l.append({'metadata':{'name':self.translation(32132)}, 'params':{'mode':'libZdfListShows'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(32133)}, 'params':{'mode':'libMediathekListDate','subParams':f'{{"mode":"libZdfListChannelDateVideos","channel":"3sat"}}'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(32134)}, 'params':{'mode':'libZdfListPage', 'url':f'{self.baseApi}/search/documents?q=%2A&contentTypes=category'}, 'type':'dir'}) + l.append({'metadata':{'name':self.translation(32139)}, 'params':{'mode':'libMediathekSearch', 'searchMode':'libZdfListSearch'}, 'type':'dir'}) + return {'items':l,'name':'root'} + + def libZdfListChannelDateVideos(self): + self.params['url'] = f"{self.baseApi}/content/documents/zdf/programm?profile=video-app&maxResults=200&airtimeDate={self.params['yyyymmdd']}T00:00:00.000Z&includeNestedObjects=true" + return self.libZdfListPage() + +o = tsat() +o.action() \ No newline at end of file diff --git a/plugin.video.3satmediathek/resources/fanart.jpg b/plugin.video.3satmediathek/resources/fanart.jpg new file mode 100644 index 0000000000..a11faf99ff Binary files /dev/null and b/plugin.video.3satmediathek/resources/fanart.jpg differ diff --git a/plugin.video.3satmediathek/resources/icon.png b/plugin.video.3satmediathek/resources/icon.png new file mode 100644 index 0000000000..ae5e469fcf Binary files /dev/null and b/plugin.video.3satmediathek/resources/icon.png differ diff --git a/plugin.video.ahltv/LICENSE.txt b/plugin.video.ahltv/LICENSE.txt new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/plugin.video.ahltv/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.video.ahltv/README.md b/plugin.video.ahltv/README.md new file mode 100644 index 0000000000..df408422f6 --- /dev/null +++ b/plugin.video.ahltv/README.md @@ -0,0 +1,5 @@ +# plugin.video.ahltv + +Watch live and archived full games with AHLTV* + +*Requires a valid AHLTV account and subscription to view full live and archived games. diff --git a/plugin.video.ahltv/addon.py b/plugin.video.ahltv/addon.py new file mode 100644 index 0000000000..95cb9b9a22 --- /dev/null +++ b/plugin.video.ahltv/addon.py @@ -0,0 +1,41 @@ +from resources.lib.ahl_tv import * + +params = get_params() +mode = None +game_day = None +game_id = None + +if 'mode' in params: + mode = int(params["mode"]) +if 'game_day' in params: + game_day = quote(params["game_day"]) +if 'game_id' in params: + game_id = quote(params["game_id"]) + +if mode is None: # or url is None: + main_menu() + +elif mode == 100 or mode == 101: + daily_games(game_day) + +elif mode == 102: + # Yesterday's Games + game_day = local_to_eastern() + display_day = string_to_date(game_day, "%Y-%m-%d") + prev_day = display_day - timedelta(days=1) + daily_games(prev_day.strftime("%Y-%m-%d")) + +elif mode == 103: + select_date() + +elif mode == 104: + select_game(game_id) + +elif mode == 401: + logout() + +elif mode == 999: + sys.exit() + +#else: +xbmcplugin.endOfDirectory(addon_handle) diff --git a/plugin.video.ahltv/addon.xml b/plugin.video.ahltv/addon.xml new file mode 100644 index 0000000000..b42d2af6e7 --- /dev/null +++ b/plugin.video.ahltv/addon.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + video + + + all + en + AHLTV + Watch AHL games on Kodi, with HD quality streaming + Requires a valid AHLTV account to view live and archived games. + + icon.png + fanart.jpg + + + - Added new team entries for Abbotsford and Bridgeport + + GPL-2.0-or-later + https://forum.kodi.tv/showthread.php?tid=351891 + https://www.theahl.com/ahltv + https://github.com/sobralik/plugin.video.ahltv + + diff --git a/plugin.video.ahltv/changelog.txt b/plugin.video.ahltv/changelog.txt new file mode 100644 index 0000000000..079d6b0cd6 --- /dev/null +++ b/plugin.video.ahltv/changelog.txt @@ -0,0 +1,2 @@ +2020.01.29 +- Initial version diff --git a/plugin.video.ahltv/fanart.jpg b/plugin.video.ahltv/fanart.jpg new file mode 100644 index 0000000000..40369bc079 Binary files /dev/null and b/plugin.video.ahltv/fanart.jpg differ diff --git a/plugin.video.ahltv/icon.png b/plugin.video.ahltv/icon.png new file mode 100644 index 0000000000..75080bb0f1 Binary files /dev/null and b/plugin.video.ahltv/icon.png differ diff --git a/plugin.video.ahltv/resources/__init__.py b/plugin.video.ahltv/resources/__init__.py new file mode 100644 index 0000000000..1aef991c0d --- /dev/null +++ b/plugin.video.ahltv/resources/__init__.py @@ -0,0 +1 @@ +__author__ = 'sobralik' diff --git a/plugin.video.ahltv/resources/language/resource.language.en_gb/strings.po b/plugin.video.ahltv/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..43ace273de --- /dev/null +++ b/plugin.video.ahltv/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,221 @@ +# Kodi Media Center language file +# Addon Name: AHLTV +# Addon id: plugin.video.ahltv +# Addon Provider: sobralik +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "Login" +msgstr "" + +msgctxt "#30001" +msgid "Username" +msgstr "" + +msgctxt "#30002" +msgid "Password" +msgstr "" + +msgctxt "#30003" +msgid "Logout" +msgstr "" + +msgctxt "#30004" +msgid "Playback" +msgstr "" + +msgctxt "#30005" +msgid "Use FFmpeg instead of InputStream Adaptive" +msgstr "" + +msgctxt "#31000" +msgid "Please enter your username" +msgstr "" + +msgctxt "#31001" +msgid "Please enter your password" +msgstr "" + +msgctxt "#31002" +msgid "Login Failed" +msgstr "" + +msgctxt "#31003" +msgid "Incorrect username/password" +msgstr "" + +msgctxt "#31004" +msgid "Login Error" +msgstr "" + +msgctxt "#31005" +msgid "There was an error when attempting to login. Please try again later." +msgstr "" + +msgctxt "#31006" +msgid "Logout Successful" +msgstr "" + +msgctxt "#31007" +msgid "Logout completed successfully" +msgstr "" + +msgctxt "#31008" +msgid "Login Expired" +msgstr "" + +msgctxt "#31009" +msgid "Your session has expired. Please try again and your login will be refreshed." +msgstr "" + +msgctxt "#31010" +msgid "Error" +msgstr "" + +msgctxt "#31011" +msgid "Error loading stream info" +msgstr "" + +msgctxt "#31012" +msgid "Home Audio" +msgstr "" + +msgctxt "#31013" +msgid "Away Audio" +msgstr "" + +msgctxt "#31014" +msgid "Access Error" +msgstr "" + +msgctxt "#31015" +msgid "You do not have an active AHLTV subscription for this game. To access this content please purchase a subscription at TheAHL.com/AHLTV." +msgstr "" + +msgctxt "#31016" +msgid "There was an error logging out. Please try again." +msgstr "" + +msgctxt "#32000" +msgid "Today’s Games" +msgstr "" + +msgctxt "#32001" +msgid "Yesterday’s Games" +msgstr "" + +msgctxt "#32002" +msgid "Select Date" +msgstr "" + +msgctxt "#32003" +msgid "Previous Day" +msgstr "" + +msgctxt "#32004" +msgid "Next Day" +msgstr "" + +msgctxt "#32006" +msgid "Failed to load schedule for" +msgstr "" + +msgctxt "#32007" +msgid "(0 games)" +msgstr "" + +msgctxt "#32008" +msgid "(Live)" +msgstr "" + +msgctxt "#32009" +msgid "(Final)" +msgstr "" + +msgctxt "#32010" +msgid "(FREE)" +msgstr "" + +msgctxt "#32011" +msgid "Streams Not Found" +msgstr "" + +msgctxt "#32012" +msgid "No playable streams found" +msgstr "" + +msgctxt "#32013" +msgid "Select Stream" +msgstr "" + +msgctxt "#32014" +msgid "Choose Year" +msgstr "" + +msgctxt "#32015" +msgid "Choose Month" +msgstr "" + +msgctxt "#32016" +msgid "January" +msgstr "" + +msgctxt "#32017" +msgid "February" +msgstr "" + +msgctxt "#32018" +msgid "March" +msgstr "" + +msgctxt "#32019" +msgid "April" +msgstr "" + +msgctxt "#32020" +msgid "May" +msgstr "" + +msgctxt "#32021" +msgid "June" +msgstr "" + +msgctxt "#32022" +msgid "September" +msgstr "" + +msgctxt "#32023" +msgid "October" +msgstr "" + +msgctxt "#32024" +msgid "November" +msgstr "" + +msgctxt "#32025" +msgid "December" +msgstr "" + +msgctxt "#32026" +msgid "Choose Day" +msgstr "" + +msgctxt "#32027" +msgid "games" +msgstr "" + +msgctxt "#32028" +msgid "at" +msgstr "" diff --git a/plugin.video.ahltv/resources/lib/__init__.py b/plugin.video.ahltv/resources/lib/__init__.py new file mode 100644 index 0000000000..1aef991c0d --- /dev/null +++ b/plugin.video.ahltv/resources/lib/__init__.py @@ -0,0 +1 @@ +__author__ = 'sobralik' diff --git a/plugin.video.ahltv/resources/lib/ahl_tv.py b/plugin.video.ahltv/resources/lib/ahl_tv.py new file mode 100644 index 0000000000..9887dcb5c3 --- /dev/null +++ b/plugin.video.ahltv/resources/lib/ahl_tv.py @@ -0,0 +1,254 @@ +from .globals import * +from .api import * +from .utils import * +import sys +import requests +from time import gmtime + +def main_menu(): + add_dir(LOCAL_STRING(32000), 100, AHL_LOGO, FANART) + add_dir(LOCAL_STRING(32001), 102, AHL_LOGO, FANART) + add_dir(LOCAL_STRING(32002), 103, AHL_LOGO, FANART) + +def daily_games(game_day): + if game_day is None: + game_day = local_to_eastern() + + display_day = string_to_date(game_day, "%Y-%m-%d") + prev_day = display_day - timedelta(days=1) + add_dir('[B]<< %s[/B]' % LOCAL_STRING(32003), 101, AHL_LOGO, FANART, prev_day.strftime("%Y-%m-%d")) + + url = API_URL + '/htv2020/schedule_league/100008?start_date=' + game_day + '&end_date=' + game_day + log('Loading games from: '+ url) + headers = get_request_headers() + + r = requests.get(url, headers=headers, verify=VERIFY) + log('games response code: '+ str(r.status_code)) + + if r.status_code >= 400: + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(31010), LOCAL_STRING(32006) + ' ' + game_day) + else: + json_source = r.json() + + # Add the date header for the list + day_games_label = game_day + if "games" in json_source: + day_games_label += " (" + str(len(json_source['games'])) + " " + LOCAL_STRING(32027) + ")" + else: + day_games_label += " " + LOCAL_STRING(32007) + + list_item = xbmcgui.ListItem(label=day_games_label) + list_item.setArt({ 'thumb': AHL_LOGO }) + list_item.setProperty('fanart_image', FANART) + + u = sys.argv[0] + "?mode=" + str(101) + "&game_day=" + quote(game_day) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=u, listitem=list_item, isFolder=False) + + if "games" in json_source: + log("num games returned: " + str(len(json_source['games']))) + try: + if len(json_source['games']) > 0: + for game in json_source['games']: + create_game_listitem(game, game_day) + except Exception as e: + log('Failed to process games: '+ str(e), True) + pass + + next_day = display_day + timedelta(days=1) + add_dir('[B]%s >>[/B]' % LOCAL_STRING(32004), 101, AHL_LOGO, FANART, next_day.strftime("%Y-%m-%d")) + +def create_game_listitem(game, game_day): + icon = None + home_team = game['homeTeam'] + visiting_team = game['visitingTeam'] + + icon = get_game_icon(home_team['iteamid'], home_team['strteamlogoimageurl'], visiting_team['iteamid'], visiting_team['strteamlogoimageurl']) + + game_time = '' + if game['blivegameflag'] == 2 and game['currentlyStreaming'] == False: + game_time = game['dateTime'] + game_time = string_to_date(game_time, "%Y-%m-%dT%H:%M:%SZ") + game_time = utc_to_local(game_time) + game_time = game_time.strftime('%I:%M %p').lstrip('0') + + elif game['blivegameflag'] == 2 and game['currentlyStreaming'] == True: + game_time = '(Live)' + else: + game_time = '(Final)' + + game_id = game['id'] + + fanart = TEAM_FANART[home_team['iteamid']] + name = '%s %s %s %s' % (game_time, visiting_team['strteamname'], LOCAL_STRING(32028), home_team['strteamname']) + desc = '%s %s %s' % (visiting_team['strteamname'], LOCAL_STRING(32028), home_team['strteamname']) + ' - ' + game_day + name = name.encode('utf-8') + + title = '%s %s %s' % (visiting_team['strteamname'], LOCAL_STRING(32028), home_team['strteamname']) + title = title.encode('utf-8') + + # Label free game of the day + try: + if bool(game['free']): + name = name + color_string(' (FREE)', COLOR_FREE) + except: + pass + + video_info = {} + audio_info = {} + + info = {'plot': desc, + 'tvshowtitle': 'AHLTV', + 'title': title, + 'aired': game_day, + 'genre': 'Hockey'} + + add_game_listitem(name, title, game_id, icon, fanart, info, video_info, audio_info) + +def color_string(string, color): + return '[COLOR=' + color + ']' + string + '[/COLOR]' + +def add_game_listitem(name, title, game_id, icon=None, fanart=None, info=None, video_info=None, audio_info=None): + ok = True + + u = sys.argv[0] + "?url=&mode=" + str(104) + "&name=" + quote(name) + "&game_id=" + quote(str(game_id)) + + liz = xbmcgui.ListItem(name) + + if icon is not None: + liz.setArt({'icon': icon, 'thumb': icon, 'clearlogo': CLEARLOGO}) + else: + liz.setArt({'icon': ICON, 'thumb': ICON, 'clearlogo': CLEARLOGO}) + + if fanart is not None: + liz.setProperty('fanart_image', fanart) + else: + liz.setProperty('fanart_image', FANART) + + liz.setProperty("IsPlayable", "true") + + liz.setInfo(type="Video", infoLabels={"Title": title}) + if info is not None: + liz.setInfo(type="Video", infoLabels=info) + if video_info is not None: + liz.addStreamInfo('video', video_info) + if audio_info is not None: + liz.addStreamInfo('audio', audio_info) + + liz.setProperty('dbtype', 'video') + ok = xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=u, listitem=liz, isFolder=False) + xbmcplugin.setContent(addon_handle, 'videos') + + return ok + +def select_game(game_id): + game_json = get_game_info(game_id) + played_id = None + + if game_json['blivegameflag'] == 2 and not game_json['currentlyStreaming']: # Game is not streaming at the moment, but should be live + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(32011), LOCAL_STRING(32012)) + sys.exit() + elif (game_json['blivegameflag'] <= -1 and game_json['blivegameflag'] >= -5) or (game_json['blivegameflag'] == 2 and game_json['currentlyStreaming']): # VOD game or Live game + dialog = xbmcgui.Dialog() + + multiple_audio = game_json['multipleAudioSetting'] == 1 + stream_info = get_game_streams(game_id, multiple_audio) + + x = dialog.select(LOCAL_STRING(32013), stream_info['list_items']) + log('user selected stream: '+ str(x)) + + if x >= 0: + seek_secs = 0 + selected_audio = stream_info['streams'][x]['audioId'] + if stream_info['streams'][x]['startSeconds'] is not None: + seek_secs = stream_info['streams'][x]['startSeconds'] + + xbmcplugin.setResolvedUrl(addon_handle, True, stream_info['list_items'][x]) + + while not xbmc.Player().isPlayingVideo(): + xbmc.Monitor().waitForAbort(0.5) + + # Seek to the start time of the video if it is set + if xbmc.Player().isPlayingVideo(): + xbmc.sleep(250) + xbmc.executebuiltin('Seek(' + str(seek_secs) + ')') + played_id = log_played(game_id, selected_audio) + + track_minute = gmtime().tm_min + + while xbmc.Player().isPlayingVideo() and not xbmc.Monitor().abortRequested(): + xbmc.Monitor().waitForAbort(10.00) + new_minute = gmtime().tm_min # Update watched status every minute + if new_minute > track_minute: + track_minute = new_minute + if played_id is not None: + marked = mark_end_time(played_id) + if marked == False: + played_id = None + else: + # No stream selected / selection aborted + sys.exit() + + else: + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(32011), LOCAL_STRING(32012)) + sys.exit() + +def select_date(): + # Goto Date + dialog = xbmcgui.Dialog() + game_day = '' + + # Year + year_list = [] + min_year = 2018 + while min_year <= datetime.now().year: + year_list.insert(0, str(min_year)) + min_year = min_year + 1 + + ret = dialog.select(LOCAL_STRING(32014), year_list) + + if ret > -1: + year = year_list[ret] + + mnth_name = [LOCAL_STRING(32016), LOCAL_STRING(32017), LOCAL_STRING(32018), LOCAL_STRING(32019), LOCAL_STRING(32020), LOCAL_STRING(32021), LOCAL_STRING(32022), LOCAL_STRING(32023), LOCAL_STRING(32024), LOCAL_STRING(32025)] + mnth_num = ['1', '2', '3', '4', '5', '6', '9', '10', '11', '12'] + + ret = dialog.select(LOCAL_STRING(32015), mnth_name) + + if ret > -1: + mnth = mnth_num[ret] + + # Day + day_list = [] + day_item = 1 + last_day = calendar.monthrange(int(year), int(mnth))[1] + while day_item <= last_day: + day_list.append(str(day_item)) + day_item = day_item + 1 + + ret = dialog.select(LOCAL_STRING(32026), day_list) + + if ret > -1: + day = day_list[ret] + game_day = year + '-' + mnth.zfill(2) + '-' + day.zfill(2) + + if game_day != '': + daily_games(game_day) + else: + sys.exit() + +def add_dir(name, mode, icon, fanart=None, game_day=None, info=None, content_type='videos'): + ok = True + u = addon_url+"?mode="+str(mode) + if game_day is not None: + u = u + "&game_day=" + quote(game_day) + liz=xbmcgui.ListItem(name) + if fanart is None: fanart = FANART + liz.setArt({'icon': icon, 'thumb': icon, 'poster': icon, 'fanart': fanart}) + if info is not None: + liz.setInfo( type="video", infoLabels=info) + ok = xbmcplugin.addDirectoryItem(handle=addon_handle,url=u,listitem=liz,isFolder=True) + xbmcplugin.setContent(addon_handle, content_type) + return ok diff --git a/plugin.video.ahltv/resources/lib/api.py b/plugin.video.ahltv/resources/lib/api.py new file mode 100644 index 0000000000..f194b93e7e --- /dev/null +++ b/plugin.video.ahltv/resources/lib/api.py @@ -0,0 +1,243 @@ +from resources.lib.globals import * +import requests +import xbmcgui +import sys +import json +import os +import inputstreamhelper +from .utils import log + +def get_request_headers(authorized = False): + headers = { + "Accept": "application/json", + "User-Agent": xbmc.getUserAgent(), + "product_id": PRODUCT_ID, + "app_id": APP_ID, + "app_key": APP_KEY + } + + if (authorized): + auth_json = None + try: + auth_json = load_auth_info() + except Exception as e: + log('Failed to load auth info: '+ str(e), True) + sys.exit() + + headers["api_key"] = auth_json['api_key'] + headers["app_id"] = auth_json['app_id'] + + return headers + +def login(): + # Check if username and password are provided + global USERNAME + if USERNAME == '""': + dialog = xbmcgui.Dialog() + USERNAME = dialog.input(LOCAL_STRING(31000), type=xbmcgui.INPUT_ALPHANUM) + settings.setSetting(id='username', value=USERNAME) + USERNAME = json.dumps(USERNAME) + sys.exit() + + global PASSWORD + if PASSWORD == '""': + dialog = xbmcgui.Dialog() + PASSWORD = dialog.input(LOCAL_STRING(31001), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT) + settings.setSetting(id='password', value=PASSWORD) + PASSWORD = json.dumps(PASSWORD) + sys.exit() + + if USERNAME != '""' and PASSWORD != '""': + url = API_URL + '/sessions/api_key' + headers = get_request_headers(False) + + body = '{"email":' + USERNAME + ', "password":' + PASSWORD + '}' + + r = requests.post(url, headers=headers, data=body, verify=VERIFY) + if r.status_code == 401: + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(31002), LOCAL_STRING(31003)) + sys.exit() + elif r.status_code >= 500: + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(31004), LOCAL_STRING(31005)) + sys.exit() + + # get the api key from the response and save it + json_source = r.json() + json_source['app_id'] = APP_ID + + save_auth_info(json_source) + + return json_source + +def logout(display_msg=None): + url = API_URL + '/sessions' + + try: + headers = get_request_headers(True) + r = requests.delete(url, headers=headers) + scode = str(r.status_code) + if r.status_code != 204: + log('Error logging out user. Error code: '+ str(r.status_code), True) + else: + try: + os.remove(ADDON_PATH_PROFILE + 'auth.json') + except: + pass + dialog = xbmcgui.Dialog() + dialog.notification(LOCAL_STRING(31006), LOCAL_STRING(31007), ICON, 5000, False) + except: + dialog = xbmcgui.Dialog() + dialog.notification(LOCAL_STRING(31006), LOCAL_STRING(31016), ICON, 5000, False) + + return + +def save_auth_info(data): + with open(ADDON_PATH_PROFILE + 'auth.json', 'w') as f: + json.dump(data, f, ensure_ascii=False) + +def load_auth_info(): + with open(ADDON_PATH_PROFILE + 'auth.json') as f: + d = json.load(f) + return d + +def get_game_info(game_id): + auth_json = None + try: + auth_json = load_auth_info() + except: + pass + + if auth_json is None: + auth_json = login() + + url = API_URL + '/htv2020/game/' + str(game_id) + headers = get_request_headers(True) + + r = requests.get(url, headers=headers) + if r.status_code == 401: + logout() + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(31008), LOCAL_STRING(31009)) + sys.exit() + + return r.json() + +def get_game_streams(game_id, multiple_audio): + stream_info = {} + stream_info["streams"] = [] + stream_info["list_items"] = [] + access_denied_cnt = 0 + + headers = get_request_headers(True) + + # Get the home stream info + url = API_URL + '/htv2020/game/' + str(game_id) + '/stream_info' + r = requests.get(url, headers=headers) + + if r.status_code == 403: + access_denied_cnt += 1 + elif r.status_code >= 400: + log("Error loading stream info - response code: " + str(r.status_code), True) + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(31010), LOCAL_STRING(31011)) + sys.exit() + else: + stream_json = r.json() + + liz = xbmcgui.ListItem(LOCAL_STRING(31012), path=stream_json['stream']['url']) + + helper = inputstreamhelper.Helper('hls') + if not xbmcaddon.Addon().getSettingBool("ffmpeg") and helper.check_inputstream(): + log("Setting inputstream.adaptive on home list item") + liz.setProperty('inputstreamaddon', 'inputstream.adaptive') + liz.setProperty('inputstream.adaptive.manifest_type', 'hls') + else: + log("Setting home list item mimeType to application/x-mpegURL") + liz.setMimeType("application/x-mpegURL") + + stream_info["list_items"].append(liz) + stream_info["streams"].append(stream_json['stream']) + + if multiple_audio == 1: # Home and away audio + url = API_URL + '/htv2020/game/' + str(game_id) + '/stream_info?audio=2' + r = requests.get(url, headers=headers) + + if r.status_code == 403: + access_denied_cnt += 1 + elif r.status_code >= 400: + log("Error loading stream info - response code: " + str(r.status_code), True) + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(31010), LOCAL_STRING(31011)) + sys.exit() + else: + stream_json = r.json() + liz = xbmcgui.ListItem(LOCAL_STRING(31013), path=stream_json['stream']['url']) + + if not xbmcaddon.Addon().getSettingBool("ffmpeg") and helper.check_inputstream(): + log("Setting inputstream.adaptive on away list item") + liz.setProperty('inputstreamaddon', 'inputstream.adaptive') + liz.setProperty('inputstream.adaptive.manifest_type', 'hls') + else: + log("Setting away list item mimeType to application/x-mpegURL") + liz.setMimeType("application/x-mpegURL") + + stream_info["list_items"].append(liz) + stream_info["streams"].append(stream_json['stream']) + + # check final results + if (access_denied_cnt == 1 and multiple_audio == 0) or (access_denied_cnt == 2 and multiple_audio == 1): + dialog = xbmcgui.Dialog() + dialog.ok(LOCAL_STRING(31014), LOCAL_STRING(31015)) + sys.exit() + + return stream_info + +def log_played(game_id, audio_id = 1): + played_id = None + + auth_json = None + try: + auth_json = load_auth_info() + except: + pass + + if auth_json is None: + auth_json = login() + + url = API_URL + '/games/'+ str(game_id) + '/played/' + str(audio_id) + headers = get_request_headers(True) + + r = requests.post(url, headers=headers, verify=VERIFY) + if r.status_code == 200: + played_response = r.json() + played_id = played_response['id'] + else: + log("Error logging played - game_id: " + str(game_id) + ', audio_id: ' + str(audio_id), True) + + return played_id + +def mark_end_time(played_id): + success = False + + auth_json = None + try: + auth_json = load_auth_info() + except: + pass + + if auth_json is None: + auth_json = login() + + url = API_URL + '/game/mark_end_time/' + str(played_id) + headers = get_request_headers(True) + + r = requests.put(url, headers=headers, verify=VERIFY) + + if r.status_code == 200: + success = True + else: + log("Error updating end time. played_id: " + str(played_id) + ', status_code: ' + str(r.status_code), True) + + return success diff --git a/plugin.video.ahltv/resources/lib/globals.py b/plugin.video.ahltv/resources/lib/globals.py new file mode 100644 index 0000000000..ed8b253720 --- /dev/null +++ b/plugin.video.ahltv/resources/lib/globals.py @@ -0,0 +1,99 @@ +import re +import sys +import json +import xbmc, xbmcplugin, xbmcgui, xbmcaddon +import random +import time + +addon_url = sys.argv[0] +addon_handle = int(sys.argv[1]) + +# Addon Info +ADDON = xbmcaddon.Addon() +ADDON_ID = ADDON.getAddonInfo('id') +ADDON_VERSION = ADDON.getAddonInfo('version') +ADDON_PATH = xbmc.translatePath(ADDON.getAddonInfo('path')) +ADDON_PATH_PROFILE = xbmc.translatePath(ADDON.getAddonInfo('profile')) +XBMC_VERSION = float(re.findall(r'\d{2}\.\d{1}', xbmc.getInfoLabel("System.BuildVersion"))[0]) +LOCAL_STRING = ADDON.getLocalizedString +ROOTDIR = xbmcaddon.Addon().getAddonInfo('path') + +# Settings +settings = xbmcaddon.Addon() +USERNAME = json.dumps(str(settings.getSetting(id="username"))) +PASSWORD = json.dumps(str(settings.getSetting(id="password"))) + +# API values +API_URL = 'https://api.watchtheahl.com' +APP_ID = str(random.randint(1,100000)) + ".KODI." + str(int(time.time())) +APP_KEY = "f33d78736fd12b3c1e30a9dafa5d3980" +API_KEY = None +PRODUCT_ID = "2" +VERIFY = True + +COLOR_FREE = 'green' + +# Images +ICON = ROOTDIR + "/icon.png" +AHL_LOGO = ROOTDIR + "/ahl_logo.png" +FANART = ROOTDIR + "/fanart.jpg" +CLEARLOGO = ROOTDIR + "/resources/media/clear_logo.png" + +TEAM_FANART = { + 60000: "https://www.bakersfieldcondors.com/wp-content/uploads/2019/11/2019_11_24_Post-495x300.jpg", # Bakersfield + 60001: "https://s3.amazonaws.com/ahl-uploads/app/uploads/belleville/2019/12/13220526/BSens-4054-1024x681.jpg", # Belleville + 60002: "https://149345975.v2.pressablecdn.com/wp-content/uploads/191004-ROSTER-RELEASE-WEB.png", # Binghamton + 60003: "https://www.soundtigers.com/assets/img/celly-autos-b39c3e38bc.jpg", # Bridgeport + 60004: "https://www.gocheckers.com/images/19-20/Brian_Gibbons_121619.jpg", # Charlotte + 60005: "https://www.chicagowolves.com/wp-content/uploads/2019/06/celly-frontpage-gallery-1024x683.jpg", # Chicago + 60006: "https://www.clevelandmonsters.com/assets/img/W4S_0806-7bac3bd869.png", # Cleveland + 60030: "https://www.coloradoeagles.com/assets/img/1180x440-GameRecap-121719-2c0cd0f381.jpg", # Colorado + 60007: "https://griffinshockey.com/imager/general/189530/55-ford-white-jersey-1_191218_153138_87b984c08ea1e9642840dd02102af27c.jpg", # Grand + 60008: "http://www.hartfordwolfpack.com/assets/img/Dmowski-Action-Shot-2-c102424c5f.jpg", # Hartford + 60036: "https://www.hendersonsilverknights.com/wp-content/uploads/51037803332_d7a84d658a_k.jpg", # Henderson + 60009: "https://gocheckers.com/images/Division_Preview_Hershey.jpg", # Hershey + 60010: "https://www.iowawild.com/assets/img/ROT_IAvsONT_121419-a046a71eed.png", # Iowa + 60011: "https://www.rocketlaval.com/wp-content/uploads/2019/12/49236067966_40ff74a7cc_h.jpg", # Laval + 60012: "http://www.phantomshockey.com/wp-content/uploads/2019/12/Aube-Kubel-10_18_19-vs-BNG-3-Cut.jpg", # Lehigh + 60013: "https://moosehockey.com/wp-content/uploads/2019/12/recapSAdec14.jpg", # Manitoba + 60014: "https://farm66.staticflickr.com/65535/49391830453_e964db6ab0_c.jpg", # Milwaukee + 60015: "https://www.ontarioreign.com/assets/img/Moulson-BAK-Web-413-68af5a4817.jpg", # Ontario + 60016: "https://pbs.twimg.com/media/EMHgfRmXYAUGjJg?format=jpg&name=small", # Providence + 60017: "https://www.amerks.com/assets/img/202102010MV0103-a2f885f781.JPG", # Rochester + 60018: "https://www.icehogs.com/imager/general/images/56887/26675183548_fe811a1358_o_c98a7ad80693ecadd28d6db983184f6b.jpg", # Rockford + 60019: "https://www.oursportscentral.com/graphics/pictures/md20181113-159863.jpg", # San Antonio + 60020: "https://www.sandiegogulls.com/assets/img/49236474651_f20b716fe0_o-0e0bd9e650.jpg", # San Diego + 60021: "http://www.sjbarracuda.com/assets/img/20191004_Knights_vs_Sharks_0048-043f42ccbb.jpg", # San Jose + 60022: "http://www.springfieldthunderbirds.com/assets/img/Final-12-13-19-slideshow-f56650b6d8.jpg", # Springfield + 60023: "https://stocktonheat.com/wp-content/uploads/2019/12/Philp-SJ-Front.jpg", # Stockton + 60024: "https://syracusecrunch.com/images/2020/1/9/Alexey_Lipanov.jpg?width=1080&height=608&mode=crop&format=jpg&quality=80", # Syracuse + 60025: "http://www.texasstars.com/assets/img/StarsSweepMoose-5a58cecd54.png", # Texas + 60026: "http://ahl-uploads.s3.amazonaws.com/app/uploads/toronto_marlies/2018/05/28222440/tix-smith-huddle-1024x680.jpg", # Toronto + 60027: "https://www.tucsonroadrunners.com/wp-content/uploads/2019/12/12.14.19-Web-Recap.png", # Tucson + 60028: "http://www.uticacomets.com/assets/img/1218-feature-5450f98f05.jpg", # Utica + 60029: "http://www.wbspenguins.com/wp-content/uploads/2019/12/1000_Larmi.jpg", # Wilkes + 194381: "https://api.abbotsford.canucks.com/uploads/Homepage_5583e2b396.jpg", # Abbotsford + 194382: "https://www.bridgeportislanders.com/assets/img/weekly5-17502d478f.jpg", # Bridgeport Islanders +} + +# User Agents +UA_IPHONE = 'AppleCoreMedia/1.0.0.15B202 (iPhone; U; CPU OS 11_1_2 like Mac OS X; en_us)' +UA_PC = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36' + +def get_params(): + param = [] + paramstring = sys.argv[2] + if len(paramstring) >= 2: + params = sys.argv[2] + cleanedparams = params.replace('?', '') + if (params[len(params) - 1] == '/'): + params = params[0:len(params) - 2] + pairsofparams = cleanedparams.split('&') + param = {} + for i in range(len(pairsofparams)): + splitparams = {} + splitparams = pairsofparams[i].split('=') + if (len(splitparams)) == 2: + param[splitparams[0]] = splitparams[1] + + return param diff --git a/plugin.video.ahltv/resources/lib/utils.py b/plugin.video.ahltv/resources/lib/utils.py new file mode 100644 index 0000000000..d42034ff95 --- /dev/null +++ b/plugin.video.ahltv/resources/lib/utils.py @@ -0,0 +1,118 @@ +import pytz, time +import os +import calendar +from datetime import date, datetime, timedelta +from io import BytesIO +from resources.lib.globals import ROOTDIR, ADDON_PATH_PROFILE, ICON +try: + from urllib import quote # Python 2.X + from urllib import urlopen +except ImportError: + from urllib.parse import quote # Python 3+ + from urllib.request import urlopen +import sys +import xbmc + +def eastern_to_local(eastern_time): + utc = pytz.utc + eastern = pytz.timezone('US/Eastern') + eastern_time = eastern.localize(eastern_time) + # Convert it from Eastern to UTC + utc_time = eastern_time.astimezone(utc) + timestamp = calendar.timegm(utc_time.timetuple()) + local_dt = datetime.fromtimestamp(timestamp) + # Convert it from UTC to local time + assert utc_time.resolution >= timedelta(microseconds=1) + return local_dt.replace(microsecond=utc_time.microsecond) + +def local_to_eastern(): + eastern = pytz.timezone('US/Eastern') + local_to_utc = datetime.now(pytz.timezone('UTC')) + + eastern_hour = local_to_utc.astimezone(eastern).strftime('%H') + eastern_date = local_to_utc.astimezone(eastern) + # Don't switch to the current day until 4:01 AM est + if int(eastern_hour) < 3: + eastern_date = eastern_date - timedelta(days=1) + + local_to_eastern = eastern_date.strftime('%Y-%m-%d') + return local_to_eastern + +def utc_to_local(utc_dt): + # get integer timestamp to avoid precision lost + timestamp = calendar.timegm(utc_dt.timetuple()) + local_dt = datetime.fromtimestamp(timestamp) + assert utc_dt.resolution >= timedelta(microseconds=1) + return local_dt.replace(microsecond=utc_dt.microsecond) + +def string_to_date(string, date_format): + try: + date = datetime.strptime(str(string), date_format) + except TypeError: + date = datetime(*(time.strptime(str(string), date_format)[0:6])) + + return date + +def get_game_icon(homeId, homeImgPath, awayId, awayImgPath): + # Check if game image already exists + image_path = ADDON_PATH_PROFILE + 'game_icons/' + str(awayId) + '_at_' + str(homeId) + '.png' + file_name = os.path.join(image_path) + if not os.path.isfile(file_name): + try: + create_game_icon(homeImgPath, awayImgPath, image_path) + except: + image_path = ICON + pass + + return image_path + + +def create_game_icon(homeSrcImg, awaySrcImg, image_path): + try: + from PIL import Image + except: + try: + from pil import Image + except: + xbmc.log("PIL not available") + sys.exit() + + bg = Image.open(ROOTDIR + '/resources/media/game_icon_bg.png') + size = 200, 200 + + img_file = urlopen(homeSrcImg) + im = BytesIO(img_file.read()) + home_image = Image.open(im) + home_image.thumbnail(size, Image.ANTIALIAS) + home_image = home_image.convert("RGBA") + + img_file = urlopen(awaySrcImg) + im = BytesIO(img_file.read()) + away_image = Image.open(im) + away_image.thumbnail(size, Image.ANTIALIAS) + away_image = away_image.convert("RGBA") + + bg.paste(away_image, (0, 60), away_image) + bg.paste(home_image, (200, 60), home_image) + + if not os.path.exists(os.path.dirname(image_path)): + try: + os.makedirs(os.path.dirname(image_path)) + except OSError as exc: # Guard against race condition + if exc.errno != errno.EEXIST: + raise + + bg.save(image_path) + +def log(msg, error = False): + """ + Log an error + @param msg The error to log + @param error error severity indicator + """ + try: + import xbmc + full_msg = "plugin.video.ahltv: {}".format(msg) + xbmc.log(full_msg, level=xbmc.LOGERROR if error else xbmc.LOGDEBUG) + except: + print(msg) diff --git a/plugin.video.ahltv/resources/media/ahl_logo.png b/plugin.video.ahltv/resources/media/ahl_logo.png new file mode 100644 index 0000000000..aef3264cc9 Binary files /dev/null and b/plugin.video.ahltv/resources/media/ahl_logo.png differ diff --git a/plugin.video.ahltv/resources/media/clear_logo.png b/plugin.video.ahltv/resources/media/clear_logo.png new file mode 100644 index 0000000000..bc8a8be73f Binary files /dev/null and b/plugin.video.ahltv/resources/media/clear_logo.png differ diff --git a/plugin.video.ahltv/resources/media/game_icon_bg.png b/plugin.video.ahltv/resources/media/game_icon_bg.png new file mode 100644 index 0000000000..532af69246 Binary files /dev/null and b/plugin.video.ahltv/resources/media/game_icon_bg.png differ diff --git a/plugin.video.ahltv/resources/settings.xml b/plugin.video.ahltv/resources/settings.xml new file mode 100644 index 0000000000..03aa3e2328 --- /dev/null +++ b/plugin.video.ahltv/resources/settings.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugin.video.archive.org/addon.xml b/plugin.video.archive.org/addon.xml new file mode 100644 index 0000000000..31cc444f11 --- /dev/null +++ b/plugin.video.archive.org/addon.xml @@ -0,0 +1,27 @@ + + + + + + + audio video + + + Internet Archive add-on + Watch and listen to media archived by Internet Archive + This third party addon is not in any way commissioned or endorsed by Internet Archive + + all + GPL-2.0 + https://forum.kodi.tv/showthread.php?tid=180623 + https://archive.org/ + gujal at protonmail dot com + + icon.png + fanart.jpg + resources/images/screenshot-01.jpg + resources/images/screenshot-02.jpg + resources/images/screenshot-03.jpg + + + \ No newline at end of file diff --git a/plugin.video.archive.org/changelog.txt b/plugin.video.archive.org/changelog.txt new file mode 100644 index 0000000000..ffdcfe8d41 --- /dev/null +++ b/plugin.video.archive.org/changelog.txt @@ -0,0 +1,4 @@ +plugin.video.archive.org: +--------------------------- +v1.0.0 (20240608) +First Release diff --git a/plugin.video.archive.org/default.py b/plugin.video.archive.org/default.py new file mode 100644 index 0000000000..4a42c3f9c0 --- /dev/null +++ b/plugin.video.archive.org/default.py @@ -0,0 +1,22 @@ +""" + Internet Archive Kodi Addon + Copyright (C) 2024 gujal + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +from resources.lib import main + +if __name__ == '__main__': + main.Main() diff --git a/plugin.video.archive.org/fanart.jpg b/plugin.video.archive.org/fanart.jpg new file mode 100644 index 0000000000..fcf7039a91 Binary files /dev/null and b/plugin.video.archive.org/fanart.jpg differ diff --git a/plugin.video.archive.org/icon.png b/plugin.video.archive.org/icon.png new file mode 100644 index 0000000000..b0dc4eaab7 Binary files /dev/null and b/plugin.video.archive.org/icon.png differ diff --git a/plugin.video.archive.org/resources/__init__.py b/plugin.video.archive.org/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.archive.org/resources/images/screenshot-01.jpg b/plugin.video.archive.org/resources/images/screenshot-01.jpg new file mode 100644 index 0000000000..2a89aa4efa Binary files /dev/null and b/plugin.video.archive.org/resources/images/screenshot-01.jpg differ diff --git a/plugin.video.archive.org/resources/images/screenshot-02.jpg b/plugin.video.archive.org/resources/images/screenshot-02.jpg new file mode 100644 index 0000000000..74ea798b90 Binary files /dev/null and b/plugin.video.archive.org/resources/images/screenshot-02.jpg differ diff --git a/plugin.video.archive.org/resources/images/screenshot-03.jpg b/plugin.video.archive.org/resources/images/screenshot-03.jpg new file mode 100644 index 0000000000..30c7a765a0 Binary files /dev/null and b/plugin.video.archive.org/resources/images/screenshot-03.jpg differ diff --git a/plugin.video.archive.org/resources/language/resource.language.en_gb/strings.po b/plugin.video.archive.org/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..e623825263 --- /dev/null +++ b/plugin.video.archive.org/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,74 @@ +# XBMC Media Center language file +# Addon Name: Internet Archive +# Addon id: plugin.video.archive.org + +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-11-03 00:21+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_GB\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "Internet Archive add-on" +msgstr "" + +msgctxt "Addon Description" +msgid "Watch and listen to media archived by Internet Archive" +msgstr "" + +msgctxt "Addon Disclaimer" +msgid "This third party addon is not in any way commissioned or endorsed by Internet Archive" +msgstr "" + + +msgctxt "#30000" +msgid "Internet Archive" +msgstr "" + +msgctxt "#30001" +msgid "Cache Timeout in hours" +msgstr "" + +msgctxt "#30002" +msgid "Clear Cache" +msgstr "" + +msgctxt "#30003" +msgid "Enable Debug Mode" +msgstr "" + +# empty strings from id 30004 to 30100 + +msgctxt "#30101" +msgid "Popular Collections" +msgstr "" + +msgctxt "#30102" +msgid "Search by Title" +msgstr "" + +# empty strings from id 30104 to 30200 + +msgctxt "#30201" +msgid "Cached Data has been cleared" +msgstr "" + +msgctxt "#30202" +msgid "Need atleast 3 characters" +msgstr "" + +msgctxt "#30203" +msgid "Choose Source" +msgstr "" + +msgctxt "#30204" +msgid "Next Page" +msgstr "" diff --git a/plugin.video.archive.org/resources/lib/__init__.py b/plugin.video.archive.org/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.archive.org/resources/lib/cache.py b/plugin.video.archive.org/resources/lib/cache.py new file mode 100644 index 0000000000..17ba47addb --- /dev/null +++ b/plugin.video.archive.org/resources/lib/cache.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +""" + 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 . +""" + +import hashlib +import json +import pickle +import re +import time +import zlib +import xbmcvfs +import xbmcaddon +from sqlite3 import dbapi2 as db, OperationalError, Binary + +cacheFile = xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('profile') + '/cache.db') +cache_table = 'cache' + + +def get(function, duration, *args, **kwargs): + # type: (function, int, object) -> object or None + """ + Gets cached value for provided function with optional arguments, or executes and stores the result + :param function: Function to be executed + :param duration: Duration of validity of cache in hours + :param args: Optional arguments for the provided function + :param kwargs: Optional keyword arguments for the provided function + """ + + key = _hash_function(function, *args, **kwargs) + cache_result = cache_get(key) + + if cache_result: + if _is_cache_valid(cache_result['date'], duration): + return pickle.loads(zlib.decompress(cache_result['value'])) + + fresh_result = function(*args, **kwargs) + + if not fresh_result: + # If the cache is old, but we didn't get fresh result, return the + # old cache + if cache_result: + return pickle.loads(zlib.decompress(cache_result['value'])) + return None + + cache_insert(key, Binary(zlib.compress(pickle.dumps(fresh_result)))) + return fresh_result + + +def remove(function, *args, **kwargs): + key = _hash_function(function, *args, **kwargs) + cursor = _get_connection_cursor() + cursor.execute("DELETE FROM %s WHERE key = ?" % cache_table, [key]) + cursor.connection.commit() + + +def timeout(function, *args, **kwargs): + key = _hash_function(function, *args, **kwargs) + result = cache_get(key) + return int(result['date']) + + +def cache_get(key): + # type: (str, str) -> dict or None + try: + cursor = _get_connection_cursor() + cursor.execute("SELECT * FROM %s WHERE key = ?" % cache_table, [key]) + return cursor.fetchone() + except OperationalError: + return None + + +def cache_insert(key, value): + # type: (str, str) -> None + cursor = _get_connection_cursor() + now = int(time.time()) + cursor.execute( + "CREATE TABLE IF NOT EXISTS %s (key TEXT, value BINARY, date INTEGER, UNIQUE(key))" % + cache_table) + cursor.execute( + "CREATE UNIQUE INDEX if not exists index_key ON %s (key)" % cache_table) + update_result = cursor.execute( + "UPDATE %s SET value=?,date=? WHERE key=?" + % cache_table, (value, now, key)) + + if update_result.rowcount == 0: + cursor.execute( + "INSERT INTO %s Values (?, ?, ?)" + % cache_table, (key, value, now) + ) + + cursor.connection.commit() + + +def cache_clear(): + cursor = _get_connection_cursor() + + for t in [cache_table, 'rel_list', 'rel_lib']: + try: + cursor.execute("DROP TABLE IF EXISTS %s" % t) + cursor.execute("VACUUM") + cursor.commit() + except BaseException: + pass + + +def _get_connection_cursor(): + conn = _get_connection() + return conn.cursor() + + +def _get_connection(): + conn = db.connect(cacheFile) + conn.row_factory = _dict_factory + return conn + + +def _dict_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + + +def _hash_function(function_instance, *args, **kwargs): + return _get_function_name(function_instance) + _generate_md5(*args, **kwargs) + + +def _get_function_name(function_instance): + return re.sub( + r'.+\smethod\s|.+function\s|\sat\s.+|\sof\s.+', + '', + repr(function_instance)) + + +def _generate_md5(*args, **kwargs): + md5_hash = hashlib.md5() + [md5_hash.update(str(arg).encode()) for arg in args] + md5_hash.update(json.dumps(kwargs).encode()) + return md5_hash.hexdigest() + + +def _is_cache_valid(cached_time, cache_timeout): + now = int(time.time()) + diff = now - cached_time + return (cache_timeout * 3600) > diff diff --git a/plugin.video.archive.org/resources/lib/client.py b/plugin.video.archive.org/resources/lib/client.py new file mode 100644 index 0000000000..b1214d6b3b --- /dev/null +++ b/plugin.video.archive.org/resources/lib/client.py @@ -0,0 +1,112 @@ +""" + 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 . +""" + +import gzip +import json +import re +import ssl +import urllib.request +import urllib.parse +import urllib.error +import xbmc +import xbmcvfs +from io import BytesIO + +CERT_FILE = xbmcvfs.translatePath('special://xbmc/system/certs/cacert.pem') + + +def request(url, headers=None, params=None, timeout='20'): + _headers = {} + if headers: + _headers.update(headers) + + handlers = [] + ssl_context = ssl.create_default_context(cafile=CERT_FILE) + ssl_context.set_alpn_protocols(['http/1.1']) + handlers += [urllib.request.HTTPSHandler(context=ssl_context)] + opener = urllib.request.build_opener(*handlers) + opener = urllib.request.install_opener(opener) + + if 'User-Agent' in _headers: + pass + else: + _headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36' + + if params is not None: + if isinstance(params, dict): + params = urllib.parse.urlencode(params) + url = url + '?' + params + + if 'Accept-Language' not in _headers: + _headers['Accept-Language'] = 'en-US,en' + + if 'Accept-Encoding' in _headers: + pass + else: + _headers['Accept-Encoding'] = 'gzip' + + req = urllib.request.Request(url) + _add_request_header(req, _headers) + + try: + response = urllib.request.urlopen(req, timeout=int(timeout)) + except urllib.error.URLError: + return '' + except urllib.error.HTTPError: + return '' + + result = response.read() + encoding = None + + if response.headers.get('content-encoding', '').lower() == 'gzip': + result = gzip.GzipFile(fileobj=BytesIO(result)).read() + + content_type = response.headers.get('content-type', '').lower() + + if 'charset=' in content_type: + encoding = content_type.split('charset=')[-1] + + if encoding is None: + epatterns = [r'. +""" + +import json +import random +import re +import sys +import urllib.parse +import xbmc +import xbmcgui +import xbmcplugin +import xbmcaddon +import xbmcvfs +from html import unescape +from resources.lib import client, cache + +_addon = xbmcaddon.Addon() +_addonID = _addon.getAddonInfo('id') +_plugin = _addon.getAddonInfo('name') +_version = _addon.getAddonInfo('version') +_icon = _addon.getAddonInfo('icon') +_fanart = _addon.getAddonInfo('fanart') +_language = _addon.getLocalizedString +_settings = _addon.getSetting +_addonpath = 'special://profile/addon_data/{}/'.format(_addonID) +_kodiver = float(xbmcaddon.Addon('xbmc.addon').getAddonInfo('version')[:4]) +# DEBUG +DEBUG = _settings("DebugMode") == "true" + +if not xbmcvfs.exists(_addonpath): + xbmcvfs.mkdir(_addonpath) + +cache_duration = int(_settings('timeout')) + +if not xbmcvfs.exists(_addonpath + 'settings.xml'): + _addon.openSettings() + + +class Main(object): + def __init__(self): + self.base_url = 'https://archive.org/' + self.search_url = self.base_url + 'services/search/beta/page_production/' + self.img_path = self.base_url + 'services/img/' + self.item_path = self.base_url + 'details/' + self.headers = {'Referer': self.base_url} + self.litems = [] + content_type = self.parameters('content_type') + if content_type and _settings('context') != content_type: + _addon.setSetting('context', content_type) + action = self.parameters('action') + content_type = _settings('context') + if action == 'list_items': + page = int(self.parameters('page')) + target = self.parameters('target') + self.list_items(target, page, content_type) + elif action == 'list_collections': + page = int(self.parameters('page')) + self.list_collections(page, content_type) + elif action == 'play': + item_id = self.parameters('target') + self.play(item_id, content_type) + elif action == 'search': + self.search(content_type) + elif action == 'search_word': + keyword = urllib.parse.unquote(self.parameters('keyword')) + page = int(self.parameters('page')) + self.search_word(keyword, page, content_type) + elif action == 'clear': + self.clear_cache() + else: + self.main_menu(content_type) + + def main_menu(self, content_type): + if DEBUG: + self.log('main_menu({0})'.format(content_type)) + category = [{'title': _language(30101), 'key': 'popular'}, + {'title': _language(30102), 'key': 'search'}, + {'title': _language(30002), 'key': 'cache'}] + for i in category: + listitem = xbmcgui.ListItem(i['title']) + listitem.setArt({'thumb': _icon, + 'fanart': _fanart, + 'icon': _icon}) + + if i['key'] == 'cache': + url = sys.argv[0] + '?action=clear' + elif i['key'] == 'search': + url = '{}?action=search&content_type={}'.format(sys.argv[0], content_type) + else: + url = '{}?action=list_collections&page=1&content_type={}'.format(sys.argv[0], content_type) + + xbmcplugin.addDirectoryItems(int(sys.argv[1]), [(url, listitem, True)]) + + # Sort methods and content type... + xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=xbmcplugin.SORT_METHOD_NONE) + xbmcplugin.setContent(int(sys.argv[1]), 'addons') + # End of directory... + xbmcplugin.endOfDirectory(int(sys.argv[1]), True) + + def clear_cache(self): + """ + Clear the cache database. + """ + if DEBUG: + self.log('clear_cache()') + cache.cache_clear() + xbmcgui.Dialog().notification(_plugin, _language(30201), _icon, 3000, False) + + def get_search_items(self, filter_map, target, page): + cd = {} + params = { + 'service_backend': 'metadata', + 'user_query': target, + 'hits_per_page': 100, + 'page': page, + 'filter_map': filter_map, + 'aggregations': 'false', + 'client_url': 'https://archive.org/' + } + resp = client.request(self.search_url, headers=self.headers, params=params) + if resp: + cd = resp.get('response').get('body').get('hits') + return cd + + def get_items(self, filter_map, target, page): + cd = {} + params = { + 'page_type': 'collection_details', + 'page_target': target, + 'hits_per_page': 100, + 'page': page, + 'filter_map': filter_map, + 'aggregations': 'false', + 'client_url': 'https://archive.org/' + } + resp = client.request(self.search_url, headers=self.headers, params=params) + if resp: + cd = resp.get('response').get('body').get('hits') + return cd + + def list_collections(self, page, content_type): + target = 'movies' if content_type == 'video' else 'audio' + if DEBUG: + self.log('list_collections({0}, {1})'.format(page, content_type)) + + filter_map = '{"mediatype":{"collection":"inc"}}' + data = cache.get(self.get_items, cache_duration, filter_map, target, page) + if data: + items = data.get('hits') + for item in items: + item = item.get('fields') + title = item.get('title') + plot = item.get('description') + if plot: + plot = unescape(plot) + slug = item.get('identifier') + count = item.get('item_count') + labels = { + 'title': '{0} [I]({1:,} items)[/I]'.format(title, count), + 'plot' if content_type == 'video' else 'comment': plot + } + listitem = self.make_listitem(labels, content_type) + listitem.setArt({ + 'icon': self.img_path + slug, + 'thumb': self.img_path + slug, + 'fanart': _fanart + }) + listitem.setProperty('IsPlayable', 'false') + url = sys.argv[0] + '?' + urllib.parse.urlencode({ + 'action': 'list_items', + 'page': 1, + 'target': slug, + 'content_type': content_type + }) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), url, listitem, True) + + total = data.get('total') + if page * 100 < total: + lastpg = -1 * (-total // 100) + page += 1 + labels = {'title': '[COLOR lime]{}...[/COLOR] ({}/{})'.format(_language(30204), page, lastpg)} + listitem = self.make_listitem(labels, content_type) + listitem.setArt({ + 'icon': _icon, + 'thumb': _icon, + 'fanart': _fanart + }) + listitem.setProperty('IsPlayable', 'false') + url = sys.argv[0] + '?' + urllib.parse.urlencode({ + 'action': 'list_collections', + 'page': page, + 'content_type': content_type + }) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), url, listitem, True) + + # Sort methods and content type... + xbmcplugin.setContent(int(sys.argv[1]), 'videos' if content_type == 'video' else 'albums') + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + # End of directory... + xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=True) + + def list_items(self, target, page, content_type): + if DEBUG: + self.log('list_items("{0}, {1}, {2}")'.format(target, page, content_type)) + + filter_map = '{{"mediatype":{{"{}":"inc","etree":"inc"}}}}'.format('movies' if content_type == 'video' else 'audio') + data = cache.get(self.get_items, cache_duration, filter_map, target, page) + if data: + items = data.get('hits') + for item in items: + item = item.get('fields') + title = item.get('title') + plot = item.get('description') + if plot: + plot = unescape(plot) + slug = item.get('identifier') + labels = { + 'title': title, + 'plot' if content_type == 'video' else 'comment': plot + } + listitem = self.make_listitem(labels, content_type) + listitem.setArt({ + 'icon': self.img_path + slug, + 'thumb': self.img_path + slug, + 'fanart': _fanart + }) + listitem.setProperty('IsPlayable', 'true') + url = sys.argv[0] + '?' + urllib.parse.urlencode({ + 'action': 'play', + 'target': slug, + 'content_type': content_type + }) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), url, listitem, False) + + total = data.get('total') + if page * 100 < total: + lastpg = -1 * (-total // 100) + page += 1 + labels = {'title': '[COLOR lime]{}...[/COLOR] ({}/{})'.format(_language(30204), page, lastpg)} + listitem = self.make_listitem(labels, content_type) + listitem.setArt({ + 'icon': _icon, + 'thumb': _icon, + 'fanart': _fanart + }) + listitem.setProperty('IsPlayable', 'false') + url = sys.argv[0] + '?' + urllib.parse.urlencode({ + 'action': 'list_items', + 'page': page, + 'target': target, + 'content_type': content_type + }) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), url, listitem, True) + + # Sort methods and content type... + xbmcplugin.setContent(int(sys.argv[1]), 'videos' if content_type == 'video' else 'songs') + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + # End of directory... + xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=True) + + def format_bytes(self, size): + n = 0 + slabels = {0: 'B', 1: 'KB', 2: 'MB', 3: 'GB', 4: 'TB'} + while size > 1024: + size /= 1024 + n += 1 + return '{0:.2f} {1}'.format(size, slabels[n]) + + def search(self, content_type): + if DEBUG: + self.log('search({0})'.format(content_type)) + keyboard = xbmc.Keyboard() + keyboard.setHeading(_language(30102)) + keyboard.doModal() + if keyboard.isConfirmed(): + search_text = urllib.parse.quote(keyboard.getText()) + else: + search_text = '' + xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=False) + if len(search_text) > 2: + url = sys.argv[0] + '?' + urllib.parse.urlencode({'action': 'search_word', + 'keyword': search_text, + 'content_type': content_type, + 'page': 1}) + xbmc.executebuiltin("Container.Update({0},replace)".format(url)) + else: + xbmcgui.Dialog().notification(_plugin, _language(30202), _icon, 3000, False) + xbmc.executebuiltin("Container.Update({0},replace)".format(sys.argv[0])) + + def search_word(self, search_text, page, content_type): + if DEBUG: + self.log('search_word("{0}, page {1}, {2}")'.format(search_text, page, content_type)) + filter_map = '{{"mediatype":{{"{}":"inc","etree":"inc"}}}}'.format('movies' if content_type == 'video' else 'audio') + data = cache.get(self.get_search_items, cache_duration, filter_map, search_text, page) + if data: + items = data.get('hits') + for item in items: + item = item.get('fields') + title = item.get('title') + plot = item.get('description') + if plot: + plot = unescape(plot) + slug = item.get('identifier') + labels = { + 'title': title, + 'plot' if content_type == 'video' else 'comment': plot + } + listitem = self.make_listitem(labels, content_type) + listitem.setArt({ + 'icon': self.img_path + slug, + 'thumb': self.img_path + slug, + 'fanart': _fanart + }) + listitem.setProperty('IsPlayable', 'true') + url = sys.argv[0] + '?' + urllib.parse.urlencode({ + 'action': 'play', + 'target': slug, + 'content_type': content_type + }) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), url, listitem, False) + + total = data.get('total') + if page * 100 < total: + lastpg = -1 * (-total // 100) + page += 1 + labels = {'title': '[COLOR lime]{}...[/COLOR] ({}/{})'.format(_language(30204), page, lastpg)} + listitem = self.make_listitem(labels, content_type) + listitem.setArt({ + 'icon': _icon, + 'thumb': _icon, + 'fanart': _fanart + }) + listitem.setProperty('IsPlayable', 'false') + url = sys.argv[0] + '?' + urllib.parse.urlencode({ + 'action': 'search_word', + 'keyword': search_text, + 'content_type': content_type, + 'page': page + }) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), url, listitem, True) + + # Sort methods and content type... + xbmcplugin.setContent(int(sys.argv[1]), 'videos' if content_type == 'video' else 'songs') + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + # End of directory... + xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=True) + + def play(self, item_id, content_type): + url = self.item_path + item_id + if DEBUG: + self.log('play("{}") {}'.format(item_id, content_type)) + + html = client.request(url, headers=self.headers) + jsob = re.search('''class="js-play8-playlist".+?value='([^']+)''', html) + if jsob: + surl = '' + data = json.loads(jsob.group(1)) + total = len(data) + if total > 1: + sources = [(i.get('title'), i.get('sources')[0].get('file')) for i in data] + if content_type == 'video': + ret = xbmcgui.Dialog().select(_language(30203), [source[0] for source in sources]) + if ret == -1: + return + surl = self.base_url + sources[ret][1] + item_id = sources[ret][0] + else: + if DEBUG: + self.log('Found {} audio items'.format(total)) + playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + playlist.clear() + for title, source in sources: + li = self.make_listitem({'title': title}, content_type) + playlist.add(url=urllib.parse.urljoin(self.base_url, source), listitem=li) + elif total == 1: + jd = client.request(url.replace('/details/', '/metadata/')) + sources = [i for i in jd.get('files') if 'height' in i.keys() and '.jpg' not in i.get('name')] + if len(sources) > 1: + sources.sort(key=lambda item: (int(item.get('height')), item.get('source'), int(item.get('size'))), reverse=True) + srcs = ['{0} ({1} {2}p) {3}'.format( + i.get('name').split('.')[-1], + i.get('source'), + i.get('height'), + self.format_bytes(int(i.get('size'))) + ) for i in sources] + ret = xbmcgui.Dialog().select(_language(30203), srcs) + if ret == -1: + return + else: + ret = 0 + surl = 'https://{0}{1}/{2}'.format( + random.choice(jd.get('workable_servers')), + jd.get('dir'), + urllib.parse.quote(sources[ret].get('name')) + ) + + if total > 1 and content_type == 'audio': + xbmc.Player().play(playlist) + else: + li = self.make_listitem({'title': item_id}, content_type) + li.setArt({'fanart': _fanart}) + li.setPath(surl) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem=li) + + def parameters(self, arg): + _parameters = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query) + val = _parameters.get(arg, '') + if isinstance(val, list): + val = val[0] + return val + + def make_listitem(self, labels, content_type): + li = xbmcgui.ListItem(labels.get('title')) + if _kodiver > 19.8: + vtag = li.getVideoInfoTag() if content_type == 'video' else li.getMusicInfoTag() + vtag.setTitle(labels.get('title')) + if content_type == 'video': + vtag.setOriginalTitle(labels.get('title')) + if labels.get('plot'): + vtag.setPlot(labels.get('plot')) + vtag.setPlotOutline(labels.get('plot')) + if labels.get('comment'): + vtag.setComment(labels.get('comment')) + if labels.get('mediatype'): + vtag.setMediaType(labels.get('mediatype')) + if labels.get('duration'): + vtag.setDuration(labels.get('duration')) + else: + li.setInfo(type='video' if content_type == 'video' else 'music', infoLabels=labels) + + return li + + def log(self, description): + xbmc.log("[ADD-ON] '{} v{}': {}".format(_plugin, _version, description), xbmc.LOGINFO) diff --git a/plugin.video.archive.org/resources/settings.xml b/plugin.video.archive.org/resources/settings.xml new file mode 100644 index 0000000000..47a786aae1 --- /dev/null +++ b/plugin.video.archive.org/resources/settings.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/plugin.video.ardaktuell/addon.py b/plugin.video.ardaktuell/addon.py new file mode 100644 index 0000000000..110636549e --- /dev/null +++ b/plugin.video.ardaktuell/addon.py @@ -0,0 +1,7 @@ +from resources.lib.ardaktuell.ardaktuelladdon import ArdAktuellAddon +import sys + +if __name__ == '__main__': + + ardAktuellAddon = ArdAktuellAddon(int(sys.argv[1])) + ardAktuellAddon.handle(sys.argv) diff --git a/plugin.video.ardaktuell/addon.xml b/plugin.video.ardaktuell/addon.xml new file mode 100644 index 0000000000..11695f19dd --- /dev/null +++ b/plugin.video.ardaktuell/addon.xml @@ -0,0 +1,69 @@ + + + + + + + + + video + + + Alle Sendungen von ARD Aktuell + German news provided by ARD Aktuell + Kodi Addon für Sendungen von ARD Aktuell wie Tageschau, Tagesthemen und Nachtmagazin. Dieses Addon verwendet Inhalte von ARD-aktuell bzw. tagesschau.de. + Kodi Addon for news provided by ARD Aktuell, e.g. Tageschau, Tagesthemen and Nachtmagazin. This addon streams contents hosted by ARD-aktuell / tagesschau.de + + Der Author des Addons ist nicht verantwortlich für die Inhalte, die auf Ihr Gerät gestreamt werden. Insbesondere liegt es in der alleinigen Verantwortung und Pflicht des Addon-Nutzers sich davon zu überzeugen, dass die Urheberrechtsvereinbarungen der zum Streaming aufgerufenden Websites und Inhalte nicht verletzt werden. Dieses Addon verwendet Inhalte von ARD-aktuell bzw. tagesschau.de. + + de_DE + en_GB + all + MIT + https://github.com/Heckie75/kodi-addon-ard-aktuell + https://github.com/Heckie75/kodi-addon-ard-aktuell/tree/main/plugin.video.ardaktuell + +v1.1.0 (2024-07-08) +- added new audio podcast '15 Minuten' +- removed podcast 'Faktenfinder' + +v1.0.6 (2023-08-20) +- Bugfix: quality setting audio not working + +v1.0.5 (2023-08-06) +- reactivated quality in settings (see issue #4) +- Refactoring + +v1.0.4 (2023-06-17) +- Updated URLs to feeds +- removed unsupported feeds +- removed quality settings since feeds don't support quality anymore +- added date information to rss items + +v1.0.3 (2023-02-02) +- Improved thumbnail if item has been added to favourites + +v1.0.2 (2022-06-03) +- new audio podcast 'Ideenimport' +- Improved performance when loading rss feed with episodes +- some code refactoring, migration to new settings format + +v1.0.1 (2021-06-02) +- Fixed broken URLs to github +- Fixed issue #2: Navigation back from episodes menu is incorrect +- Refactoring: seperate general rss code for easier maintainance in future +- Hint: restart is required in order to load moved translations + +v1.0.0 (2021-05-24) +- Initial version + + + resources/assets/icon.png + resources/assets/fanart.png + resources/assets/screenshot_1.png + resources/assets/screenshot_2.png + resources/assets/screenshot_3.png + resources/assets/screenshot_4.png + + + diff --git a/plugin.video.ardaktuell/resources/assets/fanart.png b/plugin.video.ardaktuell/resources/assets/fanart.png new file mode 100644 index 0000000000..96195858ea Binary files /dev/null and b/plugin.video.ardaktuell/resources/assets/fanart.png differ diff --git a/plugin.video.ardaktuell/resources/assets/icon.png b/plugin.video.ardaktuell/resources/assets/icon.png new file mode 100644 index 0000000000..36978d04fc Binary files /dev/null and b/plugin.video.ardaktuell/resources/assets/icon.png differ diff --git a/plugin.video.ardaktuell/resources/assets/screenshot_1.png b/plugin.video.ardaktuell/resources/assets/screenshot_1.png new file mode 100644 index 0000000000..73f592ffc7 Binary files /dev/null and b/plugin.video.ardaktuell/resources/assets/screenshot_1.png differ diff --git a/plugin.video.ardaktuell/resources/assets/screenshot_2.png b/plugin.video.ardaktuell/resources/assets/screenshot_2.png new file mode 100644 index 0000000000..fb9819c82a Binary files /dev/null and b/plugin.video.ardaktuell/resources/assets/screenshot_2.png differ diff --git a/plugin.video.ardaktuell/resources/assets/screenshot_3.png b/plugin.video.ardaktuell/resources/assets/screenshot_3.png new file mode 100644 index 0000000000..f60e926dc5 Binary files /dev/null and b/plugin.video.ardaktuell/resources/assets/screenshot_3.png differ diff --git a/plugin.video.ardaktuell/resources/assets/screenshot_4.png b/plugin.video.ardaktuell/resources/assets/screenshot_4.png new file mode 100644 index 0000000000..ff623ac929 Binary files /dev/null and b/plugin.video.ardaktuell/resources/assets/screenshot_4.png differ diff --git a/plugin.video.ardaktuell/resources/language/resource.language.de_de/strings.po b/plugin.video.ardaktuell/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..edb1eb5d3d --- /dev/null +++ b/plugin.video.ardaktuell/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,70 @@ +# Kodi Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32001" +msgid "General" +msgstr "Allgemein" + +msgctxt "#32002" +msgid "Display archive" +msgstr "Archiv anzeigen" + +msgctxt "#32003" +msgid "Anchor for most recent podcast, e.g. for bookmarking" +msgstr "Anker für neueste Sendung, z.B. zum Bookmarken" + +msgctxt "#32005" +msgid "Disclaimer" +msgstr "Haftungsauschluss" + +msgctxt "#32006" +msgid "I haven't agreed yet" +msgstr "Ich habe noch nicht zugestimmt" + +msgctxt "#32007" +msgid "I have agreed" +msgstr "Ich habe zugestimmt" + +msgctxt "#32008" +msgid "Agreement" +msgstr "Zustimmung" + +msgctxt "#32010" +msgid "The author of this addon is not responsible for the contents which are streamed to this device. Especially the user of this addon is in responsibility to convince that copyrights and right of use are not violated. All contents are taken and streamed from ARD-aktuell (https://www.tagesschau.de/infoservices/podcast/). Do you agree?" +msgstr "Der Author des Addons ist nicht verantwortlich für die Inhalte, die auf Ihr Gerät gestreamt werden. Insbesondere liegt es in der alleinigen Verantwortung und Pflicht des Addon-Nutzers sich davon zu überzeugen, dass die Urheberrechts- und Nutzungsvereinbarungen der zum Streaming aufgerufenden Websites und Inhalte nicht verletzt werden. Alle Inhalte stammen von ARD-aktuell (https://www.tagesschau.de/infoservices/podcast/). Bist Du damit einverstanden?" + +msgctxt "#32101" +msgid "most recent episode" +msgstr "aktuelle Sendung" + +msgctxt "#32151" +msgid "Connection Error" +msgstr "Verbindungsfehler" + +msgctxt "#32152" +msgid "HTTP Method %s not supported" +msgstr "HTTP Methode %s wird nicht unterstützt" + +msgctxt "#32153" +msgid "Request Exception: Pls. check URL and port. See logs for further details." +msgstr "Request Fehler: Prüfe URL und Port. Für weitere Details prüfe die Log-Datei." + +msgctxt "#32094" +msgid "Unexpected HTTP Status %i for %s" +msgstr "Unerwarteter HTTP Status %i für %s" + +msgctxt "#32155" +msgid "Unexpected content for podcast" +msgstr "Unerwarteter Inhalt für Podcast" diff --git a/plugin.video.ardaktuell/resources/language/resource.language.en_gb/strings.po b/plugin.video.ardaktuell/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..7fbbaf59cf --- /dev/null +++ b/plugin.video.ardaktuell/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,71 @@ +# Kodi Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@kodi.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +msgctxt "#32001" +msgid "General" +msgstr "" + +msgctxt "#32002" +msgid "Display archive" +msgstr "" + +msgctxt "#32003" +msgid "Anchor for most recent podcast, e.g. for bookmarking" +msgstr "" + +msgctxt "#32005" +msgid "Disclaimer" +msgstr "" + +msgctxt "#32006" +msgid "I haven't agreed yet" +msgstr "" + +msgctxt "#32007" +msgid "I have agreed" +msgstr "" + +msgctxt "#32008" +msgid "Agreement" +msgstr "" + +msgctxt "#32010" +msgid "The author of this addon is not responsible for the contents which are streamed to this device. Especially the user of this addon is in responsibility to convince that copyrights and right of use are not violated. All contents are taken and streamed from ARD-aktuell (https://www.tagesschau.de/infoservices/podcast/). Do you agree?" +msgstr "" + +msgctxt "#32101" +msgid "most recent episode" +msgstr "" + +msgctxt "#32151" +msgid "Connection Error" +msgstr "" + +msgctxt "#32152" +msgid "HTTP Method %s not supported" +msgstr "" + +msgctxt "#32153" +msgid "Request Exception: Pls. check URL and port. See logs for further details." +msgstr "" + +msgctxt "#32154" +msgid "Unexpected HTTP Status %i for %s" +msgstr "" + +msgctxt "#32155" +msgid "Unexpected content for podcast" +msgstr "" diff --git a/plugin.video.ardaktuell/resources/lib/ardaktuell/ardaktuelladdon.py b/plugin.video.ardaktuell/resources/lib/ardaktuell/ardaktuelladdon.py new file mode 100644 index 0000000000..0cfeacf8f1 --- /dev/null +++ b/plugin.video.ardaktuell/resources/lib/ardaktuell/ardaktuelladdon.py @@ -0,0 +1,184 @@ +import re +from datetime import datetime + +import xbmcgui +import xbmcplugin +from resources.lib.rssaddon.abstract_rss_addon import AbstractRssAddon + + +class ArdAktuellAddon(AbstractRssAddon): + + QUALITY_LEVEL_AUDIO = 4 + QUALITY_LEVEL = ["webxl", "webl", "webm", "webs"] + QUALITY_REGEX = r"^(.+\.)(webxl|webl|webm|webs)(\..+)$" + + BROADCASTS = [ + { + "name": "ARD Tagesschau um 20 Uhr", + "icon": "https://images.tagesschau.de/image/eb0b0d74-03ac-45ec-9300-0851fd6823d3/AAABiE1u1f0/AAABg8tMMaM/1x1-1400/sendungslogo-tagesschau-100.jpg", + "video_url": "https://www.tagesschau.de/multimedia/sendung/tagesschau_20_uhr/podcast-ts2000-video-100~podcast.xml", + "audio_url": "https://www.tagesschau.de/multimedia/sendung/tagesschau_20_uhr/podcast-ts2000-audio-100~podcast.xml", + "date_format": "%d.%m.%Y" + }, + { + "name": "ARD Tagesschau um 20 Uhr mit Gebärdensprache", + "icon": "https://images.tagesschau.de/image/eb0b0d74-03ac-45ec-9300-0851fd6823d3/AAABiE1u1f0/AAABg8tMMaM/1x1-1400/sendungslogo-tagesschau-100.jpg", + "video_url": "https://www.tagesschau.de/multimedia/sendung/tagesschau_mit_gebaerdensprache/podcast-tsg-100~podcast.xml", + "date_format": "%d.%m.%Y" + }, + { + "name": "ARD Tagesschau in 100 Sekunden", + "icon": "https://images.tagesschau.de/image/559ce6ba-91f3-495c-b36d-77115c440dd0/AAABiE1fSSM/AAABg8tMMaM/1x1-1400/sendungslogo-tsh-100.jpg", + "video_url": "https://www.tagesschau.de/multimedia/sendung/tagesschau_in_100_sekunden/podcast-ts100-video-100~podcast.xml", + "audio_url": "https://www.tagesschau.de/multimedia/sendung/tagesschau_in_100_sekunden/podcast-ts100-audio-100~podcast.xml", + "date_format": "%d.%m.%Y %H:%M" + }, + { + "name": "ARD Tagesthemen", + "icon": "https://images.tagesschau.de/image/6b0ab906-0dcf-432f-807c-1d10f8a0a73a/AAABiE1uG-U/AAABg8tMMaM/1x1-1400/sendungslogo-tagesthemen-100.jpg", + "video_url": "https://www.tagesschau.de/multimedia/sendung/tagesthemen/podcast-tt-video-100~podcast.xml", + "audio_url": "https://www.tagesschau.de/multimedia/sendung/tagesthemen/podcast-tt-audio-100~podcast.xml", + "date_format": "%d.%m.%Y" + }, + { + "name": "15 Minuten. Der tagesschau-Podcast am Morgen", + "icon": "https://images.tagesschau.de/image/5eb354fe-6af8-421f-8fa7-9d3672600d2e/AAABj4DbyB8/AAABjwnlOg4/1x1-1400/podcast-15-minuten-cover-100.jpg?overlay=542c7aa9-161c-48e8-a1cc-e49f9c91d757&overlayModificationDate=AAABgSP-H5A", + "audio_url": "https://www.tagesschau.de/multimedia/podcast/15-minuten/index~podcast.xml", + "date_format": "%d.%m.%Y" + }, + { + "name": "ARD Tagesschau vor 20 Jahren", + "icon": "https://images.tagesschau.de/image/2a6f7e91-d939-4fde-98b8-9d7fb54be721/AAABiB8Zoe4/AAABg8tMMaM/1x1-1400/tagesschau-logo-105.jpg", + "video_url": "https://www.tagesschau.de/multimedia/sendung/tagesschau_vor_20_jahren/podcast-tsv20-video-100~podcast.xml", + "audio_url": "https://www.tagesschau.de/multimedia/sendung/tagesschau_vor_20_jahren/podcast-tsv20-audio-100~podcast.xml", + "date_format": "" + }, + { + "name": "ARD Mal angenommen...", + "icon": "https://images.tagesschau.de/image/d5f4c036-3eca-4b59-9a11-1acd53814639/AAABiB7_Ajo/AAABg8tMMaM/1x1-1400/podcast-mal-angenommen-102.jpg", + "audio_url": "https://www.tagesschau.de/multimedia/podcast/malangenommen/mal-angenommen-feed-101~podcast.xml", + "date_format": "%d.%m.%Y" + }, + { + "name": "Ideenimport", + "icon": "https://images.tagesschau.de/image/ab2459a5-283e-41be-ad3f-5e323ffb7c5a/AAABiGJ5xhQ/AAABg8tMMaM/1x1-1400/podcast-ideenimport-104.jpg", + "audio_url": "https://www.tagesschau.de/multimedia/podcast/ideenimport/ideenimport-feed-105~podcast.xml", + "date_format": "%d.%m.%Y" + } + ] + + def __init__(self, addon_handle) -> None: + + super().__init__(addon_handle) + self._date_format: str = "" + + def _build_dir_structure(self) -> None: + + def _make_node(index, broadcast, type, url, latest_only): + + _node = { + "path": "latest" if latest_only else str(index), + "name": broadcast["name"], + "icon": broadcast["icon"], + "type": type, + "params": [ + { + "play_latest" if latest_only else "rss": url + }, + { + "date_format": broadcast["date_format"] + } + ] + } + + if not latest_only: + _node["node"] = [] + + return _node + + _nodes = [] + for i, broadcast in enumerate(self.BROADCASTS): + + quality = int(self.addon.getSetting("quality")) + if "video_url" in broadcast and quality < self.QUALITY_LEVEL_AUDIO: + _nodes.append(_make_node( + i, broadcast, "video", broadcast["video_url"], self.addon.getSetting("archive") != "true")) + + elif "audio_url" in broadcast: + _nodes.append(_make_node(i, broadcast, "music", + broadcast["audio_url"], self.addon.getSetting("archive") != "true")) + + return [ + { # root + "path": "", + "node": _nodes + } + ] + + def _browse(self, dir_structure, path: str, updateListing=False): + + def _get_node_by_path(path): + + if path == "/": + return dir_structure[0] + + tokens = path.split("/")[1:] + node = dir_structure[0] + + while tokens: + path = tokens.pop(0) + for n in node["node"]: + if n["path"] == path: + node = n + break + + return node + + node = _get_node_by_path(path) + for entry in node["node"]: + self.add_list_item(entry, path) + + xbmcplugin.endOfDirectory( + self.addon_handle, updateListing=updateListing) + + def check_disclaimer(self) -> bool: + + if self.addon.getSetting("agreement") != "1": + answer = xbmcgui.Dialog().yesno(self.addon.getLocalizedString(32005), + self.addon.getLocalizedString(32010)) + + if answer: + self.addon.setSetting("agreement", "1") + return True + else: + return False + + else: + return True + + def route(self, path): + + _dir_structure = self._build_dir_structure() + self._browse(dir_structure=_dir_structure, path=path) + + def build_label(self, item) -> str: + + if "date_format" in self.params and self.params["date_format"] and "date" in item: + return "%s (%s)" % (item["name"], datetime.strftime(item["date"], self.params["date_format"])) + else: + return super().build_label(item) + + def build_plot(self, item) -> str: + + if "date_format" in self.params and self.params["date_format"] and "date" in item: + return "%s\n\n%s" % (super().build_plot(item), datetime.strftime(item["date"], self.params["date_format"])) + else: + return super().build_plot(item) + + def build_url(self, item) -> str: + + quality = int(self.addon.getSetting("quality")) + match = re.match(self.QUALITY_REGEX, item["stream_url"]) + url = "".join([match.groups()[0], self.QUALITY_LEVEL[quality], match.groups()[ + 2]]) if quality < self.QUALITY_LEVEL_AUDIO and match else item["stream_url"] + return url diff --git a/plugin.video.ardaktuell/resources/lib/rssaddon/abstract_rss_addon.py b/plugin.video.ardaktuell/resources/lib/rssaddon/abstract_rss_addon.py new file mode 100644 index 0000000000..3c203e82ce --- /dev/null +++ b/plugin.video.ardaktuell/resources/lib/rssaddon/abstract_rss_addon.py @@ -0,0 +1,332 @@ +import base64 +import os +import re +import urllib.parse +from datetime import datetime +from io import StringIO +from xml.etree.ElementTree import iterparse + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +import xbmcvfs +from resources.lib.rssaddon.http_client import http_request +from resources.lib.rssaddon.http_status_error import HttpStatusError + +# see https://forum.kodi.tv/showthread.php?tid=112916 +_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", + "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + + +class AbstractRssAddon: + + addon = None + addon_handle = None + addon_dir = None + anchor_for_latest = True + + def __init__(self, addon_handle): + + self.addon = xbmcaddon.Addon() + self.addon_handle = addon_handle + self.addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + + self.params = dict() + + def handle(self, argv: 'list[str]') -> None: + + path = urllib.parse.urlparse(argv[0]).path.replace("//", "/") + url_params = urllib.parse.parse_qs(argv[2][1:]) + + if not self.check_disclaimer(): + self.route("/", dict()) + return + + self.params = {key: self.decode_param( + url_params[key][0]) for key in url_params} + + if "rss" in self.params: + url = self.params["rss"] + limit = int(self.params["limit"] + ) if "limit" in self.params else 0 + offset = int(self.params["offset"] + ) if "offset" in self.params else 0 + self.render_rss(path, url, limit=limit, offset=offset) + + elif "play_latest" in self.params: + url = self.params["play_latest"] + self.play_latest(url) + else: + self.route(path) + + def decode_param(self, encoded_param: str) -> str: + + return base64.urlsafe_b64decode(encoded_param).decode("utf-8") + + def check_disclaimer(self) -> bool: + + return True + + def route(self, path: str): + + pass + + def is_force_http(self) -> bool: + + return False + + def _load_rss(self, url: str) -> 'tuple[str,str,str,list[dict]]': + + def parse_rss_feed(xml: str) -> 'tuple[str,str,str,list[dict]]': + + path = list() + + title = None + description = "" + image = None + items = list() + + for event, elem in iterparse(StringIO(xml), ("start", "end")): + + if event == "start": + path.append(elem.tag) + + if path == ["rss", "channel", "item"]: + item = dict() + + elif event == "end": + + if path == ["rss", "channel"]: + pass + + elif path == ["rss", "channel", "title"] and elem.text: + title = elem.text.strip() + + elif path == ["rss", "channel", "description"] and elem.text: + description = elem.text.strip() + + elif path == ["rss", "channel", "image", "url"] and elem.text: + image = elem.text.strip() + + elif (path == ["rss", "channel", "{http://www.itunes.com/dtds/podcast-1.0.dtd}image"] + and "href" in elem.attrib and not image): + image = elem.attrib["href"] + + elif path == ["rss", "channel", "item", "title"] and elem.text: + item["name"] = elem.text.strip() + + elif path == ["rss", "channel", "item", "description"] and elem.text: + item["description"] = elem.text.strip() + + elif path == ["rss", "channel", "item", "enclosure"]: + item["stream_url"] = elem.attrib["url"] if not self.is_force_http( + ) else elem.attrib["url"].replace("https://", "http://") + item["type"] = "video" if elem.attrib["type"].split( + "/")[0] == "video" else "music" + + elif (path == ["rss", "channel", "item", "{http://www.itunes.com/dtds/podcast-1.0.dtd}image"] + and elem.attrib["href"]): + item["icon"] = elem.attrib["href"] + + elif path == ["rss", "channel", "item", "pubDate"] and elem.text: + _f = re.findall( + "(\d{1,2}) (\w{3}) (\d{4}) (\d{2}):(\d{2}):(\d{2})", elem.text) + + if _f: + _m = _MONTHS.index(_f[0][1]) + 1 + item["date"] = datetime(year=int(_f[0][2]), month=_m, day=int(_f[0][0]), hour=int( + _f[0][3]), minute=int(_f[0][4]), second=int(_f[0][5])) + + elif path == ["rss", "channel", "item", "{http://www.itunes.com/dtds/podcast-1.0.dtd}duration"] and elem.text: + try: + duration = 0 + for i, s in enumerate(reversed(elem.text.split(":"))): + duration += 60**i * int(s) + + item["duration"] = duration + + except: + pass + + elif path == ["rss", "channel", "item"]: + + if "description" not in item: + item["description"] = "" + + if "icon" not in item: + item["icon"] = image + + if "stream_url" in item and item["stream_url"]: + items.append(item) + + elem.clear() + path.pop() + + return title, description, image, items + + xml, cookies = http_request(self.addon, url) + + if not xml.startswith(" None: + + pass + + def build_label(self, item) -> str: + + return item["name"] + + def build_plot(self, item) -> str: + + return item["description"] if "description" in item else "" + + def build_url(self, item) -> str: + + return item["stream_url"] + + def _create_list_item(self, item: dict) -> xbmcgui.ListItem: + + li = xbmcgui.ListItem(label=self.build_label(item)) + + if "description" in item: + li.setProperty("label2", item["description"]) + + if "stream_url" in item: + li.setPath(self.build_url(item)) + + if "type" in item: + infos = { + "title": self.build_label(item) + } + + if item["type"] == "video": + infos["plot"] = self.build_plot(item) + + if "duration" in item and item["duration"] >= 0: + infos["duration"] = item["duration"] + + li.setInfo(item["type"], infos) + + if "icon" in item and item["icon"]: + li.setArt({"thumb": item["icon"]}) + else: + addon_dir = xbmcvfs.translatePath(self.addon.getAddonInfo('path')) + li.setArt({"icon": os.path.join( + addon_dir, "resources", "assets", "icon.png")} + ) + + if "date" in item and item["date"]: + if "setDateTime" in dir(li): # available since Kodi v20 + li.setDateTime(item["date"].strftime("%Y-%m-%dT%H:%M:%SZ")) + else: + pass + + if "specialsort" in item: + li.setProperty("SpecialSort", item["specialsort"]) + + return li + + def add_list_item(self, entry: dict, path: str) -> None: + + def _build_param_string(params: 'list[str]', current="") -> str: + + if params == None: + return current + + for obj in params: + for name in obj: + enc_value = base64.urlsafe_b64encode( + obj[name].encode("utf-8")) + current += "?" if len(current) == 0 else "&" + current += name + "=" + str(enc_value, "utf-8") + + return current + + if path == "/": + path = "" + + item_path = path + "/" + entry["path"] + + param_string = "" + if "params" in entry: + param_string = _build_param_string(entry["params"], + current=param_string) + + li = self._create_list_item(entry) + + if "stream_url" in entry: + url = self.build_url(entry) + + else: + url = "".join( + ["plugin://", self.addon.getAddonInfo("id"), item_path, param_string]) + + is_folder = "node" in entry + li.setProperty("IsPlayable", "false" if is_folder else "true") + + xbmcplugin.addDirectoryItem(handle=self.addon_handle, + listitem=li, + url=url, + isFolder=is_folder) + + def render_rss(self, path: str, url: str, limit=0, offset=0) -> None: + + try: + title, description, image, items = self._load_rss(url) + + except HttpStatusError as error: + xbmc.log("HTTP Status Error: %s, path=%s" % + (error.message, path), xbmc.LOGERROR) + xbmcgui.Dialog().notification(self.addon.getLocalizedString(32151), error.message) + + else: + if len(items) > 0 and self.anchor_for_latest: + entry = { + "path": "latest", + "name": "%s (%s)" % (title, self.addon.getLocalizedString(32101)), + "description": description, + "icon": image, + "specialsort": "top", + "type": items[0]["type"], + "params": [ + { + "play_latest": url + } + ] + } + self.add_list_item(entry, path) + + li = None + for i, item in enumerate(items): + if i >= offset and (not limit or i < offset + limit): + li = self._create_list_item(item) + xbmcplugin.addDirectoryItem(handle=self.addon_handle, + listitem=li, + url=self.build_url(item), + isFolder=False) + + if li and "setDateTime" in dir(li): # available since Kodi v20 + xbmcplugin.addSortMethod( + self.addon_handle, xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.endOfDirectory(self.addon_handle) + + def play_latest(self, url: str) -> None: + + try: + title, description, image, items = self._load_rss(url) + item = items[0] + li = self._create_list_item(item) + xbmcplugin.setResolvedUrl(self.addon_handle, True, li) + + except HttpStatusError as error: + + xbmcgui.Dialog().notification(self.addon.getLocalizedString(32151), error.message) diff --git a/plugin.video.ardaktuell/resources/lib/rssaddon/http_client.py b/plugin.video.ardaktuell/resources/lib/rssaddon/http_client.py new file mode 100644 index 0000000000..2f34fd4cf1 --- /dev/null +++ b/plugin.video.ardaktuell/resources/lib/rssaddon/http_client.py @@ -0,0 +1,36 @@ +from resources.lib.rssaddon.http_status_error import HttpStatusError + +import requests + +import xbmc + +def http_request(addon, url, headers=dict(), method="GET"): + + useragent = f"{addon.getAddonInfo('id')}/{addon.getAddonInfo('version')} (Kodi/{xbmc.getInfoLabel('System.BuildVersionShort')})" + headers["User-Agent"] = useragent + + if method == "GET": + req = requests.get + elif method == "POST": + req = requests.post + else: + raise HttpStatusError( + addon.getLocalizedString(32152) % method) + + try: + res = req(url, headers=headers) + except requests.exceptions.RequestException as error: + xbmc.log("Request Exception: %s" % str(error), xbmc.LOGERROR) + raise HttpStatusError(addon.getLocalizedString(32153)) + + if res.status_code == 200: + if res.encoding and res.encoding != "utf-8": + rv = res.text.encode(res.encoding).decode("utf-8") + else: + rv = res.text + + return rv, res.cookies + + else: + raise HttpStatusError(addon.getLocalizedString( + 32154) % (res.status_code, url)) \ No newline at end of file diff --git a/plugin.video.ardaktuell/resources/lib/rssaddon/http_status_error.py b/plugin.video.ardaktuell/resources/lib/rssaddon/http_status_error.py new file mode 100644 index 0000000000..ae3185f615 --- /dev/null +++ b/plugin.video.ardaktuell/resources/lib/rssaddon/http_status_error.py @@ -0,0 +1,7 @@ +class HttpStatusError(Exception): + + message = "" + + def __init__(self, msg): + + self.message = msg \ No newline at end of file diff --git a/plugin.video.ardaktuell/resources/settings.xml b/plugin.video.ardaktuell/resources/settings.xml new file mode 100644 index 0000000000..e9af7db896 --- /dev/null +++ b/plugin.video.ardaktuell/resources/settings.xml @@ -0,0 +1,47 @@ + + +
    + + + + 0 + 0 + + + + + + + + + + + + + 0 + false + + + + 0 + false + + + true + + + + 0 + 0 + + + + + + + + + + +
    +
    \ No newline at end of file diff --git a/plugin.video.ardmediathek_de/LICENSE.txt b/plugin.video.ardmediathek_de/LICENSE.txt new file mode 100644 index 0000000000..23cb790338 --- /dev/null +++ b/plugin.video.ardmediathek_de/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.video.ardmediathek_de/addon.xml b/plugin.video.ardmediathek_de/addon.xml new file mode 100644 index 0000000000..3643ae0140 --- /dev/null +++ b/plugin.video.ardmediathek_de/addon.xml @@ -0,0 +1,27 @@ + + + + + + + + video + + + all + de + GPL-2.0-only + https://github.com/sarbes/plugin.video.ardmediathek_de + https://forum.kodi.tv/showthread.php?tid=353903 + https://www.ardmediathek.de/ + Watch videos on demand from the ARD Mediathek. + Watch videos on demand from the ARD Mediathek. + Videos aus der Mediathek des ARD. + Dieses Add-on bietet Zugriff auf die Inhalte der ARD Mediathek. Vergangene Serien, Filme und Nachrichten können hier abgerufen werden. + Dieses Add-on wird von keiner Sendeanstalt unterstützt und ist daher inoffiziell. + + resources/icon.png + resources/fanart.jpg + + + diff --git a/plugin.video.ardmediathek_de/default.py b/plugin.video.ardmediathek_de/default.py new file mode 100644 index 0000000000..c92a556ce8 --- /dev/null +++ b/plugin.video.ardmediathek_de/default.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +import libard + +ard = libard.libard() +ard.action() diff --git a/plugin.video.ardmediathek_de/resources/fanart.jpg b/plugin.video.ardmediathek_de/resources/fanart.jpg new file mode 100644 index 0000000000..7d1ae2aa0f Binary files /dev/null and b/plugin.video.ardmediathek_de/resources/fanart.jpg differ diff --git a/plugin.video.ardmediathek_de/resources/icon.png b/plugin.video.ardmediathek_de/resources/icon.png new file mode 100644 index 0000000000..b0fc89f468 Binary files /dev/null and b/plugin.video.ardmediathek_de/resources/icon.png differ diff --git a/plugin.video.arloview/LICENSE b/plugin.video.arloview/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/plugin.video.arloview/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugin.video.arloview/addon.xml b/plugin.video.arloview/addon.xml new file mode 100644 index 0000000000..954f22fccf --- /dev/null +++ b/plugin.video.arloview/addon.xml @@ -0,0 +1,36 @@ + + + + + + + + video + + + Stream your Arlo cameras in Kodi + List and stream each of your Arlo cameras in Kodi. This plugin is compatible with Kodi v18 (Leia) and above. + + For questions/comments related to this plugin, contact JavaWiz1@hotmail.com + + Python interface to Arlo cameras based on: + (https://github.com/jeffreydwalter/arlo) thanks jeffreydwalter! + + https://github.com/JavaWiz1/repo-plugins + all + Apache 2.0 + JavaWiz1@hotmail.com + + v1.2.0 (7/2/2021) + * Updated script.module.arlo to v1.2.50 and logging updates. + v1.1.1 (5/30/2020) + * Initial matrix version + + + resources/icon.png + resources/fanart.jpg + resources/ArloCamera.jpg + + + + diff --git a/plugin.video.arloview/default.py b/plugin.video.arloview/default.py new file mode 100644 index 0000000000..4936764b35 --- /dev/null +++ b/plugin.video.arloview/default.py @@ -0,0 +1,11 @@ +''' +Created on Aug 16, 2018 + +@author: adamico +''' +import sys + +from resources.lib import arlo_stream + +if __name__ == '__main__': + arlo_stream.ArloStream(sys.argv).run() diff --git a/plugin.video.arloview/resources/ArloCamera.jpg b/plugin.video.arloview/resources/ArloCamera.jpg new file mode 100644 index 0000000000..ea5c505b58 Binary files /dev/null and b/plugin.video.arloview/resources/ArloCamera.jpg differ diff --git a/plugin.video.arloview/resources/__init__.py b/plugin.video.arloview/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.arloview/resources/fanart.jpg b/plugin.video.arloview/resources/fanart.jpg new file mode 100644 index 0000000000..8b8166ef21 Binary files /dev/null and b/plugin.video.arloview/resources/fanart.jpg differ diff --git a/plugin.video.arloview/resources/icon.png b/plugin.video.arloview/resources/icon.png new file mode 100644 index 0000000000..bce0532682 Binary files /dev/null and b/plugin.video.arloview/resources/icon.png differ diff --git a/plugin.video.arloview/resources/language/resource.language.en_gb/strings.po b/plugin.video.arloview/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..c63fd3e399 --- /dev/null +++ b/plugin.video.arloview/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,46 @@ +# Arlo Stream Plugin language file +# Addon Name: Arlo Stream +# Addon id: plugin.video.arloview +# Addon Provider: JavaWiz1 +msgid "" +msgstr "" +"Project-Id-Version: plugin.video.arloview Addons\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: JavaWiz1\n" +"Language-Team: JavaWiz1\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 "#32001" +msgid "General" +msgstr "" + +msgctxt "#30001" +msgid "Arlo View General Settings" +msgstr "" + +msgctxt "#30002" +msgid "Arlo Userid" +msgstr "" + +msgctxt "#30003" +msgid "Arlo Password" +msgstr "" + +msgctxt "#30004" +msgid "Show Camera Snapshot" +msgstr "" + +msgctxt "#30007" +msgid "Debug_Mode" +msgstr "" + +msgctxt "#30008" +msgid "SnapShot Type" +msgstr "" + diff --git a/plugin.video.arloview/resources/lib/__init__.py b/plugin.video.arloview/resources/lib/__init__.py new file mode 100644 index 0000000000..554ff2251a --- /dev/null +++ b/plugin.video.arloview/resources/lib/__init__.py @@ -0,0 +1,26 @@ +import sys + +__all__ = ['PY2', 'py2_encode', 'py2_decode'] + +PY2 = sys.version_info[0] == 2 + +def py2_encode(s, encoding='utf-8'): + """ + Encode Python 2 ``unicode`` to ``str`` + + In Python 3 the string is not changed. + """ + if PY2 and isinstance(s, unicode): + s = s.encode(encoding) + return s + + +def py2_decode(s, encoding='utf-8'): + """ + Decode Python 2 ``str`` to ``unicode`` + + In Python 3 the string is not changed. + """ + if PY2 and isinstance(s, str): + s = s.decode(encoding) + return s \ No newline at end of file diff --git a/plugin.video.arloview/resources/lib/arlo_stream.py b/plugin.video.arloview/resources/lib/arlo_stream.py new file mode 100644 index 0000000000..aa643277c6 --- /dev/null +++ b/plugin.video.arloview/resources/lib/arlo_stream.py @@ -0,0 +1,269 @@ +from __future__ import absolute_import, division, unicode_literals + +import datetime +import json +import os +import threading +import time + +import arlo +import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +from resources.lib import * + +try: + # For Python 3.0 and later + from urllib.parse import urlencode + from urllib.parse import parse_qsl +except ImportError: + # Fall back to Python 2's urllib2 + from urllib import urlencode + from urlparse import parse_qsl + + +# Plugin Info +ADDON_ID = 'plugin.video.arloview' +REAL_SETTINGS = xbmcaddon.Addon(id=ADDON_ID) +ADDON_NAME = REAL_SETTINGS.getAddonInfo('name') +SETTINGS_LOC = py2_decode(xbmc.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))) +ADDON_PATH = py2_decode(xbmc.translatePath(xbmcaddon.Addon().getAddonInfo('path'))) +ADDON_VERSION = REAL_SETTINGS.getAddonInfo('version') + + +def get_params(args): + return dict(parse_qsl(args[2][1:])) + + +def get_url(url, **kwargs): + """ + Create a URL for calling the plugin recursively from the given set of keyword arguments. + + :param kwargs: "argument=value" pairs + :type kwargs: dict + :return: plugin call URL + :rtype: str + """ + + url = '{0}?{1}'.format(url, urlencode(kwargs)) + return url + + +class ArloStream(object): + + def __init__(self, sys_args): + self._url = sys_args[0] + self._handle = int(sys_args[1]) + self._args = sys_args + self.arlo = None + self.basestation = None + self.cameras = None + self.addon_debug_logging = REAL_SETTINGS.getSettingBool('enable_debug') + + def log(self, msg, level=xbmc.LOGINFO): + + # Only log messages (via this function) if debug-logging is turned on in plugin settings + if self.addon_debug_logging : + if level < xbmc.LOGINFO: + level = xbmc.LOGINFO + xbmc.log("[{}-{}] {}".format(ADDON_ID, ADDON_VERSION, msg), level) + + def main_menu(self): + for camera in self._get_arlo_cameras(): + camera_info = self._get_camera_info(camera) + list_item = xbmcgui.ListItem(label=camera["deviceName"]) + list_item.setInfo('video', {'title': camera["deviceName"], + 'plot': camera_info, + 'mediatype': 'video'}) + list_item.setProperty('IsPlayable', 'true') + snapshot_file = self._get_camera_snapshot(camera['deviceId']) + list_item.setArt({'thumb': snapshot_file, + 'icon': snapshot_file, + 'poster': snapshot_file, + 'clearart': snapshot_file, + 'clearlogo': snapshot_file}) + # Create a URL for a plugin recursive call. + url = get_url("plugin://" + ADDON_ID, + cameraName=camera["deviceName"], + cameraId=camera["deviceId"]) + # Add the list item to a virtual Kodi folder. + # is_folder = False means that this item won't open any sub-list. + is_folder = False + # Add our item to the Kodi virtual folder listing. + xbmcplugin.addDirectoryItem(self._handle, url, list_item, is_folder) + # Add a sort method for the virtual folder items (alphabetically, ignore articles) + xbmcplugin.addSortMethod(self._handle, xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self._handle) + + + def _get_arlo_cameras(self): + if self.cameras is None: + self.log("Retrieving cameras...", xbmc.LOGDEBUG) + self.cameras = self.arlo.GetDevices("camera") + self._update_arlo_cameras_details() + #self.log(json.dumps(self.cameras, indent=4), xbmc.LOGDEBUG) + + return self.cameras + + def _update_arlo_cameras_details(self): + if not self.cameras is None: + detail_camera_info = self.arlo.GetCameraState(self.basestation) + for idx, camera in enumerate(self.cameras, start=0): + camera['batteryLevel'] = "" + camera['signalStrength'] = '' + camera['hwVersion'] = '' + camera['swVersion'] = '' + for camera_detail in detail_camera_info['properties']: + if camera['deviceId'] == camera_detail['serialNumber']: + camera['batteryLevel'] = camera_detail['batteryLevel'] + camera['signalStrength'] = camera_detail['signalStrength'] + camera['hwVersion'] = camera_detail['hwVersion'] + camera['swVersion'] = camera_detail['swVersion'] + self.cameras[idx] = camera + break + + def _get_camera(self, device_id): + tgt_camera = None + self.log("BEGIN _get_camera() deviceID: {}".format(device_id), xbmc.LOGDEBUG) + for camera in self._get_arlo_cameras(): + if device_id == camera["deviceId"]: + tgt_camera = camera + break + self.log("END _get_camera() returns: {}".format(tgt_camera['deviceName']), xbmc.LOGDEBUG) + return tgt_camera + + def _get_camera_info(self, camera): + fmt_str = "Device ID : {0}\n" + \ + "Model ID : {1}\n" + \ + "Batt Lvl : {2}\n" + \ + "Sig Str : {3}\n" + return fmt_str.format(camera['deviceId'], + camera['properties']['modelId'], + camera['batteryLevel'], + camera['signalStrength']) + + + def _get_camera_snapshot(self, device_id): + if not REAL_SETTINGS.getSettingBool('show_snapshots'): + snapshot_file = "{0}/resources/ArloCamera.png".format(ADDON_PATH) + else: + snapshot_path = "{0}/resources/media".format(SETTINGS_LOC) + if not os.path.exists(snapshot_path): + os.makedirs(snapshot_path) + snapshot_file = self._refresh_snapshot(device_id, snapshot_path) + + self.log("Snapshot file: {}".format(snapshot_file), xbmc.LOGDEBUG) + return snapshot_file + + def _refresh_snapshot(self, device_id, snapshot_path): + camera = self._get_camera(device_id) + snapshot_type = REAL_SETTINGS.getSetting('snapshot_type') + snapshot_file_prefix = ''.join(ch for ch in snapshot_type if ch.isupper()) + snapshot_file = "{0}/{1}_{2}.jpg".format(snapshot_path, snapshot_file_prefix, camera['deviceId']) + if self._snapshot_expired(snapshot_file): + snapshot_url = camera[snapshot_type] + try: + self.log("Download {} into {}_{}".format(snapshot_type, snapshot_file_prefix, camera['deviceId']),xbmc.LOGINFO) + self.arlo.DownloadSnapshot(snapshot_url, snapshot_file, 4096) + if os.path.getsize(snapshot_file) < 1024: + # File seems to be invalid, ?remove so it will try again the next time? + self.log("Invalid jpg? FILE: {}".format(snapshot_file), xbmc.LOGERROR) + snapshot_file = "{0}/resources/ArloCamera.png".format(ADDON_PATH) + except Exception as err: + self.log("Unable to download snapshot: {0}".format(err), xbmc.LOGERROR) + snapshot_file = "{0}/resources/ArloCamera.png".format(ADDON_PATH) + + return snapshot_file + + def _snapshot_expired(self, snapshot_file): + expired = False + if not os.path.exists(snapshot_file): + self.log("_snapshot_expired() - file does not exist", xbmc.LOGDEBUG) + expired = True + elif os.path.getsize(snapshot_file) < 1024: + self.log("_snapshot_expired() - file appears to be invalid, check contents with editor", xbmc.LOGDEBUG) + expired = True + else: + snapshot_created_time = os.path.getctime(snapshot_file) + days_old = (time.time() - snapshot_created_time) // (24 * 3600) + if days_old >= 7: + self.log("_snapshot_expired() - file is {} days old".format(days_old), xbmc.LOGDEBUG) + expired = True + else: + self.log("Found cached snapshot, will use it.", xbmc.LOGDEBUG) + + return expired + + def _stop_camera(self): + pass + + def _play_camera(self, device_id): + # Send the command to start the stream and return the stream url. + camera = self._get_camera(device_id) + stream_url = self.arlo.StartStream(self.basestation, camera) + + # Create a playable item with a path to play. + play_item = xbmcgui.ListItem(path=stream_url, label=camera['deviceName']) + play_item.setProperty("isPlayable", "true") + # Pass the item to the Kodi player. + xbmcplugin.setResolvedUrl(self._handle, True, listitem=play_item) + + def _arlo_login(self): + self.log("BEGIN _arlo_login()", xbmc.LOGDEBUG) + user_name = REAL_SETTINGS.getSetting('userid') + password = REAL_SETTINGS.getSetting('password') + self.arlo = arlo.Arlo(user_name, password) + self.basestation = self.arlo.GetDevices('basestation')[0] + #self.log(json.dumps(self.basestation, indent=4), xbmc.LOGDEBUG) + self.log("END _arlo_login()", xbmc.LOGDEBUG) + + def _arlo_logout(self): + self.log("_arlo_logout() in progress...") + self.arlo.Logout() + + def check_first_run(self): + if REAL_SETTINGS.getSetting("userid") == "": + self.log("check_first_run()- userid NOT set!", xbmc.LOGINFO) + msg = "Set ARLO credentials, otherwise...\nVideo will not stream correctly!" + icon = REAL_SETTINGS.getAddonInfo('icon') + xbmcgui.Dialog().notification(ADDON_NAME, msg, icon, 5000) + REAL_SETTINGS.openSettings() + self.addon_debug_logging = REAL_SETTINGS.getSettingBool('enable_debug') + + + def run(self): + params = get_params(self._args) + # Set plugin category. It is displayed in some skins as the name + # of the current section. + xbmcplugin.setPluginCategory(self._handle, 'Arlo Cameras') + # Set plugin content. It allows Kodi to select appropriate views + # for this type of content. + xbmcplugin.setContent(self._handle, 'files') + + self.check_first_run() + + if REAL_SETTINGS.getSetting("userid") == "": + xbmc.log("ArloView: Settings not established, ending...", xbmc.LOGERROR) + msg = "Set ARLO credentials, ArloView Exiting!" + icon = REAL_SETTINGS.getAddonInfo('icon') + xbmcgui.Dialog().notification(ADDON_NAME, msg, icon, 5000) + else: + try: + _cam_name = urllib.parse.unquote(params["cameraName"]) + except: + _cam_name = None + + try: + _cam_id = params["cameraId"] + except: + _cam_id = None + + self._arlo_login() + if _cam_id is None: + self.main_menu() + else: + self._play_camera(_cam_id) + + self._arlo_logout() diff --git a/plugin.video.arloview/resources/settings.xml b/plugin.video.arloview/resources/settings.xml new file mode 100644 index 0000000000..a962eb6515 --- /dev/null +++ b/plugin.video.arloview/resources/settings.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/plugin.video.artemediathek/LICENSE.txt b/plugin.video.artemediathek/LICENSE.txt new file mode 100644 index 0000000000..23cb790338 --- /dev/null +++ b/plugin.video.artemediathek/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.video.artemediathek/addon.xml b/plugin.video.artemediathek/addon.xml new file mode 100644 index 0000000000..63a0eecee2 --- /dev/null +++ b/plugin.video.artemediathek/addon.xml @@ -0,0 +1,27 @@ + + + + + + + + video + + + all + en de fr es it pl + GPL-2.0-only + https://forum.kodi.tv/showthread.php?tid=353903 + https://github.com/sarbes/plugin.video.artemediathek + https://www.arte.tv/ + This add-on allows access to the French/German broadcaster Arte. + This add-on allows access to the French/German broadcaster Arte. + Dieses Add-on bietet Zugriff auf die Mediathek von Arte. + Dieses Add-on bietet Zugriff auf die Mediathek von Arte. Angeboten werden einige Filme und Serien, sowie Dokumentationen. + Dieses Add-on wird von keiner Sendeanstalt unterstützt und ist daher inoffiziell. + + resources/icon.png + resources/fanart.jpg + + + diff --git a/plugin.video.artemediathek/default.py b/plugin.video.artemediathek/default.py new file mode 100644 index 0000000000..feaceeb24f --- /dev/null +++ b/plugin.video.artemediathek/default.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +import libarte + +arte = libarte.libarte() +arte.action() \ No newline at end of file diff --git a/plugin.video.artemediathek/resources/fanart.jpg b/plugin.video.artemediathek/resources/fanart.jpg new file mode 100644 index 0000000000..31707a95ed Binary files /dev/null and b/plugin.video.artemediathek/resources/fanart.jpg differ diff --git a/plugin.video.artemediathek/resources/icon.png b/plugin.video.artemediathek/resources/icon.png new file mode 100644 index 0000000000..5952b6dbed Binary files /dev/null and b/plugin.video.artemediathek/resources/icon.png differ diff --git a/plugin.video.artemediathek/resources/language/resource.language.de_de/strings.po b/plugin.video.artemediathek/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..8a805e38a2 --- /dev/null +++ b/plugin.video.artemediathek/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,60 @@ +# KODI Media Center language file +# Addon Name: artemediathek +# Addon id: plugin.video.artemediathek +# Addon version: 1.0.0 +# Addon Provider: sarbes +msgid "" +msgstr "" +"Project-Id-Version: XBMC Main\n" + +msgctxt "#30000" +msgid "General" +msgstr "Allgmein" + +msgctxt "#30010" +msgid "Language" +msgstr "Sprache" + +msgctxt "#30011" +msgid "System (if available)" +msgstr "System (wenn verfügbar)" + +msgctxt "#30012" +msgid "English" +msgstr "Englisch" + +msgctxt "#30013" +msgid "German" +msgstr "Deutsch" + +msgctxt "#30014" +msgid "Spanish" +msgstr "Spanisch" + +msgctxt "#30015" +msgid "French" +msgstr "Französisch" + +msgctxt "#30016" +msgid "Hungarian" +msgstr "Ungarisch" + +msgctxt "#30017" +msgid "Italian" +msgstr "Italienisch" + +msgctxt "#30018" +msgid "Polish" +msgstr "Polnisch" + +msgctxt "#30019" +msgid "Portuguese" +msgstr "Portugisisch" + +msgctxt "#30020" +msgid "Romainian" +msgstr "Romänisch" + +msgctxt "#30021" +msgid "Ukrainian" +msgstr "Ukrainisch" \ No newline at end of file diff --git a/plugin.video.artemediathek/resources/language/resource.language.en_gb/strings.po b/plugin.video.artemediathek/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..573d698adc --- /dev/null +++ b/plugin.video.artemediathek/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,60 @@ +# KODI Media Center language file +# Addon Name: artemediathek +# Addon id: plugin.video.artemediathek +# Addon version: 1.0.0 +# Addon Provider: sarbes +msgid "" +msgstr "" +"Project-Id-Version: XBMC Main\n" + +msgctxt "#30000" +msgid "General" +msgstr "General" + +msgctxt "#30010" +msgid "Language" +msgstr "Language" + +msgctxt "#30011" +msgid "System (if available)" +msgstr "System (if available)" + +msgctxt "#30012" +msgid "English" +msgstr "English" + +msgctxt "#30013" +msgid "German" +msgstr "German" + +msgctxt "#30014" +msgid "Spanish" +msgstr "Spanish" + +msgctxt "#30015" +msgid "French" +msgstr "French" + +msgctxt "#30016" +msgid "Hungarian" +msgstr "Hungarian" + +msgctxt "#30017" +msgid "Italian" +msgstr "Italian" + +msgctxt "#30018" +msgid "Polish" +msgstr "Polish" + +msgctxt "#30019" +msgid "Portuguese" +msgstr "Portuguese" + +msgctxt "#30020" +msgid "Romainian" +msgstr "Romainian" + +msgctxt "#30021" +msgid "Ukrainian" +msgstr "Ukrainian" \ No newline at end of file diff --git a/plugin.video.artemediathek/resources/settings.xml b/plugin.video.artemediathek/resources/settings.xml new file mode 100644 index 0000000000..d897865b39 --- /dev/null +++ b/plugin.video.artemediathek/resources/settings.xml @@ -0,0 +1,26 @@ + + +
    + + + + 0 + + + + + + + + + + + + true + + + + + +
    +
    diff --git a/plugin.video.arteplussept/CHANGELOG.md b/plugin.video.arteplussept/CHANGELOG.md new file mode 100644 index 0000000000..24ed7b80e4 --- /dev/null +++ b/plugin.video.arteplussept/CHANGELOG.md @@ -0,0 +1,103 @@ +Changelog also available in file ./addon.xml xpath /addon/extension/news following Kodi guidelines https://kodi.wiki/view/Add-on_structure#changelog.txt + +v1.4.2 (2024-1-3) +- Rename quality parameter. +- Use https to get HBB TV Stream info. +- Fix bug preventing to open series menu + +v1.4.1 (2023-10-10) +- Fix playing videos with siblings. + +v1.4.0 (2023-8-14) +- Add support for content over multiple pages. Manage pagination for favorites, history, search and collections : when there are more items in the history or favorities than the page size (currently 50), it is now possible to navigate through pages. +- Refactor most of the code in OO style. Factorize duplicated code. + +v1.3.1 (2023-8-12) +- Add context menu to view collection as menu instead of playlist +- Set resume point to 0 when video was fully watched. Avoid crash when playing seq of watched videos in playlist. + +v1.3.0 (2023-8-6) +- Improve security with better password management + - Stop storing password on filesystem though addon settings +- Make thomas-ernest fork official in addon.xml for visibility in wiki +- Minor fix/clean-up in translation + +v1.2.1 (2023-8-12) +- Add context menu to view collection as menu instead of playlist +- Set resume point to 0 when video was fully watched. Avoid crash when playing seq of watched videos in playlist. + +v1.2.0 (2023-7-26) +- Manage collections TV_SERIES and MAGAZINE as video playlist +- Add a context menu item to purge favorites +- Add a context menu item to mark as video as watched in Arte + +v1.1.10 (2023-5-28) +- Bugfix to display favorites and last vieweds following id change in Arte + +v1.1.9 (2023-4-18) +- Improve security and performance by caching token to limit authentication requests +- Fallback on clip, when stream is not available anymore. Same feature as on Arte mobile. For favorite content. +- Clean-up and lint code +- Add CI with Pylint and Kodi addon submitter + +v1.1.8 (2023-2-17) +- Improve synchronization of playback progress with Arte TV + - Synchronize progress every minute + - Fix missing synchronization when playback ends + - Enable Resume from beginning (on top of synchronized progress point) +- Improve display of collection item in home page + - Before : rely on old sub category, creating additional menu with a dead entry + - After : rely on cached category, collection videos directly poulated from home page zone +- Add label in notification when manipulating favorites +- Fix plural for Polish +- Non-functional code clean-up + +v1.1.7 (2023-2-14) +- Add feature to purge my history thanks to action in context menu of my history +- Move addon.py to root following Kodi recommendations +- Move back change log fron addon.xml //news to CHANGELOG.md because news is limited to 1500 characters +- Fix translation in English for Successfully removeD from favorite +- Log message instead of notify user, when user or password are not confiugred, while using synched player + +v1.1.6 (2023-2-11) +- Fixed playback progress / resume point retrieved from Arte TV +- Added synchronisation of playback progress with Arte TV when video playback paused, stopped or crashed +- Fixed error when live or viewed streams are not available +- Optimize navigation when cancelling or doing empty search - Avoid display empty page +- Fixed client from web to tv for more accurate content - web content contains more links not browsable in Kodi +- Factorized changelog - Keep them only in addon.xml //news following Kodi's recommendations + +v1.1.5 (2023-1-30) +- Populate root menu from Arte TV API instead of HBB TV. Still play video from HBB TV. +- Manage favorites in Arte profile from context menu +- Added Search in root menu + +v1.1.4 (2023-1-14) +- Added Live stream in root menu +- Got Magazines A-Z content from Arte TV instead of HBB TV API. +- Fixed empty categories - discrepencies with Arte TV - Bug #79 + +v1.1.3 (2022-12-29) +- Added Polish translation +- Added My list and My history content from Arte TV profile + +v1.1.2 (2021-06-27) +- better date / locale handling and prevent crash when http error + +v1.1.1 +- minor python 3 fixes and code improvements (from Kodi Travis CI) + +v1.1.0 +- API fixes +- Added add-on option to select video stream (language, subtitles...) + +v1.0.2 +- weekly browse +- bugfix (settings parsing #54) + +v1.0.1 +- major bug hotfix + +v1.0.0 +- brand new version +- support for new arte api \ No newline at end of file diff --git a/plugin.video.arteplussept/LICENSE.txt b/plugin.video.arteplussept/LICENSE.txt new file mode 100644 index 0000000000..d8cf7d463e --- /dev/null +++ b/plugin.video.arteplussept/LICENSE.txt @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/plugin.video.arteplussept/README.md b/plugin.video.arteplussept/README.md new file mode 100644 index 0000000000..00d9080247 --- /dev/null +++ b/plugin.video.arteplussept/README.md @@ -0,0 +1,116 @@ +# Arte +7 + +

    + Arte +7 logo +

    + +## Description + +Plugin "plugin.video.arteplussept" to watch Arte content on Kodi (ex XBMC). +Can be used without or with Arte account in order to benefit from a better cross-device experience. For instance starting a video or a serie on the mobile app and resume it on kodi. + +### Features + +- Browse and watch Arte replays +- Watch Arte live stream +- Search for content on Arte +- Browse or search multi-page content +- Play serie as a playlist or browse serie as a menu. +- Resume a serie from the first not completed episode thanks to Arte history (login required) +- Load serie as a playlist, when watching one of its episode +- Login with your Arte account without storing password on filesystem - only the token +- Resume videos from where you stopped them (cross device) (login required) +- Manage - view or purge - your Arte history (login required) +- Manage - view, add, delete or purge - your Arte favorites (login required) +- Supported language : FR, EN, DE, PL, IT + +### Not (very well) supported +- Multiple language content +- Subtitles +- Geo blocking +- Display of availability / broadcasting dates + +For feature requests or reporting issues go [here](https://github.com/thomas-ernest/plugin.video.arteplussept/issues). + +# Contributing + +Contributions are welcome ! +You may look at the [issues](https://github.com/thomas-ernest/plugin.video.arteplussept/issues) or unsupported features above. + +## Install the addon locally + +Follow the steps bellow depending on your system and software version + +### 1. Open the addons folder + +Kodi is installed on a different path according to the operating system it is installed on. You can refer to [this page](https://kodi.wiki/view/Kodi_data_folder). Go to $KODI_FOLDER/addons/ + +### 2. Dowload the addon + +In Kodi addons folder +- clone this repository or one of if its forks (preferred) + - `git clone https://github.com/thomas-ernest/plugin.video.arteplussept.git` +- or download the plugin : + - [any release](https://github.com/thomas-ernest/plugin.video.arteplussept/releases) + - [latest commit on master](https://github.com/thomas-ernest/plugin.video.arteplussept/archive/refs/heads/master.zip) + +### 3. Install the addon + +- If you downloaded a zip, extract the content of the zip in the `addons` folder. +- Make sure that the addon is in folder `plugin.video.arteplussept` (and not `plugin.video.arteplussept-master` if you downloaded the latest commit of master for instance). + +For instance for Linux: +``` +unzip -x plugin.video.arteplussept-master.zip +mv plugin.video.arteplussept plugin.video.arteplussept-backup OR rm -fr plugin.video.arteplussept +mv plugin.video.arteplussept-master plugin.video.arteplussept +``` + +### 4. Enjoy + +* Done ! The plugin should show up in your video add-ons section. + +## Troubleshooting + +If you get an issue after a fresh manual installation, you should try +either to restart in order to install dependencies automatically +either to install the dependancies manually. The dependancies are : + +* xbmcswift2 (script.module.xbmcswift2) +* requests (script.module.requests) +* dateutil (script.module.dateutil) + +They should be in the "addon libraries" section of the official repository. + +If you are having issues with the add-on, you can open a issue and join your log file. The log file will contain your system user name and sometimes passwords of services you use in the software, so you may want to sanitize it beforehand. Detailed procedure [here](http://kodi.wiki/view/Log_file/Easy). + +## Coding + +- Compatible with python 3 only and Kodi Matrix (based on Python 3.8) since version 1.1.5 +- Coding guideline : + - 4 space indentation. No tab. + - Snake case for variables and methods + - No parenthesis around keywords like if, elif, for, while... + - Spaces around = when used in body and script. No space around = when setting method parameters + - Double quotes for strings used to end_users or logs. + - Single quotes for dict indices and strings for internal purpose. + - Object oriented (preferred), not fully applied given original + - Pylint guidelines : pydoc for every module and methods... + - Flake8 guidelines except line length is 100 instead of 79. +- Pylint and Flake8 are run in CI. You might want to install them on your local env. + +## Releasing + +- For minor version, create a new release branch `release/$MAJOR.$MINOR`. Don't mention bugfix version. +- Add details of the news version in CHANGELOG.md. Do not touch changelog.txt. It is here for legacy purpose. +- Set the version $MAJOR.$MINOR.$BUGFIX (without v) in addon.xml /addon/@version +- Optionally update highlight changes in addon.xml /addon/extension[@point="xbmc.addon.metadata"]/news. Ensure it counts less than 1500 chars. +- Create a commit with version bump + - git add addon.xml CHANGELOG.md && git commit -m "Bump version to $MAJOR.$MINOR.$BUGFIX" +- Create and push tag "vMaj.Min.Bug" (with v) in git in order to create a GitHub release and submit [PR to official repo](https://github.com/xbmc/repo-plugins/pulls). + - git tag -a v$MAJOR.$MINOR.$BUGFIX + - git push origin --tags +- Done automatically by CI : Create a release in GitHub with name $MAJOR.$MINOR.$BUGFIX (without v). Reuse CHANGELOG.md details as description. https://github.com/thomas-ernest/plugin.video.arteplussept/releases/new +- Done automatically by CI : Submit new version to official Kodi repository + - One-commit change in matrix branch https://kodi.wiki/view/Submitting_Add-ons + - Open pull-request to official repo https://github.com/xbmc/repo-plugins/pulls diff --git a/plugin.video.arteplussept/addon.py b/plugin.video.arteplussept/addon.py new file mode 100644 index 0000000000..e9134f11b8 --- /dev/null +++ b/plugin.video.arteplussept/addon.py @@ -0,0 +1,242 @@ +"""Main module for Kodi add-on plugin.video.arteplussept""" +# coding=utf-8 +# -*- coding: utf-8 -*- +# +# plugin.video.arteplussept, Kodi add-on to watch videos from http://www.arte.tv/guide/fr/plus7/ +# Copyright (C) 2015 known-as-bmf +# Copyright (C) 2023 thomas-ernest +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# https://xbmcswift2.readthedocs.io/en/latest/api.html +# https://github.com/XBMC-Addons/script.module.xbmcswift2 +# pylint: disable=import-error +from xbmcswift2 import Plugin +# pylint: disable=import-error +from xbmcswift2 import xbmc +from resources.lib import user +from resources.lib import view +from resources.lib.mapper.artefavorites import ArteFavorites +from resources.lib.mapper.artehistory import ArteHistory +from resources.lib.mapper.artesearch import ArteSearch +from resources.lib.mapper.artezone import ArteZone +from resources.lib.player import Player +from resources.lib.settings import Settings + +# global declarations +# plugin stuff +plugin = Plugin() + +settings = Settings(plugin) + + +@plugin.route('/', name='index') +def index(): + """Display home menu""" + return view.build_home_page(plugin, settings, plugin.get_storage('cached_categories', TTL=60)) + + +@plugin.route('/api_category/', name='api_category') +def api_category(category_code): + """Display the menu for a category that needs an api call""" + return view.build_api_category(plugin, category_code, settings) + + +@plugin.route('/cached_category/', name='cached_category') +def cached_category(zone_id): + """Display the menu for a category that is stored + in cache from previous api call like home page""" + return view.get_cached_category(zone_id, plugin.get_storage('cached_categories', TTL=60)) + + +@plugin.route('/category_page///', name='category_page') +def category_page(zone_id, page, page_id): + """Display the menu for a category that needs an api call""" + return ArteZone(plugin, settings, plugin.get_storage('cached_categories', TTL=60)) \ + .build_menu(zone_id, page, page_id) + + +@plugin.route('/play_collection//', name='play_collection') +def play_collection(kind, collection_id): + """ + Load a playlist and start playing its first item. + """ + playlist = view.build_collection_playlist(plugin, settings, kind, collection_id) + + # Empty playlist, otherwise requested video is present twice in the playlist + xbmc.PlayList(xbmc.PLAYLIST_VIDEO).clear() + # Start playing with the first playlist item + synched_player = Player( + user.get_cached_token(plugin, settings.username, True), + playlist['start_program_id']) + # try to seek parent collection, when out of the context of playlist creation + # Start playing with the first playlist item + result = plugin.set_resolved_url(plugin.add_to_playlist(playlist['collection'])[0]) + synch_during_playback(synched_player) + del synched_player + return result + + +@plugin.route('/favorites', name='favorites_default') +@plugin.route('/favorites/', name='favorites') +def favorites(page=1): + """Display the menu for user favorites""" + plugin.set_content('tvshows') + return plugin.finish(ArteFavorites(plugin, settings).build_menu(page)) + + +@plugin.route('/add_favorite//
    best_deal: + # Keep the best deal + best_deal = end + + if best_deal is not None: + return dateutil.parser.parse(best_deal).astimezone(tz=dateutil.tz.tzlocal()) + + return False + + +def parse_channel(channel, offers=None, station_id=None): + """ Parse the API result of a channel into a Channel object. + + :param dict channel: The channel info from the API. + :param List[str] offers: A list of offers that we have. + :param str station_id: The station ID of the CAPI. + + :returns: A channel that is parsed. + :rtype: Channel + """ + return Channel( + uid=channel.get('id'), + station_id=station_id, + title=channel.get('title'), + icon=find_image(channel.get('images'), 'la'), # landscape + preview=find_image(channel.get('images'), 'lv'), # live + number=channel.get('params', {}).get('lcn'), + epg_now=parse_epg(channel.get('params', {}).get('now')), + epg_next=parse_epg(channel.get('params', {}).get('next')), + radio=channel.get('params', {}).get('radio', False), + replay=channel.get('params', {}).get('replayExpiry', False) is not False, + available=check_deals_entitlement(channel.get('deals'), offers), + pin=channel.get('params', {}).get('pinProtected', False), + ) + + +def parse_epg(program, offers=None): + """ Parse an Epg dict from the TV API. + + :param dict program: The program object to parse. + :param List[str] offers: A list of offers that we have. + + :returns: A program that is parsed. + :rtype: Epg + """ + if not program: + return None + + # Parse dates and convert from UTC to local timezone + start = dateutil.parser.parse(program.get('params', {}).get('start')).replace(tzinfo=dateutil.tz.UTC).astimezone(tz=dateutil.tz.tzlocal()) + end = dateutil.parser.parse(program.get('params', {}).get('end')).replace(tzinfo=dateutil.tz.UTC).astimezone(tz=dateutil.tz.tzlocal()) + + season = program.get('params', {}).get('seriesSeason') + episode = program.get('params', {}).get('seriesEpisode') + + return Epg( + uid=program.get('id'), + title=program.get('title'), + description=program.get('desc'), + cover=find_image(program.get('images'), 'po'), # poster + preview=find_image(program.get('images'), 'la'), # landscape + start=start, + end=end, + duration=(end - start).total_seconds(), + channel_id=program.get('params', {}).get('channelId'), + formats=[epg_format.get('title') for epg_format in program.get('params', {}).get('formats')], + genres=[epg_genre.get('title') for epg_genre in program.get('params', {}).get('genres')], + replay=program.get('params', {}).get('replay', False), + restart=program.get('params', {}).get('restart', False), + age=program.get('params', {}).get('age'), + series_id=program.get('params', {}).get('seriesId'), + season=int(season) if season is not None else None, + episode=int(episode) if episode is not None else None, + credit=[ + Credit(credit.get('role'), credit.get('person'), credit.get('character')) + for credit in program.get('params', {}).get('credits', []) + ], + available=check_deals_entitlement(program.get('deals'), offers), + ) + + +def parse_epg_series(program): + """ Parse a EpgSeries dict from the TV API. + + :param dict program: The series object to parse. + + :returns: A program that is parsed. + :rtype: Epg + """ + if not program: + return None + + return EpgSeries( + uid=program.get('id'), + title=program.get('title'), + description=program.get('desc'), + cover=find_image(program.get('images'), 'po'), # poster + preview=find_image(program.get('images'), 'la'), # landscape + channel_id=program.get('params', {}).get('channelId'), + formats=[epg_format.get('title') for epg_format in program.get('params', {}).get('formats')], + genres=[epg_genre.get('title') for epg_genre in program.get('params', {}).get('genres')], + age=program.get('params', {}).get('age'), + ) + + +def parse_epg_capi(program, tenant): + """ Parse a program dict from the CAPI. + + :param dict program: The program object to parse. + :param dict tenant: The tenant object to help with some URL's. + + :returns: A program that is parsed. + :rtype: Epg + """ + if not program: + return None + + # Parse dates and convert from UTC to local timezone + start = datetime.fromtimestamp(program.get('start') / 1000, tz=dateutil.tz.UTC).astimezone(tz=dateutil.tz.tzlocal()) + end = datetime.fromtimestamp(program.get('end') / 1000, tz=dateutil.tz.UTC).astimezone(tz=dateutil.tz.tzlocal()) + now = datetime.now(tz=dateutil.tz.tzlocal()) + + # Parse credits + credit_list = [] + for credit in program.get('credits', []) or []: + if not credit.get('r'): # Actor + credit_list.append(Credit(role=Credit.ROLE_ACTOR, person=credit.get('p'), character=credit.get('c'))) + elif credit.get('r') == 1: # Director + credit_list.append(Credit(role=Credit.ROLE_DIRECTOR, person=credit.get('p'))) + elif credit.get('r') == 3: # Producer + credit_list.append(Credit(role=Credit.ROLE_PRODUCER, person=credit.get('p'))) + elif credit.get('r') == 4: # Presenter + credit_list.append(Credit(role=Credit.ROLE_PRESENTER, person=credit.get('p'))) + elif credit.get('r') == 5: # Guest + credit_list.append(Credit(role=Credit.ROLE_GUEST, person=credit.get('p'))) + elif credit.get('r') == 7: # Composer + credit_list.append(Credit(role=Credit.ROLE_COMPOSER, person=credit.get('p'))) + + return Epg( + uid=program.get('locId'), + title=program.get('title'), + description=program.get('description'), + cover='https://{domain}/{env}/mmchan/mpimages/447x251/{file}'.format(domain=tenant.get('domain'), + env=tenant.get('env'), + file=program.get('cover').split('/')[-1]) if program.get('cover') else None, + preview=None, + start=start, + end=end, + duration=(end - start).total_seconds(), + channel_id=None, + formats=[program.get('formats')], # We only have one format + genres=program.get('genres'), + replay=program.get('flags') & 16, # BIT_EPG_FLAG_REPLAY + restart=program.get('flags') & 32, # BIT_EPG_FLAG_RESTART + age=program.get('age'), + series_id=program.get('seriesId'), + season=program.get('seasonNo'), + episode=program.get('episodeNo'), + credit=credit_list, + available=(program.get('flags') & 16) and (start < now), # BIT_EPG_FLAG_REPLAY + ) + + +def parse_vod_genre(collection): + """ Parse a genre from the Collections API. + + :param dict collection: The collection object to parse. + :returns: A genre that is parsed. + :rtype: VodGenre + """ + return VodGenre( + uid=None, + title=collection.get('title').capitalize() if collection.get('title') else VodGenre.map_label(collection.get('label')), + query=collection.get('query'), + ) + + +def parse_vod_movie(asset): + """ Parse a movie from the Collections API. + + :param dict asset: The asset object to parse. + :returns: A movie that is parsed. + :rtype: VodMovie + """ + return VodMovie( + uid=asset.get('id'), + title=asset.get('title'), + year=asset.get('params', {}).get('year'), + duration=asset.get('params', {}).get('duration'), + age=asset.get('params', {}).get('age'), + cover=find_image(asset.get('images'), 'po'), # poster + preview=find_image(asset.get('images'), 'la'), # landscape + ) + + +def parse_vod_series(asset): + """ Parse a series from the Collections API. + + :param dict asset: The asset object to parse. + :returns: A series that is parsed. + :rtype: VodSeries + """ + return VodSeries( + uid=asset.get('id'), + title=asset.get('title'), + year=asset.get('params', {}).get('year'), + age=asset.get('params', {}).get('age'), + cover=find_image(asset.get('images'), 'po'), # poster + preview=find_image(asset.get('images'), 'la'), # landscape + ) + + +def parse_vod_episode(asset): + """ Parse an episode from the Collections API. + + :param dict asset: The asset object to parse. + :returns: An epsiode that is parsed. + :rtype: VodEpisode + """ + return VodEpisode( + uid=asset.get('id'), + title=asset.get('title'), + year=asset.get('params', {}).get('year'), + duration=asset.get('params', {}).get('duration'), + age=asset.get('params', {}).get('age'), + cover=find_image(asset.get('images'), 'po'), # poster + preview=find_image(asset.get('images'), 'la'), # landscape + series_id=asset.get('params', {}).get('seriesId'), + season=asset.get('params', {}).get('seriesSeason'), + episode=asset.get('params', {}).get('seriesEpisode'), + ) + + +def http_get(url, params=None, token_bearer=None, token_cookie=None): + """ Make a HTTP GET request for the specified URL. + + :param str url: The URL to call. + :param dict params: The query parameters to include to the URL. + :param str token_bearer: The token to use in Bearer authentication. + :param str token_cookie: The token to use in Cookie authentication. + + :returns: The HTTP Response object. + :rtype: requests.Response + """ + try: + return _request('GET', url=url, params=params, token_bearer=token_bearer, token_cookie=token_cookie) + except HTTPError as ex: + if ex.response.status_code == 401: + raise InvalidTokenException + raise + + +def http_post(url, params=None, form=None, data=None, token_bearer=None, token_cookie=None): + """ Make a HTTP POST request for the specified URL. + + :param str url: The URL to call. + :param dict params: The query parameters to include to the URL. + :param dict form: A dictionary with form parameters to POST. + :param dict data: A dictionary with json parameters to POST. + :param str token_bearer: The token to use in Bearer authentication. + :param str token_cookie: The token to use in Cookie authentication. + + :returns: The HTTP Response object. + :rtype: requests.Response + """ + try: + return _request('POST', url=url, params=params, form=form, data=data, token_bearer=token_bearer, + token_cookie=token_cookie) + except HTTPError as ex: + if ex.response.status_code == 401: + raise InvalidTokenException + raise + + +def _request(method, url, params=None, form=None, data=None, token_bearer=None, token_cookie=None): + """ Makes a request for the specified URL. + + :param str method: The HTTP Method to use. + :param str url: The URL to call. + :param dict params: The query parameters to include to the URL. + :param dict form: A dictionary with form parameters to POST. + :param dict data: A dictionary with json parameters to POST. + :param str token_bearer: The token to use in Bearer authentication. + :param str token_cookie: The token to use in Cookie authentication. + + :returns: The HTTP Response object. + :rtype: requests.Response + """ + if form or data: + # Make sure we don't log the password + debug_data = {} + debug_data.update(form or data) + if 'Password' in debug_data: + debug_data['Password'] = '**redacted**' + _LOGGER.debug('Sending %s %s: %s', method, url, debug_data) + else: + _LOGGER.debug('Sending %s %s', method, url) + + if token_bearer: + headers = { + 'authorization': 'Bearer ' + token_bearer, + } + else: + headers = {} + + if token_cookie: + cookies = { + '.ASPXAUTH': token_cookie + } + else: + cookies = {} + + response = SESSION.request(method, url, params=params, data=form, json=data, headers=headers, cookies=cookies, proxies=PROXIES) + + # Set encoding to UTF-8 if no charset is indicated in http headers (https://github.com/psf/requests/issues/1604) + if not response.encoding: + response.encoding = 'utf-8' + + _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) + + # Raise a generic HTTPError exception when we got an non-okay status code. + response.raise_for_status() + + return response diff --git a/plugin.video.canaldigitaal.nl/resources/screenshot01.jpg b/plugin.video.canaldigitaal.nl/resources/screenshot01.jpg new file mode 100644 index 0000000000..32d1816a66 Binary files /dev/null and b/plugin.video.canaldigitaal.nl/resources/screenshot01.jpg differ diff --git a/plugin.video.canaldigitaal.nl/resources/screenshot02.jpg b/plugin.video.canaldigitaal.nl/resources/screenshot02.jpg new file mode 100644 index 0000000000..f0e18a2ecd Binary files /dev/null and b/plugin.video.canaldigitaal.nl/resources/screenshot02.jpg differ diff --git a/plugin.video.canaldigitaal.nl/resources/screenshot03.jpg b/plugin.video.canaldigitaal.nl/resources/screenshot03.jpg new file mode 100644 index 0000000000..fa182fb347 Binary files /dev/null and b/plugin.video.canaldigitaal.nl/resources/screenshot03.jpg differ diff --git a/plugin.video.canaldigitaal.nl/resources/settings.xml b/plugin.video.canaldigitaal.nl/resources/settings.xml new file mode 100644 index 0000000000..f3bf992fab --- /dev/null +++ b/plugin.video.canaldigitaal.nl/resources/settings.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin.video.catchuptvandmore/LICENSE.txt b/plugin.video.catchuptvandmore/LICENSE.txt new file mode 100644 index 0000000000..0d6e108747 --- /dev/null +++ b/plugin.video.catchuptvandmore/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + Catch-up TV & More + Copyright (C) 2016 SylvainCecchetto + + Catch-up TV & More is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Catch-up TV & More 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 Catch-up TV & More; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/plugin.video.catchuptvandmore/addon.py b/plugin.video.catchuptvandmore/addon.py new file mode 100644 index 0000000000..13b8cdc95b --- /dev/null +++ b/plugin.video.catchuptvandmore/addon.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2016, SylvainCecchetto +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +import importlib +from codequick import run + + +def main(): + """Entry point function executed by Kodi for each menu of the addon""" + + # Let CodeQuick check for functions to register and call + # the correct function according to the Kodi URL + exception = run() + if isinstance(exception, Exception): + main = importlib.import_module('resources.lib.main') + main.error_handler(exception) + + +if __name__ == "__main__": + main() diff --git a/plugin.video.catchuptvandmore/addon.xml b/plugin.video.catchuptvandmore/addon.xml new file mode 100644 index 0000000000..98836dfaa1 --- /dev/null +++ b/plugin.video.catchuptvandmore/addon.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + video + + + + + + true + Catch-Up TV & More : Replay TV, Live TV et bien plus encore. + Catch-Up TV & More regroupe dans un même plugin Kodi l'ensemble des vidéos des différents services et chaînes de Replay TV (rattrapage TV) ainsi que l'accès au direct. De plus, cette extension vous permet d'accéder rapidement aux vidéos et contenus proposés par certains sites internet. + Retours de bugs, propositions d'améliorations ou d'ajout de contenus sont les bienvenue ! GitHub ou e-mail. + Catch-Up TV & More: It's all in the title. + Catch-Up TV & More brings together in one Kodi add-on all the videos of the various services and channels of catch-up TV and live TV. Furthermore, this add-on allows you to quickly access the videos and content offered by certain websites. + Bug reports, suggestions for improvements or content additions are welcome! GitHub or e-mail. + ההרחבה מאפשרת לך להתעדכן בתוכן השידורים תוסף אחד של קודי מרכז את כל קטעי וידאו של שירותים שונים וערוצי תוכן משודר. בנוסף, תוספת זו מאפשרת לך לגשת במהירות לסרטונים ולתכנים המוצעים על ידי אתרים מסוימים. + דוחות שגיאה, הצעות לשיפור או תוספות תוכן יתקבלו בברכה! ל-GitHub או אימייל. + Catch-Up TV & More: Replay tv, Live tv en meer. + Catch-Up TV & More brengt replay en live tv van verschikkende diensten en kanalen samen in één Kodi add-on. Bovendien stelt deze add-on u instaat video's van bepaalde websites makkelijk te benaderen. + Bug meldingen, suggesties ter verbetering of additionele inhouden zijn welkom! Via GitHub of e-mail. + + + all + GPL-2.0 + https://forum.kodi.tv/showthread.php?tid=307107 + https://catch-up-tv-and-more.github.io/ + catch.up.tv.and.more at gmail dot com + https://github.com/Catch-up-TV-and-More/plugin.video.catchuptvandmore + +[Version 0.2.40] +Changelog is too big for this version. +See https://github.com/Catch-up-TV-and-More/plugin.video.catchuptvandmore/releases/tag/v0.2.40 from the changelog. + + + icon.png + fanart.jpg + resources/screenshots/screenshot-01.jpg + resources/screenshots/screenshot-02.jpg + resources/screenshots/screenshot-03.jpg + resources/screenshots/screenshot-04.jpg + resources/screenshots/screenshot-05.jpg + + + diff --git a/plugin.video.catchuptvandmore/fanart.jpg b/plugin.video.catchuptvandmore/fanart.jpg new file mode 100644 index 0000000000..e580af9313 Binary files /dev/null and b/plugin.video.catchuptvandmore/fanart.jpg differ diff --git a/plugin.video.catchuptvandmore/icon.png b/plugin.video.catchuptvandmore/icon.png new file mode 100644 index 0000000000..53c1e7e771 Binary files /dev/null and b/plugin.video.catchuptvandmore/icon.png differ diff --git a/plugin.video.catchuptvandmore/resources/__init__.py b/plugin.video.catchuptvandmore/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.catchuptvandmore/resources/language/resource.language.en_gb/strings.po b/plugin.video.catchuptvandmore/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..ea9974689d --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,963 @@ +# Kodi Media Center language file +# Addon Name: Catch-up TV & More +# Addon id: plugin.video.catchuptvandmore +# Addon Provider: SylvainCecchetto +msgid "" +msgstr "" +"Language: en\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +# Settings tabs/categories (from 30000 to 30019) + +msgctxt "#30000" +msgid "" +msgstr "" + +msgctxt "#30001" +msgid "" +msgstr "" + +msgctxt "#30002" +msgid "Quality and content" +msgstr "" + +msgctxt "#30003" +msgid "Downloads" +msgstr "" + +msgctxt "#30004" +msgid "Accounts" +msgstr "" + +msgctxt "#30005" +msgid "Advanced settings" +msgstr "" + +msgctxt "#30006" +msgid "Channels" +msgstr "" + +msgctxt "#30007" +msgid "" +msgstr "" + +msgctxt "#30008" +msgid "" +msgstr "" + +msgctxt "#30009" +msgid "General" +msgstr "" + + +# Settings line separators (from 30010 to 30029) + +msgctxt "#30010" +msgid "Hide main menu categories" +msgstr "" + +msgctxt "#30011" +msgid "Hide channels" +msgstr "" + +msgctxt "#30012" +msgid "YTDL settings" +msgstr "" + +msgctxt "#30013" +msgid "Hide countries" +msgstr "" + +msgctxt "#30014" +msgid "Hide websites" +msgstr "" + +msgctxt "#30015" +msgid "Hide live TV countries" +msgstr "" + +msgctxt "#30016" +msgid "Hide catch-up TV countries" +msgstr "" + +msgctxt "#30017" +msgid "Default values" +msgstr "" + +msgctxt "#30018" +msgid "Unmask main menus items" +msgstr "" + +msgctxt "#30019" +msgid "Unmask live TV channels" +msgstr "" + +msgctxt "#30020" +msgid "Unmask replay TV channels" +msgstr "" + + +# Main menu (from 30030 to 30049) + +msgctxt "#30030" +msgid "Live TV" +msgstr "" + +msgctxt "#30031" +msgid "Catch-up TV" +msgstr "" + +msgctxt "#30032" +msgid "Websites" +msgstr "" + +msgctxt "#30033" +msgid "Favourites" +msgstr "" + +msgctxt "#30034" +msgid "Internet service providers" +msgstr "" + + +# Countries (from 30050 to 30079) + +msgctxt "#30050" +msgid "France" +msgstr "" + +msgctxt "#30051" +msgid "Switzerland" +msgstr "" + +msgctxt "#30052" +msgid "United Kingdom" +msgstr "" + +msgctxt "#30053" +msgid "International" +msgstr "" + +msgctxt "#30054" +msgid "Belgium" +msgstr "" + +msgctxt "#30055" +msgid "Japan" +msgstr "" + +msgctxt "#30056" +msgid "Canada" +msgstr "" + +msgctxt "#30057" +msgid "United States of America" +msgstr "" + +msgctxt "#30058" +msgid "Poland" +msgstr "" + +msgctxt "#30059" +msgid "Spain" +msgstr "" + +msgctxt "#30060" +msgid "Tunisia" +msgstr "" + +msgctxt "#30061" +msgid "Italia" +msgstr "" + +msgctxt "#30062" +msgid "Netherlands" +msgstr "" + +msgctxt "#30063" +msgid "China" +msgstr "" + +msgctxt "#30064" +msgid "Cameroon" +msgstr "" + +msgctxt "#30065" +msgid "Slovenia" +msgstr "" + +msgctxt "#30066" +msgid "Ethiopia" +msgstr "" + +msgctxt "#30067" +msgid "Morocco" +msgstr "" + +msgctxt "#30068" +msgid "Singapore" +msgstr "" + +msgctxt "#30069" +msgid "Lituania" +msgstr "" + +msgctxt "#30070" +msgid "Turkey" +msgstr "" + +msgctxt "#30071" +msgid "Slovakia" +msgstr "" + +msgctxt "#30072" +msgid "Greece" +msgstr "" + +msgctxt "#30073" +msgid "Peru" +msgstr "" + +msgctxt "#30074" +msgid "Venezuela" +msgstr "" + +msgctxt "#30075" +msgid "Luxembourg" +msgstr "" + +msgctxt "#30076" +msgid "Bolivia" +msgstr "" + +# Channels (from 30080 to 30129) + +msgctxt "#30080" +msgid "French channels" +msgstr "" + +msgctxt "#30081" +msgid "Belgian channels" +msgstr "" + +msgctxt "#30082" +msgid "Japanese channels" +msgstr "" + +msgctxt "#30083" +msgid "Switzerland channels" +msgstr "" + +msgctxt "#30084" +msgid "United Kingdom channels" +msgstr "" + +msgctxt "#30085" +msgid "International channels" +msgstr "" + +msgctxt "#30086" +msgid "Canadian channels" +msgstr "" + +msgctxt "#30087" +msgid "United States channels" +msgstr "" + +msgctxt "#30088" +msgid "Polish channels" +msgstr "" + +msgctxt "#30089" +msgid "Spanish channels" +msgstr "" + +msgctxt "#30090" +msgid "Tunisia channels" +msgstr "" + +msgctxt "#30091" +msgid "Italian channels" +msgstr "" + +msgctxt "#30092" +msgid "Dutch channels" +msgstr "" + +msgctxt "#30093" +msgid "Chinese channels" +msgstr "" + +msgctxt "#30094" +msgid "Cameroon channels" +msgstr "" + +msgctxt "#30095" +msgid "Slovenian channels" +msgstr "" + +msgctxt "#30096" +msgid "Ethiopian channels" +msgstr "" + +msgctxt "#30097" +msgid "Moroccan channels" +msgstr "" + +msgctxt "#30098" +msgid "Peruvian channels" +msgstr "" + +# Websites (from 30130 to 30149) + + + +# Quality and content (from 30150 to 30199) + +msgctxt "#30150" +msgid "Video quality" +msgstr "" + +msgctxt "#30151" +msgid "Device is L1 certified" +msgstr "" + +msgctxt "#30152" +msgid "Contents" +msgstr "" + +msgctxt "#30153" +msgid "Arte: Choose Channel" +msgstr "" + +msgctxt "#30154" +msgid "France 24: Choose Channel" +msgstr "" + +msgctxt "#30155" +msgid "Euronews: Choose Channel" +msgstr "" + +msgctxt "#30156" +msgid "MTV: Choose Channel" +msgstr "" + +msgctxt "#30157" +msgid "DW: Choose Channel" +msgstr "" + +msgctxt "#30158" +msgid "France 3 Régions: Choose region" +msgstr "" + +msgctxt "#30159" +msgid "La 1ère: Choose region" +msgstr "" + +msgctxt "#30160" +msgid "Bein Sports: Choose Channel" +msgstr "" + +msgctxt "#30161" +msgid "QVC: Choose Channel" +msgstr "" + +msgctxt "#30162" +msgid "NHK World: Choose Country" +msgstr "" + +msgctxt "#30163" +msgid "CGTN: Choose Channel" +msgstr "" + +msgctxt "#30164" +msgid "ICI Télé: Choose region" +msgstr "" + +msgctxt "#30165" +msgid "Realmadrid TV: Choose Channel" +msgstr "" + +msgctxt "#30166" +msgid "RT: Choose Channel" +msgstr "" + +msgctxt "#30167" +msgid "TVP 3: Choose region" +msgstr "" + +msgctxt "#30168" +msgid "Enable TV guide (slower)" +msgstr "" + +msgctxt "#30169" +msgid "Enable Subtitle if present in the content" +msgstr "" + +msgctxt "#30170" +msgid "CBC: Choose region" +msgstr "" + +msgctxt "#30171" +msgid "BFM Régions: Choose region" +msgstr "" + +msgctxt "#30173" +msgid "TV5 monde plus: Choose language" +msgstr "" + +msgctxt "#30174" +msgid "Choose live channel" +msgstr "" + +msgctxt "#30175" +msgid "Equidia Racing: Choose race" +msgstr "" + +msgctxt "#30177" +msgid "TV5 monde: Choose region" +msgstr "" + +msgctxt "#30178" +msgid "France, Belgium, Switzerland" +msgstr "" + +msgctxt "#30179" +msgid "Europe, outside FBS" +msgstr "" + +msgctxt "#30180" +msgid "Video quality" +msgstr "" + +msgctxt "#30181" +msgid "Choose version" +msgstr "" + +msgctxt "#30182" +msgid "France 3 Régions: Choose subregion" +msgstr "" + +msgctxt "#30192" +msgid "BEST" +msgstr "" + +msgctxt "#30193" +msgid "DEFAULT" +msgstr "" + +msgctxt "#30194" +msgid "DIALOG" +msgstr "" + +msgctxt "#30195" +msgid "WORST" +msgstr "" + +msgctxt "#30196" +msgid "Maximum stream bitrate [COLOR=gray](0=disabled)[/COLOR]" +msgstr "" + +msgctxt "#30197" +msgid "Settings - InputStream adaptive" +msgstr "" + +msgctxt "#30198" +msgid "Use inputstream HLS?" +msgstr "" + +msgctxt "#30199" +msgid "Use ytdl for stream?" +msgstr "" + +# Download (from 30200 to 30239) + +msgctxt "#30200" +msgid "Download directory" +msgstr "" + +msgctxt "#30201" +msgid "Quality of the video to download" +msgstr "" + +msgctxt "#30202" +msgid "Download in background" +msgstr "" + +msgctxt "#30203" +msgid "Automatically rename the video (download directory required)" +msgstr "" + + +# Accounts (from 30240 to 30269) + +msgctxt "#30242" +msgid "VRT NU Login" +msgstr "" + +msgctxt "#30243" +msgid "VRT NU Password" +msgstr "" + +msgctxt "#30244" +msgid "6play Login" +msgstr "" + +msgctxt "#30245" +msgid "6play Password" +msgstr "" + +msgctxt "#30246" +msgid "RTLplay Login" +msgstr "" + +msgctxt "#30247" +msgid "RTLplay Password" +msgstr "" + +msgctxt "#30248" +msgid "ABWeb Login" +msgstr "" + +msgctxt "#30249" +msgid "ABWeb Password" +msgstr "" + +msgctxt "#30250" +msgid "UKTVPlay Login" +msgstr "" + +msgctxt "#30251" +msgid "UKTVPlay Password" +msgstr "" + +msgctxt "#30252" +msgid "RMCBFM Play Login" +msgstr "" + +msgctxt "#30253" +msgid "RMCBFM Play Password" +msgstr "" + +msgctxt "#30254" +msgid "TV guides" +msgstr "" + +msgctxt "#30255" +msgid "Schedules Direct login" +msgstr "" + +msgctxt "#30256" +msgid "Schedules Direct password" +msgstr "" + +msgctxt "#30257" +msgid "Schedules Direct lineup" +msgstr "" + +msgctxt "#30258" +msgid "RTBF auvio Login" +msgstr "" + +msgctxt "#30259" +msgid "RTBF auvio Password" +msgstr "" + +msgctxt "#30260" +msgid "TF1+ Login" +msgstr "" + +msgctxt "#30261" +msgid "TF1+ Password" +msgstr "" + +msgctxt "#30262" +msgid "SFR TV login" +msgstr "" + +msgctxt "#30263" +msgid "SFR TV password" +msgstr "" + + +# TV integration (from 30270 to 30289) + +msgctxt "#30270" +msgid "TV integration" +msgstr "" + +msgctxt "#30271" +msgid "Kodi Live TV integration" +msgstr "" + +msgctxt "#30272" +msgid "Install IPTV Manager add-on" +msgstr "" + +msgctxt "#30273" +msgid "Enable Kodi Live TV integration" +msgstr "" + +msgctxt "#30274" +msgid "Open IPTV Manager settings" +msgstr "" + +msgctxt "#30275" +msgid "Select channels to enable" +msgstr "" + +msgctxt "#30276" +msgid "Failed to save TV integration settings" +msgstr "" + +msgctxt "#30277" +msgid "Select channels to enable in Kodi Live TV" +msgstr "" + + + +# Reserved space for other tab/category in settings (from 30290 to 30339) + + + + +# Advanced settings (from 30370 to 30399) + +msgctxt "#30370" +msgid "Clear cache" +msgstr "" + +msgctxt "#30371" +msgid "Cache cleared" +msgstr "" + +msgctxt "#30372" +msgid "Send Kodi log to the developers when an error occurred" +msgstr "" + +msgctxt "#30373" +msgid "Delete favourites" +msgstr "" + +msgctxt "#30374" +msgid "Favourites deleted" +msgstr "" + + +# Settings of 'General' category (from 30400 to 30429) + +msgctxt "#30400" +msgid "Restore default order of all menus" +msgstr "" + +msgctxt "#30401" +msgid "Unmask all hidden items" +msgstr "" + +msgctxt "#30402" +msgid "Select items to unmask" +msgstr "" + +msgctxt "#30403" +msgid "Menu items" +msgstr "" + +msgctxt "#30404" +msgid "" +msgstr "" + +msgctxt "#30405" +msgid "" +msgstr "" + +msgctxt "#30406" +msgid "" +msgstr "" + +msgctxt "#30407" +msgid "Default order of all menus have been restored" +msgstr "" + +msgctxt "#30408" +msgid "All hidden items have been unmasked" +msgstr "" + + +# Advanced settings (from 30430 to 30449) + +msgctxt "#30430" +msgid "Automatically start Catch-up TV & More when Kodi starts" +msgstr "" + + +# Reserved space for other tab/category in settins (from 30450 to 30499) + + + +# Context menu (from 30500 to 30599) + +msgctxt "#30500" +msgid "Move down" +msgstr "" + +msgctxt "#30501" +msgid "Move up" +msgstr "" + +msgctxt "#30502" +msgid "Hide" +msgstr "" + +msgctxt "#30503" +msgid "Download" +msgstr "" + +msgctxt "#30504" +msgid "List Videos (type=extrait)" +msgstr "" + +msgctxt "#30505" +msgid "List Videos (type=episode)" +msgstr "" + + +# Dialog boxes (from 30600 to 30699) + +msgctxt "#30600" +msgid "Information" +msgstr "" + +msgctxt "#30601" +msgid "To re-enable hidden items go to the plugin settings" +msgstr "" + +msgctxt "#30602" +msgid "This content is DRM protected. To be able to play it we invite you to update Kodi to the version 18 or above. Thank you for your understanding." +msgstr "" + +msgctxt "#30603" +msgid "This content is DRM protected. The download mode doesn't work for this kind of protection. Thank you for your understanding." +msgstr "" + +msgctxt "#30604" +msgid "Access to this content requires an account %s. You can provide your credentials for it in the settings of this add-on. If you don't have an account, you can create one at this url %s." +msgstr "" + +msgctxt "#30605" +msgid "You can now use Kodi's TV feature to watch live TV channels from Catch-up TV & More, follow the tutorial at https://catch-up-tv-and-more.github.io/live_tv_installation/." +msgstr "" + +msgctxt "#30606" +msgid "Do you want to see this information next time?" +msgstr "" + + +# Others (from 30700 to 30799) + +msgctxt "#30700" +msgid "More videos..." +msgstr "" + +msgctxt "#30701" +msgid "All videos" +msgstr "" + +msgctxt "#30702" +msgid "DRM protected video" +msgstr "" + +msgctxt "#30703" +msgid "Search" +msgstr "" + +msgctxt "#30704" +msgid "Last videos" +msgstr "" + +msgctxt "#30705" +msgid "From A to Z" +msgstr "" + +msgctxt "#30706" +msgid "Ascending" +msgstr "" + +msgctxt "#30707" +msgid "Descending" +msgstr "" + +msgctxt "#30708" +msgid "More programs..." +msgstr "" + +msgctxt "#30709" +msgid "Choose video quality" +msgstr "" + +msgctxt "#30710" +msgid "Video stream no longer exists" +msgstr "" + +msgctxt "#30711" +msgid "Authentication failed" +msgstr "" + +msgctxt "#30712" +msgid "Video with an account needed" +msgstr "" + +msgctxt "#30713" +msgid "Geo-blocked video" +msgstr "" + +msgctxt "#30714" +msgid "Search videos" +msgstr "" + +msgctxt "#30715" +msgid "Search programs" +msgstr "" + +msgctxt "#30716" +msgid "Video stream is not available" +msgstr "" + +msgctxt "#30717" +msgid "All programs" +msgstr "" + +msgctxt "#30718" +msgid "No videos found" +msgstr "" + +msgctxt "#30719" +msgid "Inputstream Adaptive not enabled" +msgstr "" + +msgctxt "#30720" +msgid "Kodi 17.6 or > is required" +msgstr "" + +msgctxt "#30721" +msgid "Content requiring a subscription" +msgstr "" + +msgctxt "#30723" +msgid "An error occurred while getting TV guide" +msgstr "" + +msgctxt "#30724" +msgid "Failed to guess your country based on your IP" +msgstr "" + +msgctxt "#30725" +msgid "Categories" +msgstr "" + +msgctxt "#30726" +msgid "Races" +msgstr "" + +msgctxt "#30727" +msgid "Most popular" +msgstr "" + +msgctxt "#30728" +msgid "Recently added" +msgstr "" + +msgctxt "#30729" +msgid "Emissions" +msgstr "" + +msgctxt "#30731" +msgid "Homepage" +msgstr "" + +msgctxt "#30732" +msgid "This content requires a payment. To buy or rent it, go to %s website at this URL : %s" +msgstr "" + + +# Favourites (from 30800 to 30859) + +msgctxt "#30800" +msgid "Add to add-on favourites" +msgstr "" + +msgctxt "#30801" +msgid "Favourite name" +msgstr "" + +msgctxt "#30802" +msgid "Remove" +msgstr "" + +msgctxt "#30803" +msgid "Favourites" +msgstr "" + +msgctxt "#30804" +msgid "Rename" +msgstr "" + +msgctxt "#30805" +msgid "The item has been added to the favourites of the add-on" +msgstr "" + +msgctxt "#30806" +msgid "Add at least one favourite to access this menu" +msgstr "" + +msgctxt "#30807" +msgid "This favourite raises an error, would you like to delete it?" +msgstr "" + + +# Log uploader (from 30860 to 30889) + +msgctxt "#30860" +msgid "This selection triggered an unexpected error, would you like to share your error log so that we can try to resolve this issue?" +msgstr "" + +msgctxt "#30861" +msgid "Share us the URL URL_TO_REPLACE by email directly via the QRcode or at catch.up.tv.and.more@gmail.com or in an issue on GitHub. Thank you!" +msgstr "" + +msgctxt "#30862" +msgid "Sorry, sharing your error log failed" +msgstr "" + + +# HTTP error (from 30890 to 30919) + +msgctxt "#30890" +msgid "HTTP Error code" +msgstr "" + +msgctxt "#30891" +msgid "Inaccessible content" +msgstr "" + +msgctxt "#30892" +msgid "Authentication is required to access the resource" +msgstr "" + +msgctxt "#30893" +msgid "Access denied (Geographical restriction?)" +msgstr "" + +msgctxt "#30894" +msgid "This resource requires a payment or a subscription" +msgstr "" + +msgctxt "#30895" +msgid "This resource is no longer available" +msgstr "" + +msgctxt "#30896" +msgid "No item" +msgstr "" diff --git a/plugin.video.catchuptvandmore/resources/language/resource.language.fr_fr/strings.po b/plugin.video.catchuptvandmore/resources/language/resource.language.fr_fr/strings.po new file mode 100644 index 0000000000..0012165921 --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/language/resource.language.fr_fr/strings.po @@ -0,0 +1,961 @@ +# Kodi Media Center language file +# Addon Name: Catch-up TV & More +# Addon id: plugin.video.catchuptvandmore +# Addon Provider: SylvainCecchetto +msgid "" +msgstr "" +"Language: fr\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +# Settings tabs/categories (from 30000 to 30019) + +msgctxt "#30000" +msgid "" +msgstr "" + +msgctxt "#30001" +msgid "" +msgstr "" + +msgctxt "#30002" +msgid "Quality and content" +msgstr "Qualité et Contenu" + +msgctxt "#30003" +msgid "Downloads" +msgstr "Téléchargements" + +msgctxt "#30004" +msgid "Accounts" +msgstr "Comptes" + +msgctxt "#30005" +msgid "Advanced settings" +msgstr "Paramètres avancés" + +msgctxt "#30006" +msgid "Channels" +msgstr "Chaînes" + +msgctxt "#30007" +msgid "" +msgstr "" + +msgctxt "#30008" +msgid "" +msgstr "" + +msgctxt "#30009" +msgid "General" +msgstr "Général" + + +# Settings line separators (from 30010 to 30029) + +msgctxt "#30010" +msgid "Hide main menu categories" +msgstr "Masquer des catégories du menu principal" + +msgctxt "#30011" +msgid "Hide channels" +msgstr "Masquer des chaînes" + +msgctxt "#30012" +msgid "YTDL settings" +msgstr "Paramètres YTDL" + +msgctxt "#30013" +msgid "Hide countries" +msgstr "Masquer des pays" + +msgctxt "#30014" +msgid "Hide websites" +msgstr "Masquer des sites internet" + +msgctxt "#30015" +msgid "Hide live TV countries" +msgstr "Masquer des pays pour la TV en direct" + +msgctxt "#30016" +msgid "Hide catch-up TV countries" +msgstr "Masquer des pays de la télévision de rattrapage" + +msgctxt "#30017" +msgid "Default values" +msgstr "Valeurs par défaut" + +msgctxt "#30018" +msgid "Unmask main menus items" +msgstr "Démasquer les éléments des menus principaux" + +msgctxt "#30019" +msgid "Unmask live TV channels" +msgstr "Démasquer les chaînes de la TV en direct" + +msgctxt "#30020" +msgid "Unmask replay TV channels" +msgstr "Démasquer les chaînes de la télévision de rattrapage" + + +# Main menu (from 30030 to 30049) + +msgctxt "#30030" +msgid "Live TV" +msgstr "TV en direct" + +msgctxt "#30031" +msgid "Catch-up TV" +msgstr "Télévision de rattrapage" + +msgctxt "#30032" +msgid "Websites" +msgstr "Sites internet" + +msgctxt "#30033" +msgid "Favourites" +msgstr "Favoris" + +msgctxt "#30034" +msgid "Internet service providers" +msgstr "Fournisseurs d'accès internet" + + +# Countries (from 30050 to 30079) + +msgctxt "#30050" +msgid "France" +msgstr "France" + +msgctxt "#30051" +msgid "Switzerland" +msgstr "Suisse" + +msgctxt "#30052" +msgid "United Kingdom" +msgstr "Royaume-Uni" + +msgctxt "#30053" +msgid "International" +msgstr "International" + +msgctxt "#30054" +msgid "Belgium" +msgstr "Belgique" + +msgctxt "#30055" +msgid "Japan" +msgstr "Japon" + +msgctxt "#30056" +msgid "Canada" +msgstr "Canada" + +msgctxt "#30057" +msgid "United States of America" +msgstr "États-Unis d'Amérique" + +msgctxt "#30058" +msgid "Poland" +msgstr "Pologne" + +msgctxt "#30059" +msgid "Spain" +msgstr "Espagne" + +msgctxt "#30060" +msgid "Tunisia" +msgstr "Tunisie" + +msgctxt "#30061" +msgid "Italia" +msgstr "Italie" + +msgctxt "#30062" +msgid "Netherlands" +msgstr "Pays-Bas" + +msgctxt "#30063" +msgid "China" +msgstr "Chine" + +msgctxt "#30064" +msgid "Cameroon" +msgstr "Cameroun" + +msgctxt "#30065" +msgid "Slovenia" +msgstr "Slovénie" + +msgctxt "#30066" +msgid "Ethiopia" +msgstr "Ethiopie" + +msgctxt "#30067" +msgid "Morocco" +msgstr "Maroc" + +msgctxt "#30068" +msgid "Singapore" +msgstr "Singapour" + +msgctxt "#30069" +msgid "Lituania" +msgstr "Lituanie" + +msgctxt "#30070" +msgid "Turkey" +msgstr "Turquie" + +msgctxt "#30071" +msgid "Slovakia" +msgstr "Slovaquie" + +msgctxt "#30072" +msgid "Greece" +msgstr "Grèce" + +msgctxt "#30073" +msgid "Peru" +msgstr "Pérou" + +msgctxt "#30074" +msgid "Venezuela" +msgstr "Venezuela" + +msgctxt "#30075" +msgid "Luxembourg" +msgstr "Luxembourg" + +msgctxt "#30076" +msgid "Bolivia" +msgstr "Bolivie" + +# Channels (from 30080 to 30129) + +msgctxt "#30080" +msgid "French channels" +msgstr "Chaînes françaises" + +msgctxt "#30081" +msgid "Belgian channels" +msgstr "Chaînes belges" + +msgctxt "#30082" +msgid "Japanese channels" +msgstr "Chaînes japonaises" + +msgctxt "#30083" +msgid "Switzerland channels" +msgstr "Chaînes suisses" + +msgctxt "#30084" +msgid "United Kingdom channels" +msgstr "Chaînes anglaises" + +msgctxt "#30085" +msgid "International channels" +msgstr "Chaînes internationales" + +msgctxt "#30086" +msgid "Canadian channels" +msgstr "Chaînes canadiennes" + +msgctxt "#30087" +msgid "United States channels" +msgstr "Chaînes américaines" + +msgctxt "#30088" +msgid "Polish channels" +msgstr "Chaînes polonaises" + +msgctxt "#30089" +msgid "Spanish channels" +msgstr "Chaînes espagnoles" + +msgctxt "#30090" +msgid "Tunisia channels" +msgstr "Chaînes tunisiennes" + +msgctxt "#30091" +msgid "Italian channels" +msgstr "Chaînes italiennes" + +msgctxt "#30092" +msgid "Dutch channels" +msgstr "Chaînes néerlandaises" + +msgctxt "#30093" +msgid "Chinese channels" +msgstr "Chaînes chinoises" + +msgctxt "#30094" +msgid "Cameroon channels" +msgstr "Chaînes Camerounaise" + +msgctxt "#30095" +msgid "Slovenian channels" +msgstr "Chaînes slovène" + +msgctxt "#30096" +msgid "Ethiopian channels" +msgstr "Chaînes ethiopiennes" + +msgctxt "#30097" +msgid "Moroccan channels" +msgstr "Chaînes marocaines" + +msgctxt "#30098" +msgid "Peruvian channels" +msgstr "Chaînes péruviennes" + +# Websites (from 30130 to 30149) + + + +# Quality and content (from 30150 to 30199) + +msgctxt "#30150" +msgid "Video quality" +msgstr "Qualité vidéo" + +msgctxt "#30151" +msgid "Device is L1 certified" +msgstr "Périph. certifié L1" + +msgctxt "#30152" +msgid "Contents" +msgstr "Contenus" + +msgctxt "#30153" +msgid "Arte: Choose Channel" +msgstr "Arte : Choix de la chaîne" + +msgctxt "#30154" +msgid "France 24: Choose Channel" +msgstr "France 24 : Choix de la chaîne" + +msgctxt "#30155" +msgid "Euronews: Choose Channel" +msgstr "Euronews : Choix de la chaîne" + +msgctxt "#30156" +msgid "MTV: Choose Channel" +msgstr "MTV : Choix de la chaîne" + +msgctxt "#30157" +msgid "DW: Choose Channel" +msgstr "DW : Choix de la chaîne" + +msgctxt "#30158" +msgid "France 3 Régions: Choose region" +msgstr "France 3 Régions : Choix de la région" + +msgctxt "#30159" +msgid "La 1ère: Choose region" +msgstr "La 1ère : Choix de la région" + +msgctxt "#30160" +msgid "Bein Sports: Choose Channel" +msgstr "Bein Sports : Choix de la chaîne" + +msgctxt "#30161" +msgid "QVC: Choose Channel" +msgstr "QVC : Choix de la chaîne" + +msgctxt "#30162" +msgid "NHK World: Choose Country" +msgstr "NHK World : Choix du pays" + +msgctxt "#30163" +msgid "CGTN: Choose Channel" +msgstr "CGTN : Choix de la chaîne" + +msgctxt "#30164" +msgid "ICI Télé: Choose region" +msgstr "ICI Télé : Choix de la région" + +msgctxt "#30165" +msgid "Realmadrid TV: Choose Channel" +msgstr "Realmadrid TV : Choix de la chaîne" + +msgctxt "#30166" +msgid "RT: Choose Channel" +msgstr "RT : Choix de la chaîne" + +msgctxt "#30167" +msgid "TVP 3: Choose region" +msgstr "TVP 3 : Choix de la région" + +msgctxt "#30168" +msgid "Enable TV guide (slower)" +msgstr "Activer le programme TV (plus lent)" + +msgctxt "#30169" +msgid "Enable Subtitle if present in the content" +msgstr "Activer les sous-titres si présent dans le contenu" + +msgctxt "#30170" +msgid "CBC: Choose region" +msgstr "CBC : Choix de la région" + +msgctxt "#30171" +msgid "BFM Régions: Choose region" +msgstr "BFM Régions: Choix de la région" + +msgctxt "#30173" +msgid "TV5 monde plus: Choose language" +msgstr "TV5 monde plus: Choix de la langue" + +msgctxt "#30174" +msgid "Choose live channel" +msgstr "Choix du direct" + +msgctxt "#30175" +msgid "Equidia Racing: Choose race" +msgstr "Equidia Racing: Choix de la course" + +msgctxt "#30177" +msgid "TV5 monde: Choose region" +msgstr "TV5 monde: Choix de la région" + +msgctxt "#30178" +msgid "France, Belgium, Switzerland" +msgstr "France, Begique, Suisse" + +msgctxt "#30179" +msgid "Europe, outside FBS" +msgstr "Europe, hors FBS" + +msgctxt "#30180" +msgid "Video quality" +msgstr "Qualité vidéo" + +msgctxt "#30181" +msgid "Choose version" +msgstr "Choix de la version" + +msgctxt "#3082" +msgid "France 3 Régions: Choose region" +msgstr "France 3 Régions : Choix de la sous-région" + +msgctxt "#30192" +msgid "BEST" +msgstr "MAXIMALE" + +msgctxt "#30193" +msgid "DEFAULT" +msgstr "DEFAUT" + +msgctxt "#30194" +msgid "DIALOG" +msgstr "DIALOGUE" + +msgctxt "#30195" +msgid "WORST" +msgstr "MINIMALE" + +msgctxt "#30196" +msgid "Maximum stream bitrate [COLOR=gray](0=disabled)[/COLOR]" +msgstr "Débit maximal des flux [COLOR=gray](0=désactivé)[/COLOR]" + +msgctxt "#30197" +msgid "Settings - InputStream adaptive" +msgstr "Paramètres - InputStream adaptive" + +msgctxt "#30198" +msgid "Use inputstream HLS?" +msgstr "Utiliser inputstream HLS ?" + +msgctxt "#30199" +msgid "Use ytdl for stream?" +msgstr "Utiliser ytdl pour le flux ?" + +# Download (from 30200 to 30239) + +msgctxt "#30200" +msgid "Download directory" +msgstr "Répertoire de téléchargement" + +msgctxt "#30201" +msgid "Quality of the video to download" +msgstr "Qualité de la vidéo à télécharger" + +msgctxt "#30202" +msgid "Download in background" +msgstr "Télécharger en arrière plan" + +msgctxt "#30203" +msgid "Automatically rename the video (download directory required)" +msgstr "Renommer automatiquement la vidéo (répertoire de téléchargement requis)" + + +# Accounts (from 30240 to 30269) + +msgctxt "#30242" +msgid "VRT NU Login" +msgstr "Login VRT NU" + +msgctxt "#30243" +msgid "VRT NU Password" +msgstr "Mot de passe VRT NU" + +msgctxt "#30244" +msgid "6play Login" +msgstr "Login 6play" + +msgctxt "#30245" +msgid "6play Password" +msgstr "Mot de passe 6play" + +msgctxt "#30246" +msgid "RTLplay Login" +msgstr "Login RTLplay" + +msgctxt "#30247" +msgid "RTLplay Password" +msgstr "Mot de passe RTLplay" + +msgctxt "#30248" +msgid "ABWeb Login" +msgstr "Login ABWeb" + +msgctxt "#30249" +msgid "ABWeb Password" +msgstr "Mot de passe ABWeb" + +msgctxt "#30250" +msgid "UKTVPlay Login" +msgstr "Login UKTVPlay" + +msgctxt "#30251" +msgid "UKTVPlay Password" +msgstr "Mot de passe UKTVPlay" + +msgctxt "#30252" +msgid "RMCBFM Play Login" +msgstr "Login RMCBFM Play" + +msgctxt "#30253" +msgid "RMCBFM Play Password" +msgstr "Mot de passe RMCBFM Play" + +msgctxt "#30254" +msgid "TV guides" +msgstr "Guides TV" + +msgctxt "#30255" +msgid "Schedules Direct login" +msgstr "Login Schedules Direct" + +msgctxt "#30256" +msgid "Schedules Direct password" +msgstr "Mot de passe Schedules Direct" + +msgctxt "#30257" +msgid "Schedules Direct lineup" +msgstr "Lineup Schedules Direct" + +msgctxt "#30258" +msgid "RTBF auvio Login" +msgstr "Login RTBF auvio" + +msgctxt "#30259" +msgid "RTBF auvio Password" +msgstr "Mot de passe RTBF auvio" + +msgctxt "#30260" +msgid "TF1+ Login" +msgstr "Identifiant TF1+" + +msgctxt "#30261" +msgid "TF1+ Password" +msgstr "Mot de passe TF1+" + +msgctxt "#30262" +msgid "SFR TV login" +msgstr "Login SFR TV" + +msgctxt "#30263" +msgid "SFR TV password" +msgstr "Mot de passe SFR TV" + + +# TV integration (from 30270 to 30289) + +msgctxt "#30270" +msgid "TV integration" +msgstr "Intégration TV" + +msgctxt "#30271" +msgid "Kodi Live TV integration" +msgstr "Intégration de la TV en direct dans Kodi" + +msgctxt "#30272" +msgid "Install IPTV Manager add-on" +msgstr "Installer le plugin IPTV Manager" + +msgctxt "#30273" +msgid "Enable Kodi Live TV integration" +msgstr "Activer la TV en direct dans Kodi" + +msgctxt "#30274" +msgid "Open IPTV Manager settings" +msgstr "Ouvrir les paramètres de IPTV Manager" + +msgctxt "#30275" +msgid "Select channels to enable" +msgstr "Sélectionner les chaînes TV à activer" + +msgctxt "#30276" +msgid "Failed to save TV integration settings" +msgstr "Échec lors de la sauvegarde des paramètres de l'intégration TV" + +msgctxt "#30277" +msgid "Select channels to enable in Kodi Live TV" +msgstr "Sélectionner les chaînes à activer dans la section TV de Kodi" + + + +# Reserved space for other tab/category in settings (from 30290 to 30339) + + +# Advanced settings (from 30370 to 30399) + +msgctxt "#30370" +msgid "Clear cache" +msgstr "Vider le cache" + +msgctxt "#30371" +msgid "Cache cleared" +msgstr "Cache vidé" + +msgctxt "#30372" +msgid "Send Kodi log to the developers when an error occurred" +msgstr "Envoyer le log de Kodi aux développeurs en cas d'erreur" + +msgctxt "#30373" +msgid "Delete favourites" +msgstr "Supprimer les favoris" + +msgctxt "#30374" +msgid "Favourites deleted" +msgstr "Favoris supprimés" + + +# Settings of 'General' category (from 30400 to 30429) + +msgctxt "#30400" +msgid "Restore default order of all menus" +msgstr "Restaurer l'ordre par défaut de tous les menus" + +msgctxt "#30401" +msgid "Unmask all hidden items" +msgstr "Démasquez tous les éléments cachés" + +msgctxt "#30402" +msgid "Select items to unmask" +msgstr "Sélectionner les éléments à démasquer" + +msgctxt "#30403" +msgid "Menu items" +msgstr "Éléments des menus" + +msgctxt "#30404" +msgid "" +msgstr "" + +msgctxt "#30405" +msgid "" +msgstr "" + +msgctxt "#30406" +msgid "" +msgstr "" + +msgctxt "#30407" +msgid "Default order of all menus have been restored" +msgstr "L'ordre par défaut de tous les menu a été restauré" + +msgctxt "#30408" +msgid "All hidden items have been unmasked" +msgstr "Tous les éléments masqués ont été démasqué" + + +# Advanced settings (from 30430 to 30449) + +msgctxt "#30430" +msgid "Automatically start Catch-up TV & More when Kodi starts" +msgstr "Lancer automatiquement Catch-up TV & More au démarrage de Kodi" + + +# Reserved space for other tab/category in settins (from 30450 to 30499) + + + +# Context menu (from 30500 to 30599) + +msgctxt "#30500" +msgid "Move down" +msgstr "Descendre" + +msgctxt "#30501" +msgid "Move up" +msgstr "Monter" + +msgctxt "#30502" +msgid "Hide" +msgstr "Masquer" + +msgctxt "#30503" +msgid "Download" +msgstr "Télécharger" + +msgctxt "#30504" +msgid "List Videos (type=extrait)" +msgstr "Liste des Videos (type=extrait)" + +msgctxt "#30505" +msgid "List Videos (type=episode)" +msgstr "Liste des Videos (type=episode)" + + +# Dialog boxes (from 30600 to 30699) + +msgctxt "#30600" +msgid "Information" +msgstr "Information" + +msgctxt "#30601" +msgid "To re-enable hidden items go to the plugin settings" +msgstr "Pour réactiver les éléments masqués rendez-vous dans les réglages du plugin" + +msgctxt "#30602" +msgid "This content is DRM protected. To be able to play it we invite you to update Kodi to the version 18 or above. Thank you for your understanding." +msgstr "Ce contenu est protégé par DRM. Pour pouvoir le lire nous vous invitons à mettre à jour Kodi à la version 18 ou supérieure. Merci pour votre compréhension" + +msgctxt "#30603" +msgid "This content is DRM protected. The download mode doesn't work for this kind of protection. Thank you for your understanding." +msgstr "Ce contenu est protégé par DRM. Le mode téléchargement ne fonctionne pas pour un contenu protégé par DRM. Merci pour votre compréhension" + +msgctxt "#30604" +msgid "Access to this content requires an account %s. You can provide your credentials for it in the settings of this add-on. If you don't have an account, you can create one at this url %s." +msgstr "Accéder à ce contenu nécessite un compte %s. Vous pouvez configurer votre identifiant et votre mot de passe dans la configuration de cette extension. Si vous n'avez pas de compte, vous pouvez le créer à cette URL %s." + +msgctxt "#30605" +msgid "You can now use Kodi's TV feature to watch live TV channels from Catch-up TV & More, follow the tutorial at https://catch-up-tv-and-more.github.io/live_tv_installation/." +msgstr "Vous pouvez maintenant utiliser la fonctionnalité TV de Kodi pour regarder les chaînes de TV en direct de Catch-up TV & More, suivez le tutoriel à l'adresse https://catch-up-tv-and-more.github.io/fr/live_tv_installation/." + +msgctxt "#30606" +msgid "Do you want to see this information next time?" +msgstr "Voulez-vous voir cette information la prochaine fois ?" + + +# Others (from 30700 to 30799) + +msgctxt "#30700" +msgid "More videos..." +msgstr "Plus de vidéos..." + +msgctxt "#30701" +msgid "All videos" +msgstr "Toutes les vidéos" + +msgctxt "#30702" +msgid "DRM protected video" +msgstr "Vidéo protégée par DRM" + +msgctxt "#30703" +msgid "Search" +msgstr "Rechercher" + +msgctxt "#30704" +msgid "Last videos" +msgstr "Dernières vidéos" + +msgctxt "#30705" +msgid "From A to Z" +msgstr "De A à Z" + +msgctxt "#30706" +msgid "Ascending" +msgstr "Ascendant" + +msgctxt "#30707" +msgid "Descending" +msgstr "Descendant" + +msgctxt "#30708" +msgid "More programs..." +msgstr "Plus de programmes..." + +msgctxt "#30709" +msgid "Choose video quality" +msgstr "Choisir la qualité vidéo" + +msgctxt "#30710" +msgid "Video stream no longer exists" +msgstr "Le flux vidéo n'existe plus" + +msgctxt "#30711" +msgid "Authentication failed" +msgstr "Echec de l'authentification" + +msgctxt "#30712" +msgid "Video with an account needed" +msgstr "Vidéo avec un compte requis" + +msgctxt "#30713" +msgid "Geo-blocked video" +msgstr "Vidéo géo-bloquée" + +msgctxt "#30714" +msgid "Search videos" +msgstr "Rechercher des vidéos" + +msgctxt "#30715" +msgid "Search programs" +msgstr "Rechercher des programmes" + +msgctxt "#30716" +msgid "Video stream is not available" +msgstr "Le flux vidéo n'est pas disponible" + +msgctxt "#30717" +msgid "All programs" +msgstr "Tous les programmes" + +msgctxt "#30718" +msgid "No videos found" +msgstr "Aucune vidéo trouvée" + +msgctxt "#30719" +msgid "Inputstream Adaptive not enabled" +msgstr "Inputstream Adaptive n'est pas activé" + +msgctxt "#30720" +msgid "Kodi 17.6 or > is required" +msgstr "Kodi 17.6 ou > est requis" + +msgctxt "#30721" +msgid "Content requiring a subscription" +msgstr "Contenu nécessitant un abonnement" + +msgctxt "#30723" +msgid "An error occurred while getting TV guide" +msgstr "Une erreur s'est produite lors de l'obtention du guide TV" + +msgctxt "#30724" +msgid "Failed to guess your country based on your IP" +msgstr "Impossible de deviner votre pays en fonction de votre adresse IP" + +msgctxt "#30725" +msgid "Categories" +msgstr "Catégories" + +msgctxt "#30726" +msgid "Races" +msgstr "Courses" + +msgctxt "#30727" +msgid "Most popular" +msgstr "Les plus populaires" + +msgctxt "#30728" +msgid "Recently added" +msgstr "Ajouté récemment" + +msgctxt "#30729" +msgid "Emissions" +msgstr "Émissions" + +msgctxt "#30731" +msgid "Homepage" +msgstr "Accueil" + +msgctxt "#30732" +msgid "This content requires a payment. To buy or rent it, go to %s website at this URL : %s" +msgstr "Ce contenu est payant. Pour l'acheter ou le louer, rendez-vous sur le site %s à cette URL : %s" + + +# Favourites (from 30800 to 30859) + +msgctxt "#30800" +msgid "Add to add-on favourites" +msgstr "Ajouter aux favoris du plugin" + +msgctxt "#30801" +msgid "Favourite name" +msgstr "Nom du favori" + +msgctxt "#30802" +msgid "Remove" +msgstr "Supprimer" + +msgctxt "#30803" +msgid "Favourites" +msgstr "Favoris" + +msgctxt "#30804" +msgid "Rename" +msgstr "Renommer" + +msgctxt "#30805" +msgid "The item has been added to the favourites of the add-on" +msgstr "L'élément a été ajouté à aux favoris de l'extension" + +msgctxt "#30806" +msgid "Add at least one favourite to access this menu" +msgstr "Ajoutez au moins un favori pour accéder à ce menu" + +msgctxt "#30807" +msgid "This favourite raises an error, would you like to delete it?" +msgstr "Ce favori déclenche une erreur, souhaitez vous le supprimer ?" + + +# Log uploader (from 30860 to 30889) + +msgctxt "#30860" +msgid "This selection triggered an unexpected error, would you like to share your error log so that we can try to resolve this issue?" +msgstr "Cette sélection a déclenché une erreur inattendue, souhaitez-vous nous partager votre journal d'erreurs afin que nous puissions essayer de résoudre ce problème ?" + +msgctxt "#30861" +msgid "Share us the URL URL_TO_REPLACE by email directly via the QRcode or at catch.up.tv.and.more@gmail.com or in an issue on GitHub. Thank you!" +msgstr "Partagez nous l'URL URL_TO_REPLACE par mail directement via le QR code ou bien à catch.up.tv.and.more@gmail.com ou encore dans un ticket sur GitHub. Merci !" + +msgctxt "#30862" +msgid "Sorry, sharing your error log failed" +msgstr "Désolé, le partage de votre journal d'erreurs a échoué" + + +# HTTP error (from 30890 to 30919) + +msgctxt "#30890" +msgid "HTTP Error code" +msgstr "Erreur HTTP code" + +msgctxt "#30891" +msgid "Inaccessible content" +msgstr "Contenu inaccessible" + +msgctxt "#30892" +msgid "Authentication is required to access the resource" +msgstr "Une authentification est requise pour accéder à cette ressource" + +msgctxt "#30893" +msgid "Access denied (Geographical restriction?)" +msgstr "Accès refusé (restriction géographique ?)" + +msgctxt "#30894" +msgid "This resource requires a payment or a subscription" +msgstr "Cette ressource requiert un paiement ou un abonnement" + +msgctxt "#30895" +msgid "This resource is no longer available" +msgstr "Cette ressource n'est plus disponible" + +msgctxt "#30896" +msgid "No item" +msgstr "Aucun élément" diff --git a/plugin.video.catchuptvandmore/resources/language/resource.language.he_il/strings.po b/plugin.video.catchuptvandmore/resources/language/resource.language.he_il/strings.po new file mode 100644 index 0000000000..c7e3ef085e --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/language/resource.language.he_il/strings.po @@ -0,0 +1,958 @@ +# Kodi Media Center language file +# Addon Name: Catch-up TV & More +# Addon id: plugin.video.catchuptvandmore +# Addon Provider: SylvainCecchetto +msgid "" +msgstr "" +"Language: he\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +# Settings tabs/categories (from 30000 to 30019) + +msgctxt "#30000" +msgid "" +msgstr "" + +msgctxt "#30001" +msgid "" +msgstr "" + +msgctxt "#30002" +msgid "Quality and content" +msgstr "איכות ותוכן" + +msgctxt "#30003" +msgid "Downloads" +msgstr "הורד" + +msgctxt "#30004" +msgid "Accounts" +msgstr "חשבונות" + +msgctxt "#30005" +msgid "Advanced settings" +msgstr "" + +msgctxt "#30006" +msgid "Channels" +msgstr "ערוצים" + +msgctxt "#30007" +msgid "" +msgstr "" + +msgctxt "#30008" +msgid "" +msgstr "" + +msgctxt "#30009" +msgid "General" +msgstr "" + + +# Settings line separators (from 30010 to 30029) + +msgctxt "#30010" +msgid "Hide main menu categories" +msgstr "הסתר קטגוריות בתפריט הראשי" + +msgctxt "#30011" +msgid "Hide channels" +msgstr "" + +msgctxt "#30012" +msgid "YTDL settings" +msgstr "" + +msgctxt "#30013" +msgid "Hide countries" +msgstr "" + +msgctxt "#30014" +msgid "Hide websites" +msgstr "" + +msgctxt "#30015" +msgid "Hide live TV countries" +msgstr "" + +msgctxt "#30016" +msgid "Hide catch-up TV countries" +msgstr "" + +msgctxt "#30017" +msgid "Default values" +msgstr "" + +msgctxt "#30018" +msgid "Unmask main menus items" +msgstr "" + +msgctxt "#30019" +msgid "Unmask live TV channels" +msgstr "" + +msgctxt "#30020" +msgid "Unmask replay TV channels" +msgstr "" + + +# Main menu (from 30030 to 30049) + +msgctxt "#30030" +msgid "Live TV" +msgstr "טלוויזיה חיה" + +msgctxt "#30031" +msgid "Catch-up TV" +msgstr "" + +msgctxt "#30032" +msgid "Websites" +msgstr "אתרי רשת" + +msgctxt "#30033" +msgid "Favourites" +msgstr "" + + +# Countries (from 30050 to 30079) + +msgctxt "#30050" +msgid "France" +msgstr "צָרְפַת" + +msgctxt "#30051" +msgid "Switzerland" +msgstr "שְׁוֵיצָרִי" + +msgctxt "#30052" +msgid "United Kingdom" +msgstr "בְּרִיטַנִיָה" + +msgctxt "#30053" +msgid "International" +msgstr "בינלאומי" + +msgctxt "#30054" +msgid "Belgium" +msgstr "בלגיה" + +msgctxt "#30055" +msgid "Japan" +msgstr "יפן" + +msgctxt "#30056" +msgid "Canada" +msgstr "קנדה" + +msgctxt "#30057" +msgid "United States of America" +msgstr "ארצות הברית" + +msgctxt "#30058" +msgid "Poland" +msgstr "פּוֹלִין" + +msgctxt "#30059" +msgid "Spain" +msgstr "סְפָרַד" + +msgctxt "#30060" +msgid "Tunisia" +msgstr "תוניסיה" + +msgctxt "#30061" +msgid "Italia" +msgstr "אִיטַלִיָה" + +msgctxt "#30062" +msgid "Netherlands" +msgstr "הולנד" + +msgctxt "#30063" +msgid "China" +msgstr "חרסינה" + +msgctxt "#30064" +msgid "Cameroon" +msgstr "קמרון" + +msgctxt "#30065" +msgid "Slovenia" +msgstr "סלובניה" + +msgctxt "#30066" +msgid "Ethiopia" +msgstr "סלובניה" + +msgctxt "#30067" +msgid "Morocco" +msgstr "מָרוֹקוֹ" + +msgctxt "#30068" +msgid "Singapore" +msgstr "סינגפור" + +msgctxt "#30069" +msgid "Lituania" +msgstr "ליטא" + +msgctxt "#30070" +msgid "Turkey" +msgstr "טורקיה" + +msgctxt "#30071" +msgid "Slovakia" +msgstr "סלובקיה" + +msgctxt "#30072" +msgid "Greece" +msgstr "יָוָן" + +msgctxt "#30073" +msgid "Peru" +msgstr "פרו" + +msgctxt "#30074" +msgid "Venezuela" +msgstr "ונצואלה" + +msgctxt "#30075" +msgid "Luxembourg" +msgstr "לוקסמבורג" + +msgctxt "#30076" +msgid "Bolivia" +msgstr "בוליביה" + +# Channels (from 30080 to 30129) + +msgctxt "#30080" +msgid "French channels" +msgstr "ערוצים צרפתיים" + +msgctxt "#30081" +msgid "Belgian channels" +msgstr "ערוצים בלגיים" + +msgctxt "#30082" +msgid "Japanese channels" +msgstr "ערוצים יפניים" + +msgctxt "#30083" +msgid "Switzerland channels" +msgstr "" + +msgctxt "#30084" +msgid "United Kingdom channels" +msgstr "ערוצים אנגליים" + +msgctxt "#30085" +msgid "International channels" +msgstr "" + +msgctxt "#30086" +msgid "Canadian channels" +msgstr "" + +msgctxt "#30087" +msgid "United States channels" +msgstr "" + +msgctxt "#30088" +msgid "Polish channels" +msgstr "" + +msgctxt "#30089" +msgid "Spanish channels" +msgstr "" + +msgctxt "#30090" +msgid "Tunisia channels" +msgstr "" + +msgctxt "#30091" +msgid "Italian channels" +msgstr "" + +msgctxt "#30092" +msgid "Dutch channels" +msgstr "" + +msgctxt "#30093" +msgid "Chinese channels" +msgstr "" + +msgctxt "#30094" +msgid "Cameroon channels" +msgstr "" + +msgctxt "#30095" +msgid "Slovenian channels" +msgstr "" + +msgctxt "#30096" +msgid "Ethiopian channels" +msgstr "" + +msgctxt "#30097" +msgid "Moroccan channels" +msgstr "" + +msgctxt "#30097" +msgid "Peruvian channels" +msgstr "" + +# Websites (from 30130 to 30149) + + + +# Quality and content (from 30150 to 30199) + +msgctxt "#30150" +msgid "Video quality" +msgstr "איכות וידאו מיו-טיוב" + +msgctxt "#30151" +msgid "Device is L1 certified" +msgstr "" + +msgctxt "#30152" +msgid "Contents" +msgstr "תוכן העניינים" + +msgctxt "#30153" +msgid "Arte: Choose Channel" +msgstr "Arte: בחר ערוץ" + +msgctxt "#30154" +msgid "France 24: Choose Channel" +msgstr "France 24: בחר ערוץ" + +msgctxt "#30155" +msgid "Euronews: Choose Channel" +msgstr "Euronews: בחר ערוץ" + +msgctxt "#30156" +msgid "MTV: Choose Channel" +msgstr "MTV: בחר ערוץ" + +msgctxt "#30157" +msgid "DW: Choose Channel" +msgstr "DW: בחר ערוץ" + +msgctxt "#30158" +msgid "France 3 Régions: Choose region" +msgstr "" + +msgctxt "#30159" +msgid "La 1ère: Choose region" +msgstr "" + +msgctxt "#30160" +msgid "Bein Sports: Choose Channel" +msgstr "Bein Sports : בחר ערוץ" + +msgctxt "#30161" +msgid "QVC: Choose Channel" +msgstr "QVC : בחר ערוץ" + +msgctxt "#30162" +msgid "NHK World: Choose Country" +msgstr "" + +msgctxt "#30163" +msgid "CGTN: Choose Channel" +msgstr "CGTN : בחר ערוץ" + +msgctxt "#30164" +msgid "ICI Télé: Choose region" +msgstr "" + +msgctxt "#30165" +msgid "Realmadrid TV: Choose Channel" +msgstr "Realmadrid TV : בחר ערוץ" + +msgctxt "#30166" +msgid "RT: Choose Channel" +msgstr "" + +msgctxt "#30167" +msgid "TVP 3: Choose region" +msgstr "" + +msgctxt "#30168" +msgid "Enable TV guide (slower)" +msgstr "" + +msgctxt "#30169" +msgid "Enable Subtitle if present in the content" +msgstr "" + +msgctxt "#30170" +msgid "CBC: Choose region" +msgstr "" + +msgctxt "#30171" +msgid "BFM Régions: Choose region" +msgstr "" + +msgctxt "#30173" +msgid "TV5 monde plus: Choose language" +msgstr "" + +msgctxt "#30174" +msgid "Choose live channel" +msgstr "" + +msgctxt "#30175" +msgid "Equidia Racing: Choose race" +msgstr "" + +msgctxt "#30177" +msgid "TV5 monde: Choose region" +msgstr "" + +msgctxt "#30178" +msgid "France, Belgium, Switzerland" +msgstr "" + +msgctxt "#30179" +msgid "Europe, outside FBS" +msgstr "" + +msgctxt "#30180" +msgid "Video quality" +msgstr "" + +msgctxt "#30181" +msgid "Choose version" +msgstr "" + +msgctxt "#30182" +msgid "France 3 Régions: Choose subregion" +msgstr "" + +msgctxt "#30192" +msgid "BEST" +msgstr "" + +msgctxt "#30193" +msgid "DEFAULT" +msgstr "" + +msgctxt "#30194" +msgid "DIALOG" +msgstr "" + +msgctxt "#30195" +msgid "WORST" +msgstr "" + +msgctxt "#30196" +msgid "Maximum stream bitrate [COLOR=gray](0=disabled)[/COLOR]" +msgstr "" + +msgctxt "#30197" +msgid "Settings - InputStream adaptive" +msgstr "" + +msgctxt "#30198" +msgid "Use inputstream HLS?" +msgstr "" + +msgctxt "#30199" +msgid "Use ytdl for stream?" +msgstr "" + +# Download (from 30200 to 30239) + +msgctxt "#30200" +msgid "Download directory" +msgstr "תיקיה להורדה" + +msgctxt "#30201" +msgid "Quality of the video to download" +msgstr "" + +msgctxt "#30202" +msgid "Download in background" +msgstr "" + +msgctxt "#30203" +msgid "Automatically rename the video (download directory required)" +msgstr "" + + +# Accounts (from 30240 to 30269) + +msgctxt "#30242" +msgid "VRT NU Login" +msgstr "" + +msgctxt "#30243" +msgid "VRT NU Password" +msgstr "" + +msgctxt "#30244" +msgid "6play Login" +msgstr "" + +msgctxt "#30245" +msgid "6play Password" +msgstr "" + +msgctxt "#30246" +msgid "RTLplay Login" +msgstr "" + +msgctxt "#30247" +msgid "RTLplay Password" +msgstr "" + +msgctxt "#30248" +msgid "ABWeb Login" +msgstr "" + +msgctxt "#30249" +msgid "ABWeb Password" +msgstr "" + +msgctxt "#30250" +msgid "UKTVPlay Login" +msgstr "" + +msgctxt "#30251" +msgid "UKTVPlay Password" +msgstr "" + +msgctxt "#30252" +msgid "RMCBFM Play Login" +msgstr "" + +msgctxt "#30253" +msgid "RMCBFM Play Password" +msgstr "" + +msgctxt "#30254" +msgid "TV guides" +msgstr "" + +msgctxt "#30255" +msgid "Schedules Direct login" +msgstr "" + +msgctxt "#30256" +msgid "Schedules Direct password" +msgstr "" + +msgctxt "#30257" +msgid "Schedules Direct lineup" +msgstr "" + +msgctxt "#30258" +msgid "RTBF auvio Login" +msgstr "" + +msgctxt "#30259" +msgid "RTBF auvio Password" +msgstr "" + +msgctxt "#30260" +msgid "TF1+ Login" +msgstr "" + +msgctxt "#30261" +msgid "TF1+ Password" +msgstr "" + +msgctxt "#30262" +msgid "SFR TV login" +msgstr "" + +msgctxt "#30263" +msgid "SFR TV password" +msgstr "" + + +# TV integration (from 30270 to 30289) + +msgctxt "#30270" +msgid "TV integration" +msgstr "" + +msgctxt "#30271" +msgid "Kodi Live TV integration" +msgstr "" + +msgctxt "#30272" +msgid "Install IPTV Manager add-on" +msgstr "" + +msgctxt "#30273" +msgid "Enable Kodi Live TV integration" +msgstr "" + +msgctxt "#30274" +msgid "Open IPTV Manager settings" +msgstr "" + +msgctxt "#30275" +msgid "Select channels to enable" +msgstr "" + +msgctxt "#30276" +msgid "Failed to save TV integration settings" +msgstr "" + +msgctxt "#30277" +msgid "Select channels to enable in Kodi Live TV" +msgstr "" + + + +# Reserved space for other tab/category in settings (from 30290 to 30339) + + + +# Advanced settings (from 30370 to 30399) + +msgctxt "#30370" +msgid "Clear cache" +msgstr "" + +msgctxt "#30371" +msgid "Cache cleared" +msgstr "" + +msgctxt "#30372" +msgid "Send Kodi log to the developers when an error occurred" +msgstr "" + +msgctxt "#30373" +msgid "Delete favourites" +msgstr "" + +msgctxt "#30374" +msgid "Favourites deleted" +msgstr "" + + +# Settings of 'General' category (from 30400 to 30429) + +msgctxt "#30400" +msgid "Restore default order of all menus" +msgstr "" + +msgctxt "#30401" +msgid "Unmask all hidden items" +msgstr "" + +msgctxt "#30402" +msgid "Select items to unmask" +msgstr "" + +msgctxt "#30403" +msgid "Menu items" +msgstr "" + +msgctxt "#30404" +msgid "" +msgstr "" + +msgctxt "#30405" +msgid "" +msgstr "" + +msgctxt "#30406" +msgid "" +msgstr "" + +msgctxt "#30407" +msgid "Default order of all menus have been restored" +msgstr "" + +msgctxt "#30408" +msgid "All hidden items have been unmasked" +msgstr "" + + +# Advanced settings (from 30430 to 30449) + +msgctxt "#30430" +msgid "Automatically start Catch-up TV & More when Kodi starts" +msgstr "" + + +# Reserved space for other tab/category in settins (from 30450 to 30499) + + + +# Context menu (from 30500 to 30599) + +msgctxt "#30500" +msgid "Move down" +msgstr "רד למטה" + +msgctxt "#30501" +msgid "Move up" +msgstr "עבור למעלה" + +msgctxt "#30502" +msgid "Hide" +msgstr "הסתר" + +msgctxt "#30503" +msgid "Download" +msgstr "הורד" + +msgctxt "#30504" +msgid "List Videos (type=extrait)" +msgstr "" + +msgctxt "#30505" +msgid "List Videos (type=episode)" +msgstr "" + + +# Dialog boxes (from 30600 to 30699) + +msgctxt "#30600" +msgid "Information" +msgstr "מידע" + +msgctxt "#30601" +msgid "To re-enable hidden items go to the plugin settings" +msgstr "כדי להפעיל מחדש את הפריטים המוסתרים, עבור אל ההגדרות של ההרחבה" + +msgctxt "#30602" +msgid "This content is DRM protected. To be able to play it we invite you to update Kodi to the version 18 or above. Thank you for your understanding." +msgstr "" + +msgctxt "#30603" +msgid "This content is DRM protected. The download mode doesn't work for this kind of protection. Thank you for your understanding." +msgstr "" + +msgctxt "#30604" +msgid "Access to this content requires an account %s. You can provide your credentials for it in the settings of this add-on. If you don't have an account, you can create one at this url %s." +msgstr "" + +msgctxt "#30605" +msgid "You can now use Kodi's TV feature to watch live TV channels from Catch-up TV & More, follow the tutorial at https://catch-up-tv-and-more.github.io/live_tv_installation/." +msgstr "" + +msgctxt "#30606" +msgid "Do you want to see this information next time?" +msgstr "" + + +# Others (from 30700 to 30799) + +msgctxt "#30700" +msgid "More videos..." +msgstr "קטעי וידאו נוספים..." + +msgctxt "#30701" +msgid "All videos" +msgstr "נגן את כל הסרטונים" + +msgctxt "#30702" +msgid "DRM protected video" +msgstr "וידאו מוגן DRM" + +msgctxt "#30703" +msgid "Search" +msgstr "חיפוש" + +msgctxt "#30704" +msgid "Last videos" +msgstr "קטעי וידאו אחרונים" + +msgctxt "#30705" +msgid "From A to Z" +msgstr "מ-א עד ת" + +msgctxt "#30706" +msgid "Ascending" +msgstr "בסדר עולה" + +msgctxt "#30707" +msgid "Descending" +msgstr "בסדר יורד" + +msgctxt "#30708" +msgid "More programs..." +msgstr "עוד תוכניות..." + +msgctxt "#30709" +msgid "Choose video quality" +msgstr "בחר איכות וידאו" + +msgctxt "#30710" +msgid "Video stream no longer exists" +msgstr "הזרמת הווידאו כבר לא קיימת" + +msgctxt "#30711" +msgid "Authentication failed" +msgstr "" + +msgctxt "#30712" +msgid "Video with an account needed" +msgstr "" + +msgctxt "#30713" +msgid "Geo-blocked video" +msgstr "" + +msgctxt "#30714" +msgid "Search videos" +msgstr "" + +msgctxt "#30715" +msgid "Search programs" +msgstr "" + +msgctxt "#30716" +msgid "Video stream is not available" +msgstr "" + +msgctxt "#30717" +msgid "All programs" +msgstr "" + +msgctxt "#30718" +msgid "No videos found" +msgstr "" + +msgctxt "#30719" +msgid "Inputstream Adaptive not enabled" +msgstr "" + +msgctxt "#30720" +msgid "Kodi 17.6 or > is required" +msgstr "" + +msgctxt "#30721" +msgid "Content requiring a subscription" +msgstr "" + +msgctxt "#30723" +msgid "An error occurred while getting TV guide" +msgstr "" + +msgctxt "#30724" +msgid "Failed to guess your country based on your IP" +msgstr "" + +msgctxt "#30725" +msgid "Categories" +msgstr "" + +msgctxt "#30726" +msgid "Races" +msgstr "" + +msgctxt "#30727" +msgid "Most popular" +msgstr "" + +msgctxt "#30728" +msgid "Recently added" +msgstr "" + +msgctxt "#30729" +msgid "Emissions" +msgstr "" + +msgctxt "#30731" +msgid "Homepage" +msgstr "" + +msgctxt "#30732" +msgid "This content requires a payment. To buy or rent it, go to %s website at this URL : %s" +msgstr "" + + +# Favourites (from 30800 to 30859) + +msgctxt "#30800" +msgid "Add to add-on favourites" +msgstr "" + +msgctxt "#30801" +msgid "Favourite name" +msgstr "" + +msgctxt "#30802" +msgid "Remove" +msgstr "" + +msgctxt "#30803" +msgid "Favourites" +msgstr "" + +msgctxt "#30804" +msgid "Rename" +msgstr "" + +msgctxt "#30805" +msgid "The item has been added to the favourites of the add-on" +msgstr "" + +msgctxt "#30806" +msgid "Add at least one favourite to access this menu" +msgstr "" + +msgctxt "#30807" +msgid "This favourite raises an error, would you like to delete it?" +msgstr "" + + +# Log uploader (from 30860 to 30889) + +msgctxt "#30860" +msgid "This selection triggered an unexpected error, would you like to share your error log so that we can try to resolve this issue?" +msgstr "" + +msgctxt "#30861" +msgid "Share us the URL URL_TO_REPLACE by email directly via the QRcode or at catch.up.tv.and.more@gmail.com or in an issue on GitHub. Thank you!" +msgstr "" + +msgctxt "#30862" +msgid "Sorry, sharing your error log failed" +msgstr "" + + +# HTTP error (from 30890 to 30919) + +msgctxt "#30890" +msgid "HTTP Error code" +msgstr "" + +msgctxt "#30891" +msgid "Inaccessible content" +msgstr "" + +msgctxt "#30892" +msgid "Authentication is required to access the resource" +msgstr "" + +msgctxt "#30893" +msgid "Access denied (Geographical restriction?)" +msgstr "" + +msgctxt "#30894" +msgid "This resource requires a payment or a subscription" +msgstr "" + +msgctxt "#30895" +msgid "This resource is no longer available" +msgstr "" + +msgctxt "#30896" +msgid "No item" +msgstr "" diff --git a/plugin.video.catchuptvandmore/resources/language/resource.language.nl_nl/strings.po b/plugin.video.catchuptvandmore/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..1c4a9148d7 --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,962 @@ +# Kodi Media Center language file +# Addon Name: Catch-up TV & More +# Addon id: plugin.video.catchuptvandmore +# Addon Provider: SylvainCecchetto +msgid "" +msgstr "" +"Language: nl\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + + +# Settings tabs/categories (from 30000 to 30019) + +msgctxt "#30000" +msgid "" +msgstr "" + +msgctxt "#30001" +msgid "" +msgstr "" + +msgctxt "#30002" +msgid "Quality and content" +msgstr "Kwaliteit en inhoud" + +msgctxt "#30003" +msgid "Downloads" +msgstr "Downloads" + +msgctxt "#30004" +msgid "Accounts" +msgstr "Accounts" + +msgctxt "#30005" +msgid "Advanced settings" +msgstr "Geavanceerde instellingen" + +msgctxt "#30006" +msgid "Channels" +msgstr "Kanalen" + +msgctxt "#30007" +msgid "" +msgstr "" + +msgctxt "#30008" +msgid "" +msgstr "" + +msgctxt "#30009" +msgid "General" +msgstr "Algemeen" + + +# Settings line separators (from 30010 to 30029) + +msgctxt "#30010" +msgid "Hide main menu categories" +msgstr "Verberg hoofdmenu categorieën" + +msgctxt "#30011" +msgid "Hide channels" +msgstr "Verberg kanalen" + +msgctxt "#30012" +msgid "YTDL settings" +msgstr "" + +msgctxt "#30013" +msgid "Hide countries" +msgstr "Verberg landen" + +msgctxt "#30014" +msgid "Hide websites" +msgstr "Verberg websites" + +msgctxt "#30015" +msgid "Hide live TV countries" +msgstr "Verberg live tv landen" + +msgctxt "#30016" +msgid "Hide catch-up TV countries" +msgstr "Verberg replay tv landen" + +msgctxt "#30017" +msgid "Default values" +msgstr "Standaardwaarden" + +msgctxt "#30018" +msgid "Unmask main menus items" +msgstr "" + +msgctxt "#30019" +msgid "Unmask live TV channels" +msgstr "" + +msgctxt "#30020" +msgid "Unmask replay TV channels" +msgstr "" + + +# Main menu (from 30030 to 30049) + +msgctxt "#30030" +msgid "Live TV" +msgstr "Live tv" + +msgctxt "#30031" +msgid "Catch-up TV" +msgstr "Replay tv" + +msgctxt "#30032" +msgid "Websites" +msgstr "Websites" + +msgctxt "#30033" +msgid "Favourites" +msgstr "Favorieten" + +msgctxt "#30034" +msgid "Internet service providers" +msgstr "Internetproviders" + + +# Countries (from 30050 to 30079) + +msgctxt "#30050" +msgid "France" +msgstr "Frankrijk" + +msgctxt "#30051" +msgid "Switzerland" +msgstr "Zwitserland" + +msgctxt "#30052" +msgid "United Kingdom" +msgstr "Groot-Brittannië" + +msgctxt "#30053" +msgid "International" +msgstr "Internationaal" + +msgctxt "#30054" +msgid "Belgium" +msgstr "België" + +msgctxt "#30055" +msgid "Japan" +msgstr "Japan" + +msgctxt "#30056" +msgid "Canada" +msgstr "Canada" + +msgctxt "#30057" +msgid "United States of America" +msgstr "Verenigde Staten" + +msgctxt "#30058" +msgid "Poland" +msgstr "Polen" + +msgctxt "#30059" +msgid "Spain" +msgstr "Spanje" + +msgctxt "#30060" +msgid "Tunisia" +msgstr "Tunesië" + +msgctxt "#30061" +msgid "Italia" +msgstr "Italië" + +msgctxt "#30062" +msgid "Netherlands" +msgstr "Nederland" + +msgctxt "#30063" +msgid "China" +msgstr "China" + +msgctxt "#30064" +msgid "Cameroon" +msgstr "Kameroen" + +msgctxt "#30065" +msgid "Slovenia" +msgstr "Slovenië" + +msgctxt "#30066" +msgid "Ethiopia" +msgstr "Ethiopië" + +msgctxt "#30067" +msgid "Morocco" +msgstr "Marokko" + +msgctxt "#30068" +msgid "Singapore" +msgstr "Singapore" + +msgctxt "#30069" +msgid "Lituania" +msgstr "Litouwen" + +msgctxt "#30070" +msgid "Turkey" +msgstr "Turkije" + +msgctxt "#30071" +msgid "Slovakia" +msgstr "Slowakije" + +msgctxt "#30072" +msgid "Greece" +msgstr "Griekenland" + +msgctxt "#30073" +msgid "Peru" +msgstr "Peru" + +msgctxt "#30074" +msgid "Venezuela" +msgstr "Venezuela" + +msgctxt "#30075" +msgid "Luxembourg" +msgstr "Luxemburgs" + +msgctxt "#30076" +msgid "Bolivia" +msgstr "Bolivia" + +# Channels (from 30080 to 30129) + +msgctxt "#30080" +msgid "French channels" +msgstr "Franse kanalen" + +msgctxt "#30081" +msgid "Belgian channels" +msgstr "Belgische kanalen" + +msgctxt "#30082" +msgid "Japanese channels" +msgstr "Japanse kanalen" + +msgctxt "#30083" +msgid "Switzerland channels" +msgstr "Zwitserse Kanalen" + +msgctxt "#30084" +msgid "United Kingdom channels" +msgstr "Engelse kanalen" + +msgctxt "#30085" +msgid "International channels" +msgstr "Internationale kanalen" + +msgctxt "#30086" +msgid "Canadian channels" +msgstr "Canadese kanalen" + +msgctxt "#30087" +msgid "United States channels" +msgstr "Amerikaanse kanalen" + +msgctxt "#30088" +msgid "Polish channels" +msgstr "Poolse kanalen" + +msgctxt "#30089" +msgid "Spanish channels" +msgstr "Spaanse kanalen" + +msgctxt "#30090" +msgid "Tunisia channels" +msgstr "Tunesische kanalen" + +msgctxt "#30091" +msgid "Italian channels" +msgstr "Italiaanse kanalen" + +msgctxt "#30092" +msgid "Dutch channels" +msgstr "Nederlandse kanalen" + +msgctxt "#30093" +msgid "Chinese channels" +msgstr "Chinese kanalen" + +msgctxt "#30094" +msgid "Cameroon channels" +msgstr "Kameroense kanalen" + +msgctxt "#30095" +msgid "Slovenian channels" +msgstr "Sloveense kanalen" + +msgctxt "#30096" +msgid "Ethiopian channels" +msgstr "Ethiopische kanalen" + +msgctxt "#30097" +msgid "Moroccan channels" +msgstr "Marokkaans kanalen" + +msgctxt "#30098" +msgid "Peruvian channels" +msgstr "Peruaans kanalen" + +# Websites (from 30130 to 30149) + + + +# Quality and content (from 30150 to 30199) + +msgctxt "#30150" +msgid "Video quality" +msgstr "Video kwaliteit" + +msgctxt "#30151" +msgid "Device is L1 certified" +msgstr "" + +msgctxt "#30152" +msgid "Contents" +msgstr "Inhoud" + +msgctxt "#30153" +msgid "Arte: Choose Channel" +msgstr "Arte: Kies kanaal" + +msgctxt "#30154" +msgid "France 24: Choose Channel" +msgstr "France 24: Kies kanaal" + +msgctxt "#30155" +msgid "Euronews: Choose Channel" +msgstr "Euronews: Kies kanaal" + +msgctxt "#30156" +msgid "MTV: Choose Channel" +msgstr "MTV: Kies kanaal" + +msgctxt "#30157" +msgid "DW: Choose Channel" +msgstr "DW: Kies kanaal" + +msgctxt "#30158" +msgid "France 3 Régions: Choose region" +msgstr "France 3 Régions: Kies regio" + +msgctxt "#30159" +msgid "La 1ère: Choose region" +msgstr "La 1ère: Kies regio" + +msgctxt "#30160" +msgid "Bein Sports: Choose Channel" +msgstr "Bein Sports: Kies kanaal" + +msgctxt "#30161" +msgid "QVC: Choose Channel" +msgstr "QVC: Kies kanaal" + +msgctxt "#30162" +msgid "NHK World: Choose Country" +msgstr "NHK World: Kies land" + +msgctxt "#30163" +msgid "CGTN: Choose Channel" +msgstr "CGTN: Kies kanaal" + +msgctxt "#30164" +msgid "ICI Télé: Choose region" +msgstr "ICI Télé : Kies regio" + +msgctxt "#30165" +msgid "Realmadrid TV: Choose Channel" +msgstr "Realmadrid TV: Kies kanaal" + +msgctxt "#30166" +msgid "RT: Choose Channel" +msgstr "RT: Kies kanaal" + +msgctxt "#30167" +msgid "TVP 3: Choose region" +msgstr "TVP 3: Kies regio" + +msgctxt "#30168" +msgid "Enable TV guide (slower)" +msgstr "Activeer tv-gids (langzamer)" + +msgctxt "#30169" +msgid "Enable Subtitle if present in the content" +msgstr "Activeer ondertiteling wanneer beschikbaar" + +msgctxt "#30170" +msgid "CBC: Choose region" +msgstr "CBC : Kies regio" + +msgctxt "#30171" +msgid "BFM Régions: Choose region" +msgstr "" + +msgctxt "#30173" +msgid "TV5 monde plus: Choose language" +msgstr "" + +msgctxt "#30174" +msgid "Choose live channel" +msgstr "" + +msgctxt "#30175" +msgid "Equidia Racing: Choose race" +msgstr "" + +msgctxt "#30177" +msgid "TV5 monde: Choose region" +msgstr "" + +msgctxt "#30178" +msgid "France, Belgium, Switzerland" +msgstr "" + +msgctxt "#30179" +msgid "Europe, outside FBS" +msgstr "" + +msgctxt "#30180" +msgid "Video quality" +msgstr "" + +msgctxt "#30181" +msgid "Choose version" +msgstr "" + +msgctxt "#30182" +msgid "France 3 Régions: Choose subregion" +msgstr "" + +msgctxt "#30192" +msgid "BEST" +msgstr "" + +msgctxt "#30193" +msgid "DEFAULT" +msgstr "" + +msgctxt "#30194" +msgid "DIALOG" +msgstr "" + +msgctxt "#30195" +msgid "WORST" +msgstr "" + +msgctxt "#30196" +msgid "Maximum stream bitrate [COLOR=gray](0=disabled)[/COLOR]" +msgstr "" + +msgctxt "#30197" +msgid "Settings - InputStream adaptive" +msgstr "" + +msgctxt "#30198" +msgid "Use inputstream HLS?" +msgstr "" + +msgctxt "#30199" +msgid "Use ytdl for stream?" +msgstr "" + +# Download (from 30200 to 30239) + +msgctxt "#30200" +msgid "Download directory" +msgstr "Download map" + +msgctxt "#30201" +msgid "Quality of the video to download" +msgstr "Video download kwaliteit" + +msgctxt "#30202" +msgid "Download in background" +msgstr "Download op de achtergrond" + +msgctxt "#30203" +msgid "Automatically rename the video (download directory required)" +msgstr "Hernoem de video automatisch (download map vereist)" + + +# Accounts (from 30240 to 30269) + +msgctxt "#30242" +msgid "VRT NU Login" +msgstr "VRT NU gebruikersnaam" + +msgctxt "#30243" +msgid "VRT NU Password" +msgstr "VRT NU wachtwoord" + +msgctxt "#30244" +msgid "6play Login" +msgstr "6play gebruikersnaam" + +msgctxt "#30245" +msgid "6play Password" +msgstr "6play wachtwoord" + +msgctxt "#30246" +msgid "RTLplay Login" +msgstr "RTLplay gebruikersnaam" + +msgctxt "#30247" +msgid "RTLplay Password" +msgstr "RTLplay wachtwoord" + +msgctxt "#30248" +msgid "ABWeb Login" +msgstr "ABWeb gebruikersnaam" + +msgctxt "#30249" +msgid "ABWeb Password" +msgstr "ABWeb wachtwoord" + +msgctxt "#30250" +msgid "UKTVPlay Login" +msgstr "UKTVPlay gebruikersnaam" + +msgctxt "#30251" +msgid "UKTVPlay Password" +msgstr "UKTVPlay wachtwoord" + +msgctxt "#30252" +msgid "RMCBFM Play Login" +msgstr "" + +msgctxt "#30253" +msgid "RMCBFM Play Password" +msgstr "" + +msgctxt "#30254" +msgid "TV guides" +msgstr "" + +msgctxt "#30255" +msgid "Schedules Direct login" +msgstr "" + +msgctxt "#30256" +msgid "Schedules Direct password" +msgstr "" + +msgctxt "#30257" +msgid "Schedules Direct lineup" +msgstr "" + +msgctxt "#30258" +msgid "RTBF auvio Login" +msgstr "" + +msgctxt "#30259" +msgid "RTBF auvio Password" +msgstr "" + +msgctxt "#30260" +msgid "TF1+ Login" +msgstr "" + +msgctxt "#30261" +msgid "TF1+ Password" +msgstr "" + +msgctxt "#30262" +msgid "SFR TV login" +msgstr "" + +msgctxt "#30263" +msgid "SFR TV password" +msgstr "" + + +# TV integration (from 30270 to 30289) + +msgctxt "#30270" +msgid "TV integration" +msgstr "TV integratie" + +msgctxt "#30271" +msgid "Kodi Live TV integration" +msgstr "Kodi Live TV integratie" + +msgctxt "#30272" +msgid "Install IPTV Manager add-on" +msgstr "Installeer IPTV Manager add-on" + +msgctxt "#30273" +msgid "Enable Kodi Live TV integration" +msgstr "Activeer Kodi Live TV intergatie" + +msgctxt "#30274" +msgid "Open IPTV Manager settings" +msgstr "Open IPTV Manager instellingen" + +msgctxt "#30275" +msgid "Select channels to enable" +msgstr "Selecteer de kanalen om te activeren" + +msgctxt "#30276" +msgid "Failed to save TV integration settings" +msgstr "Kon de TV integratie instellingen niet opslaan" + +msgctxt "#30277" +msgid "Select channels to enable in Kodi Live TV" +msgstr "Selecteer de kanalen om te activeren in Kodi Live TV" + + + +# Reserved space for other tab/category in settings (from 30290 to 30339) + + + +# Advanced settings (from 30370 to 30399) + +msgctxt "#30370" +msgid "Clear cache" +msgstr "Cache legen" + +msgctxt "#30371" +msgid "Cache cleared" +msgstr "Cache geleegd" + +msgctxt "#30372" +msgid "Send Kodi log to the developers when an error occurred" +msgstr "Stuur de Kodi log naar de ontwikkelaars wanneer er zich een fout voordoet" + +msgctxt "#30373" +msgid "Delete favourites" +msgstr "Verwijder favorieten" + +msgctxt "#30374" +msgid "Favourites deleted" +msgstr "Favorieten verwijderd" + + +# Settings of 'General' category (from 30400 to 30429) + +msgctxt "#30400" +msgid "Restore default order of all menus" +msgstr "Herstel de standaard volgorde van alle menus" + +msgctxt "#30401" +msgid "Unmask all hidden items" +msgstr "" + +msgctxt "#30402" +msgid "Select items to unmask" +msgstr "" + +msgctxt "#30403" +msgid "Menu items" +msgstr "Menu items" + +msgctxt "#30404" +msgid "" +msgstr "" + +msgctxt "#30405" +msgid "" +msgstr "" + +msgctxt "#30406" +msgid "" +msgstr "" + +msgctxt "#30407" +msgid "Default order of all menus have been restored" +msgstr "De standaard volgorde van alle menus is hersteld" + +msgctxt "#30408" +msgid "All hidden items have been unmasked" +msgstr "" + + +# Advanced settings (from 30430 to 30449) + +msgctxt "#30430" +msgid "Automatically start Catch-up TV & More when Kodi starts" +msgstr "Start Catch-up TV & More automatisch wanneer Kodi start" + + +# Reserved space for other tab/category in settins (from 30450 to 30499) + + + +# Context menu (from 30500 to 30599) + +msgctxt "#30500" +msgid "Move down" +msgstr "Verplaats omlaag" + +msgctxt "#30501" +msgid "Move up" +msgstr "Verplaats omhoog" + +msgctxt "#30502" +msgid "Hide" +msgstr "Verbergen" + +msgctxt "#30503" +msgid "Download" +msgstr "Downloaden" + +msgctxt "#30504" +msgid "List Videos (type=extrait)" +msgstr "Videos weergeven (type=extrait)" + +msgctxt "#30505" +msgid "List Videos (type=episode)" +msgstr "Videos weergeven (type=episode)" + + +# Dialog boxes (from 30600 to 30699) + +msgctxt "#30600" +msgid "Information" +msgstr "Informatie" + +msgctxt "#30601" +msgid "To re-enable hidden items go to the plugin settings" +msgstr "Ga naar de add-on instellingen om verborgen items te heractiveren" + +msgctxt "#30602" +msgid "This content is DRM protected. To be able to play it we invite you to update Kodi to the version 18 or above. Thank you for your understanding." +msgstr "Deze inhoud is DRM beveiligd. Om het af te kunnen spelen nodigen wij u uit Kodi te updaten naar versie 18 of hoger. Bedankt voor uw begrip." + +msgctxt "#30603" +msgid "This content is DRM protected. The download mode doesn't work for this kind of protection. Thank you for your understanding." +msgstr "Deze inhoud is DRM beveiligd. Downloaden is niet mogelijk met een dergelijke beveiliging. Bedankt voor uw begrip." + +msgctxt "#30604" +msgid "Access to this content requires an account %s. You can provide your credentials for it in the settings of this add-on. If you don't have an account, you can create one at this url %s." +msgstr "Toegang tot deze inhoud vereist een account %s. U kunt de gegevens hiervoor verstrekken in de instellingen van deze add-on. Wanneer u geen account heeft kunt u deze aanmaken op het volgende adres %s." + +msgctxt "#30605" +msgid "You can now use Kodi's TV feature to watch live TV channels from Catch-up TV & More, follow the tutorial at https://catch-up-tv-and-more.github.io/live_tv_installation/." +msgstr "U kunt nu Kodi's TV functionaliteit gebruiken om live TV kanalen via Catch-up TV & More te kijken, volg de handleiding op https://catch-up-tv-and-more.github.io/live_tv_installation/" + +msgctxt "#30606" +msgid "Do you want to see this information next time?" +msgstr "Wilt u deze informatie een volgende keer zien?" + + +# Others (from 30700 to 30799) + +msgctxt "#30700" +msgid "More videos..." +msgstr "Meer video's..." + +msgctxt "#30701" +msgid "All videos" +msgstr "Alle video's" + +msgctxt "#30702" +msgid "DRM protected video" +msgstr "DRM beveiligde video" + +msgctxt "#30703" +msgid "Search" +msgstr "Zoeken" + +msgctxt "#30704" +msgid "Last videos" +msgstr "Laatste video's" + +msgctxt "#30705" +msgid "From A to Z" +msgstr "Van A naar Z" + +msgctxt "#30706" +msgid "Ascending" +msgstr "Oplopend" + +msgctxt "#30707" +msgid "Descending" +msgstr "Aflopend" + +msgctxt "#30708" +msgid "More programs..." +msgstr "Meer programma's..." + +msgctxt "#30709" +msgid "Choose video quality" +msgstr "Kies video kwaliteit" + +msgctxt "#30710" +msgid "Video stream no longer exists" +msgstr "Video stream niet langer beschikbaar" + +msgctxt "#30711" +msgid "Authentication failed" +msgstr "Authenticatie mislukt" + +msgctxt "#30712" +msgid "Video with an account needed" +msgstr "Video met een account nodig" + +msgctxt "#30713" +msgid "Geo-blocked video" +msgstr "Geo-geblokkeerde video" + +msgctxt "#30714" +msgid "Search videos" +msgstr "Zoek video's" + +msgctxt "#30715" +msgid "Search programs" +msgstr "Zoek programma's" + +msgctxt "#30716" +msgid "Video stream is not available" +msgstr "Video stream niet beschikbaar" + +msgctxt "#30717" +msgid "All programs" +msgstr "Alle programma's" + +msgctxt "#30718" +msgid "No videos found" +msgstr "Geen video's gevonden" + +msgctxt "#30719" +msgid "Inputstream Adaptive not enabled" +msgstr "Inputstream Adaptive is niet geactiveerd" + +msgctxt "#30720" +msgid "Kodi 17.6 or > is required" +msgstr "Kodi 17.6 of > is vereist" + +msgctxt "#30721" +msgid "Content requiring a subscription" +msgstr "Inhoud vereist een abonnement" + +msgctxt "#30723" +msgid "An error occurred while getting TV guide" +msgstr "Er trap een fout op bij het ophalen van de Tv-gids" + +msgctxt "#30724" +msgid "Failed to guess your country based on your IP" +msgstr "Uw land kon niet bepaald worden op basis van uw IP" + +msgctxt "#30725" +msgid "Categories" +msgstr "Categoriën" + +msgctxt "#30726" +msgid "Races" +msgstr "Races" + +msgctxt "#30727" +msgid "Most popular" +msgstr "" + +msgctxt "#30728" +msgid "Recently added" +msgstr "" + +msgctxt "#30729" +msgid "Emissions" +msgstr "" + +msgctxt "#30731" +msgid "Homepage" +msgstr "" + +msgctxt "#30732" +msgid "This content requires a payment. To buy or rent it, go to %s website at this URL : %s" +msgstr "" + + +# Favourites (from 30800 to 30859) + +msgctxt "#30800" +msgid "Add to add-on favourites" +msgstr "Toevoegen aan add-on favorieten" + +msgctxt "#30801" +msgid "Favourite name" +msgstr "Favorieten naam" + +msgctxt "#30802" +msgid "Remove" +msgstr "Verwijderen" + +msgctxt "#30803" +msgid "Favourites" +msgstr "Favorieten" + +msgctxt "#30804" +msgid "Rename" +msgstr "Hernoemen" + +msgctxt "#30805" +msgid "The item has been added to the favourites of the add-on" +msgstr "Het item is aan de add-on favorieten toegevoegd" + +msgctxt "#30806" +msgid "Add at least one favourite to access this menu" +msgstr "Voeg minimaal één favoriet toe om dit menu toegankelijk te maken" + +msgctxt "#30807" +msgid "This favourite raises an error, would you like to delete it?" +msgstr "Deze favoriet veroorzaakt een fout, wilt het verwijderen?" + + +# Log uploader (from 30860 to 30889) + +msgctxt "#30860" +msgid "This selection triggered an unexpected error, would you like to share your error log so that we can try to resolve this issue?" +msgstr "Deze selectie heeft een onverwachte fout veroorzaakt, wilt u het logboek delen zodat we kunnen proberen de fout op te lossen?" + +msgctxt "#30861" +msgid "Share us the URL URL_TO_REPLACE by email directly via the QRcode or at catch.up.tv.and.more@gmail.com or in an issue on GitHub. Thank you!" +msgstr "Deel met ons de URL URL_TO_REPLACE direct via e-mail via de QR-code of catch.up.tv.and.more@gmail.com of via een ticket op GitHub. Dank u wel!" + +msgctxt "#30862" +msgid "Sorry, sharing your error log failed" +msgstr "Sorry, delen van het logboek is mislukt" + + +# HTTP error (from 30890 to 30919) + +msgctxt "#30890" +msgid "HTTP Error code" +msgstr "HTTP foutcode" + +msgctxt "#30891" +msgid "Inaccessible content" +msgstr "Ontoegankelijke inhoud" + +msgctxt "#30892" +msgid "Authentication is required to access the resource" +msgstr "Authenticatie is nodig voor deze bron" + +msgctxt "#30893" +msgid "Access denied (Geographical restriction?)" +msgstr "Toegang geweigerd (geografische beperking?)" + +msgctxt "#30894" +msgid "This resource requires a payment or a subscription" +msgstr "Deze bron vereist een betaling of abonnement" + +msgctxt "#30895" +msgid "This resource is no longer available" +msgstr "Deze bron is niet langer beschikbaar" + +msgctxt "#30896" +msgid "No item" +msgstr "" diff --git a/plugin.video.catchuptvandmore/resources/lib/__init__.py b/plugin.video.catchuptvandmore/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.catchuptvandmore/resources/lib/addon_utils.py b/plugin.video.catchuptvandmore/resources/lib/addon_utils.py new file mode 100644 index 0000000000..bd2ddecaa6 --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/addon_utils.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2016, SylvainCecchetto +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals +import os + +# noinspection PyUnresolvedReferences +from codequick import Script, utils +import urlquick +# noinspection PyUnresolvedReferences +from kodi_six import xbmcgui, xbmcvfs + + +Quality = { + "BEST": "0", + "DEFAULT": "1", + "DIALOG": "2", + "WORST": "3" +} + + +def get_item_label(item_id, item_infos={}, append_selected_lang=True): + """Get (translated) label of 'item_id' + + Args: + item_id (str) + item_infos (dict): Information from the skeleton 'menu' dict + append_selected_lang (bool, optional): Append selected language by the user in the label + Returns: + str: (translated) label of 'item_id' + """ + if 'label' in item_infos: + label = item_infos['label'] + else: + label = item_id + + if isinstance(label, int): + label = Script.localize(label) + + if append_selected_lang and 'available_languages' in item_infos: + label = '{} ({})'.format(label, utils.ensure_unicode(Script.setting['{}.language'.format(item_id)])) + return label + + +def get_item_media_path(item_media_path): + """Get full path or URL of an item_media + + Args: + item_media_path (str or list): Partial media path of the item (e.g. channels/fr/tf1.png) + Returns: + str: Full path or URL of the item_pedia + """ + full_path = '' + + # Local image in ressources/media folder + if type(item_media_path) is list: + full_path = os.path.join(Script.get_info("path"), "resources", "media", + *(item_media_path)) + + # Remote image with complete URL + elif 'http' in item_media_path: + full_path = item_media_path + + # Image in our resource.images add-on + else: + full_path = 'resource://resource.images.catchuptvandmore/' + item_media_path + + return utils.ensure_native_str(full_path) + + +def get_quality_YTDL(download_mode=False): + """Get YoutTubeDL quality setting + + Args: + download_mode (bool) + Returns: + int: YoutTubeDL quality + """ + + # If not download mode get the 'quality' setting + if not download_mode: + quality = Script.setting.get_string('quality') + if quality == Quality['BEST']: + return 3 + + if quality == Quality['DEFAULT']: + return 3 + + if quality == Quality['DIALOG']: + youtube_dl_quality = ['SD', '720p', '1080p', 'Highest Available'] + selected_item = xbmcgui.Dialog().select( + Script.localize(30709), + youtube_dl_quality) + return selected_item + + if quality == Quality['WORST']: + return 0 + + return 3 + + # Else we need to use the 'dl_quality' setting + dl_quality = Script.setting.get_string('dl_quality') + if dl_quality == 'SD': + return 0 + + if dl_quality == '720p': + return 1 + + if dl_quality == '1080p': + return 2 + + if dl_quality == 'Highest available': + return 3 + + return 3 + + +@Script.register +def clear_cache(plugin): + """Callback function of clear cache setting button + + Args: + plugin (codequick.script.Script) + """ + + # Clear urlquick cache + urlquick.cache_cleanup(-1) + Script.notify(plugin.localize(30371), '') + + # Remove all tv guides + dirs, files = xbmcvfs.listdir(Script.get_info('profile')) + for fn in files: + if '.xml' in fn and fn != 'settings.xml': + Script.log('Remove xmltv file: {}'.format(fn)) + xbmcvfs.delete(os.path.join(Script.get_info('profile'), fn)) diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/__init__.py b/plugin.video.catchuptvandmore/resources/lib/channels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/__init__.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/abbe.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/abbe.py new file mode 100644 index 0000000000..8cde1b70a1 --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/abbe.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2017, SylvainCecchetto +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals + +import json +import random +import re +from codecs import decode as codec_decode +from codecs import encode as codec_encode +from hashlib import sha256 + +# noinspection PyUnresolvedReferences +import inputstreamhelper +import urlquick +# noinspection PyUnresolvedReferences +from codequick import Resolver, Listitem +# noinspection PyUnresolvedReferences +from kodi_six import xbmcgui + +from resources.lib import web_utils, resolver_proxy +from resources.lib.web_utils import urlencode + +# TO DO +# Add Paid contents ? + +URL_ROOT = 'https://live-replay.%s.be' + +URL_ROOT_LOGIN = 'https://app.auth.digital.abweb.com' + +URL_CONNECT_AUTHORIZE = URL_ROOT_LOGIN + '/connect/authorize' + +URL_ACCOUNT_LOGIN = URL_ROOT_LOGIN + '/Account/Login' + +URL_CONNECT_TOKEN = URL_ROOT_LOGIN + '/connect/token' + +URL_AUTH_CALLBACK = URL_ROOT + '/auth-callback' + +URL_API = 'https://subscription.digital.api.abweb.com/api/player/flux/live/%s/%s/dash' + +URL_LICENCE_KEY = 'https://mediawan.fe.cloud.vo.services/drmgateway/v1/drm/widevine/license?drmLicenseToken=%s' + +PATTERN_TOKEN = re.compile(r'__RequestVerificationToken\" type=\"hidden\" value=\"(.*?)\"') + + +def genparams(item_id): + state = ''.join(random.choice('0123456789abcdef') for _ in range(32)) + while True: + code_verifier = ''.join(random.choice('0123456789abcdef') for _ in range(96)).encode('utf-8') + hashed = sha256(code_verifier).hexdigest() + code_challenge = codec_encode(codec_decode(hashed, 'hex'), 'base64').decode("utf-8").strip().replace('=', '') + # make sure that the hashed string doesn't contain + / = + if not any(c in '+/=' for c in code_challenge): + result = json.dumps( + { + 'code_verifier': code_verifier.decode("utf-8"), + "params": { + 'client_id': item_id, + 'redirect_uri': URL_AUTH_CALLBACK % item_id, + 'response_type': 'code', + 'scope': 'openid profile email', + 'state': state, + 'code_challenge': code_challenge, + 'code_challenge_method': 'S256', + 'response_mode': 'query', + 'action': 'undefined' + } + } + ) + return result + + +@Resolver.register +def get_live_url(plugin, item_id, **kwargs): + # Using script (https://github.com/Catch-up-TV-and-More/plugin.video.catchuptvandmore/issues/484) + + # Create session + session_urlquick = urlquick.Session() + json_parser = json.loads(genparams(item_id)) + params = json_parser['params'] + resp = session_urlquick.get(URL_CONNECT_AUTHORIZE, params=params, max_age=-1) + value_token = PATTERN_TOKEN.findall(resp.text)[0] + if plugin.setting.get_string('abweb.login') == '' or \ + plugin.setting.get_string('abweb.password') == '': + xbmcgui.Dialog().ok( + 'Info', + plugin.localize(30604) % + ('ABWeb', 'https://live-replay.ab3.be/signup')) + return False + # Build PAYLOAD + payload = { + "__RequestVerificationToken": value_token, + "Email": plugin.setting.get_string('abweb.login'), + "Password": plugin.setting.get_string('abweb.password'), + "button": 'login' + } + paramslogin = { + 'ReturnUrl': '/connect/authorize/callback?%s' % urlencode(params) + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + resp2 = session_urlquick.post(URL_ACCOUNT_LOGIN, + params=paramslogin, + data=payload, + headers=headers, + verify=False) + next_url = resp2.history[1].headers['location'] + code_value = re.compile(r'code=(.*?)&').findall(next_url)[0] + code_verifier = json_parser['code_verifier'] + + paramtoken = { + 'client_id': item_id, + 'code': code_value, + 'redirect_uri': URL_AUTH_CALLBACK % item_id, + 'code_verifier': code_verifier, + 'grant_type': 'authorization_code' + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': URL_ROOT % item_id, + 'User-Agent': web_utils.get_random_ua() + } + resp3 = session_urlquick.post(URL_CONNECT_TOKEN, headers=headers, data=paramtoken, max_age=-1) + json_parser3 = json.loads(resp3.text) + token = json_parser3['id_token'] + + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Authorization': 'Bearer %s' % token, + 'User-Agent': web_utils.get_random_ua(), + 'Referer': URL_AUTH_CALLBACK % item_id + } + resp4 = session_urlquick.get(URL_API % (item_id, item_id), headers=headers, max_age=-1) + json_parser4 = json.loads(resp4.text) + + video_url = json_parser4["streamBaseUrl"] + ("/%s/dash/manifest.mpd?" % item_id) + if "hdnts" not in json_parser4["hdnts"]: + video_url += "hdnts=" + video_url += json_parser4["hdnts"] + + license_url = URL_LICENCE_KEY % (json_parser4["smToken"]) + + headers = { + 'User-Agent': web_utils.get_random_ua(), + 'Origin': URL_ROOT % item_id, + 'Referer': URL_ROOT % item_id, + 'Authority': 'mediawan.fe.cloud.vo.services', + 'Content-Type': '' + } + + return resolver_proxy.get_stream_with_quality(plugin, + video_url=video_url, + manifest_type='mpd', + headers=headers, + license_url=license_url) diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/actv.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/actv.py new file mode 100644 index 0000000000..641b812dae --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/actv.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, SylvainCecchetto +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals + +import json +import re + +import urlquick +# noinspection PyUnresolvedReferences +from codequick import Resolver, Route +# noinspection PyUnresolvedReferences +from codequick.utils import urljoin_partial + +from resources.lib import resolver_proxy, web_utils + +URL_ROOT = 'https://www.antennecentre.tv/' +URL_LIVE = URL_ROOT + 'direct' + +LIVE_PLAYER = 'https://tvlocales-player.freecaster.com/embed/%s.json' + +PATTERN_PLAYER = re.compile(r'actv\.fcst\.tv/player/embed/(.*?)\?') +PATTERN_STREAM = re.compile(r'file\":\"(.*?)\"') + +PATTERN_LIVE_TOKEN = re.compile(r'\"live_token\":\s*\"(.*?)\"') + + +@Route.register +def list_programs(plugin, item_id, **kwargs): + # TODO Add Replay + pass + + +@Resolver.register +def get_live_url(plugin, item_id, **kwargs): + headers = { + "User-Agent": web_utils.get_random_ua(), + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "fr-BE,en-US;q=0.7,en;q=0.3", + "Pragma": "no-cache", + "Cache-Control": "no-cache", + "referrer": URL_ROOT, + } + + resp = urlquick.get(URL_LIVE, headers=headers, max_age=-1) + m3u8_array = PATTERN_LIVE_TOKEN.findall(resp.text) + if len(m3u8_array) == 0: + plugin.notify(plugin.localize(30600), plugin.localize(30716)) + return False + + resp = urlquick.get(LIVE_PLAYER % m3u8_array[0], max_age=-1) + video_url = json.loads(resp.text)['video']['src'][0]['src'] + + return resolver_proxy.get_stream_with_quality(plugin, video_url) diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/atv.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/atv.py new file mode 100644 index 0000000000..573fe66e7e --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/atv.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2017, SylvainCecchetto +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals + +# noinspection PyUnresolvedReferences +from codequick import Resolver + +from resources.lib import resolver_proxy + +URL_LIVE_STREAM = 'https://live.zendzend.com/cmaf/29375_107244/master.m3u8?HLS_version=ts' + + +@Resolver.register +def get_live_url(plugin, item_id, **kwargs): + return resolver_proxy.get_stream_with_quality(plugin, video_url=URL_LIVE_STREAM, manifest_type="hls") diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/bouke.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/bouke.py new file mode 100644 index 0000000000..38915b4e39 --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/bouke.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2022, darodi +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals +import re + +import json +# noinspection PyUnresolvedReferences +from codequick import Listitem, Resolver, Route +# noinspection PyUnresolvedReferences +from codequick.utils import urljoin_partial +import urlquick +from resources.lib import resolver_proxy +from resources.lib.addon_utils import get_item_media_path +from resources.lib.web_utils import html_parser, unquote_plus, get_random_ua + +URL_ROOT = 'https://www.bouke.media/' +url_constructor = urljoin_partial(URL_ROOT) + +URL_LIVE = url_constructor('direct') + +LIVE_PLAYER = 'https://tvlocales-player.freecaster.com/embed/%s.json' + +# +PATTERN_VIDEO_URL = re.compile(r'script src=\"(.*?/embed/.+.js.*?)\"') + +# \"src\":[\"https:\\\/\\\/bouke-vod.freecaster.com\\\/vod\\\/bouke\\\/2NFCKxV842\\\/master.m3u8\" +PATTERN_VIDEO_M3U8 = re.compile(r'\\\"src\\\":\[\\\"(.*?\.m3u8)\\\"') + +HEADERS = { + "User-Agent": get_random_ua(), + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", + "Upgrade-Insecure-Requests": "1", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-User": "?1", + "Pragma": "no-cache", + "Cache-Control": "no-cache", + "referrer": "https://www.bouke.media/", +} + + +@Route.register +def list_programs(plugin, item_id, **kwargs): + resp = urlquick.get(url_constructor("/"), headers=HEADERS) + root_elem = resp.parse("ul", attrs={"block": "block-main-nav-page"}) + for url_tag in root_elem.iterfind("li/a"): + item = Listitem() + item.label = url_tag.text + url = url_tag.get("href") + if url == '/': + continue + + item.art["thumb"] = get_item_media_path('channels/be/bouke.png') + item.set_callback(video_list, url=url) + yield item + + +@Route.register +def video_list(plugin, url): + resp = urlquick.get(url_constructor(url), headers=HEADERS) + + root_elem = resp.parse("section", attrs={"id": "block-tv-theme-content"}) + results = root_elem.iterfind(".//article[@role='article']") + + for article in results: + item = Listitem() + + date = article.findtext(".//time") + if date is not None: + trimmed_date = re.sub(r'\s', '', date) + item.info.date(trimmed_date, "%d/%m/%Y") # 21/03/2022 + + video_url = article.find(".//a").get("href") + + content = article.find(".//div[@class='content']") + item.label = content.findtext(".//h2//span") + + plot_containers = content.iterfind(".//div[@class]") + for plot_container in plot_containers: + if 'field-type-text-with-summary' in plot_container.get('class'): + item.info['plot'] = plot_container.findtext(".//div[@class='field-item']") + break + + image = article.find(".//img").get("src") + item.art["thumb"] = url_constructor(image) + item.set_callback(play_video, url=video_url) + yield item + + next_tag = root_elem.find(".//ul[@class='js-pager__items pager']//a[@rel='next']") + if next_tag is not None: + next_url = re.sub(r'\?.*', '', url) + next_tag.get("href") + yield Listitem.next_page(url=next_url, callback=video_list) + + +@Resolver.register +def play_video(plugin, url): + resp = urlquick.get(url_constructor(url), headers=HEADERS, max_age=-1) + player_urls = PATTERN_VIDEO_URL.findall(resp.text) + if len(player_urls) == 0: + return False + + player_url = html_parser.unescape(player_urls[0]) + resp2 = urlquick.get(player_url, max_age=-1) + m3u8_array = PATTERN_VIDEO_M3U8.findall(resp2.text) + if len(m3u8_array) == 0: + return False + video_url = m3u8_array[0].replace("\\", "") + + return resolver_proxy.get_stream_with_quality(plugin, video_url=video_url, manifest_type="hls") + + +@Resolver.register +def get_live_url(plugin, item_id, **kwargs): + resp = urlquick.get(URL_LIVE, headers=HEADERS, max_age=-1) + live_data = re.compile(r"live_token\":\"(.*?)\"").findall(resp.text)[0] + resp2 = urlquick.get(LIVE_PLAYER % live_data, max_age=-1) + video_url = json.loads(resp2.text)['video']['src'][0]['src'] + + return resolver_proxy.get_stream_with_quality(plugin, video_url, manifest_type="hls") diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/brf.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/brf.py new file mode 100644 index 0000000000..66d4ba71cb --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/brf.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2017, SylvainCecchetto +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals +from builtins import str +import re + +from codequick import Route, Resolver, Listitem +import urlquick + +from resources.lib import download +from resources.lib.menu_utils import item_post_treatment + + +# TO DO +# ... + +URL_ROOT_BRF = 'https://m.brf.be/' + + +@Route.register +def list_categories(plugin, item_id, **kwargs): + """ + Build categories listing + - Tous les programmes + - Séries + - Informations + - ... + """ + resp = urlquick.get(URL_ROOT_BRF) + root = resp.parse("ul", attrs={"class": "off-canvas-list"}) + + for category_data in root.iterfind(".//a"): + + if 'http' in category_data.get('href'): + category_title = category_data.text + category_url = category_data.get('href') + + item = Listitem() + item.label = category_title + item.set_callback(list_videos, + item_id=item_id, + category_url=category_url, + page='1') + item_post_treatment(item) + yield item + + +@Route.register +def list_videos(plugin, item_id, category_url, page, **kwargs): + + resp = urlquick.get(category_url + 'page/%s' % page) + root = resp.parse() + + for video_datas in root.iterfind( + ".//article[@class='post column small-12 medium-6 large-4 left']"): + video_title = video_datas.find('.//a').get('title') + video_image = video_datas.find('.//a').find('.//img').get('src') + duration_list_value = video_datas.find('.//time').text.split( + '-')[1].strip().split(':') + video_duration = int(duration_list_value[0]) * 60 + int( + duration_list_value[1]) + date_list_value = video_datas.find('.//time').text.split( + '-')[0].strip().split('.') + if len(date_list_value[0]) == 1: + day = "0" + date_list_value[0] + else: + day = date_list_value[0] + if len(date_list_value[1]) == 1: + month = "0" + date_list_value[1] + else: + month = date_list_value[1] + year = date_list_value[2] + date_value = year + '-' + month + '-' + day + video_url = video_datas.find('.//a').get('href') + + item = Listitem() + item.label = video_title + item.art['thumb'] = item.art['landscape'] = video_image + item.info['duration'] = video_duration + item.info.date(date_value, '%Y-%m-%d') + + item.set_callback(get_video_url, + item_id=item_id, + video_url=video_url) + item_post_treatment(item, is_playable=True, is_downloadable=True) + yield item + + yield Listitem.next_page(item_id=item_id, + category_url=category_url, + page=str(int(page) + 1)) + + +@Resolver.register +def get_video_url(plugin, + item_id, + video_url, + download_mode=False, + **kwargs): + + resp = urlquick.get(video_url) + stream_datas_url = re.compile(r'jQuery.get\("(.*?)"').findall(resp.text)[0] + resp2 = urlquick.get(stream_datas_url) + final_video_url = re.compile(r'src="(.*?)"').findall(resp2.text)[0] + + if download_mode: + return download.download_video(final_video_url) + return final_video_url diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/bruzz.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/bruzz.py new file mode 100644 index 0000000000..f3ca623741 --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/bruzz.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2017, SylvainCecchetto +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals + +from codequick import Resolver + + +@Resolver.register +def get_live_url(plugin, item_id, **kwargs): + + return 'https://hls-origin01-bruzz.cdn02.rambla.be/main/adliveorigin-bruzz/_definst_/V3n5YY.smil/playlist.m3u8|referer=https://player.cdn01.rambla.be/players/video-js/' diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/bx1.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/bx1.py new file mode 100644 index 0000000000..cd62bcc076 --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/bx1.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2017, SylvainCecchetto +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals +from builtins import str +import re + +from codequick import Listitem, Resolver, Route +import urlquick + +from resources.lib import download +from resources.lib.menu_utils import item_post_treatment + + +# TO DO + +URL_ROOT = 'https://bx1.be' + +URL_LIVE = URL_ROOT + '/lives/direct-tv/' + +URL_EMISSIONS = URL_ROOT + '/emissions' + + +@Route.register +def list_programs(plugin, item_id, **kwargs): + + resp = urlquick.get(URL_EMISSIONS) + root = resp.parse() + + for program_datas in root.iterfind(".//article[@class='news__article']"): + + program_title = program_datas.find('.//h3').text + program_image = program_datas.find('.//img').get('src') + program_url = program_datas.find(".//a").get("href") + + item = Listitem() + item.label = program_title + item.art['thumb'] = item.art['landscape'] = program_image + item.set_callback(list_videos, + item_id=item_id, + program_url=program_url, + page='1') + item_post_treatment(item) + yield item + + +@Route.register +def list_videos(plugin, item_id, program_url, page, **kwargs): + + resp = urlquick.get(program_url + 'page/%s/' % page) + root = resp.parse("div", attrs={"class": "articles"}) + + for video_datas in root.iterfind(".//article"): + video_title = video_datas.find( + './/h3').text.strip() + ' - ' + video_datas.find('.//span').text + video_image = video_datas.find('.//img').get('src') + video_url = video_datas.find('.//a').get('href') + + item = Listitem() + item.label = video_title + item.art['thumb'] = item.art['landscape'] = video_image + + item.set_callback(get_video_url, + item_id=item_id, + video_url=video_url) + item_post_treatment(item, is_playable=True, is_downloadable=True) + + yield item + + root_change_pages = resp.parse() + if root_change_pages.find(".//ol[@class='wp-paginate font-inherit']") is not None: + change_page_node = root_change_pages.find(".//ol[@class='wp-paginate font-inherit']") + if change_page_node.find(".//a[@class='next']") is not None: + yield Listitem.next_page( + item_id=item_id, program_url=program_url, page=str(int(page) + 1)) + + +@Resolver.register +def get_video_url(plugin, + item_id, + video_url, + download_mode=False, + **kwargs): + + resp = urlquick.get(video_url) + stream_url = re.compile(r'file: "(.*?)m3u8').findall(resp.text)[0] + final_video_url = stream_url.replace('" + "', '') + 'm3u8' + + if download_mode: + return download.download_video(final_video_url) + return final_video_url + + +@Resolver.register +def get_live_url(plugin, item_id, **kwargs): + + resp = urlquick.get(URL_LIVE) + return re.compile(r'"file": "(.*?)"').findall(resp.text)[0] diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/canalzoom.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/canalzoom.py new file mode 100644 index 0000000000..1ca4ada392 --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/canalzoom.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2022, darodi +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals + +import re + +import urlquick +# noinspection PyUnresolvedReferences +from codequick import Listitem, Resolver, Route +# noinspection PyUnresolvedReferences +from codequick.utils import urljoin_partial + +from resources.lib import resolver_proxy, web_utils + +URL_ROOT = 'https://www.canalzoom.be/' +url_constructor = urljoin_partial(URL_ROOT) + +URL_LIVE = url_constructor('direct') + +URL_M3U8 = "https://tvlocales-live.freecaster.com/%s/%s.isml/master.m3u8" + +# "live_token": "95d2e3af-5ab8-45a9-9dc9-f544d006b5d5", +PATTERN_TOKEN = re.compile(r'"live_token":\s*"(.*?)",') + + +@Resolver.register +def get_live_url(plugin, item_id, **kwargs): + headers = { + 'User-Agent': web_utils.get_random_ua() + } + + resp = urlquick.get(URL_LIVE, headers=headers, max_age=-1) + found_items = PATTERN_TOKEN.findall(resp.text) + if len(found_items) == 0: + return False + token = found_items[0] + url = URL_M3U8 % (token, token) + return resolver_proxy.get_stream_with_quality(plugin, video_url=url, manifest_type="hls") diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/be/citymusic.py b/plugin.video.catchuptvandmore/resources/lib/channels/be/citymusic.py new file mode 100644 index 0000000000..3789a0275f --- /dev/null +++ b/plugin.video.catchuptvandmore/resources/lib/channels/be/citymusic.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2022, darodi +# GNU General Public License v2.0+ (see LICENSE.txt or https://www.gnu.org/licenses/gpl-2.0.txt) + +# This file is part of Catch-up TV & More + +from __future__ import unicode_literals + +# noinspection PyUnresolvedReferences +from codequick import Listitem, Resolver, Route +# noinspection PyUnresolvedReferences +from codequick.utils import urljoin_partial + +from resources.lib import resolver_proxy + +URL_LIVE = "https://www.citymusic.be/video/" +URL_M3U8 = "https://5592f056abba8.streamlock.net/citytv/citytv/playlist.m3u8" + + +# TODO add replay https://www.citymusic.be/videos/ + +@Resolver.register +def get_live_url(plugin, item_id, **kwargs): + # URL_LIVE contains + # Watch live video from monolithlive on www.twitch.tv + # + + items = soup.findAll("tr") + + log("len(items", len(items)) + + for item in items: + + # Skip these items + if str(item).find('youtube') < 0: + + log("skipped item without youtube", item) + + continue + + log("item", item) + + try: + youtubeID = item.div['id'] + except KeyError as error: + + log("skipped item without youtube id1", item) + + continue + + except TypeError as error: + + log("skipped item without youtube id2", item) + + continue + + log("youtubeID", youtubeID) + + title = item.a.string + + title = title.capitalize() + title = title.replace('/', ' ') + title = title.replace(' i ', ' I ') + title = title.replace(' ii ', ' II ') + title = title.replace(' iii ', ' III ') + title = title.replace(' iv ', ' IV ') + title = title.replace(' v ', ' V ') + title = title.replace(' vi ', ' VI ') + title = title.replace(' vii ', ' VII ') + title = title.replace(' viii ', ' VIII ') + title = title.replace(' ix ', ' IX ') + title = title.replace(' x ', ' X ') + title = title.replace(' xi ', ' XI ') + title = title.replace(' xii ', ' XII ') + title = title.replace(' xiii ', ' XIII ') + title = title.replace(' xiv ', ' XIV ') + title = title.replace(' xv ', ' XV ') + title = title.replace(' xvi ', ' XVI ') + title = title.replace(' xvii ', ' XVII ') + title = title.replace(' xviii ', ' XVIII ') + title = title.replace(' xix ', ' XIX ') + title = title.replace(' xx ', ' XXX ') + title = title.replace(' xxi ', ' XXI ') + title = title.replace(' xxii ', ' XXII ') + title = title.replace(' xxiii ', ' XXIII ') + title = title.replace(' xxiv ', ' XXIV ') + title = title.replace(' xxv ', ' XXV ') + title = title.replace(' xxvi ', ' XXVI ') + title = title.replace(' xxvii ', ' XXVII ') + title = title.replace(' xxviii ', ' XXVIII ') + title = title.replace(' xxix ', ' XXIX ') + title = title.replace(' xxx ', ' XXX ') + + log("title", title) + + youtube_url = 'plugin://plugin.video.youtube/?action=play_video&videoid=%s' % youtubeID + + log("youtube_url", youtube_url) + + item_string = str(item) + start_pos_of_plot = item_string.find('"textik"> ', 0) + len('"textik"> ') + end_pos_of_plot = item_string.find('<', start_pos_of_plot) + plot = item_string[start_pos_of_plot:end_pos_of_plot] + + log("plot", plot) + + meta = {'plot': plot, + 'duration': '', + 'year': '', + 'dateadded': ''} + + add_sort_methods() + + context_menu_items = [] + # Add refresh option to context menu + context_menu_items.append((LANGUAGE(30104), 'Container.Refresh')) + # Add episode info to context menu + context_menu_items.append((LANGUAGE(30105), 'XBMC.Action(Info)')) + + # Add to list... + list_item = xbmcgui.ListItem(title) + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + list_item.setProperty('IsPlayable', 'true') + is_folder = False + url = youtube_url + + log("url", url) + + list_item.setInfo("mediatype", "video") + list_item.setInfo("video", meta) + # Adding context menu items to context menu + list_item.addContextMenuItems(context_menu_items, replaceItems=False) + # Add our item to the listing as a 3-element tuple. + listing.append((url, list_item, is_folder)) + + # Next page entry... + if self.next_page_possible == 'True': + context_menu_items = [] + # Add refresh option to context menu + context_menu_items.append((LANGUAGE(30104), 'Container.Refresh')) + + list_item = xbmcgui.ListItem(LANGUAGE(30503)) + list_item.setArt({'thumb': os.path.join(IMAGES_PATH, 'next-page.png'), 'icon': os.path.join(IMAGES_PATH, 'next-page.png'), + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + list_item.setProperty('IsPlayable', 'false') + parameters = {"action": "list-play", "plugin_category": self.plugin_category, "url": str(self.next_url), + "next_page_possible": self.next_page_possible} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + is_folder = True + # Adding context menu items to context menu + list_item.addContextMenuItems(context_menu_items, replaceItems=False) + # Add our item to the listing as a 3-element tuple. + listing.append((url, list_item, is_folder)) + + # Add our listing to Kodi. + # Large lists and/or slower systems benefit from adding all items at once via addDirectoryItems + # instead of adding one by ove via addDirectoryItem. + xbmcplugin.addDirectoryItems(self.plugin_handle, listing, len(listing)) + # Disable sorting + xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self.plugin_handle) + + +def add_sort_methods(): + sort_methods = [xbmcplugin.SORT_METHOD_UNSORTED,xbmcplugin.SORT_METHOD_LABEL,xbmcplugin.SORT_METHOD_DATE,xbmcplugin.SORT_METHOD_DURATION,xbmcplugin.SORT_METHOD_EPISODE] + for method in sort_methods: + xbmcplugin.addSortMethod(int(sys.argv[1]), sortMethod=method) \ No newline at end of file diff --git a/plugin.video.gamegurumania/resources/lib/gamegurumania_main.py b/plugin.video.gamegurumania/resources/lib/gamegurumania_main.py new file mode 100644 index 0000000000..49959b9526 --- /dev/null +++ b/plugin.video.gamegurumania/resources/lib/gamegurumania_main.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from __future__ import absolute_import +from future import standard_library +standard_library.install_aliases() +from builtins import object +from .gamegurumania_const import LANGUAGE, IMAGES_PATH +import sys +import urllib.request, urllib.parse, urllib.error +import xbmcgui +import xbmcplugin +import os + + +# +# Main class +# +class Main(object): + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + # + # All Videos + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30000), + "url": "https://www.ggmania.com/more.php3?next=000", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30000)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Movie + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30001), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=movie", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30001)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Console + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30002), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=console", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30002)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Preview + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30003), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=preview", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30003)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Tech + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30004), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=tech", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30004)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Demo + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30005), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=demo", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30005)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Interview + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30006), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=interview", + "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30006)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # FreeGame + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30007), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=freegame", + "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30007)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Media + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30008), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=media", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30008)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Gold + # + parameters = {"action": "list-play", "plugin_category": LANGUAGE(30009), + "url": "https://www.ggmania.com/more.php3?next=000&kategory=gold", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30009)) + thumbnail_url = 'DefaultFolder.png' + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # Disable sorting + xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self.plugin_handle) diff --git a/plugin.video.gamegurumania/resources/next-page.png b/plugin.video.gamegurumania/resources/next-page.png new file mode 100644 index 0000000000..b12e695539 Binary files /dev/null and b/plugin.video.gamegurumania/resources/next-page.png differ diff --git a/plugin.video.gamegurumania/resources/settings.xml b/plugin.video.gamegurumania/resources/settings.xml new file mode 100644 index 0000000000..0622ab8e97 --- /dev/null +++ b/plugin.video.gamegurumania/resources/settings.xml @@ -0,0 +1,14 @@ + + +
    + + + + 0 + false + + + + +
    +
    diff --git a/plugin.video.gamekings/LICENSE.txt b/plugin.video.gamekings/LICENSE.txt new file mode 100644 index 0000000000..f71b0cf39b --- /dev/null +++ b/plugin.video.gamekings/LICENSE.txt @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + \ No newline at end of file diff --git a/plugin.video.gamekings/addon.py b/plugin.video.gamekings/addon.py new file mode 100644 index 0000000000..ea9814383b --- /dev/null +++ b/plugin.video.gamekings/addon.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import str +import sys +import urllib.parse +import xbmc + +from resources.lib.gamekings_const import ADDON, SETTINGS, DATE, VERSION + +# Parse parameters +if len(sys.argv[2]) == 0: + # + # Main menu + # + xbmc.log("[ADDON] %s, Python Version %s" % (ADDON, str(sys.version)), xbmc.LOGDEBUG) + xbmc.log("[ADDON] %s v%s (%s) is starting, ARGV = %s" % (ADDON, VERSION, DATE, repr(sys.argv)), + xbmc.LOGDEBUG) + + if SETTINGS.getSettingBool('onlyshowvideoscategory'): + import resources.lib.gamekings_list as plugin + else: + import resources.lib.gamekings_main as plugin + +else: + action = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['action'][0] + # + # List + # + if action == 'list': + import resources.lib.gamekings_list as plugin + # + # Play + # + elif action == 'play': + import resources.lib.gamekings_play as plugin + # + # Play + # + elif action == 'search': + import resources.lib.gamekings_search as plugin + +plugin.Main() diff --git a/plugin.video.gamekings/addon.xml b/plugin.video.gamekings/addon.xml new file mode 100644 index 0000000000..faf78cefde --- /dev/null +++ b/plugin.video.gamekings/addon.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + video + + + Watch videos from Gamekings.tv (dutch) + Watch videos from Gamekings.tv (dutch) + For bugs, requests or general questions visit the Gamekings.tv thread on the Kodi forum. + Bekijk videos van Gamekings.tv (dutch) + Bekijk videos van Gamekings.tv (dutch) + Bugs of andere feedback op deze plugin kan geplaatst worden in de Gamekings.tv thread op het Kodi forum. + nl + all + GPL-2.0-or-later + https://forum.kodi.tv/showthread.php?tid=161375 + https://www.gamekings.tv + https://github.com/skipmodea1/plugin.video.gamekings.python3 + v1.2.23 (2023-03-12) + - improved constructing video urls + + + resources/icon.png + resources/fanart.jpg + + + \ No newline at end of file diff --git a/plugin.video.gamekings/changelog.txt b/plugin.video.gamekings/changelog.txt new file mode 100644 index 0000000000..75274b396a --- /dev/null +++ b/plugin.video.gamekings/changelog.txt @@ -0,0 +1,163 @@ +v1.2.23 (2023-03-12) +- improved constructing video urls + +v1.2.22 (2023-02-26) +- fixed youtube videos + +v1.2.21 (2023-02-21) +- fixed premium videos + +v1.2.20 (2023-01-15) +- fixed some youtube-id's by removing leading/trailing spaces +- fixed playing of videos using MPEG-DASH (kudo's to peak3d) +- updated name +- updated logo +- less loging + +v1.2.19 (2020-11-10) +- fixed premium videos after a website change + +v1.2.18 (2020-06-05) +- fixed playback for youtube videos + +v1.2.17 (2020-05-02) +- added video on frontpage category + +v1.2.16 (2019-09-15) +- added premium video category +- fixes due to website change (september 2019). Let's hope they keep working +- added maximum video quality setting for the new m3u8 videos +- fix for trailers +- removed http:// urls + +v1.2.15 (2019-03-01) +- vimeo disabled oauth1, causing the vimeo addon to break. Therefore not using that addon anymore +- improved detecting if the video is a twitch live-stream +- changed fanart +- switch to Leia (kodi 18) +- removed gamekings extra category + +v1.2.14 (2019-02-23) +- marking the addon broken for gotham. A working version will be available in Leia (Kodi 18) + +v1.2.13 (2018-05-18) +- prefix the titles of premium-only videos with an asterisk +- using news-tag in addon.xml +- showing a popup when it's a twitch live stream + +v1.2.12 (2018-03-24) +- fixed video category (website changes) + +v1.2.11 (2018-01-20) +- removed looking for video dialogue +- addon now works in kode python 2 and should also work in python 3 (!!) once all dependencies work in python 3. +Kudo's to the python future package for making this possible. Kudo's to RomanVM for the help. +- changed order of categories +- adding a setting to only view Videos category + +v1.2.10 (2017-05-17) +- removed base url setting + +v1.2.9 (2017-05-16) +- fix for website switch to https + +v1.2.8 (2017-05-07) +- small improvements to scraping of the video-id in all categories +- fix for gamekings extra video's + +v1.2.7 (2017-04-14) +- small fix for twitch stream +- changed text from xbmc to kodi +- not using addon-name in xbmcaddon.Addon() anymore (thanks enen92) + +v1.2.6 (2017-03-12) +- added twitch as a dependency +- fixed url in addon.xml as per request + +v1.2.5 (2017-03-11) +- small fix for tv-episodes +- added twitch 'Gamekings live' stream support by using the twitch plugin (much respect to authors of the plugin) + +v1.2.4 (2016-07-11) +- fixed premium videos (!!!!) by using the vimeo plugin (big kudo's to bromix for the plugin) +- don't change titles anymore for premium video's +- small fix for checking if the login was successful + +v1.2.3 (2016-06-08) +- added support for premium content +- using requests + +v1.2.2 (2016-06-07) +- change in base-url due to website change +- made it possible to overrule the base-url in settings +- changed addon debugging info to kodi debugging info + +v1.2.1 (2016-05-29) +- using po-files + +v1.2.0 (2016-05-20) +- updated due to website change +- removed E3 +- added Search in Videos + +v1.1.9 (2016-02-20) +- updated to latest requirements for a Kodi-addon +- changed fanart +- added fanart-blurred +- changed icon + +v1.1.8 (2015-09-11) : +- changes due to website url extension change + +v1.1.7 (2015-08-27) : +- small fix for gogoVideo (Gamekings Extra) + +v1.1.6 (2015-08-15) : +- added youtube as a dependency +- added some utf8 stuff + +v1.1.5 (2015-07-20) : +- added support for youtube files +- added support for gogoVideo youtube files (odd but true) + +v1.1.4 (2015-06-25) : +- minor fixes in category "More E3 2015" + +v1.1.3 (2015-06-20) : +- added category "More E3 2015" + +v1.1.2 (2015-04-01) : +- changes in video category due to website changes +- changed category Gamekings Extra due to due to website changes +- added category Trailers + +v1.1.1 (2015-02-14) : +- small fix for category Gamekings Extra + +v1.1.0 (2014-11-14) : +- added category Gamekings Extra (mad props to Amelandbor!) +- retired category Pc because of lack of updates +- changed fanart.jpg + +v1.0.4 (2014-05-11) : +- support vimeo files + +v1.0.3 (2013-07-07) : +- added beautiful soup as dependency +- added dutch strings + +v1.0.2 (2013-05-29) : +- sometimes the video-url in the content-tag is not (!) correct, in that case trying with added '/large' in the video-url + +v1.0.1 (2013-04-10) : +- fixed thumbnails for pc category +- removed repeating episodes in afleveringen category +- added popup if media file cannnot be played or is not found +- filled in optional fields in addon.xml +- used title attribute more if possible + +v1.0.0 (2013-04-02) : +- initial version +- filled in optional fields in addon.xml +- added a display of an httperror +- published it on github \ No newline at end of file diff --git a/plugin.video.gamekings/resources/fanart-blur.jpg b/plugin.video.gamekings/resources/fanart-blur.jpg new file mode 100644 index 0000000000..680bfdf8f6 Binary files /dev/null and b/plugin.video.gamekings/resources/fanart-blur.jpg differ diff --git a/plugin.video.gamekings/resources/fanart.jpg b/plugin.video.gamekings/resources/fanart.jpg new file mode 100644 index 0000000000..a14b7605e6 Binary files /dev/null and b/plugin.video.gamekings/resources/fanart.jpg differ diff --git a/plugin.video.gamekings/resources/icon.png b/plugin.video.gamekings/resources/icon.png new file mode 100644 index 0000000000..0f2550718c Binary files /dev/null and b/plugin.video.gamekings/resources/icon.png differ diff --git a/plugin.video.gamekings/resources/language/resource.language.en_gb/strings.po b/plugin.video.gamekings/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..980e3afd48 --- /dev/null +++ b/plugin.video.gamekings/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,196 @@ +# XBMC Media Center language file +# Addon Name: GameKings +# Addon id: plugin.video.gamekings +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-05-29 15:46+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\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 "Watch videos from Gamekings.tv (dutch)" +msgstr "" + +msgctxt "Addon Description" +msgid "Watch videos from Gamekings.tv (dutch)" +msgstr "" + +msgctxt "Addon Disclaimer" +msgid "For bugs, requests or general questions visit the Gamekings.tv thread on the Kodi forum." +msgstr "" + +msgctxt "#30000" +msgid "Videos" +msgstr "" + +msgctxt "#30001" +msgid "TV Episodes" +msgstr "" + +msgctxt "#30002" +msgid "Gamekings Extra" +msgstr "" + +msgctxt "#30003" +msgid "Trailers" +msgstr "" + +msgctxt "#30004" +msgid "Search in Videos" +msgstr "" + +msgctxt "#30005" +msgid "Premium Videos" +msgstr "" + +msgctxt "#30006" +msgid "Videos on Frontpage" +msgstr "" + +msgctxt "#30100" +msgid "Version" +msgstr "" + +msgctxt "#30101" +msgid "Author" +msgstr "" + +msgctxt "#30102" +msgid "Website" +msgstr "" + +msgctxt "#30103" +msgid "Base Url" +msgstr "" + +msgctxt "#30104" +msgid "Gamekings Premium Member" +msgstr "" + +msgctxt "#30105" +msgid "User Name" +msgstr "" + +msgctxt "#30106" +msgid "Password" +msgstr "" + +msgctxt "#30200" +msgid "Video player" +msgstr "" + +msgctxt "#30201" +msgid "Auto" +msgstr "" + +msgctxt "#30202" +msgid "DVDPlayer" +msgstr "" + +msgctxt "#30203" +msgid "MPlayer" +msgstr "" + +msgctxt "#30204" +msgid "Episode" +msgstr "" + +msgctxt "#30300" +msgid "Maximum Video quality" +msgstr "" + +msgctxt "#30301" +msgid "Standard" +msgstr "" + +msgctxt "#30302" +msgid "High" +msgstr "" + +msgctxt "#30400" +msgid "Debug" +msgstr "" + +msgctxt "#30021" +msgid "4K" +msgstr "" + +msgctxt "#30022" +msgid "1080P" +msgstr "" + +msgctxt "#30023" +msgid "720P" +msgstr "" + +msgctxt "#30024" +msgid "480P" +msgstr "" + +msgctxt "#30025" +msgid "360P" +msgstr "" + +msgctxt "#30501" +msgid "%s to %s" +msgstr "" + +msgctxt "#30502" +msgid "Page %u" +msgstr "" + +msgctxt "#30503" +msgid "Next page" +msgstr "" + +msgctxt "#30504" +msgid "Getting video location..." +msgstr "" + +msgctxt "#30505" +msgid "No video found." +msgstr "" + +msgctxt "#30506" +msgid "Error playing media file." +msgstr "" + +msgctxt "#30507" +msgid "Error getting page: %s" +msgstr "" + +msgctxt "#30601" +msgid "Login has failed for this Premium video. In settings " +msgstr "" + +msgctxt "#30602" +msgid "enter the correct username and password. Or turn off" +msgstr "" + +msgctxt "#30603" +msgid "the Gamekings-Premium-membership-switch if you are not a member." +msgstr "" + +msgctxt "#30604" +msgid "Login Failed: %s." +msgstr "" + +msgctxt "#30605" +msgid "Sorry. You must be a Premium member to see this video." +msgstr "" + +msgctxt "#30610" +msgid "Only show Videos category" +msgstr "" + +msgctxt "#30611" +msgid "Start the Kodi twitch video addon and go to the Gamekings channel to see the live stream." +msgstr "" \ No newline at end of file diff --git a/plugin.video.gamekings/resources/language/resource.language.nl_nl/strings.po b/plugin.video.gamekings/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 0000000000..8a7cf2f71a --- /dev/null +++ b/plugin.video.gamekings/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,196 @@ +# XBMC Media Center language file +# Addon Name: GameKings +# Addon id: plugin.video.gamekings +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-05-29 15:46+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "Watch videos from Gamekings.tv (dutch)" +msgstr "Bekijk videos van Gamekings.tv (dutch)" + +msgctxt "Addon Description" +msgid "Watch videos from Gamekings.tv (dutch)" +msgstr "Bekijk videos van Gamekings.tv (dutch)" + +msgctxt "Addon Disclaimer" +msgid "For bugs, requests or general questions visit the Gamekings.tv thread on the Kodi forum." +msgstr "Bugs of andere feedback op deze plugin kan geplaatst worden in de Gamekings.tv thread op het Kodi forum." + +msgctxt "#30000" +msgid "Videos" +msgstr "Videos" + +msgctxt "#30001" +msgid "TV Episodes" +msgstr "TV Afleveringen" + +msgctxt "#30002" +msgid "Gamekings Extra" +msgstr "Gamekings Extra" + +msgctxt "#30003" +msgid "Trailers" +msgstr "Trailers" + +msgctxt "#30004" +msgid "Search in Videos" +msgstr "Zoeken in Videos" + +msgctxt "#30005" +msgid "Premium Videos" +msgstr "Premium Videos" + +msgctxt "#30006" +msgid "Videos on Frontpage" +msgstr "Videos op Voorpagina" + +msgctxt "#30100" +msgid "Version" +msgstr "Versie" + +msgctxt "#30101" +msgid "Author" +msgstr "Auteur" + +msgctxt "#30102" +msgid "Website" +msgstr "Website" + +msgctxt "#30103" +msgid "Base Url" +msgstr "Basis Url" + +msgctxt "#30104" +msgid "Gamekings Premium Member" +msgstr "Gamekings Premium Lid" + +msgctxt "#30105" +msgid "User Name" +msgstr "Gebruikers Naam" + +msgctxt "#30106" +msgid "Password" +msgstr "Wachtwoord" + +msgctxt "#30200" +msgid "Video player" +msgstr "Video player" + +msgctxt "#30201" +msgid "Auto" +msgstr "Auto" + +msgctxt "#30202" +msgid "DVDPlayer" +msgstr "DVDPlayer" + +msgctxt "#30203" +msgid "MPlayer" +msgstr "MPlayer" + +msgctxt "#30204" +msgid "Episode" +msgstr "Aflevering" + +msgctxt "#30300" +msgid "Maximum Video quality" +msgstr "Mamimum Video kwaliteit" + +msgctxt "#30301" +msgid "Standard" +msgstr "Standaard" + +msgctxt "#30302" +msgid "High" +msgstr "Hoog" + +msgctxt "#30400" +msgid "Debug" +msgstr "Debug" + +msgctxt "#30501" +msgid "%s to %s" +msgstr "%s tot %s" + +msgctxt "#30021" +msgid "4K" +msgstr "4K" + +msgctxt "#30022" +msgid "1080P" +msgstr "1080P" + +msgctxt "#30023" +msgid "720P" +msgstr "720P" + +msgctxt "#30024" +msgid "480P" +msgstr "480P" + +msgctxt "#30025" +msgid "360P" +msgstr "360P" + +msgctxt "#30502" +msgid "Page %u" +msgstr "Pagina %u" + +msgctxt "#30503" +msgid "Next page" +msgstr "Volgende pagina" + +msgctxt "#30504" +msgid "Getting video location..." +msgstr "Video wordt opgehaald..." + +msgctxt "#30505" +msgid "No video found." +msgstr "Video niet gevonden." + +msgctxt "#30506" +msgid "Error playing media file." +msgstr "Fout bij het afspelen van video." + +msgctxt "#30507" +msgid "Error getting page: %s." +msgstr "Fout bij ophalen van pagina %s." + +msgctxt "#30601" +msgid "Login has failed for this Premium video. In settings " +msgstr "Login is mislukt voor deze Premium video. In settings" + +msgctxt "#30602" +msgid "enter the correct username and password. Or turn off" +msgstr "voer de correcte gebruikersnaam en wachtwoord in. Of" + +msgctxt "#30603" +msgid "the Gamekings-Premium-membership-switch if you are not a member." +msgstr "zet Gameking Premium-lidmaatschap-switch uit als u geen lid bent." + +msgctxt "#30604" +msgid "Login Failed: %s." +msgstr "Login gefaald: %s" + +msgctxt "#30605" +msgid "Sorry. You must be a Premium member to see this video." +msgstr "Sorry. Je moet Premium lid zijn om deze video te zien." + +msgctxt "#30610" +msgid "Only show Videos category" +msgstr "Toon alleen Videos categorie" + +msgctxt "#30611" +msgid "Start the Kodi twitch video addon and go to the Gamekings channel to see the live stream." +msgstr "Start de Kodi twitch video addon en ga naar het Gamekings kanaal om de live stream te zien" \ No newline at end of file diff --git a/plugin.video.gamekings/resources/lib/gamekings_const.py b/plugin.video.gamekings/resources/lib/gamekings_const.py new file mode 100644 index 0000000000..82d0a6e9a8 --- /dev/null +++ b/plugin.video.gamekings/resources/lib/gamekings_const.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import sys +import os +import xbmc +import xbmcaddon +from bs4 import BeautifulSoup + + +# +# Constants +# +ADDON = "plugin.video.gamekings" +SETTINGS = xbmcaddon.Addon() +LANGUAGE = SETTINGS.getLocalizedString +IMAGES_PATH = os.path.join(xbmcaddon.Addon().getAddonInfo('path'), 'resources') +BASE_URL_GAMEKINGS_TV = "https://www.gamekings.tv/" +TWITCH_URL_GAMEKINGS_TV = "https://player.twitch.tv/?channel=gamekings" +PREMIUM_ONLY_VIDEO_TITLE_PREFIX = '* ' +LOGIN_URL = 'https://www.gamekings.tv/wp-login.php' +NUMBER_OF_ENCODED_DIGITS_FOR_ONE_DECRYPTED_DIGIT = 2 +MASTER_DOT_M3U8 = "master.m3u8" +VQ4K = '4k' +VQ1080P = '1080p' +VQ720P = '720p' +VQ480P = '480p' +VQ360P = '360p' +VQ1080N = '1080n' +VQ720N = '720n' +VQ480N = '480n' +VQ360N = '360n' +HTTPSCOLONSLASHSLASH_ENCODED = "8647470737a3f2f27" +END_TAG = " 2: + unicode = str + + +def convertToUnicodeString(s, encoding='utf-8'): + """Safe decode byte strings to Unicode""" + if isinstance(s, bytes): # This works in Python 2.7 and 3+ + s = s.decode(encoding) + return s + + +def convertToByteString(s, encoding='utf-8'): + """Safe encode Unicode strings to bytes""" + if isinstance(s, unicode): + s = s.encode(encoding) + return s + + +def log(name_object, object): + try: + # Let's try and remove any non-ascii stuff first + object = object.encode('ascii', 'ignore') + except: + pass + + try: + xbmc.log("[ADDON] %s v%s (%s) debug mode, %s = %s" % ( + ADDON, VERSION, DATE, name_object, convertToUnicodeString(object)), xbmc.LOGDEBUG) + except: + xbmc.log("[ADDON] %s v%s (%s) debug mode, %s = %s" % ( + ADDON, VERSION, DATE, name_object, "Unable to log the object due to an error while converting it to an unicode string"), xbmc.LOGDEBUG) + + +def getSoup(html,default_parser="html5lib"): + soup = BeautifulSoup(html, default_parser) + return soup + + +def decodeString(encoded_string): + chunks = len(encoded_string)//NUMBER_OF_ENCODED_DIGITS_FOR_ONE_DECRYPTED_DIGIT + chunk_size = NUMBER_OF_ENCODED_DIGITS_FOR_ONE_DECRYPTED_DIGIT + decoded_string = "" + for i in range(0, chunks * NUMBER_OF_ENCODED_DIGITS_FOR_ONE_DECRYPTED_DIGIT, chunk_size): + encoded_chunk = encoded_string[i:i+chunk_size] + + decoded_digit = decryptEncodedDigit(encoded_chunk) + + # log("decoded_digit", decoded_digit) + + if decoded_digit == STOPDECODINGNOW: + # exit the loop + break + + decoded_string = decoded_string + str(decoded_digit) + + # log("decoded_string", decoded_string) + + # log("final decoded_string", decoded_string) + + return decoded_string + + +# No idea what kinda magic the encoding is. +# This is the encoding that should work for the m3u8 url part in the encoded container. +# It won't work on the rest of the container. +def decryptEncodedDigit(encoded_digit): + key_value = {} + key_value['16'] = 'a' + key_value['26'] = 'b' + key_value['36'] = 'c' + key_value['46'] = 'd' + key_value['56'] = 'e' + key_value['66'] = 'f' + key_value['76'] = 'g' + key_value['86'] = 'h' + key_value['96'] = 'I' + key_value['A6'] = 'j' + key_value['B6'] = 'k' + key_value['C6'] = 'l' + key_value['D6'] = 'm' + key_value['E6'] = 'n' + key_value['F6'] = 'o' + key_value['07'] = 'p' + key_value['17'] = 'q' + key_value['27'] = 'r' + key_value['37'] = 's' + key_value['47'] = 't' + key_value['57'] = 'u' + key_value['67'] = 'v' + key_value['77'] = 'w' + key_value['87'] = 'x' + key_value['97'] = 'y' + key_value['A7'] = 'z' + + key_value['14'] = 'A' + key_value['24'] = 'B' + key_value['34'] = 'C' + key_value['44'] = 'D' + key_value['54'] = 'E' + key_value['64'] = 'F' + key_value['74'] = 'G' + key_value['84'] = 'H' + key_value['94'] = 'I' + key_value['A4'] = 'J' + key_value['B4'] = 'K' + key_value['C4'] = 'L' + key_value['D4'] = 'M' + key_value['E4'] = 'N' + key_value['F4'] = 'O' + key_value['05'] = 'P' + key_value['15'] = 'Q' + key_value['25'] = 'R' + key_value['35'] = 'S' + key_value['45'] = 'T' + key_value['55'] = 'U' + key_value['65'] = 'V' + key_value['75'] = 'W' + key_value['85'] = 'X' + key_value['95'] = 'Y' + key_value['A5'] = 'Z' + + key_value['03'] = '0' + key_value['13'] = '1' + key_value['23'] = '2' + key_value['33'] = '3' + key_value['43'] = '4' + key_value['53'] = '5' + key_value['63'] = '6' + key_value['73'] = '7' + key_value['83'] = '8' + key_value['93'] = '9' + + key_value['A3'] = ':' + key_value['E2'] = '.' + key_value['F2'] = '/' + key_value['F5'] = '_' + + encoded_digit = str(encoded_digit).capitalize() + + # log("encoded_digit", encoded_digit) + + try: + decoded_digit = key_value[encoded_digit] + + # log("decoded_digit", decoded_digit) + + # Only the encoded m3u8 url part in the container, can be decoded. + # When we get a decoding error, we assume (ulp!) that we are past the m3u8 part and can quit decoding. + except: + + log("exception while decoding this digit", encoded_digit) + + decoded_digit = STOPDECODINGNOW + + return decoded_digit \ No newline at end of file diff --git a/plugin.video.gamekings/resources/lib/gamekings_list.py b/plugin.video.gamekings/resources/lib/gamekings_list.py new file mode 100644 index 0000000000..f137c7c6b7 --- /dev/null +++ b/plugin.video.gamekings/resources/lib/gamekings_list.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import object +import os +import re +import requests +import sys +import urllib.request, urllib.parse, urllib.error +import xbmcgui +import xbmcplugin + +from resources.lib.gamekings_const import ADDON, LANGUAGE, IMAGES_PATH, BASE_URL_GAMEKINGS_TV, PREMIUM_ONLY_VIDEO_TITLE_PREFIX, \ + convertToUnicodeString, log, getSoup + +# +# Main class +# +class Main(object): + # + # Init + # + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + # Parse parameters + try: + self.plugin_category = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['plugin_category'][0] + self.video_list_page_url = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['url'][0] + self.next_page_possible = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['next_page_possible'][0] + except KeyError: + self.plugin_category = LANGUAGE(30000) + self.video_list_page_url = BASE_URL_GAMEKINGS_TV + "category/videos/page/001/" + self.next_page_possible = "True" + + log("self.video_list_page_url", self.video_list_page_url) + + if self.next_page_possible == 'True': + # Determine current item number, next item number, next_url + # f.e. https://www.gamekings.tv/category/videos/page/001/ + pos_of_page = self.video_list_page_url.rfind('/page/') + if pos_of_page >= 0: + page_number_str = str( + self.video_list_page_url[pos_of_page + len('/page/'):pos_of_page + len('/page/') + len('000')]) + page_number = int(page_number_str) + page_number_next = page_number + 1 + if page_number_next >= 100: + page_number_next_str = str(page_number_next) + elif page_number_next >= 10: + page_number_next_str = '0' + str(page_number_next) + else: + page_number_next_str = '00' + str(page_number_next) + self.next_url = str(self.video_list_page_url).replace(page_number_str, page_number_next_str) + + log("self.next_url", self.next_url) + + # + # Get the videos + # + self.getVideos() + + # + # Get videos + # + def getVideos(self): + # + # Init + # + # Create a list for our items. + listing = [] + + # + # Get HTML page + # + response = requests.get(self.video_list_page_url) + + html_source = response.text + html_source = convertToUnicodeString(html_source) + + # Parse response + soup = getSoup(html_source) + + # Get the items. Each item contains a title, a video page url and a thumbnail url + # + #
    + # + # E3 2016 Vooruitblik met Shelly + # + # + #

    + # or + #

    + # + # E3 2016 Vooruitblik met Shelly + #

    + #

    De regeltante aan het woord in deze vooruitblik!

    + #
    + # Jan & Shelly + # 07/06/2016 + # 0 + #
    + + # Sometimes the videos on the frontpage are not yet present on the video page, therefore added an videos on frontpage section + if self.video_list_page_url == BASE_URL_GAMEKINGS_TV: + # find the videos on the frontpage + items = soup.findAll('a', attrs={'class': re.compile("^" + "slider__link")}) + else: + # find the videos on the video page + items = soup.findAll('div', attrs={'class': re.compile("^" + "post")}) + + log("len(items", len(items)) + + for item in items: + + item = convertToUnicodeString(item) + + # log("item", item) + + if self.video_list_page_url == BASE_URL_GAMEKINGS_TV: + title = item.text + + video_page_url = item['href'] + + log("video_page_url", video_page_url) + + thumbnail_url = "" + + else: + # if item contains 'postcontainer, skip the item + if str(item).find('postcontainer') >= 0: + + # log("skipped item containing 'postcontainer'", item) + + continue + + video_page_url = item.a['href'] + + log("video_page_url", video_page_url) + + # if link ends with a '/': process the link, if not: skip the link + if video_page_url.endswith('/'): + pass + else: + + log("skipped video_page_url not ending on '/'", video_page_url) + + continue + + # Make title + try: + title = item.a['title'] + except: + # skip the item if it's got no title + continue + + # this is category Gamekings Extra + if self.plugin_category == LANGUAGE(30002): + if str(title).lower().find('extra') >= 0: + pass + elif str(title).lower().find('extra') >= 0: + pass + else: + # skip the url + + log("skipped non-extra title in gamekings extra category", video_page_url) + + continue + + title = title.replace('-', ' ') + title = title.replace('/', ' ') + title = title.replace(' i ', ' I ') + title = title.replace(' ii ', ' II ') + title = title.replace(' iii ', ' III ') + title = title.replace(' iv ', ' IV ') + title = title.replace(' v ', ' V ') + title = title.replace(' vi ', ' VI ') + title = title.replace(' vii ', ' VII ') + title = title.replace(' viii ', ' VIII ') + title = title.replace(' ix ', ' IX ') + title = title.replace(' x ', ' X ') + title = title.replace(' xi ', ' XI ') + title = title.replace(' xii ', ' XII ') + title = title.replace(' xiii ', ' XIII ') + title = title.replace(' xiv ', ' XIV ') + title = title.replace(' xv ', ' XV ') + title = title.replace(' xvi ', ' XVI ') + title = title.replace(' xvii ', ' XVII ') + title = title.replace(' xviii ', ' XVIII ') + title = title.replace(' xix ', ' XIX ') + title = title.replace(' xx ', ' XXX ') + title = title.replace(' xxi ', ' XXI ') + title = title.replace(' xxii ', ' XXII ') + title = title.replace(' xxiii ', ' XXIII ') + title = title.replace(' xxiv ', ' XXIV ') + title = title.replace(' xxv ', ' XXV ') + title = title.replace(' xxvi ', ' XXVI ') + title = title.replace(' xxvii ', ' XXVII ') + title = title.replace(' xxviii ', ' XXVIII ') + title = title.replace(' xxix ', ' XXIX ') + title = title.replace(' xxx ', ' XXX ') + title = title.replace("aflevering", "Aflevering") + title = title.replace("Aflevering", (LANGUAGE(30204))) + title = title.replace("Gamekings Extra: ", "") + title = title.replace("Gamekings Extra over ", "") + title = title.replace("Extra: ", "") + title = title.replace("Extra over ", "") + title = title.strip() + title = title.capitalize() + + # log("title", title) + + if str(item).find("title--premium") >= 0: + title = PREMIUM_ONLY_VIDEO_TITLE_PREFIX + ' ' + title + elif str(item).lower().find("premium") >= 0: + title = PREMIUM_ONLY_VIDEO_TITLE_PREFIX + ' ' + title + + # Make thumbnail + try: + thumbnail_url = item.a.img['data-original'] + except: + if self.video_list_page_url == BASE_URL_GAMEKINGS_TV: + pass + else: + # skip the item if it has no thumbnail + + log("skipping item with no thumbnail", item) + + continue + + list_item = xbmcgui.ListItem(label=title) + list_item.setInfo("video", {"title": title, "studio": ADDON}) + list_item.setInfo("mediatype", "video") + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + list_item.setProperty('IsPlayable', 'true') + parameters = {"action": "play", "video_page_url": video_page_url, "plugin_category": self.plugin_category} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + is_folder = False + # Add refresh option to context menu + list_item.addContextMenuItems([('Refresh', 'Container.Refresh')]) + # Add our item to the listing as a 3-element tuple. + listing.append((url, list_item, is_folder)) + + # Next page entry + if self.next_page_possible == 'True': + thumbnail_url = os.path.join(IMAGES_PATH, 'next-page.png') + list_item = xbmcgui.ListItem(LANGUAGE(30503)) + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + list_item.setProperty('IsPlayable', 'false') + parameters = {"action": "list", "plugin_category": self.plugin_category, "url": str(self.next_url), + "next_page_possible": self.next_page_possible} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + is_folder = True + # Add refresh option to context menu + list_item.addContextMenuItems([('Refresh', 'Container.Refresh')]) + # Add our item to the listing as a 3-element tuple. + listing.append((url, list_item, is_folder)) + + # Add our listing to Kodi. + # Large lists and/or slower systems benefit from adding all items at once via addDirectoryItems + # instead of adding one by ove via addDirectoryItem. + xbmcplugin.addDirectoryItems(self.plugin_handle, listing, len(listing)) + # Disable sorting + xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self.plugin_handle) \ No newline at end of file diff --git a/plugin.video.gamekings/resources/lib/gamekings_main.py b/plugin.video.gamekings/resources/lib/gamekings_main.py new file mode 100644 index 0000000000..41c43a339a --- /dev/null +++ b/plugin.video.gamekings/resources/lib/gamekings_main.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import object +import os +import sys +import urllib.request, urllib.parse, urllib.error +import xbmcgui +import xbmcplugin + +from resources.lib.gamekings_const import LANGUAGE, IMAGES_PATH, BASE_URL_GAMEKINGS_TV + + +# +# Main class +# +class Main(object): + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + # + # Premium Videos + # + parameters = {"action": "list", "plugin_category": LANGUAGE(30005), + "url": BASE_URL_GAMEKINGS_TV + "category/premium/page/001/", "next_page_possible": "False"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30005)) + list_item.setArt({'thumb': "DefaultFolder.png", 'icon': "DefaultFolder.png", + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Videos on frontpage + # + # Sometimes videos are not present yet in the video category, but they are present on the frontpage of the site + parameters = {"action": "list", "plugin_category": LANGUAGE(30006), + "url": BASE_URL_GAMEKINGS_TV, "next_page_possible": "False"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30006)) + list_item.setArt({'thumb': "DefaultFolder.png", 'icon': "DefaultFolder.png", + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Videos + # + parameters = {"action": "list", "plugin_category": LANGUAGE(30000), + "url": BASE_URL_GAMEKINGS_TV + "category/videos/page/001/", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30000)) + list_item.setArt({'thumb': "DefaultFolder.png", 'icon': "DefaultFolder.png", + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Trailers + # + parameters = {"action": "list", "plugin_category": LANGUAGE(30003), + "url": BASE_URL_GAMEKINGS_TV + "?s=trailer", "next_page_possible": "False"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30003)) + list_item.setArt({'thumb': "DefaultFolder.png", 'icon': "DefaultFolder.png", + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Afleveringen + # + parameters = {"action": "list", "plugin_category": LANGUAGE(30001), + "url": BASE_URL_GAMEKINGS_TV + "page/001/?cat=3&s=gamekings+s", "next_page_possible": "True"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30001)) + list_item.setArt({'thumb': "DefaultFolder.png", 'icon': "DefaultFolder.png", + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # + # Search in Videos + # + parameters = {"action": "search", "plugin_category": LANGUAGE(30004), + "url": BASE_URL_GAMEKINGS_TV + "?cat=3&s=%s", "next_page_possible": "False"} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + list_item = xbmcgui.ListItem(LANGUAGE(30004)) + list_item.setArt({'thumb': "DefaultFolder.png", 'icon': "DefaultFolder.png", + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + is_folder = True + list_item.setProperty('IsPlayable', 'false') + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=list_item, isFolder=is_folder) + + # Disable sorting + xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self.plugin_handle) diff --git a/plugin.video.gamekings/resources/lib/gamekings_play.py b/plugin.video.gamekings/resources/lib/gamekings_play.py new file mode 100644 index 0000000000..28bcdc2120 --- /dev/null +++ b/plugin.video.gamekings/resources/lib/gamekings_play.py @@ -0,0 +1,627 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import object +import requests +import sys +import urllib.request, urllib.error, urllib.parse +import xbmc +import xbmcgui +import xbmcplugin + +from resources.lib.gamekings_const import SETTINGS, LANGUAGE, LOGIN_URL, convertToUnicodeString, log, TWITCH_URL_GAMEKINGS_TV, \ + VQ4K, VQ1080P, VQ720P, VQ480P, VQ360P, VQ1080N, VQ720N, VQ480N, VQ360N, decodeString, MASTER_DOT_M3U8, \ + HTTPSCOLONSLASHSLASH_ENCODED, END_TAG, STREAM + +# +# Main class +# +class Main(object): + # + # Init + # + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + # Get plugin settings + self.IS_PREMIUM_MEMBER = SETTINGS.getSettingBool('is-premium-member') + self.PREFERRED_QUALITY = SETTINGS.getSetting('quality') + + # Parse parameters + self.plugin_category = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['plugin_category'][0] + self.video_page_url = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['video_page_url'][0] + + log("self.video_page_url", self.video_page_url) + + # + # Play video + # + self.playVideo() + + # + # Play video + # + def playVideo(self): + # + # Init + # + video_url = "" + dialog_wait = xbmcgui.DialogProgress() + + # + # Get current list item details... + # + # title = convertToUnicodeString(xbmc.getInfoLabel("list_item.Title")) + thumbnail_url = convertToUnicodeString(xbmc.getInfoImage("list_item.Thumb")) + # studio = convertToUnicodeString(xbmc.getInfoLabel("list_item.Studio")) + # plot = convertToUnicodeString(xbmc.getInfoLabel("list_item.Plot")) + # genre = convertToUnicodeString(xbmc.getInfoLabel("list_item.Genre")) + + try: + # requests is sooooo nice, respect! + session = requests.Session() + + # get the page that contains the video + response = session.get(self.video_page_url) + + html_source = response.text + html_source = convertToUnicodeString(html_source) + + # is it a premium-only video? (f.e. https://www.gamekings.tv/premium/110853/) + #
    + #
    + #

    Premium Content

    + # + # of + # Word Premium + #
    + #
    + + if str(html_source).find('premiumonly') >= 0: + if self.IS_PREMIUM_MEMBER: + try: + # we need a NEW (!!!) session + session = requests.Session() + + # # get the login-page + # response = session.get(LOGINURL) + # html_source = reply.text + # html_source = convertToUnicodeString(html_source) + # + # log("login-page", html_source) + # + # the login page should contain something like this + # = 0: + + log("Login Error!", "login was NOT successful!") + + xbmcgui.Dialog().ok(LANGUAGE(30000), LANGUAGE(30601), LANGUAGE(30602), + LANGUAGE(30603)) + sys.exit(1) + else: + # dialog_wait.create("Login Successful", "Currently looking for video") + + log("Login", "login was successful!!") + + # let's try getting the page after a successful login, hopefully it contains a link to + # the video now + self.video_page_url = self.video_page_url + "?login=success" + + # log("self.video_page_url", self.video_page_url) + + response = session.get(self.video_page_url) + + html_source = response.text + html_source = convertToUnicodeString(html_source) + else: + # Something went wrong with logging in + + log("Login Error!!, statuscode: ", str(response.status_code)) + + xbmcgui.Dialog().ok(LANGUAGE(30000), LANGUAGE(30604) % (str(response.status_code))) + sys.exit(1) + + except urllib.error.HTTPError as error: + + log("HTTPerror1", error) + + xbmcgui.Dialog().ok(LANGUAGE(30000), LANGUAGE(30606) % (str(error))) + sys.exit(1) + except: + exception = sys.exc_info()[0] + + log("Exception1", exception) + + sys.exit(1) + # This is a premium video and the Premium-membership-switch in the settings is off + else: + xbmcgui.Dialog().ok(LANGUAGE(30000), LANGUAGE(30605)) + sys.exit(1) + + except urllib.error.HTTPError as error: + + log("HTTPerror2", error) + + xbmcgui.Dialog().ok(LANGUAGE(30000), LANGUAGE(30606) % (str(error))) + sys.exit(1) + except: + exception = sys.exc_info()[0] + + log("Exception2", exception) + + sys.exit(1) + + + have_valid_url = False + no_url_found = True + youtube_id = '' + + if have_valid_url: + pass + else: + + log("trying method ", "1 (m3u8)") + + # encoded container with the m3u8 url: + # "c34696670236c6163737d32256d6265646d236f6e6471696e6562722e3c396662716d656027796464786d3226343032202865696768647d3223363032202372736d322 + # 8647470737a3f2f27616d656b696e67637e2763646e6e236f6f267964656f637f243435373f58507235454f4d6463335074405262335 + # 2202662716d65626f627465627d32203220216c6c6f6776657c6c63736275656e6e3c3f296662716d656e3c3f2469667e3" + # Let's get the encoded video url, in the example above that's the second line + start_pos_encoded_m3u8_url = html_source.find(HTTPSCOLONSLASHSLASH_ENCODED) + if start_pos_encoded_m3u8_url >= 0: + + # log("start_pos_encoded_m3u8_url", start_pos_encoded_m3u8_url) + + end_pos_encoded_m3u8_url = html_source[start_pos_encoded_m3u8_url:].find(END_TAG) + if end_pos_encoded_m3u8_url >= 0: + + # log("end_pos_encoded_m3u8_url", end_pos_encoded_m3u8_url) + + encoded_m3u8_url = html_source[start_pos_encoded_m3u8_url:start_pos_encoded_m3u8_url + end_pos_encoded_m3u8_url] + + # log("encoded_m3u8_url", encoded_m3u8_url) + + decoded_m3u8_url = decodeString(encoded_m3u8_url) + + # log("decoded_m3u8_url", decoded_m3u8_url) + + # The decoded m3u8 url should look something like this: https://gamekings.gcdn.co/videos/4457_Xp2EEOmd3SpDPb2S + # We have to lowercase the part before the last '/' to fix any uppercase digits (i guess my magic decoding isn't perfect ;)) + pos_last_slash = decoded_m3u8_url.rfind("/") + if pos_last_slash >= 0: + first_part = decoded_m3u8_url[0:pos_last_slash] + + # log("first_part", first_part) + + second_part = decoded_m3u8_url[pos_last_slash:] + + # log("second_part", second_part) + + # Lowercase the first part and add the filename of the m3u8-file + decoded_m3u8_url = first_part.lower() + second_part + "/" + MASTER_DOT_M3U8 + + # log("decoded_m3u8_url completed", decoded_m3u8_url) + + response = session.get(decoded_m3u8_url) + + # determine the wanted video max_video_quality + if self.PREFERRED_QUALITY == '0': # Low + max_video_quality = VQ360P + elif self.PREFERRED_QUALITY == '1': # Medium + max_video_quality = VQ480P + elif self.PREFERRED_QUALITY == '2': # High Quality + max_video_quality = VQ720P + elif self.PREFERRED_QUALITY == '3': # Very High Quality + max_video_quality = VQ1080P + elif self.PREFERRED_QUALITY == '4': # Ultra High Quality + max_video_quality = VQ4K + else: # Default in case max_video_quality is not found + max_video_quality = VQ720P + + # log("wanted max_video_quality", max_video_quality) + + # an example of the content of a m3u8 file (2019): + # #EXTM3U + # #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=464000,RESOLUTION=640x360,FRAME-RATE=60.000,CODECS="avc1.4d401f,mp4a.40.2" + # index-s360p-v1-a1.m3u8 + # #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=928000,RESOLUTION=854x480,FRAME-RATE=60.000,CODECS="avc1.4d401f,mp4a.40.2" + # index-s480p-v1-a1.m3u8 + # #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2128000,RESOLUTION=1280x720,FRAME-RATE=60.000,CODECS="avc1.4d4020,mp4a.40.2" + # index-s720p-v1-a1.m3u8 + # #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=6128000,RESOLUTION=1920x1080,FRAME-RATE=60.000,CODECS="avc1.4d402a,mp4a.40.2" + # index-s1080p-v1-a1.m3u8 + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=18304,RESOLUTION=640x360,CODECS="avc1.4d401f",URI="iframes-s360p-v1-a1.m3u8" + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=36479,RESOLUTION=854x480,CODECS="avc1.4d401f",URI="iframes-s480p-v1-a1.m3u8" + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=92824,RESOLUTION=1280x720,CODECS="avc1.4d4020",URI="iframes-s720p-v1-a1.m3u8" + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=240783,RESOLUTION=1920x1080,CODECS="avc1.4d402a",URI="iframes-s1080p-v1-a1.m3u8" + + # an example of the content of a m3u8 file (2020/11/10): + # #EXTM3U + # #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=464000,RESOLUTION=640x360,FRAME-RATE=30.000,CODECS="avc1.64001e,mp4a.40.2" + # index-svod360n-v1-a1.m3u8 + # #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=928000,RESOLUTION=854x480,FRAME-RATE=30.000,CODECS="avc1.64001f,mp4a.40.2" + # index-svod480n-v1-a1.m3u8 + # #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2128000,RESOLUTION=1280x720,FRAME-RATE=30.000,CODECS="avc1.64001f,mp4a.40.2" + # index-svod720n-v1-a1.m3u8 + # #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=6128000,RESOLUTION=1920x1080,FRAME-RATE=30.000,CODECS="avc1.640028,mp4a.40.2" + # index-svod1080n-v1-a1.m3u8 + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=65908,RESOLUTION=640x360,CODECS="avc1.64001e",URI="iframes-svod360n-v1-a1.m3u8" + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=978632,RESOLUTION=854x480,CODECS="avc1.64001f",URI="iframes-svod480n-v1-a1.m3u8" + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=235616,RESOLUTION=1280x720,CODECS="avc1.64001f",URI="iframes-svod720n-v1-a1.m3u8" + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=488781,RESOLUTION=1920x1080,CODECS="avc1.640028",URI="iframes-svod1080n-v1-a1.m3u8" + + # Let's try and find a video of the desired video max_video_quality. + # If that can't be found, try to find a video with less than the desired video max_video_quality + # 4K doesn't seem to be available as of now (september 2019) but it may be in the future + video_quality_url = '' + if video_quality_url == '': + if max_video_quality in [VQ4K]: + video_quality_url = self.find_video_quality_url(VQ4K, response, decoded_m3u8_url) + if video_quality_url == '': + if max_video_quality in [VQ4K, VQ1080P]: + video_quality_url = self.find_video_quality_url(VQ1080P, response, decoded_m3u8_url) + if video_quality_url == '': + video_quality_url = self.find_video_quality_url(VQ1080N, response, decoded_m3u8_url) + if video_quality_url == '': + if max_video_quality in [VQ4K, VQ1080P, VQ720P]: + video_quality_url = self.find_video_quality_url(VQ720P, response, decoded_m3u8_url) + if video_quality_url == '': + video_quality_url = self.find_video_quality_url(VQ720N, response, decoded_m3u8_url) + if video_quality_url == '': + if max_video_quality in [VQ4K, VQ1080P, VQ720P, VQ480P]: + video_quality_url = self.find_video_quality_url(VQ480P, response, decoded_m3u8_url) + if video_quality_url == '': + video_quality_url = self.find_video_quality_url(VQ480N, response, decoded_m3u8_url) + if video_quality_url == '': + if max_video_quality in [VQ4K, VQ1080P, VQ720P, VQ480P, VQ360P]: + video_quality_url = self.find_video_quality_url(VQ360P, response, decoded_m3u8_url) + if video_quality_url == '': + video_quality_url = self.find_video_quality_url(VQ360N, response, decoded_m3u8_url) + + # If we didn't find a video url with the desired video max_video_quality or lower, use the m3u8 file url + if video_quality_url == '': + video_url = decoded_m3u8_url + else: + + # log("video_quality_url", video_quality_url) + + # Find out if the altered m3u8 url exists + response = session.get(video_quality_url) + + # log("response.status_code", response.status_code) + + # if we find a m3u8 file with the altered url, let's use that. + # If it is not found, let's use the unaltered url. + if response.status_code in [200]: + video_url = video_quality_url + else: + video_url = decoded_m3u8_url + + # log("decoded video_url m3u8", video_url) + + log("success with method ", "1 (m3u8)") + + have_valid_url = True + no_url_found = False + + + dash_file_found = False + if have_valid_url: + pass + else: + + log("trying method ", "2 (dash)") + + # https://muse.ai/embed/6EG5Wob?search=0&links=0&logo=0 + start_pos_video_url_embed = html_source.find("https://muse.ai/embed/") + if start_pos_video_url_embed >= 0: + end_pos_video_url_embed = html_source.find('"', start_pos_video_url_embed) + if end_pos_video_url_embed >= 0: + video_url_embed = html_source[start_pos_video_url_embed:end_pos_video_url_embed] + + # log("video_url_embed", video_url_embed) + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Referer': 'https://www.gamekings.tv/', + 'DNT': '1', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'Sec-Fetch-Dest': 'iframe', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'cross-site' + } + + # response = session.get("https://muse.ai/embed/6EG5Wob?search=0&links=0&logo=0", headers=headers) + response = session.get(video_url_embed, headers=headers) + + html_source = response.text + html_source = convertToUnicodeString(html_source) + + # log("html_source embed", html_source) + + search_for_string = 'url": "' + video_url_start_pos = html_source.find(search_for_string) + if video_url_start_pos >= 0: + # url": "https://cdn.muse.ai/u/Czi97La/f4e5310bc42adcde16ff1b14fa7a56f7e61380b78befa674f666f1bca7ad8953/data", "views": 2401, "visibility": "hidden", "width": 1920}, + video_url_start_pos = video_url_start_pos + len(search_for_string) + video_url_end_pos = html_source.find('"', video_url_start_pos) + if video_url_end_pos >= 0: + # https://cdn.muse.ai/u/Czi97La/f4e5310bc42adcde16ff1b14fa7a56f7e61380b78befa674f666f1bca7ad8953/data + video_url_data = html_source[video_url_start_pos:video_url_end_pos] + + # log("video_url_data", video_url_data) + + # https://cdn-eu.muse.ai/u/Czi97La/f4e5310bc42adcde16ff1b14fa7a56f7e61380b78befa674f666f1bca7ad8953/videos/dash.mpd + video_url_dash = video_url_data.replace("/data", "/videos/dash.mpd") + + # log("video_url_dash", video_url_dash) + + video_url = video_url_dash + + dash_file_found = True + no_url_found = False + have_valid_url = True + + # log("video_url dash", video_url) + + log("success with method ", "2 (dash)") + + + if have_valid_url: + pass + else: + + log("trying method ", "3 (vimeo)") + + # Get the video url + #
    + #
    + #
    + # ',re.DOTALL).search(html) + if html is None: + return(ilist) + vids = json.loads(html.group(1))['channels'][0]['videos'] + for b in vids: + url = b['releaseUrl'] + name = UNESCAPE(b['title']) + thumb = ''.join(['http://www.hgtv.com',b['thumbnailUrl']]) + fanart = thumb + infoList = {'mediatype': 'episode', + 'Title': name, + 'Studio': b.get('publisherId'), + 'Duration': b.get('length'), + 'Plot': UNESCAPE(b.get('description')), + 'TVShowTitle': xbmc.getInfoLabel('ListItem.Title')} + ilist = self.addMenuItem(name,'GV', ilist, url, thumb, fanart, infoList, isFolder=False) + return(ilist) + + + def getAddonVideo(self,url): + html = requests.get(url, headers=self.defaultHeaders).text + subs = re.compile(' + + + + + + + + + video + + + IMDb Trailers video add-on + IMDb Fragmanları video eklentisi + IMDb Trailers video añadido + IMDb Trailers Video-Add-On + Module vidéo IMDb Trailers + Watch movie trailers available on IMDb + IMDb'de bulunan film fragmanlarını izle + Ver trailers de películas disponibles en IMDb + Filmtrailer auf IMDb ansehen + Regarder les bandes-annonces disponibles sur IMDb + This third party addon is not in any way commissioned or endorsed by Amazon or IMDb + Bu üçüncü taraf eklentisi, Amazon veya IMDb tarafından hiçbir şekilde görevlendirilmemiştir veya onaylanmamıştır. + Este complemento de terceros no está en modo alguno comisionado o respaldado por Amazon o IMDb + Dieses Drittanbieter-Addon wird in keiner Weise von Amazon oder IMDb in Auftrag gegeben oder unterstützt + Cet additif tiers n'est en aucun cas commandé ou approuvé par Amazon ou IMDb + + all + GPL-2.0 + https://forum.kodi.tv/showthread.php?tid=352127 + https://www.imdb.com/trailers/ + gujal at protonmail dot com + + icon.png + fanart.jpg + resources/images/screenshot-01.jpg + resources/images/screenshot-02.jpg + resources/images/screenshot-03.jpg + resources/images/screenshot-04.jpg + + + \ No newline at end of file diff --git a/plugin.video.imdb.trailers/changelog.txt b/plugin.video.imdb.trailers/changelog.txt new file mode 100644 index 0000000000..da64ce93ad --- /dev/null +++ b/plugin.video.imdb.trailers/changelog.txt @@ -0,0 +1,137 @@ +plugin.video.imdb.trailers: +--------------------------- +v2.1.20 (20250209) +[fix] listing +[new] charcater names in meta +[update] improve caching + +v2.1.19 (20231121) +[fix] playback + +v2.1.18 (20230825) +[fix] Popular +[new] Threading + +v2.1.17 (20230103) +[fix] Coming soon +[fix] Recently added +[new] Kodi Nexus compliance + +v2.1.16 (20220815) +[fix] Now Playing +[fix] Coming soon +[new] cast images in meta + +v2.1.15 (20220522) +[fix] playback fix + +v2.1.14 (20211015) +[fix] code breaking on Kodi18 +[fix] Search dialog popup after playing a searched trailer +[fix] play by imdb_id + +v2.1.13 (20210815) +[fix] html parser breaking on Kodi20 +[fix] handle exceptions such as empty plot or no art +[new] Use common plugin cache to improve caching + +v2.1.12 (20200918) +[fix] api changes + +v2.1.11 (20200516) +[new] Add new api calls for categories +[fix] Fix occasional playback issues + +v2.1.10 (29.02.2020) +[fix] Python2 syntax issue breaking on Kodi18 + +v2.1.9 (28.02.2020) +[fix] Playback shifted to HLS +[new] Search option + +v2.1.8 (15.06.2019) +[fix] Rewrite using requests-cache module + +v2.1.7 (1.06.2019) +[new] In Theatres and Coming soon sections +[fix] Python3 compatible + +v2.1.5 (01.2.2019) +-Add view modes (Tested on Estuary Only) + +v2.1.4 (08.12.2018) +-fix playback issue +-process html encoding + +v2.1.3 (19.08.2018) +-removed couchpotato support +-scrape the site as no api calls available +-Krypton and above only due to use of TLS1.2 + +v2.1.1 (14.06.2017) +-display non English European characters corectly + +v2.1.0 (19.11.2016) +-forked the code from queeup +-removed tempfile library and used common plugin cache +-fix bug when no release year for the movie + +v2.0.7 (30.03.2013) +-removed couchpotato V1 support + +v2.0.6 (06.03.2013) +-fixes Useragent filtering +-add all genres as comma separated list +-thanks to thomaswr. + +v2.0.5 (09.01.2013) +-fix content list if there is no genre. + +v2.0.4 (23.12.2012) +-tormovies mailwarn removed +-xbmc label2 duration removed. +-params parse code changed. +-use jsonrpc notification insted of executebuiltin + +v2.0.3 (23.12.2012) +-fixed video link +-fixed title + +v2.0.2 (24.06.2012) +-fixed listing without poster movies + +v2.0.1 (16.06.2012) +-fixed new website structure +-remove unused Beautifulsoup import +-cosmetic + +v2.0.0 (27.04.2012) +-video quality setting change to enum to labelenum +-code cleanup and reorder with pep8 rules +-added fanart support for all levels. +-updated beautifulsoup to 3.2.0 +-updated xbmc.python to 2.0 + +v1.0.5 (14.03.2012) +-fixed error on infoLabels writer and cast + +v1.0.4 (14.03.2012) +-added new infoLabels: tagline, genre, mpaa, writer, cast +-port setting type changed text to number. +-chanced recent trailer second to first on list. + +v1.0.3 (12.03.2012) +-fixed ascii error on Turkish notification +-CouchPotato quality sort reordered. + +v1.0.2 (12.03.2012) +-added tormovies.org mail warn integration +-removed unneeded variables and imports. +-some code cleaning and organizing. + +v1.0.1 (03.07.2011) +-added CouchPotato download support (BETA) +-added Cache support. + +v1.0.0 (19.02.2011) +First Release \ No newline at end of file diff --git a/plugin.video.imdb.trailers/default.py b/plugin.video.imdb.trailers/default.py new file mode 100644 index 0000000000..7007b76e43 --- /dev/null +++ b/plugin.video.imdb.trailers/default.py @@ -0,0 +1,22 @@ +""" + IMDB Trailers Kodi Addon + Copyright (C) 2018 gujal + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +from resources.lib import imdb_trailers + +if __name__ == '__main__': + imdb_trailers.Main() diff --git a/plugin.video.imdb.trailers/fanart.jpg b/plugin.video.imdb.trailers/fanart.jpg new file mode 100644 index 0000000000..ecdb9174b7 Binary files /dev/null and b/plugin.video.imdb.trailers/fanart.jpg differ diff --git a/plugin.video.imdb.trailers/icon.png b/plugin.video.imdb.trailers/icon.png new file mode 100644 index 0000000000..5f0193197e Binary files /dev/null and b/plugin.video.imdb.trailers/icon.png differ diff --git a/plugin.video.imdb.trailers/resources/__init__.py b/plugin.video.imdb.trailers/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.imdb.trailers/resources/images/screenshot-01.jpg b/plugin.video.imdb.trailers/resources/images/screenshot-01.jpg new file mode 100644 index 0000000000..e111de0b29 Binary files /dev/null and b/plugin.video.imdb.trailers/resources/images/screenshot-01.jpg differ diff --git a/plugin.video.imdb.trailers/resources/images/screenshot-02.jpg b/plugin.video.imdb.trailers/resources/images/screenshot-02.jpg new file mode 100644 index 0000000000..ec1478d79c Binary files /dev/null and b/plugin.video.imdb.trailers/resources/images/screenshot-02.jpg differ diff --git a/plugin.video.imdb.trailers/resources/images/screenshot-03.jpg b/plugin.video.imdb.trailers/resources/images/screenshot-03.jpg new file mode 100644 index 0000000000..06abfec122 Binary files /dev/null and b/plugin.video.imdb.trailers/resources/images/screenshot-03.jpg differ diff --git a/plugin.video.imdb.trailers/resources/images/screenshot-04.jpg b/plugin.video.imdb.trailers/resources/images/screenshot-04.jpg new file mode 100644 index 0000000000..a269478ddc Binary files /dev/null and b/plugin.video.imdb.trailers/resources/images/screenshot-04.jpg differ diff --git a/plugin.video.imdb.trailers/resources/language/resource.language.de_de/strings.po b/plugin.video.imdb.trailers/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000000..b206b2d403 --- /dev/null +++ b/plugin.video.imdb.trailers/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,108 @@ +# XBMC Media Center language file +# Addon Name: IMDb Trailers +# Addon id: plugin.video.imdb.trailers +# +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-11-03 00:21+0000\n" +"PO-Revision-Date: 2019-05-31 05:00+0000\n" +"Last-Translator: FULL NAME , 2019\n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de_DE\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "IMDb Trailers video add-on" +msgstr "IMDb Trailers Video-Add-On" + +msgctxt "Addon Description" +msgid "Watch movie trailers available on IMDb." +msgstr "Filmtrailer auf IMDb ansehen" + +msgctxt "Addon Disclaimer" +msgid "This third party addon is not in any way commissioned or endorsed by Amazon or IMDb" +msgstr "Dieses Drittanbieter-Addon wird in keiner Weise von Amazon oder IMDb in Auftrag gegeben oder unterstützt" + + +msgctxt "#30000" +msgid "IMDb Trailers" +msgstr "IMDb Trailers" + +msgctxt "#30001" +msgid "Video Quality" +msgstr "Videoqualität" + +msgctxt "#30002" +msgid "Cache Timeout in hours" +msgstr "Cache Timeout in Stunden" + +msgctxt "#30003" +msgid "View Mode" +msgstr "Ansichtsmodus" + +msgctxt "#30004" +msgid "Force View Mode" +msgstr "Ansichtsmodus erzwingen" + +msgctxt "#30005" +msgid "Main Menu Mode" +msgstr "Hauptmenü-Modus" + +msgctxt "#30006" +msgid "Video Menu Mode" +msgstr "Videomenümodus" + +msgctxt "#30007" +msgid "Enable Debug Mode" +msgstr "Debug-Modus aktivieren" + +# empty strings from id 30008 to 30200 + +msgctxt "#30201" +msgid "In Cinemas" +msgstr "In Kinos" + +msgctxt "#30202" +msgid "Coming Soon" +msgstr "Demnächst" + +msgctxt "#30203" +msgid "Popular Trailers" +msgstr "Beliebte Trailer" + +msgctxt "#30204" +msgid "Recent Trailers" +msgstr "Aktuelle Trailer" + +msgctxt "#30205" +msgid "Televison Tonight" +msgstr "TV heute Abend" + +msgctxt "#30206" +msgid "Search Title" +msgstr "Titel suchen" + +msgctxt "#30207" +msgid "Clear Cache" +msgstr "Cache leeren" + +msgctxt "#30208" +msgid "Trending Trailers" +msgstr "Trending Trailers" + +msgctxt "#30209" +msgid "Most Anticipated" +msgstr "Am meisten erwartet" + +msgctxt "#30210" +msgid "Most Popular" +msgstr "Am beliebtesten" + +msgctxt "#30211" +msgid "Recently Added" +msgstr "Kürzlich hinzugefügt" diff --git a/plugin.video.imdb.trailers/resources/language/resource.language.en_gb/strings.po b/plugin.video.imdb.trailers/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..5a5bfb2cc0 --- /dev/null +++ b/plugin.video.imdb.trailers/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,108 @@ +# XBMC Media Center language file +# Addon Name: IMDb Trailers +# Addon id: plugin.video.imdb.trailers + +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-11-03 00:21+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_GB\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "IMDb Trailers video add-on" +msgstr "" + +msgctxt "Addon Description" +msgid "Watch movie trailers available on IMDb." +msgstr "" + +msgctxt "Addon Disclaimer" +msgid "This third party addon is not in any way commissioned or endorsed by Amazon or IMDb" +msgstr "" + + +msgctxt "#30000" +msgid "IMDb Trailers" +msgstr "" + +msgctxt "#30001" +msgid "Video Quality" +msgstr "" + +msgctxt "#30002" +msgid "Cache Timeout in hours" +msgstr "" + +msgctxt "#30003" +msgid "View Mode" +msgstr "" + +msgctxt "#30004" +msgid "Force View Mode" +msgstr "" + +msgctxt "#30005" +msgid "Main Menu Mode" +msgstr "" + +msgctxt "#30006" +msgid "Video Menu Mode" +msgstr "" + +msgctxt "#30007" +msgid "Enable Debug Mode" +msgstr "" + +# empty strings from id 30008 to 30200 + +msgctxt "#30201" +msgid "In Cinemas" +msgstr "" + +msgctxt "#30202" +msgid "Coming Soon" +msgstr "" + +msgctxt "#30203" +msgid "Popular Trailers" +msgstr "" + +msgctxt "#30204" +msgid "Recent Trailers" +msgstr "" + +msgctxt "#30205" +msgid "Television Tonight" +msgstr "" + +msgctxt "#30206" +msgid "Search Title" +msgstr "" + +msgctxt "#30207" +msgid "Clear Cache" +msgstr "" + +msgctxt "#30208" +msgid "Trending Trailers" +msgstr "" + +msgctxt "#30209" +msgid "Most Anticipated" +msgstr "" + +msgctxt "#30210" +msgid "Most Popular" +msgstr "" + +msgctxt "#30211" +msgid "Recently Added" +msgstr "" diff --git a/plugin.video.imdb.trailers/resources/language/resource.language.es_es/strings.po b/plugin.video.imdb.trailers/resources/language/resource.language.es_es/strings.po new file mode 100644 index 0000000000..7fc36d3d86 --- /dev/null +++ b/plugin.video.imdb.trailers/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,108 @@ +# XBMC Media Center language file +# Addon Name: IMDb Trailers +# Addon id: plugin.video.imdb.trailers + +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-11-03 00:21+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es_ES\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "IMDb Trailers video add-on" +msgstr "IMDb Trailers video añadido" + +msgctxt "Addon Description" +msgid "Watch movie trailers available on IMDb" +msgstr "Ver trailers de películas disponibles en IMDb" + +msgctxt "Addon Disclaimer" +msgid "This third party addon is not in any way commissioned or endorsed by Amazon or IMDb" +msgstr "Este complemento de terceros no está en modo alguno comisionado o respaldado por Amazon o IMDb" + + +msgctxt "#30000" +msgid "IMDb Trailers" +msgstr "IMDb Trailers" + +msgctxt "#30001" +msgid "Video Quality" +msgstr "Calidad de video" + +msgctxt "#30002" +msgid "Cache Timeout in hours" +msgstr "Tiempo de espera de caché en horas" + +msgctxt "#30003" +msgid "View Mode" +msgstr "Modo de vista" + +msgctxt "#30004" +msgid "Force View Mode" +msgstr "Modo de vista forzada" + +msgctxt "#30005" +msgid "Main Menu Mode" +msgstr "Modo de menú principal" + +msgctxt "#30006" +msgid "Video Menu Mode" +msgstr "Modo de menú de video" + +msgctxt "#30007" +msgid "Enable Debug Mode" +msgstr "Habilitar el modo de depuración" + +# empty strings from id 30008 to 30200 + +msgctxt "#30201" +msgid "In Cinemas" +msgstr "En cines" + +msgctxt "#30202" +msgid "Coming Soon" +msgstr "Próximamente" + +msgctxt "#30203" +msgid "Popular Trailers" +msgstr "Trailers populares" + +msgctxt "#30204" +msgid "Recent Trailers" +msgstr "Trailers recientes" + +msgctxt "#30205" +msgid "Television Tonight" +msgstr "Televisión esta noche" + +msgctxt "#30206" +msgid "Search Title" +msgstr "Buscar título" + +msgctxt "#30207" +msgid "Clear Cache" +msgstr "Limpiar cache" + +msgctxt "#30208" +msgid "Trending Trailers" +msgstr "Tráilers de tendencias" + +msgctxt "#30209" +msgid "Most Anticipated" +msgstr "Más esperado" + +msgctxt "#30210" +msgid "Most Popular" +msgstr "Más popular" + +msgctxt "#30211" +msgid "Recently Added" +msgstr "Agregado recientemente" diff --git a/plugin.video.imdb.trailers/resources/language/resource.language.fr_fr/strings.po b/plugin.video.imdb.trailers/resources/language/resource.language.fr_fr/strings.po new file mode 100644 index 0000000000..3950202498 --- /dev/null +++ b/plugin.video.imdb.trailers/resources/language/resource.language.fr_fr/strings.po @@ -0,0 +1,108 @@ +# XBMC Media Center language file +# Addon Name: IMDb Trailers +# Addon id: plugin.video.imdb.trailers + +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-11-03 00:21+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr_FR\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "IMDb Trailers video add-on" +msgstr "Module vidéo IMDb Trailers" + +msgctxt "Addon Description" +msgid "Watch movie trailers available on IMDb" +msgstr "Regarder les bandes-annonces disponibles sur IMDb" + +msgctxt "Addon Disclaimer" +msgid "This third party addon is not in any way commissioned or endorsed by Amazon or IMDb" +msgstr "Cet additif tiers n'est en aucun cas commandé ou approuvé par Amazon ou IMDb." + + +msgctxt "#30000" +msgid "IMDb Trailers" +msgstr "Trailers IMDb" + +msgctxt "#30001" +msgid "Video Quality" +msgstr "Qualité vidéo" + +msgctxt "#30002" +msgid "Cache Timeout in hours" +msgstr "Cache Timeout en heures" + +msgctxt "#30003" +msgid "View Mode" +msgstr "Mode d'affichage" + +msgctxt "#30004" +msgid "Force View Mode" +msgstr "Mode d'affichage forcé" + +msgctxt "#30005" +msgid "Main Menu Mode" +msgstr "Mode du menu principal" + +msgctxt "#30006" +msgid "Video Menu Mode" +msgstr "Mode Menu Vidéo" + +msgctxt "#30007" +msgid "Enable Debug Mode" +msgstr "Activer le mode de débogage" + +# empty strings from id 30008 to 30200 + +msgctxt "#30201" +msgid "In Cinemas" +msgstr "Dans les cinémas" + +msgctxt "#30202" +msgid "Coming Soon" +msgstr "Arrive bientôt" + +msgctxt "#30203" +msgid "Popular Trailers" +msgstr "Trailers populaires" + +msgctxt "#30204" +msgid "Recent Trailers" +msgstr "Trailers récents" + +msgctxt "#30205" +msgid "Television Tonight" +msgstr "Télévision ce soir" + +msgctxt "#30206" +msgid "Search Title" +msgstr "Recherche Titre" + +msgctxt "#30207" +msgid "Clear Cache" +msgstr "Vider le cache" + +msgctxt "#30208" +msgid "Trending Trailers" +msgstr "Trailers tendance" + +msgctxt "#30209" +msgid "Most Anticipated" +msgstr "Le plus attendu" + +msgctxt "#30210" +msgid "Most Popular" +msgstr "Les plus populaires" + +msgctxt "#30211" +msgid "Recently Added" +msgstr "Récemment ajouté" diff --git a/plugin.video.imdb.trailers/resources/language/resource.language.tr_tr/strings.po b/plugin.video.imdb.trailers/resources/language/resource.language.tr_tr/strings.po new file mode 100644 index 0000000000..01d62ce7ce --- /dev/null +++ b/plugin.video.imdb.trailers/resources/language/resource.language.tr_tr/strings.po @@ -0,0 +1,107 @@ +# XBMC Media Center language file +# Addon Name: IMDb Trailers +# Addon id: plugin.video.imdb.trailers + +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2016-11-03 00:21+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tr_TR\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "Addon Summary" +msgid "IMDb Trailers video add-on" +msgstr "IMDb Fragmanları video eklentisi" + +msgctxt "Addon Description" +msgid "Watch movie trailers available on IMDb" +msgstr "IMDb'de bulunan film fragmanlarını izle" + +msgctxt "Addon Disclaimer" +msgid "This third party addon is not in any way commissioned or endorsed by Amazon or IMDb" +msgstr "Bu üçüncü taraf eklentisi, Amazon veya IMDb tarafından hiçbir şekilde görevlendirilmemiştir veya onaylanmamıştır" + +msgctxt "#30000" +msgid "IMDb Trailers" +msgstr "IMDb Fragmanlar" + +msgctxt "#30001" +msgid "Video Quality" +msgstr "Video kalitesi" + +msgctxt "#30002" +msgid "Cache Timeout in hours" +msgstr "Önbellek Zaman Aşımı, saat olarak" + +msgctxt "#30003" +msgid "View Mode" +msgstr "Görünüm modu" + +msgctxt "#30004" +msgid "Force View Mode" +msgstr "Görünümü zorla modu" + +msgctxt "#30005" +msgid "Main Menu Mode" +msgstr "Ana Menü Modu" + +msgctxt "#30006" +msgid "Video Menu Mode" +msgstr "Video Menüsü Modu" + +msgctxt "#30007" +msgid "Enable Debug Mode" +msgstr "Hata Ayıklama Modunu Etkinleştir" + +# empty strings from id 30008 to 30200 + +msgctxt "#30201" +msgid "In Cinemas" +msgstr "Sinemalarda" + +msgctxt "#30202" +msgid "Coming Soon" +msgstr "Çok yakında" + +msgctxt "#30203" +msgid "Popular Trailers" +msgstr "Popüler Fragmanlar" + +msgctxt "#30204" +msgid "Recent Trailers" +msgstr "Son Fragmanlar" + +msgctxt "#30205" +msgid "Television Tonight" +msgstr "Bu akşam televizyon" + +msgctxt "#30206" +msgid "Search Title" +msgstr "Başlık Ara" + +msgctxt "#30207" +msgid "Clear Cache" +msgstr "Önbelleği Temizle" + +msgctxt "#30208" +msgid "Trending Trailers" +msgstr "Trend Olan Fragmanlar" + +msgctxt "#30209" +msgid "Most Anticipated" +msgstr "En Çok Beklenen" + +msgctxt "#30210" +msgid "Most Popular" +msgstr "En Popüler" + +msgctxt "#30211" +msgid "Recently Added" +msgstr "Son Eklenenler" diff --git a/plugin.video.imdb.trailers/resources/lib/__init__.py b/plugin.video.imdb.trailers/resources/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugin.video.imdb.trailers/resources/lib/cache.py b/plugin.video.imdb.trailers/resources/lib/cache.py new file mode 100644 index 0000000000..a34dfa21de --- /dev/null +++ b/plugin.video.imdb.trailers/resources/lib/cache.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +""" + 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 . +""" + +import hashlib +import json +import pickle +import re +import six +import time +import zlib +from kodi_six import xbmc, xbmcvfs, xbmcaddon +try: + from sqlite3 import dbapi2 as db, OperationalError, Binary +except ImportError: + from pysqlite2 import dbapi2 as db, OperationalError, Binary + + +TRANSLATEPATH = xbmcvfs.translatePath if six.PY3 else xbmc.translatePath +cacheFile = TRANSLATEPATH(xbmcaddon.Addon().getAddonInfo('profile') + '/cache.db') +cache_table = 'cache' + + +def get(function, duration, *args, **kwargs): + # type: (function, int, object) -> object or None + """ + Gets cached value for provided function with optional arguments, or executes and stores the result + :param function: Function to be executed + :param duration: Duration of validity of cache in hours + :param args: Optional arguments for the provided function + :param kwargs: Optional keyword arguments for the provided function + """ + + key = _hash_function(function, *args, **kwargs) + cache_result = cache_get(key) + + if cache_result: + if _is_cache_valid(cache_result['date'], duration): + return pickle.loads(zlib.decompress(cache_result['value'])) + + fresh_result = function(*args, **kwargs) + + if not fresh_result: + # If the cache is old, but we didn't get fresh result, return the + # old cache + if cache_result: + return pickle.loads(zlib.decompress(cache_result['value'])) + return None + + cache_insert(key, Binary(zlib.compress(pickle.dumps(fresh_result)))) + return fresh_result + + +def remove(function, *args, **kwargs): + key = _hash_function(function, *args, **kwargs) + cursor = _get_connection_cursor() + cursor.execute("DELETE FROM %s WHERE key = ?" % cache_table, [key]) + cursor.connection.commit() + + +def timeout(function, *args, **kwargs): + key = _hash_function(function, *args, **kwargs) + result = cache_get(key) + return int(result['date']) + + +def cache_get(key): + # type: (str, str) -> dict or None + try: + cursor = _get_connection_cursor() + cursor.execute("SELECT * FROM %s WHERE key = ?" % cache_table, [key]) + return cursor.fetchone() + except OperationalError: + return None + + +def cache_insert(key, value): + # type: (str, str) -> None + cursor = _get_connection_cursor() + now = int(time.time()) + cursor.execute( + "CREATE TABLE IF NOT EXISTS %s (key TEXT, value BINARY, date INTEGER, UNIQUE(key))" % + cache_table) + cursor.execute( + "CREATE UNIQUE INDEX if not exists index_key ON %s (key)" % cache_table) + update_result = cursor.execute( + "UPDATE %s SET value=?,date=? WHERE key=?" + % cache_table, (value, now, key)) + + if update_result.rowcount == 0: + cursor.execute( + "INSERT INTO %s Values (?, ?, ?)" + % cache_table, (key, value, now) + ) + + cursor.connection.commit() + + +def cache_clear(): + cursor = _get_connection_cursor() + + for t in [cache_table, 'rel_list', 'rel_lib']: + try: + cursor.execute("DROP TABLE IF EXISTS %s" % t) + cursor.execute("VACUUM") + cursor.commit() + except BaseException: + pass + + +def _get_connection_cursor(): + conn = _get_connection() + return conn.cursor() + + +def _get_connection(): + conn = db.connect(cacheFile) + conn.row_factory = _dict_factory + return conn + + +def _dict_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + + +def _hash_function(function_instance, *args, **kwargs): + return _get_function_name(function_instance) + _generate_md5(*args, **kwargs) + + +def _get_function_name(function_instance): + return re.sub( + r'.+\smethod\s|.+function\s|\sat\s.+|\sof\s.+', + '', + repr(function_instance)) + + +def _generate_md5(*args, **kwargs): + md5_hash = hashlib.md5() + [md5_hash.update(str(arg) if six.PY2 else str(arg).encode()) for arg in args] + md5_hash.update(json.dumps(kwargs) if six.PY2 else json.dumps(kwargs).encode()) + return md5_hash.hexdigest() + + +def _is_cache_valid(cached_time, cache_timeout): + now = int(time.time()) + diff = now - cached_time + return (cache_timeout * 3600) > diff diff --git a/plugin.video.imdb.trailers/resources/lib/client.py b/plugin.video.imdb.trailers/resources/lib/client.py new file mode 100644 index 0000000000..f20b0a9476 --- /dev/null +++ b/plugin.video.imdb.trailers/resources/lib/client.py @@ -0,0 +1,120 @@ +""" + IMdB Trailers Add-on + + 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 . +""" + +import re +import six +from six.moves import urllib_request, urllib_parse, urllib_error +import gzip +import json +import ssl +from kodi_six import xbmc, xbmcvfs + +TRANSLATEPATH = xbmcvfs.translatePath if six.PY3 else xbmc.translatePath +CERT_FILE = TRANSLATEPATH('special://xbmc/system/certs/cacert.pem') + + +def request(url, headers=None, post=None, timeout='20'): + _headers = {} + if headers: + _headers.update(headers) + + handlers = [] + ssl_context = ssl.create_default_context(cafile=CERT_FILE) + ssl_context.set_alpn_protocols(['http/1.1']) + handlers += [urllib_request.HTTPSHandler(context=ssl_context)] + opener = urllib_request.build_opener(*handlers) + opener = urllib_request.install_opener(opener) + + if 'User-Agent' in _headers: + pass + else: + _headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0' + + if 'Accept-Language' not in _headers: + _headers['Accept-Language'] = 'en-US,en' + + if 'Accept-Encoding' in _headers: + pass + else: + _headers['Accept-Encoding'] = 'gzip' + + req = urllib_request.Request(url) + + if post is not None: + post = json.dumps(post) + post = six.ensure_binary(post) + req = urllib_request.Request(url, post) + req.add_header('Content-Type', 'application/json') + + _add_request_header(req, _headers) + + try: + response = urllib_request.urlopen(req, timeout=int(timeout)) + except urllib_error.URLError: + return '' + except urllib_error.HTTPError: + return '' + + result = response.read() + encoding = None + + if response.headers.get('content-encoding', '').lower() == 'gzip': + result = gzip.GzipFile(fileobj=six.BytesIO(result)).read() + + content_type = response.headers.get('content-type', '').lower() + + if 'charset=' in content_type: + encoding = content_type.split('charset=')[-1] + + if encoding is None: + epatterns = [r'. +""" + +# Imports +import re +import sys +import datetime +import json +import threading +from kodi_six import xbmc, xbmcgui, xbmcplugin, xbmcaddon, xbmcvfs +from bs4 import BeautifulSoup, SoupStrainer +from resources.lib import client, cache +import six +from six.moves import urllib_parse + +# HTMLParser() deprecated in Python 3.4 and removed in Python 3.9 +if sys.version_info >= (3, 4, 0): + import html + _html_parser = html +else: + from six.moves import html_parser + _html_parser = html_parser.HTMLParser() + +_addon = xbmcaddon.Addon() +_addonID = _addon.getAddonInfo('id') +_plugin = _addon.getAddonInfo('name') +_version = _addon.getAddonInfo('version') +_icon = _addon.getAddonInfo('icon') +_fanart = _addon.getAddonInfo('fanart') +_language = _addon.getLocalizedString +_settings = _addon.getSetting +_addonpath = 'special://profile/addon_data/{}/'.format(_addonID) +_kodiver = float(xbmcaddon.Addon('xbmc.addon').getAddonInfo('version')[:4]) +# DEBUG +DEBUG = _settings("DebugMode") == "true" +# View Mode +force_mode = _settings("forceViewMode") == "true" +if force_mode: + menu_mode = int(_settings('MenuMode')) + view_mode = int(_settings('VideoMode')) + +if not xbmcvfs.exists(_addonpath): + xbmcvfs.mkdir(_addonpath) + +SHOWING_URL = 'https://www.imdb.com/showtimes/_ajax/location/' +COMING_URL = 'https://www.imdb.com/calendar/?type=MOVIE' +DETAILS_PAGE = "https://www.imdb.com/video/{0}/" +quality = int(_settings("video_quality")[:-1]) +cache_duration = int(_settings('timeout')) +LOGINFO = xbmc.LOGINFO if six.PY3 else xbmc.LOGNOTICE + +if not xbmcvfs.exists(_addonpath + 'settings.xml'): + _addon.openSettings() + + +class Main(object): + def __init__(self): + self.api_url = 'https://graphql.prod.api.imdb.a2z.com/' + self.headers = { + 'Referer': 'https://www.imdb.com/', + 'Origin': 'https://www.imdb.com' + } + self.litems = [] + action = self.parameters('action') + if action == 'list2': + self.list_contents2() + elif action == 'list1': + self.list_contents1() + elif action == 'play_id': + self.play_id() + elif action == 'play': + self.play() + elif action == 'search': + self.search() + elif action == 'search_word': + self.search_word() + elif action == 'clear': + self.clear_cache() + else: + self.main_menu() + + def gqlmin(self, q): + q = re.sub(' {4}', '', q) + return q + + def main_menu(self): + if DEBUG: + self.log('main_menu()') + category = [{'title': _language(30201), 'key': 'showing'}, + {'title': _language(30202), 'key': 'coming'}, + {'title': _language(30208), 'key': 'trending'}, + {'title': _language(30209), 'key': 'anticipated'}, + {'title': _language(30210), 'key': 'popular'}, + {'title': _language(30211), 'key': 'recent'}, + {'title': _language(30206), 'key': 'search'}, + {'title': _language(30207), 'key': 'cache'}] + for i in category: + listitem = xbmcgui.ListItem(i['title']) + listitem.setArt({'thumb': _icon, + 'fanart': _fanart, + 'icon': _icon}) + + if i['key'] == 'cache': + url = sys.argv[0] + '?' + urllib_parse.urlencode({'action': 'clear'}) + elif i['key'] == 'search': + url = sys.argv[0] + '?' + urllib_parse.urlencode({'action': 'search'}) + elif i['key'] == 'showing' or i['key'] == 'coming': + url = sys.argv[0] + '?' + urllib_parse.urlencode({'action': 'list1', + 'key': i['key']}) + else: + url = sys.argv[0] + '?' + urllib_parse.urlencode({'action': 'list2', + 'key': i['key']}) + + xbmcplugin.addDirectoryItems(int(sys.argv[1]), [(url, listitem, True)]) + + # Sort methods and content type... + xbmcplugin.addSortMethod(handle=int(sys.argv[1]), sortMethod=xbmcplugin.SORT_METHOD_NONE) + xbmcplugin.setContent(int(sys.argv[1]), 'addons') + if force_mode: + xbmc.executebuiltin('Container.SetViewMode({})'.format(menu_mode)) + # End of directory... + xbmcplugin.endOfDirectory(int(sys.argv[1]), True) + + def clear_cache(self): + """ + Clear the cache database. + """ + if DEBUG: + self.log('clear_cache()') + msg = 'Cached Data has been cleared' + cache.cache_clear() + xbmcgui.Dialog().notification(_plugin, msg, _icon, 3000, False) + + def search(self): + if DEBUG: + self.log('search()') + keyboard = xbmc.Keyboard() + keyboard.setHeading('Search IMDb by Title') + keyboard.doModal() + if keyboard.isConfirmed(): + search_text = urllib_parse.quote(keyboard.getText()) + else: + search_text = '' + xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=False) + if len(search_text) > 2: + url = sys.argv[0] + '?' + urllib_parse.urlencode({'action': 'search_word', + 'keyword': search_text}) + xbmc.executebuiltin("Container.Update({0})".format(url)) + else: + msg = 'Need atleast 3 characters' + xbmcgui.Dialog().notification(_plugin, msg, _icon, 3000, False) + xbmc.executebuiltin("Container.Update({0},replace)".format(sys.argv[0])) + + def search_word(self): + search_text = urllib_parse.unquote(self.parameters('keyword')) + if DEBUG: + self.log('search_word("{0}")'.format(search_text)) + + variables = { + 'searchTerm': search_text, + } + query = '''query ( + $searchTerm: String! + ) { + mainSearch( + first: 5 + options: { + searchTerm: $searchTerm + isExactMatch: false + type: TITLE + titleSearchOptions: { type: MOVIE } + } + ) { + edges { + node { + entity { + ... on Title { + id + titleText { + text + } + plot { + plotText { + plainText + } + } + primaryImage { + url + } + releaseDate { + year + } + ratingsSummary { + aggregateRating + voteCount + } + certificate { + rating + } + titleGenres { + genres { + genre { + text + } + } + } + directors: credits(first: 5, filter: { categories: ["director"] }) { + edges { + node { + name { + nameText { + text + } + } + } + } + } + cast: credits(first: 10, filter: { categories: ["actor", "actress"] }) { + edges { + node { + name { + nameText { + text + } + primaryImage { + url + } + } + } + } + } + latestTrailer { + id + runtime { + value + } + thumbnail { + url + } + } + } + } + } + } + } + } + ''' + + pdata = {'query': self.gqlmin(query), 'variables': variables} + data = client.request(self.api_url, headers=self.headers, post=pdata) + items = data.get('data').get('mainSearch').get('edges') + for item in items: + video = item.get('node').get('entity') + title = video.get('titleText').get('text') + if video.get('latestTrailer'): + videoId = video.get('latestTrailer').get('id') + duration = video.get('latestTrailer').get('runtime').get('value') + labels = {'title': title, + 'duration': duration} + plot = video.get('plot') + if plot: + labels.update({'plot': plot.get('plotText').get('plainText')}) + + try: + director = [x.get('node').get('name').get('nameText').get('text') for x in video.get('directors').get('edges')] + labels.update({'director': director}) + except AttributeError: + pass + + try: + writer = [x.get('node').get('name').get('nameText').get('text') for x in video.get('writers').get('edges')] + labels.update({'writer': writer}) + except AttributeError: + pass + + try: + cast = [x.get('node').get('name').get('nameText').get('text') for x in video.get('cast').get('edges')] + labels.update({'cast': cast}) + cast2 = [ + {'name': x.get('node').get('name').get('nameText').get('text'), + 'thumbnail': x.get('node').get('name').get('primaryImage').get('url') + if x.get('node').get('name').get('primaryImage') else ''} + for x in video.get('cast').get('edges') + ] + + except AttributeError: + cast2 = [] + pass + + try: + genre = [x.get('genre').get('text') for x in video.get('titleGenres').get('genres')] + labels.update({'genre': genre}) + except AttributeError: + pass + + cert = video.get('certificate') + if cert: + labels.update({'mpaa': cert.get('rating')}) + + rating = video.get('ratingsSummary') + if rating.get('aggregateRating'): + labels.update({'rating': rating.get('aggregateRating'), + 'votes': rating.get('voteCount')}) + + try: + fanart = video.get('latestTrailer').get('thumbnail').get('url') + except AttributeError: + fanart = '' + try: + poster = video.get('primaryImage').get('url') + except AttributeError: + poster = fanart + try: + year = video.get('releaseDate').get('year') + except AttributeError: + year = '' + + if year: + labels.update({'year': year}) + + labels.update({'mediatype': 'movie'}) + if 'mpaa' in labels.keys(): + if 'TV' in labels.get('mpaa'): + labels.update({'mediatype': 'tvshow'}) + + listitem = self.make_listitem(labels, cast2) + listitem.setArt({'thumb': poster, + 'icon': poster, + 'poster': poster, + 'fanart': fanart}) + + listitem.setProperty('IsPlayable', 'true') + url = sys.argv[0] + '?' + urllib_parse.urlencode({'action': 'play', + 'videoid': videoId}) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), url, listitem, False) + + # Sort methods and content type... + xbmcplugin.setContent(int(sys.argv[1]), 'movies') + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + if force_mode: + xbmc.executebuiltin('Container.SetViewMode({})'.format(view_mode)) + # End of directory... + xbmcplugin.endOfDirectory(int(sys.argv[1]), updateListing=True, cacheToDisc=False) + + def process_imdbid(self, imdbID): + video = self.fetchdata_id(imdbID) + video = video.get('data').get('title') + title = video.get('titleText').get('text') + if video.get('latestTrailer'): + videoId = video.get('latestTrailer').get('id') + duration = video.get('latestTrailer').get('runtime').get('value') + labels = {'title': title, + 'duration': duration} + plot = video.get('plot') + if plot: + labels.update({'plot': plot.get('plotText').get('plainText')}) + + try: + director = [x.get('node').get('name').get('nameText').get('text') for x in video.get('directors').get('edges')] + labels.update({'director': director}) + except AttributeError: + pass + + try: + writer = [x.get('node').get('name').get('nameText').get('text') for x in video.get('writers').get('edges')] + labels.update({'writer': writer}) + except AttributeError: + pass + + try: + cast = [ + (x.get('node').get('name').get('nameText').get('text'), + x.get('node').get('characters')[0].get('name') + if x.get('node').get('characters') else '') + for x in video.get('cast').get('edges') + ] + labels.update({'cast': cast}) + cast2 = [ + {'name': x.get('node').get('name').get('nameText').get('text'), + 'role': x.get('node').get('characters')[0].get('name') if x.get('node').get('characters') else '', + 'thumbnail': x.get('node').get('name').get('primaryImage').get('url') + if x.get('node').get('name').get('primaryImage') else ''} + for x in video.get('cast').get('edges') + ] + + except AttributeError: + cast2 = [] + pass + + try: + genre = [x.get('genre').get('text') for x in video.get('titleGenres').get('genres')] + labels.update({'genre': genre}) + except AttributeError: + pass + + cert = video.get('certificate') + if cert: + labels.update({'mpaa': cert.get('rating')}) + + rating = video.get('ratingsSummary') + if rating.get('aggregateRating'): + labels.update({'rating': rating.get('aggregateRating'), + 'votes': rating.get('voteCount')}) + + try: + fanart = video.get('latestTrailer').get('thumbnail').get('url') + except AttributeError: + fanart = '' + try: + poster = video.get('primaryImage').get('url') + except AttributeError: + poster = fanart + try: + year = video.get('releaseDate').get('year') + except AttributeError: + year = '' + + if year: + labels.update({'year': year}) + + labels.update({'mediatype': 'movie'}) + if 'mpaa' in labels.keys(): + if 'TV' in labels.get('mpaa'): + labels.update({'mediatype': 'tvshow'}) + + art = { + 'thumb': poster, + 'icon': poster, + 'poster': poster, + 'fanart': fanart + } + self.litems.append({'labels': labels, 'cast2': cast2, 'art': art, 'videoId': videoId}) + return + + def get_contents1(self, key): + if key == 'showing': + page_data = client.request(SHOWING_URL, headers=self.headers) + tlink = SoupStrainer('div', {'class': 'lister-list'}) + mdiv = BeautifulSoup(page_data, "html.parser", parse_only=tlink) + videos = mdiv.find_all('div', {'class': 'lister-item'}) + imdbIDs = [x.find('div', {'class': 'lister-item-image'}).get('data-tconst') for x in videos] + else: + page_data = client.request(COMING_URL, headers=self.headers) + imdbIDs = re.findall(r'
  • ', paginate.group(0)) + if next_page_match: + page_base_url = next_page_match.group(1) + next_page = int(next_page_match.group(2)) + else: + next_page = current_page + page_range = list(range(current_page, current_page+1)) + else: + pages = re.findall(r'',paginate.group(0),re.DOTALL) + if pages: + last = pages[-1] + last_page = re.search(r' current_page: + page_url = page_base_url + str(page) + html = OpenURL(page_url) + + masthead_title = '' + masthead_title_match = re.search(r'(.+?)', html) + if masthead_title_match: + masthead_title = masthead_title_match.group(1) + else: + alternative_masthead_title_match = re.search(r'
    .*?([^<]+?)', html, re.M | re.S) + if alternative_masthead_title_match: + masthead_title = alternative_masthead_title_match.group(1) + + list_item_num = 1 + + programmes = html.split('
  • ') + for programme in programmes: + + if not re.search(r'programme--radio', programme): + continue + + series_id = '' + series_id_match = re.search(r'/programmes/(.+?)/episodes/player', programme) + if series_id_match: + series_id = series_id_match.group(1) + + programme_id = '' + programme_id_match = re.search(r'data-pid="(.+?)"', programme) + if programme_id_match: + programme_id = programme_id_match.group(1) + + name = '' + name_match = re.search(r'(.+?)', programme) + if name_match: + name = name_match.group(1) + else: + alternative_name_match = re.search(r'', programme) + if image_match: + image = image_match.group(1) + + synopsis = '' + synopsis_match = re.search(r'p class="programme__synopsis.*?(.+?)<\/span>', programme) + if synopsis_match: + synopsis = synopsis_match.group(1) + + station = '' + station_match = re.search(r'

    (.+?)<\/p>', programme) + if station_match: + station = station_match.group(1).strip() + + series_title = "[B]%s - %s[/B]" % (station, name) + if just_episodes: + title = "[B]%s[/B] - %s" % (masthead_title, name) + else: + title = "[B]%s[/B] - %s" % (station, name) + + if series_id: + AddMenuEntry(series_title, series_id, 131, image, synopsis, '') + elif programme_id: #TODO maybe they are not always mutually exclusive + url = "https://www.bbc.co.uk/sounds/play/%s" % programme_id + CheckAutoplay(title, url, image, ' ', '') + + percent = int(100*(page+list_item_num/len(programmes))/total_pages) + pDialog.update(percent,translation(30319),name) + + list_item_num += 1 + + percent = int(100*page/total_pages) + pDialog.update(percent,translation(30319)) + + + if int(ADDON.getSetting('radio_paginate_episodes')) == 0: + if current_page < next_page: + page_url = 'http://www.bbc.co.uk' + page_base_url + str(next_page) + AddMenuEntry(" [COLOR ffffa500]%s >>[/COLOR]" % translation(30320), page_url, 138, '', '', '') + + #BUG: this should sort by original order but it doesn't (see http://trac.kodi.tv/ticket/10252) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + + +def GetPage(page_url, just_episodes=False): + """ Generic Radio page scraper. """ + + with ProgressDlg(translation(30319)) as pDialog: + html = OpenURL(page_url) + + total_pages = 1 + current_page = 1 + page_range = list(range(1)) + paginate = re.search(r'',html) + next_page = 1 + if paginate: + if int(ADDON.getSetting('radio_paginate_episodes')) == 0: + current_page_match = re.search(r'page=(\d*)', page_url) + if current_page_match: + current_page = int(current_page_match.group(1)) + page_range = list(range(current_page, current_page+1)) + next_page_match = re.search(r'

  • ', paginate.group(0)) + if next_page_match: + page_base_url = next_page_match.group(1) + next_page = int(next_page_match.group(2)) + else: + next_page = current_page + page_range = list(range(current_page, current_page+1)) + else: + pages = re.findall(r'',paginate.group(0)) + if pages: + last = pages[-1] + last_page = re.search(r' current_page: + page_url = 'http://www.bbc.co.uk' + page_base_url + str(page) + html = OpenURL(page_url) + + masthead_title = '' + masthead_title_match = re.search(r'(.+?)', html) + if masthead_title_match: + masthead_title = masthead_title_match.group(1) + else: + alternative_masthead_title_match = re.search(r'
    .*?([^<]+?)', html, re.M | re.S) + if alternative_masthead_title_match: + masthead_title = alternative_masthead_title_match.group(1) + + list_item_num = 1 + data = '' + data_match = re.findall(r'', html, re.DOTALL) + if match: + data = match.group(1) + json_data = json.loads(data) + # print(json_data) + if 'modules' in json_data: + # print('Has modules') + if 'data' in json_data['modules']: + # print('Has data') + for data in json_data['modules']['data']: + # print('Data-ID: '+data['id']) + if ('id' in data and data['id'] == 'container_list'): + if 'data' in data: + for programme in data['data']: + # print(programme) + pro_name = [] + pro_url = [] + pro_icon = [] + pro_syn = [] + pro_brand = [] + pro_brand_url = [] + pro_brand_syn = [] + if 'titles' in programme: + pro_name = programme['titles']['primary'] + if ('secondary' in programme['titles'] and programme['titles']['secondary'] is not None): + pro_name += ' - '+programme['titles']['secondary'] + if ('tertiary' in programme['titles'] and programme['titles']['tertiary'] is not None): + pro_name += ' - '+programme['titles']['tertiary'] + if 'image_url' in programme: + pro_icon = programme['image_url'].replace("{recipe}","624x624") + if 'urn' in programme: + pro_url = 'https://www.bbc.co.uk/sounds/play/'+programme['urn'][-8:] + if 'synopses' in programme: + if ('long' in programme['synopses'] and programme['synopses']['long'] is not None): + pro_syn = programme['synopses']['long'] + elif ('medium' in programme['synopses'] and programme['synopses']['medium'] is not None): + pro_syn = programme['synopses']['medium'] + elif ('short' in programme['synopses'] and programme['synopses']['short'] is not None): + pro_syn = programme['synopses']['short'] + # print(pro_name) + # print(pro_icon) + # print(pro_url) + # print(pro_syn) + CheckAutoplay(pro_name, pro_url, pro_icon, pro_syn, '') + if ('container' in programme and programme['container'] is not None): + # print('Has container') + if programme['container']['type'] == 'brand': + pro_brand = '[B]'+programme['container']['title']+'[/B]' + pro_brand_url = 'https://www.bbc.co.uk/sounds/brand/'+programme['container']['id'] + # print(pro_brand_url) + if 'synopses' in programme['container']: + if ('long' in programme['container']['synopses'] and + programme['container']['synopses']['long'] is not None): + pro_brand_syn = programme['container']['synopses']['long'] + elif ('medium' in programme['container']['synopses'] and + programme['container']['synopses']['medium'] is not None): + pro_brand_syn = programme['container']['synopses']['medium'] + elif ('short' in programme['container']['synopses'] and + programme['container']['synopses']['short'] is not None): + pro_brand_syn = programme['container']['synopses']['short'] + if not(page_base_url.startswith(pro_brand_url)): + AddMenuEntry(pro_brand, pro_brand_url, 137, pro_icon, pro_brand_syn, '') + percent = int(100*page/total_pages) + pDialog.update(percent,translation(30319)) + + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + + +def GetEpisodes(url): + new_url = 'https://www.bbc.co.uk/programmes/%s/episodes/player' % url + GetPage(new_url,True) + + +def AddAvailableLiveStreamItem(name, channelname, iconimage): + """Play a live stream based on settings for preferred live source and bitrate.""" + URL = 'https://www.bbc.co.uk/sounds/play/live:'+channelname + jwt = GetJWT(URL) + streams = ParseStreams(channelname, jwt) + # print('Located live streams') + # print(streams) + source = int(ADDON.getSetting('radio_source')) + if source > 0: + # Case 1: Selected source + match = [x for x in streams if (x[2] == source)] + if len(match) == 0: + # Fallback: Use any source and any bitrate + match = streams + else: + # Case 3: Any source + # Play highest available bitrate + match = streams + PlayStream(name, match[0][0], iconimage, '', '') + + +def AddAvailableLiveStreamsDirectory(name, channelname, iconimage): + URL = 'https://www.bbc.co.uk/sounds/play/live:'+channelname + jwt = GetJWT(URL) + streams = ParseStreams(channelname, jwt) + suppliers = ['', 'Akamai', 'Limelight', 'Cloudfront'] + for href, protocol, supplier, transfer_format, bitrate in streams: + title = name + ' - [I][COLOR ffd3d3d3]%s - %s kbps[/COLOR][/I]' % (suppliers[supplier], bitrate) + AddMenuEntry(title, href, 211, iconimage, '', '', '') + + +def PlayStream(name, url, iconimage, description, subtitles_url): + html = OpenURL(url) + + check_geo = re.search( + '

    Access Denied

    ', html) + if check_geo or not html: + # print "Geoblock detected, raising error message" + raise GeoBlockedError(translation(30414)) + liz = xbmcgui.ListItem(name) + liz.setArt({'icon':'DefaultVideo.png', 'thumb':iconimage}) + + liz.setInfo(type='Audio', infoLabels={'Title': name}) + liz.setProperty("IsPlayable", "true") + liz.setPath(url) + liz.setProperty('inputstream', 'inputstream.adaptive') + liz.setProperty('inputstream.adaptive.manifest_type', 'mpd') + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, liz) + + +def AddAvailableStreamsDirectory(name, stream_id, iconimage, description): + """Will create one menu entry for each available stream of a particular stream_id""" + + streams = ParseStreams(stream_id, None) + suppliers = ['', 'Akamai', 'Limelight', 'Cloudfront'] + for href, protocol, supplier, transfer_format, bitrate in streams: + title = name + ' - [I][COLOR ffd3d3d3]%s - %s kbps[/COLOR][/I]' % (suppliers[supplier], bitrate) + AddMenuEntry(title, href, 211, iconimage, description, '', '') + + +def AddAvailableStreamItem(name, url, iconimage, description): + """Play a streamm based on settings for preferred catchup source and bitrate.""" + stream_ids = ScrapeAvailableStreams(url) + if len(stream_ids) < 1: + #TODO check CBeeBies for special cases + xbmcgui.Dialog().ok(translation(30403), translation(30404)) + return + streams = ParseStreams(stream_ids, None) + source = int(ADDON.getSetting('radio_source')) + if source > 0: + # Case 1: Selected source + match = [x for x in streams if (x[2] == source)] + if len(match) == 0: + # Fallback: Use any source and any bitrate + match = streams + else: + # Case 3: Any source + # Play highest available bitrate + match = streams + PlayStream(name, match[0][0], iconimage, description, '') + + + +def ListAtoZ(): + """List programmes based on alphabetical order. + + Only creates the corresponding directories for each character. + """ + characters = [ + ('A', 'a'), ('B', 'b'), ('C', 'c'), ('D', 'd'), ('E', 'e'), ('F', 'f'), + ('G', 'g'), ('H', 'h'), ('I', 'i'), ('J', 'j'), ('K', 'k'), ('L', 'l'), + ('M', 'm'), ('N', 'n'), ('O', 'o'), ('P', 'p'), ('Q', 'q'), ('R', 'r'), + ('S', 's'), ('T', 't'), ('U', 'u'), ('V', 'v'), ('W', 'w'), ('X', 'x'), + ('Y', 'y'), ('Z', 'z'), ('0-9', '@')] + + for name, url in characters: + url = 'https://www.bbc.co.uk/programmes/a-z/by/%s/player' % url + AddMenuEntry(name, url, 138, '', '', '') + + +def ListGenres(): + """List programmes based on alphabetical order. + + Only creates the corresponding directories for each character. + """ + genres = [] + html = OpenURL('https://www.bbc.co.uk/sounds/categories') + match = re.search(r'window.__PRELOADED_STATE__ = (.*?);\s*', html, re.DOTALL) + if match: + data = match.group(1) + json_data = json.loads(data) + # print(json_data) + if 'modules' in json_data: + if 'data' in json_data['modules']: + for type_id in json_data['modules']['data']: + # print(type_id) + if 'data' in type_id: + for category in type_id['data']: + # print(category) + cat_name = [] + cat_image = [] + cat_url = [] + if 'titles' in category: + cat_name = category['titles']['primary'] + if ('secondary' in category['titles'] and category['titles']['secondary'] is not None): + cat_name += ' - '+category['titles']['secondary'] + if ('tertiary' in category['titles'] and category['titles']['tertiary'] is not None): + cat_name += ' - '+category['titles']['tertiary'] + if 'image_url' in category: + cat_image = category['image_url'].replace("{recipe}","624x624") + if 'id' in category: + cat_url = 'https://www.bbc.co.uk/sounds/category/'+category['id'] + # print(cat_name) + # print(cat_image) + # print(cat_url) + AddMenuEntry(cat_name, cat_url, 137, cat_image, '', '') + + +def ListLive(): + channel_list = [ + ('bbc_radio_one', 'BBC Radio 1'), + ('bbc_radio_one_dance', 'BBC Radio 1 Dance'), + ('bbc_radio_one_relax', 'BBC Radio 1 Relax'), + ('bbc_1xtra', 'BBC Radio 1Xtra'), + ('bbc_radio_two', 'BBC Radio 2'), + ('bbc_radio_three', 'BBC Radio 3'), + ('bbc_radio_fourfm', 'BBC Radio 4'), + ('bbc_radio_fourlw', 'BBC Radio 4 LW'), + ('bbc_radio_four_extra', 'BBC Radio 4 Extra'), + ('bbc_radio_five_live', 'BBC Radio 5 Live'), + ('bbc_radio_five_live_sports_extra', 'BBC Radio 5 Sports Extra'), + ('bbc_6music', 'BBC Radio 6 Music'), + ('bbc_asian_network', 'BBC Asian Network'), + ('bbc_world_service', 'BBC World Service'), + ('bbc_radio_scotland_fm', 'BBC Radio Scotland'), + ('bbc_radio_scotland_mw', 'BBC Radio Scotland Extra'), + ('bbc_radio_orkney', 'BBC Radio Orkney'), + ('bbc_radio_shetland', 'BBC Radio Shetland'), + ('bbc_radio_nan_gaidheal', u'BBC Radio nan Gàidheal'), + ('bbc_radio_ulster', 'BBC Radio Ulster'), + ('bbc_radio_foyle', 'BBC Radio Foyle'), + ('bbc_radio_wales_fm', 'BBC Radio Wales'), + ('bbc_radio_wales_am', 'BBC Radio Wales Extra'), + ('bbc_radio_cymru', 'BBC Radio Cymru'), + ('bbc_radio_cymru_2', 'BBC Radio Cymru 2'), + ('cbeebies_radio', 'CBeebies Radio'), + ('bbc_radio_berkshire', 'BBC Radio Berkshire'), + ('bbc_radio_bristol', 'BBC Radio Bristol'), + ('bbc_radio_cambridge', 'BBC Radio Cambridgeshire'), + ('bbc_radio_cornwall', 'BBC Radio Cornwall'), + ('bbc_radio_coventry_warwickshire', 'BBC Coventry & Warwickshire'), + ('bbc_radio_cumbria', 'BBC Radio Cumbria'), + ('bbc_radio_derby', 'BBC Radio Derby'), + ('bbc_radio_devon', 'BBC Radio Devon'), + ('bbc_radio_essex', 'BBC Essex'), + ('bbc_radio_gloucestershire', 'BBC Radio Gloucestershire'), + ('bbc_radio_guernsey', 'BBC Radio Guernsey'), + ('bbc_radio_hereford_worcester', 'BBC Hereford & Worcester'), + ('bbc_radio_humberside', 'BBC Radio Humberside'), + ('bbc_radio_jersey', 'BBC Radio Jersey'), + ('bbc_radio_kent', 'BBC Radio Kent'), + ('bbc_radio_lancashire', 'BBC Radio Lancashire'), + ('bbc_radio_leeds', 'BBC Radio Leeds'), + ('bbc_radio_leicester', 'BBC Radio Leicester'), + ('bbc_radio_lincolnshire', 'BBC Radio Lincolnshire'), + ('bbc_london', 'BBC Radio London'), + ('bbc_radio_manchester', 'BBC Radio Manchester'), + ('bbc_radio_merseyside', 'BBC Radio Merseyside'), + ('bbc_radio_newcastle', 'BBC Newcastle'), + ('bbc_radio_norfolk', 'BBC Radio Norfolk'), + ('bbc_radio_northampton', 'BBC Radio Northampton'), + ('bbc_radio_nottingham', 'BBC Radio Nottingham'), + ('bbc_radio_oxford', 'BBC Radio Oxford'), + ('bbc_radio_sheffield', 'BBC Radio Sheffield'), + ('bbc_radio_shropshire', 'BBC Radio Shropshire'), + ('bbc_radio_solent', 'BBC Radio Solent'), + ('bbc_radio_solent_west_dorset', 'BBC Radio Solent Dorset'), + ('bbc_radio_somerset_sound', 'BBC Somerset'), + ('bbc_radio_stoke', 'BBC Radio Stoke'), + ('bbc_radio_suffolk', 'BBC Radio Suffolk'), + ('bbc_radio_surrey', 'BBC Surrey'), + ('bbc_radio_sussex', 'BBC Sussex'), + ('bbc_tees', 'BBC Tees'), + ('bbc_three_counties_radio', 'BBC Three Counties Radio'), + ('bbc_radio_wiltshire', 'BBC Wiltshire'), + ('bbc_wm', 'BBC WM'), + ('bbc_radio_york', 'BBC Radio York'), + ] + for id, name in channel_list: + iconimage = 'resource://resource.images.iplayerwww/media/'+id+'.png' + if ADDON.getSetting('streams_autoplay') == 'true': + AddMenuEntry(name, id, 213, iconimage, '', '') + else: + AddMenuEntry(name, id, 133, iconimage, '', '') + + +def ListListenList(): + """Scrapes all episodes of the favourites page.""" + html = OpenURL('http://www.bbc.co.uk/radio/favourites') + + programmes = html.split('
  • \s*(.+?)\s*', programme) + if subtitle_match: + if subtitle_match.group(1).strip(): + subtitle = "(%s)" % subtitle_match.group(1) + + image = '' + image_match = re.search(r'\s*(.+?)\s*', programme) + if station_match: + station = station_match.group(1) + + title = "[B]%s[/B] - %s %s" % (station, name, subtitle) + + if programme_id and title and image: + url = "http://www.bbc.co.uk/radio/play/%s" % programme_id + CheckAutoplay(title, url, image, ' ', '') + + #BUG: this should sort by original order but it doesn't (see http://trac.kodi.tv/ticket/10252) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + + + +def Search(search_entered): + """Simply calls the online search function. The search is then evaluated in EvaluateSearch.""" + if search_entered is None: + keyboard = xbmc.Keyboard('', 'Search iPlayer') + keyboard.doModal() + if keyboard.isConfirmed(): + search_entered = keyboard.getText() + + if search_entered is None: + return False + + url = 'https://www.bbc.co.uk/sounds/search?q=%s' % search_entered + html = OpenURL(url) + + with ProgressDlg(translation(30319)) as pDialog: + total_pages = 1 + current_page = 1 + page_range = list(range(1)) + pages = re.findall(r'
    (.+?)
    ',html) + next_page = 1 + if pages: + total_pages = int(pages[-2]) + page_base_url = url+'&page=' + page_range = list(range(1, total_pages+1)) + + for page in page_range: + + if page > current_page: + page_url = page_base_url + str(page) + html = OpenURL(page_url) + + match = re.search(r'window.__INITIAL_DATA__=(.*?);\s*', html, re.DOTALL) + + if match: + data = match.group(1) + json_data = json.loads(data) + # print(json_data) + if 'data' in json_data: + # print('Has data') + for data in json_data['data']: + if data.startswith('search-results'): + data = json_data['data'][data] + # print(data) + if ('name' in data and data['name'] == 'search-results'): + # print(data['name']) + if 'data' in data: + if 'initialResults' in data['data']: + if 'items' in data['data']['initialResults']: + for programme in data['data']['initialResults']['items']: + pro_name = [] + pro_url = [] + pro_icon = [] + pro_syn = [] + if 'headline' in programme: + pro_name = programme['headline'] + if 'image' in programme: + if 'src' in programme['image']: + pro_icon = programme['image']['src'].replace("314x176","416x234") + if 'url' in programme: + pro_url = 'https://www.bbc.co.uk/sounds/play/'+programme['url'][-8:] + if 'description' in programme: + pro_syn = programme['description'] + # print(pro_name) + # print(pro_icon) + # print(pro_url) + # print(pro_syn) + CheckAutoplay(pro_name, pro_url, pro_icon, pro_syn, '') + + percent = int(100*page/total_pages) + pDialog.update(percent,translation(30319)) + + if int(ADDON.getSetting('radio_paginate_episodes')) == 0: + if current_page < next_page: + page_url = 'http://www.bbc.co.uk' + page_base_url + str(next_page) + AddMenuEntry(" [COLOR ffffa500]%s >>[/COLOR]" % translation(30320), page_url, 136, '', '', '') + + #BUG: this should sort by original order but it doesn't (see http://trac.kodi.tv/ticket/10252) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + + +def GetAvailableStreams(name, url, iconimage, description): + """Calls AddAvailableStreamsDirectory based on user settings""" + stream_ids = ScrapeAvailableStreams(url) + # print('Scraped streams for '+url) + # print(stream_ids) + if stream_ids: + AddAvailableStreamsDirectory(name, stream_ids, iconimage, description) + + +def ParseStreams(stream_id, jwt): + retlist = [] + # print('Parsing streams for PID:') + # print(stream_id) + # Open the page with the actual strem information and display the various available streams. + if jwt: + NEW_URL = 'https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/pc/vpid/%s/format/json/jsfunc/JS_callbacks0?jwt_auth=%s' % (stream_id, jwt) + else: + NEW_URL = 'https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/pc/vpid/%s/format/json/jsfunc/JS_callbacks0' % stream_id + # print(NEW_URL) + html = OpenURL(NEW_URL) + + match = re.search(r'JS_callbacks0.*?\((.*?)\);', html, re.DOTALL) + if match: + json_data = json.loads(match.group(1)) + if json_data: + # print(json.dumps(json_data, sort_keys=True, indent=2)) + if 'media' in json_data: + for media in json_data['media']: + if 'kind' in media: + if media['kind'].startswith('audio'): + bitrate = '' + if 'bitrate' in media: + bitrate = media['bitrate'] + if 'connection' in media: + for connection in media['connection']: + href = '' + protocol = '' + supplier = '' + transfer_format = '' + if 'href' in connection: + href = connection['href'] + if 'protocol' in connection: + protocol = connection['protocol'] + if 'supplier' in connection: + supplier = connection['supplier'] + if ('akamai' in supplier): + supplier = 1 + elif ('limelight' in supplier or 'll' in supplier): + supplier = 2 + elif ('cloudfront' in supplier): + supplier = 3 + else: + supplier = 0 + if 'transferFormat' in connection: + transfer_format = connection['transferFormat'] + if transfer_format == 'dash': + retlist.append((href, protocol, supplier, transfer_format, bitrate)) + elif 'result' in json_data: + if json_data['result'] == 'geolocation': + # print "Geoblock detected, raising error message" + raise GeoBlockedError(translation(30414)) + return retlist + + +def ScrapeAvailableStreams(url): + # Open page and retrieve the stream ID + html = OpenURL(url) + stream_id_st = None + # Search for standard programmes. + stream_id_st = re.compile('"vpid":"(.+?)"').findall(html) + if not stream_id_st: + match = re.search(r'window.__PRELOADED_STATE__ = (.*?);\s*', html, re.DOTALL) + if match: + data = match.group(1) + json_data = json.loads(data) + stream_id_st = json_data['programmes']['current']['id'] + # print json.dumps(json_data, indent=2, sort_keys=True) + else: + dialog = xbmcgui.Dialog() + dialog.ok(translation(30400), translation(30412)) + raise + return stream_id_st + + +def CheckAutoplay(name, url, iconimage, plot, aired=None): + if ADDON.getSetting('streams_autoplay') == 'true': + AddMenuEntry(name, url, 212, iconimage, plot, '', aired=aired) + else: + AddMenuEntry(name, url, 132, iconimage, plot, '', aired=aired) + diff --git a/plugin.video.iplayerwww/resources/lib/ipwww_video.py b/plugin.video.iplayerwww/resources/lib/ipwww_video.py new file mode 100644 index 0000000000..3697ef45cf --- /dev/null +++ b/plugin.video.iplayerwww/resources/lib/ipwww_video.py @@ -0,0 +1,1593 @@ +# -*- coding: utf-8 -*- + +from __future__ import division + +import os +import sys +import re +import datetime +import time +import json + +from datetime import timedelta +from operator import itemgetter + +from resources.lib.ipwww_common import ( + translation, AddMenuEntry, OpenURL, OpenRequest, CheckLogin, CreateBaseDirectory, + GetCookieJar, ParseImageUrl, download_subtitles, GeoBlockedError, WebRequestError, + iso_duration_2_seconds, PostJson, strptime, addonid, DeleteUrl, ProgressDlg) +from resources.lib import ipwww_progress + +import xbmc +import xbmcvfs +import xbmcgui +import xbmcplugin +import xbmcaddon +from random import randint + +ADDON = xbmcaddon.Addon(id='plugin.video.iplayerwww') + + +def tp(path): + return xbmcvfs.translatePath(path) + + +def RedButtonDialog(): + if ADDON.getSetting('redbutton_warning') == 'true': + dialog = xbmcgui.Dialog() + ret = dialog.yesno(translation(30405), translation(30406) + "\n" + translation(30407), + translation(30409), translation(30408)) + if ret: + ListRedButton() + else: + ListRedButton() + + +def ListRedButton(): + channel_list = [ + ('sport_stream_01', 'BBC Red Button 1'), + ('sport_stream_02', 'BBC Red Button 2'), + ('sport_stream_03', 'BBC Red Button 3'), + ('sport_stream_04', 'BBC Red Button 4'), + ('sport_stream_05', 'BBC Red Button 5'), + ('sport_stream_06', 'BBC Red Button 6'), + ('sport_stream_07', 'BBC Red Button 7'), + ('sport_stream_08', 'BBC Red Button 8'), + ('sport_stream_09', 'BBC Red Button 9'), + ('sport_stream_10', 'BBC Red Button 10'), + ('sport_stream_11', 'BBC Red Button 11'), + ('sport_stream_12', 'BBC Red Button 12'), + ('sport_stream_13', 'BBC Red Button 13'), + ('sport_stream_14', 'BBC Red Button 14'), + ('sport_stream_15', 'BBC Red Button 15'), + ('sport_stream_16', 'BBC Red Button 16'), + ('sport_stream_17', 'BBC Red Button 17'), + ('sport_stream_18', 'BBC Red Button 18'), + ('sport_stream_19', 'BBC Red Button 19'), + ('sport_stream_20', 'BBC Red Button 20'), + ('sport_stream_21', 'BBC Red Button 21'), + ('sport_stream_22', 'BBC Red Button 22'), + ('sport_stream_23', 'BBC Red Button 23'), + ('sport_stream_24', 'BBC Red Button 24'), + ('sport_stream_01b', 'BBC Red Button 1b'), + ('sport_stream_02b', 'BBC Red Button 2b'), + ('sport_stream_03b', 'BBC Red Button 3b'), + ('sport_stream_04b', 'BBC Red Button 4b'), + ('sport_stream_05b', 'BBC Red Button 5b'), + ('sport_stream_06b', 'BBC Red Button 6b'), + ('sport_stream_07b', 'BBC Red Button 7b'), + ('sport_stream_08b', 'BBC Red Button 8b'), + ('sport_stream_09b', 'BBC Red Button 9b'), + ('sport_stream_10b', 'BBC Red Button 10b'), + ('sport_stream_11b', 'BBC Red Button 11b'), + ('sport_stream_12b', 'BBC Red Button 12b'), + ('sport_stream_13b', 'BBC Red Button 13b'), + ('sport_stream_14b', 'BBC Red Button 14b'), + ('sport_stream_15b', 'BBC Red Button 15b'), + ('sport_stream_16b', 'BBC Red Button 16b'), + ('sport_stream_17b', 'BBC Red Button 17b'), + ('sport_stream_18b', 'BBC Red Button 18b'), + ('sport_stream_19b', 'BBC Red Button 19b'), + ('sport_stream_20b', 'BBC Red Button 20b'), + ('sport_stream_21b', 'BBC Red Button 21b'), + ('sport_stream_22b', 'BBC Red Button 22b'), + ('sport_stream_23b', 'BBC Red Button 23b'), + ('sport_stream_24b', 'BBC Red Button 24b'), + ] + iconimage = tp('special://home/addons/plugin.video.iplayerwww/media/red_button.png') + for id, name in channel_list: + if ADDON.getSetting('streams_autoplay') == 'true': + AddMenuEntry(name, id, 203, iconimage, '', '') + else: + AddMenuEntry(name, id, 123, iconimage, '', '') + + +def ListUHDTrial(): + channel_list = [ + ('uhd_stream_01', 'UHD Trial 1'), + ('uhd_stream_02', 'UHD Trial 2'), + ('uhd_stream_03', 'UHD Trial 3'), + ('uhd_stream_04', 'UHD Trial 4'), + ('uhd_stream_05', 'UHD Trial 5'), + ] + iconimage = 'resource://resource.images.iplayerwww/media/red_button.png' + for id, name in channel_list: + AddMenuEntry(name, id, 205, iconimage, '', '') + + +def AddAvailableUHDTrialItem(name, channelname): + source = int(ADDON.getSetting('live_source')) + if (source == 1): + provider = "ak" + elif (source == 2): + provider = "llnw" + else: + provider = "ak" + + url = "http://a.files.bbci.co.uk/media/live/manifesto/audio_video/webcast/dash/uk/full/%s/%s.mpd" % (provider,channelname) + + PlayStream(name, url, "", "", "") + + +# ListLive creates menu entries for all live channels. +def ListLive(): + channel_list = [ + ('bbc_one_hd', 'BBC One', 'bbc_one_london'), + ('bbc_two_england', 'BBC Two', 'bbc_two_england'), + ('bbc_three_hd', 'BBC Three', 'bbc_three'), + ('bbc_four_hd', 'BBC Four', 'bbc_four'), + ('cbbc_hd', 'CBBC', 'cbbc'), + ('cbeebies_hd', 'CBeebies', 'cbeebies'), + ('bbc_news24', 'BBC News Channel', 'bbc_news24'), + ('bbc_parliament', 'BBC Parliament', 'bbc_parliament'), + ('bbc_alba', 'Alba', 'bbc_alba'), + ('bbc_scotland_hd', 'BBC Scotland', 'bbc_scotland'), + ('s4cpbs', 'S4C', 's4cpbs'), + ('bbc_one_london', 'BBC One London', 'bbc_one_london'), + ('bbc_one_scotland_hd', 'BBC One Scotland', 'bbc_one_scotland'), + ('bbc_one_northern_ireland_hd', 'BBC One Northern Ireland', 'bbc_one_northern_ireland'), + ('bbc_one_wales_hd', 'BBC One Wales', 'bbc_one_wales'), + ('bbc_two_scotland', 'BBC Two Scotland', 'bbc_two_england'), + ('bbc_two_northern_ireland_digital', 'BBC Two Northern Ireland', 'bbc_two_northern_ireland_digital'), + ('bbc_two_wales_digital', 'BBC Two Wales', 'bbc_two_wales_digital'), + ('bbc_two_england', 'BBC Two England', 'bbc_two_england'), + ('bbc_one_cambridge', 'BBC One Cambridge', 'bbc_one_london'), + ('bbc_one_channel_islands', 'BBC One Channel Islands', 'bbc_one_london'), + ('bbc_one_east', 'BBC One East', 'bbc_one_london'), + ('bbc_one_east_midlands', 'BBC One East Midlands', 'bbc_one_london'), + ('bbc_one_east_yorkshire', 'BBC One East Yorkshire', 'bbc_one_london'), + ('bbc_one_north_east', 'BBC One North East', 'bbc_one_london'), + ('bbc_one_north_west', 'BBC One North West', 'bbc_one_london'), + ('bbc_one_oxford', 'BBC One Oxford', 'bbc_one_london'), + ('bbc_one_south', 'BBC One South', 'bbc_one_london'), + ('bbc_one_south_east', 'BBC One South East', 'bbc_one_london'), + ('bbc_one_south_west', 'BBC One South West', 'bbc_one_london'), + ('bbc_one_west', 'BBC One West', 'bbc_one_london'), + ('bbc_one_west_midlands', 'BBC One West Midlands', 'bbc_one_london'), + ('bbc_one_yorks', 'BBC One Yorks', 'bbc_one_london'), + ] + from urllib.parse import urlencode + schedules = GetSchedules(channel_list) + for id, name, schedule_chan_id in channel_list: + now_on, schedule = schedules.get(schedule_chan_id, ('', '')) + title = '{} [COLOR orange]{}[/COLOR]'.format(name, now_on) + iconimage = 'resource://resource.images.iplayerwww/media/'+id+'.png' + + if ADDON.getSetting('streams_autoplay') == 'true': + mode = 203 + restart_action = 'PlayMedia' + else: + mode = 123 + restart_action = "Container.Update" + querystring = urlencode({'name': name, + 'url': id, + 'mode': mode, + 'iconimage': iconimage, + 'watch_from_start': 'True'}) + ctx_mnu = [(translation(30603), # 'Watch from the start' + ''.join((restart_action, '(plugin://', addonid, '?', querystring, + ', noresume)' if mode == 203 else ')')) + )] + AddMenuEntry(title, id, mode, iconimage, schedule, '', resume_time='0', context_mnu=ctx_mnu) + xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=False) + sys.exit() + + +def ListAtoZ(): + """List programmes based on alphabetical order. + + Only creates the corresponding directories for each character. + """ + characters = [ + ('A', 'a'), ('B', 'b'), ('C', 'c'), ('D', 'd'), ('E', 'e'), ('F', 'f'), + ('G', 'g'), ('H', 'h'), ('I', 'i'), ('J', 'j'), ('K', 'k'), ('L', 'l'), + ('M', 'm'), ('N', 'n'), ('O', 'o'), ('P', 'p'), ('Q', 'q'), ('R', 'r'), + ('S', 's'), ('T', 't'), ('U', 'u'), ('V', 'v'), ('W', 'w'), ('X', 'x'), + ('Y', 'y'), ('Z', 'z'), ('0-9', '0-9')] + + if int(ADDON.getSetting('scrape_atoz')) == 1: + with ProgressDlg(translation(30319)) as pDialog: + page = 1 + total_pages = len(characters) + for name, url in characters: + GetAtoZPage(url) + percent = int(100*page/total_pages) + pDialog.update(percent,translation(30319),name) + page += 1 + else: + for name, url in characters: + AddMenuEntry(name, url, 124, '', '', '') + +def ListChannelAtoZ(): + """List programmes for each channel based on alphabetical order. + + Only creates the corresponding directories for each channel. + """ + channel_list = [ + ('bbcone', 'bbc_one_hd', 'BBC One'), + ('bbctwo', 'bbc_two_hd', 'BBC Two'), + ('tv/bbcthree', 'bbc_three_hd', 'BBC Three'), + ('bbcfour', 'bbc_four_hd', 'BBC Four'), + ('tv/cbbc', 'cbbc_hd', 'CBBC'), + ('tv/cbeebies', 'cbeebies_hd', 'CBeebies'), + ('tv/bbcnews', 'bbc_news24', 'BBC News Channel'), + ('tv/bbcparliament', 'bbc_parliament', 'BBC Parliament'), + ('tv/bbcalba', 'bbc_alba', 'Alba'), + ('tv/bbcscotland', 'bbc_scotland_hd', 'BBC Scotland'), + ('tv/s4c', 's4cpbs', 'S4C'), + ] + for id, img, name in channel_list: + iconimage = 'resource://resource.images.iplayerwww/media/'+img+'.png' + url = "https://www.bbc.co.uk/%s/a-z" % id + AddMenuEntry(name, url, 134, iconimage, '', '') + + +def GetAtoZPage(url): + """Allows to list programmes based on alphabetical order. + + Creates the list of programmes for one character. + """ + if int(ADDON.getSetting('scrape_atoz')) == 1: + GetSingleAtoZPage(url) + else: + with ProgressDlg(translation(30319)) as pDialog: + GetSingleAtoZPage(url,pDialog) + + +def GetSingleAtoZPage(url, pDialog=None): + current_url = 'https://www.bbc.co.uk/iplayer/a-z/%s' % url + # print("Opening "+current_url) + try: + html = OpenURL(current_url) + except WebRequestError as err: + # Ignore missing pages; there are simply no programmes starting with this letter. + if err.status_code == 404: + return + else: + raise + + total_pages = 1 + current_page = 1 + page_range = list(range(1)) + + json_data = ScrapeJSON(html) + if json_data: + ParseJSON(json_data, current_url) + if 'pagination' in json_data: + current_page = int(json_data['pagination']['currentPage']) + total_pages = int(json_data['pagination']['totalPages']) + if pDialog: + percent = int(100*current_page/total_pages) + pDialog.update(percent,translation(30319)) + # print('Current page: %s'%current_page) + # print('Total pages: %s'%total_pages) + if current_page', html, re.DOTALL) + next_page = 1 + if paginate: + if int(ADDON.getSetting('paginate_episodes')) == 0: + current_page_match = re.search(r'page=(\d*)', page_url) + if current_page_match: + current_page = int(current_page_match.group(1)) + pages = re.findall(r'
  • ',paginate.group(0),re.DOTALL) + if pages: + last = pages[-1] + last_page = re.search(r'page=(\d*)', last) + split_page_url = page_url.replace('&','?').split('?') + page_base_url = split_page_url[0] + for part in split_page_url[1:len(split_page_url)]: + if not part.startswith('page'): + page_base_url = page_base_url+'?'+part + if '?' in page_base_url: + page_base_url = page_base_url.replace('https://www.bbc.co.uk','')+'&page=' + else: + page_base_url = page_base_url.replace('https://www.bbc.co.uk','')+'?page=' + total_pages = int(last_page.group(1)) + page_range = list(range(1, total_pages+1)) + + for page in page_range: + + if page > current_page: + page_url = 'https://www.bbc.co.uk' + page_base_url + str(page) + html = OpenURL(page_url) + + json_data = ScrapeJSON(html) + if json_data: + ParseJSON(json_data, page_url) + + percent = int(100*page/total_pages) + pDialog.update(percent,translation(30319)) + + if int(ADDON.getSetting('paginate_episodes')) == 0: + if current_page < next_page: + page_url = 'https://www.bbc.co.uk' + page_base_url + str(next_page) + AddMenuEntry(" [COLOR ffffa500]%s >>[/COLOR]" % translation(30320), page_url, 128, '', '', '') + + +def ScrapeAtoZEpisodes(page_url): + """Creates a list of programmes on one standard HTML page. + + ScrapeEpisodes contains a number of special treatments, which are only needed for + specific pages, e.g. Search, but allows to use a single function for all kinds + of pages. + """ + + with ProgressDlg(translation(30319)) as pDialog: + html = OpenURL(page_url) + + total_pages = 1 + current_page = 1 + page_range = list(range(1)) + + json_data = ScrapeJSON(html) + if json_data: + + last_page = 1 + current_page = 1 + if 'pagination' in json_data: + page_base_url_match = re.search(r'(.+?)page=', page_url) + if page_base_url_match: + page_base_url = page_base_url_match.group(0) + else: + page_base_url = page_url+"?page=" + current_page = json_data['pagination'].get('currentPage') + last_page = json_data['pagination'].get('totalPages') + if int(ADDON.getSetting('paginate_episodes')) == 0: + current_page_match = re.search(r'page=(\d*)', page_url) + if current_page_match: + current_page = int(current_page_match.group(1)) + page_base_url_match = re.search(r'(.+?)page=', page_url) + if page_base_url_match: + page_base_url = page_base_url_match.group(0) + else: + page_base_url = page_url+"?page=" + if current_page < last_page: + next_page = current_page+1 + else: + next_page = current_page + page_range = list(range(current_page, current_page+1)) + else: + page_range = list(range(1, last_page+1)) + + for page in page_range: + + if page > current_page: + page_url = page_base_url + str(page) + html = OpenURL(page_url) + + json_data = ScrapeJSON(html) + if json_data: + ParseJSON(json_data, page_url) + + percent = int(100*page/last_page) + pDialog.update(percent,translation(30319)) + + if int(ADDON.getSetting('paginate_episodes')) == 0: + if current_page < next_page: + page_url = page_base_url + str(next_page) + AddMenuEntry(" [COLOR ffffa500]%s >>[/COLOR]" % translation(30320), page_url, 134, '', '', '') + + +def ListCategories(): + """Parses the available categories and creates directories for selecting one of them. + The category names are scraped from the website. + """ + html = OpenURL('https://www.bbc.co.uk/iplayer') + match = re.compile( + '(.+?)' + ).findall(html) + for url, name in match: + if ((name == "View all") or (name == "A-Z")): + continue + AddMenuEntry(name, url, 126, '', '', '') + + +def ListCategoryFilters(url): + """Parses the available category filters (if available) and creates directories for selcting them. + If there are no filters available, all programmes will be listed using GetFilteredCategory. + """ + url = url.split('/')[0] + NEW_URL = 'https://www.bbc.co.uk/iplayer/categories/%s/a-z' % url + + # Read selected category's page. + html = OpenURL(NEW_URL) + # Some categories offer filters, we want to provide these filters as options. + match1 = re.findall( + '(.+?)', + html, + re.DOTALL) + if match1: + AddMenuEntry('All', url, 126, '', '', '') + for url, name in match1: + AddMenuEntry(name, url, 126, '', '', '') + else: + GetFilteredCategory(url) + + +def GetFilteredCategory(url): + """Parses the programmes available in the category view.""" + NEW_URL = 'https://www.bbc.co.uk/iplayer/categories/%s/a-z' % url + + ScrapeEpisodes(NEW_URL) + + +def ListChannelHighlights(): + """Creates a list directories linked to the highlights section of each channel.""" + channel_list = [ + ('bbcone', 'bbc_one_hd', 'BBC One'), + ('bbctwo', 'bbc_two_hd', 'BBC Two'), + ('tv/bbcthree', 'bbc_three_hd', 'BBC Three'), + ('bbcfour', 'bbc_four_hd', 'BBC Four'), + ('tv/cbbc', 'cbbc_hd', 'CBBC'), + ('tv/cbeebies', 'cbeebies_hd', 'CBeebies'), + ('tv/bbcnews', 'bbc_news24', 'BBC News Channel'), + ('tv/bbcparliament', 'bbc_parliament', 'BBC Parliament'), + ('tv/bbcalba', 'bbc_alba', 'Alba'), + ('tv/bbcscotland', 'bbc_scotland_hd', 'BBC Scotland'), + ('tv/s4c', 's4cpbs', 'S4C'), + ] + for id, img, name in channel_list: + iconimage = 'resource://resource.images.iplayerwww/media/'+img+'.png' + AddMenuEntry(name, id, 106, iconimage, '', '') + + +def ParseSingleJSON(meta, item, name, added_playables, added_directories): + main_url = None + episodes_url = '' + episodes_title = '' + num_episodes = None + synopsis = '' + icon = '' + aired = '' + title = '' + + if 'episode' in item: + subitem = item['episode'] + if 'id' in subitem: + main_url = 'https://www.bbc.co.uk/iplayer/episode/' + subitem.get('id') + if subitem.get('subtitle'): + if 'default' in subitem.get('subtitle'): + sub = subitem['subtitle'].get('default') + if sub is not None: + sub = ' - '+sub + else: + sub = '' + if 'title' in subitem: + if 'default' in subitem.get('title'): + title = '%s%s' % (subitem['title'].get('default'), sub) + else: + title = '%s%s' % (name, sub) + elif subitem.get('title'): + if 'default' in subitem.get('title'): + title = subitem['title'].get('default') + else: + title = name + if subitem.get('synopsis'): + if 'small' in subitem.get('synopsis'): + synopsis = subitem['synopsis'].get('small') + if subitem.get('image'): + if 'default' in subitem.get('image'): + icon = subitem['image'].get('default').replace("{recipe}","832x468") + else: + if 'count' in item: + if item['count']>1: + num_episodes = str(item['count']) + if 'id' in item: + main_url = 'https://www.bbc.co.uk/iplayer/episodes/' + item.get('id') + elif 'id' in item: + main_url = 'https://www.bbc.co.uk/iplayer/episode/' + item.get('id') + elif 'id' in item: + if 'initial_children' in item: + for ic in item['initial_children']: + if 'id' in ic: + if item['id'] == ic['id']: + # This is just a playable, do not create a directory. + # Use the initial children to create the entry as it normally contains more information. + ParseSingleJSON(meta, ic, name, added_playables, added_directories) + else: + # There is a directory, and an initial episode. + # We need to create one directory and one playable. + ParseSingleJSON(meta, ic, name, added_playables, added_directories) + episodes_url = 'https://www.bbc.co.uk/iplayer/episodes/' + item.get('id') + else: + # Never seen this, but seems possible. This is just a directory. + episodes_url = 'https://www.bbc.co.uk/iplayer/episodes/' + item.get('id') + else: + main_url = 'https://www.bbc.co.uk/iplayer/episode/' + item.get('id') + elif 'href' in item: + # Some strings already contain the full URL, need to work around this. + url = item['href'].replace('http://www.bbc.co.uk','') + url = url.replace('https://www.bbc.co.uk','') + if url: + if meta == 'tleo-item': + episodes_url = 'https://www.bbc.co.uk' + url + # print(episodes_url) + else: + main_url = 'https://www.bbc.co.uk' + url + + if 'secondaryHref' in item: + # Some strings already contain the full URL, need to work around this. + url = item['secondaryHref'].replace('http://www.bbc.co.uk','') + url = url.replace('https://www.bbc.co.uk','') + if url: + episodes_url = 'https://www.bbc.co.uk' + url + episodes_title = item["title"] + elif meta: + if 'secondaryHref' in meta: + # Some strings already contain the full URL, need to work around this. + url = meta['secondaryHref'].replace('http://www.bbc.co.uk','') + url = url.replace('https://www.bbc.co.uk','') + if url: + episodes_url = 'https://www.bbc.co.uk' + url + episodes_title = item["title"] + if 'episodesAvailable' in meta: + if meta['episodesAvailable'] > 1: + num_episodes = str(meta['episodesAvailable']) + + if 'subtitle' in item: + if 'title' in item: + title = "%s - %s" % (item['title'], item['subtitle']) + else: + title = name + elif 'title' in item: + title = item['title'] + else: + title = name + + if 'synopsis' in item: + synopsis = item['synopsis'] + if 'synopses' in item: + if 'editorial' in item['synopses']: + synopsis = item['synopses']['editorial'] + elif 'large' in item['synopses']: + synopsis = item['synopses']['large'] + elif 'medium' in item['synopses']: + synopsis = item['synopses']['medium'] + elif 'small' in item['synopses']: + synopsis = item['synopses']['small'] + + if 'imageTemplate' in item: + icon = item['imageTemplate'].replace("{recipe}","832x468") + if 'images' in item: + icon = item['images']['standard'].replace("{recipe}","832x468") + elif 'sources' in item: + temp = item['sources'][0]['srcset'].split()[0] + icon = re.sub(r'ic/.+?/','ic/832x468/',temp) + + + if num_episodes: + if not main_url in added_directories: + title = '[B]'+item['title']+'[/B] - '+num_episodes+' episodes available' + AddMenuEntry(title, main_url, 139, icon, synopsis, '') + added_directories.append(main_url) + + elif episodes_url: + if not episodes_url in added_directories: + if episodes_title=='': + episodes_title = title + AddMenuEntry('[B]%s[/B]' % (episodes_title), + episodes_url, 128, icon, synopsis, '') + added_directories.append(main_url) + elif main_url: + if not main_url in added_playables: + CheckAutoplay(title , main_url, icon, synopsis, aired) + added_playables.append(main_url) + + +def ParseJSON(programme_data, current_url): + """Parses the JSON data containing programme information of a page. Contains a lot of fallbacks + """ + + added_playables = [] + added_directories = [] + + if programme_data: + name = '' + if 'header' in programme_data: + if 'title' in programme_data['header']: + name = programme_data['header']['title'] + url_split = current_url.replace('&','?').split('?') + is_paginated = False + """ Avoid duplicate entries by checking if we are on page >1 + """ + for part in url_split: + if part.startswith('page'): + is_paginated = True + if not is_paginated: + if 'availableSlices' in programme_data['header']: + current_series = programme_data['header']['currentSliceId'] + slices = programme_data['header']['availableSlices'] + if slices is not None: + for series in slices: + if series['id'] == current_series: + continue + base_url = url_split[0] + series_url = base_url + '?seriesId=' + series['id'] + AddMenuEntry('[B]%s: %s[/B]' % (name, series['title']), + series_url, 128, '', '', '') + + programmes = None + if 'currentLetter' in programme_data: + # This must be an A-Z page. + current_letter = programme_data['currentLetter'] + programmes = programme_data['programmes'][current_letter]['entities'] + elif 'entities' in programme_data: + if 'elements' in programme_data['entities']: + programmes = programme_data['entities']['elements'] + if 'results' in programme_data['entities']: + programmes = programme_data['entities']['results'] + elif 'relatedEpisodes' in programme_data: + if 'episodes' in programme_data['relatedEpisodes']: + programmes = programme_data['relatedEpisodes']['episodes'] + if 'slices' in programme_data['relatedEpisodes']: + if programme_data['relatedEpisodes']['slices'] is not None: + if 'episode' in programme_data: + if 'title' in programme_data['episode']: + name = programme_data['episode']['title'] + url_split = current_url.replace('&','?').split('?') + current_series = programme_data['relatedEpisodes']['currentSliceId'] + for series in programme_data['relatedEpisodes']['slices']: + if series['id'] == current_series: + continue + base_url = url_split[0] + series_url = base_url + '?seriesId=' + series['id'] + AddMenuEntry('[B]%s: %s[/B]' % (name, series['title']['default']), + series_url, 128, '', '', '') + elif 'items' in programme_data: + # This must be Watchlist or Continue Watching. + programmes = programme_data['items'] + + if programmes: + for item in programmes: + meta = None + if 'props' in item: + meta = item.get('meta') + item = item.get('props') + elif 'contentItemProps' in item: + meta = item.get('type') + item = item.get('contentItemProps') + ParseSingleJSON(meta, item, name, added_playables, added_directories) + + # The next section is for global and channel highlights. They are a bit tricky. + groups = None + highlights = None + bundles = None + if 'groups' in programme_data: + groups = programme_data.get('groups') + for entity in groups: + for item in entity['entities']: + if 'props' in item: + item = item.get("props") + ParseSingleJSON(None, item, None, added_playables, added_directories) + + title = '' + id = '' + title = entity.get('title') + id = entity.get('id') + if (title and id): + episodes_url = 'https://www.bbc.co.uk/iplayer/group/%s' % id + if not episodes_url in added_directories: + AddMenuEntry('[B]%s: %s[/B]' % (translation(30314), title), + episodes_url, 128, '', '', '') + + if 'highlights' in programme_data: + highlights = programme_data.get('highlights') + entity = highlights.get("items") + if entity: + for item in entity: + if 'props' in item: + item = item.get("props") + ParseSingleJSON(None, item, None, added_playables, added_directories) + + if 'bundles' in programme_data: + bundles = programme_data.get('bundles') + for bundle in bundles: + entity = '' + entity = bundle.get('entities') + if entity: + for item in entity: + ParseSingleJSON(None, item, None, added_playables, added_directories) + journey = '' + journey = bundle.get('journey') + if journey: + id = '' + id = journey.get('id') + type = '' + type = journey.get('type') + title = '' + title = bundle.get('title').get('default') + if title: + if (id and (type == 'group')): + if (id == 'popular'): + AddMenuEntry('[B]%s: %s[/B]' % (translation(30314), title), + 'url', 105, '', '', '') + else: + episodes_url = 'https://www.bbc.co.uk/iplayer/group/%s' % id + if not episodes_url in added_directories: + AddMenuEntry('[B]%s: %s[/B]' % (translation(30314), title), + episodes_url, 128, '', '', '') + if (id and (type == 'category')): + AddMenuEntry('[B]%s: %s[/B]' % (translation(30314), title), + id, 126, '', '', '') + + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) + + +def SetSortMethods(*additional_methods): + """Set a few standard sort methods and optional additional methods""" + sort_methods = [xbmcplugin.SORT_METHOD_UNSORTED, + xbmcplugin.SORT_METHOD_TITLE, + xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE] + sort_methods.extend(additional_methods) + handle = int(sys.argv[1]) + for method in sort_methods: + xbmcplugin.addSortMethod(handle, method) + + +def SelectSynopsis(synopses): + if synopses is None: + return '' + try: + return (synopses.get('editorial') + or synopses.get('medium') + or synopses.get('large') + or synopses.get('small') + or synopses.get('programme_small') + or synopses.get('programmeSmall') # Seen in bundles on iplayer's main page. + or synopses.get('live') + or '') + except AttributeError: + # Allow for the odd event that `synopses` is a single string. + if isinstance(synopses, str): + return synopses + else: + raise + + +def SelectImage(images): + if not images: + return 'DefaultFolder.png' + return(images.get('standard') + or images.get('default') + or images.get('promotional') + or images.get('promotional_with_logo') + or images.get('portrait') + or 'DefaultFolder.png').replace('{recipe}', '832x468') + + +def ParseProgramme(progr_data, playable=False): + if playable: + programme = { + 'url': 'https://www.bbc.co.uk/iplayer/episode/' + progr_data['id'], + 'name': progr_data['title'] + } + else: + programme = { + 'url': 'https://www.bbc.co.uk/iplayer/episodes/' + progr_data['id'], + 'name': '[B]{}[/B] - {} episodes available'.format(progr_data['title'], progr_data['count']) + } + + programme.update({ + 'iconimage': progr_data.get('images', {}).get('standard', 'DefaultFolder.png').replace('{recipe}', '832x468'), + 'description': SelectSynopsis(progr_data['synopses']) + }) + return programme + + +def ParseEpisode(episode_data): + title = episode_data.get('title', '') + if isinstance(title, dict): + title= title.get('default', '') + subtitle = episode_data.get('subtitle') + if isinstance(subtitle, dict): + subtitle = subtitle.get('default') + if subtitle: + title = ' - '.join((title, subtitle)) + description = SelectSynopsis(episode_data.get('synopses') or episode_data.get('synopsis')) + duration = '' + version_data = episode_data.get('versions') + if version_data: + version = version_data[0] + duration = iso_duration_2_seconds(version.get('duration', {}).get('value', '')) or '' + availability = version.get('availability', {}).get('remaining') + if isinstance(availability, dict): + availability = availability.get('text', '') + if availability: + description = '\n\n'.join((description, availability)) + + return { + 'url': 'https://www.bbc.co.uk/iplayer/episode/' + episode_data['id'], + 'name': title, + 'iconimage': SelectImage(episode_data.get('images') or episode_data.get('image')), + 'description': description, + 'aired': episode_data.get('release_date_time', '').split('T')[0], + 'total_time': str(duration) + } + + +def ListHighlights(highlights_url): + """Creates a list of the programmes in the highlights section. + """ + + current_url = 'https://www.bbc.co.uk/%s' % highlights_url + html = OpenURL(current_url) + + json_data = ScrapeJSON(html) + if json_data: + ParseJSON(json_data, current_url) + + +def ListMostPopular(): + """Scrapes all episodes of the most popular page.""" + current_url = 'https://www.bbc.co.uk/iplayer/group/most-popular' + html = OpenURL(current_url) + + json_data = ScrapeJSON(html) + if json_data: + ParseJSON(json_data, current_url) + + +def AddAvailableStreamItem(name, url, iconimage, description): + """Play a streamm based on settings for preferred catchup source and bitrate.""" + stream_ids = ScrapeAvailableStreams(url) + if stream_ids['name']: + name = stream_ids['name'] + if not iconimage or iconimage == u"DefaultVideo.png" and stream_ids['image']: + iconimage = stream_ids['image'] + if stream_ids['description']: + description = stream_ids['description'] + if ((not stream_ids['stream_id_st']) or (ADDON.getSetting('search_ad') == 'true')) and stream_ids['stream_id_ad']: + streams_all = ParseStreamsHLSDASH(stream_ids['stream_id_ad']) + strm_id = stream_ids['stream_id_ad'] + elif ((not stream_ids['stream_id_st']) or (ADDON.getSetting('search_signed') == 'true')) and stream_ids['stream_id_sl']: + streams_all = ParseStreamsHLSDASH(stream_ids['stream_id_sl']) + strm_id = stream_ids['stream_id_sl'] + else: + streams_all = ParseStreamsHLSDASH(stream_ids['stream_id_st']) + strm_id = stream_ids['stream_id_st'] + if streams_all[1]: + # print "Setting subtitles URL" + subtitles_url = streams_all[1][0][1] + # print subtitles_url + else: + subtitles_url = '' + streams = streams_all[0] + source = int(ADDON.getSetting('catchup_source')) + # print "Selected source is %s"%source + # print streams + if source > 0: + match = [x for x in streams if (x[0] == source)] + else: + match = streams + PlayStream(name, match[0][2], iconimage, description, subtitles_url, + episode_id=stream_ids['episode_id'], stream_id=strm_id) + + +def GetAvailableStreams(name, url, iconimage, description, resume_time='', total_time=''): + """Calls AddAvailableStreamsDirectory based on user settings""" + #print url + stream_ids = ScrapeAvailableStreams(url) + if stream_ids['name']: + name = stream_ids['name'] + if stream_ids['image']: + iconimage = stream_ids['image'] + if stream_ids['description']: + description = stream_ids['description'] + # If we found standard streams, append them to the list. + if stream_ids['stream_id_st']: + AddAvailableStreamsDirectory(name, stream_ids['stream_id_st'], iconimage, description, + stream_ids['episode_id'], resume_time, total_time) + # If we searched for Audio Described programmes and they have been found, append them to the list. + if stream_ids['stream_id_ad'] or not stream_ids['stream_id_st']: + AddAvailableStreamsDirectory(name + ' - (Audio Described)', stream_ids['stream_id_ad'], iconimage, + description, stream_ids['episode_id'], resume_time, total_time) + # If we search for Signed programmes and they have been found, append them to the list. + if stream_ids['stream_id_sl'] or not stream_ids['stream_id_st']: + AddAvailableStreamsDirectory(name + ' - (Signed)', stream_ids['stream_id_sl'], iconimage, + description, stream_ids['episode_id'], resume_time, total_time) + + +def Search(search_entered): + """Simply calls the online search function. The search is then evaluated in EvaluateSearch.""" + if search_entered is None: + keyboard = xbmc.Keyboard('', 'Search iPlayer') + keyboard.doModal() + if keyboard.isConfirmed(): + search_entered = keyboard.getText() .replace(' ', '%20') # sometimes you need to replace spaces with + or %20 + + if search_entered is None: + return False + + NEW_URL = 'https://www.bbc.co.uk/iplayer/search?q=%s' % search_entered + ScrapeEpisodes(NEW_URL) + + +def AddAvailableLiveStreamItemSelector(name, channelname, iconimage, watch_from_start=False): + return AddAvailableLiveDASHStreamItem(name, channelname, iconimage, watch_from_start) + + +def AddAvailableLiveDASHStreamItem(name, channelname, iconimage, watch_from_start=False): + streams = ParseLiveDASHStreams(channelname) + + source = int(ADDON.getSetting('live_source')) + if source > 0: + match = [x for x in streams if (x[0] == source)] + if len(match) == 0: + match = streams + else: + match = streams + if watch_from_start: + PlayStream(name, match[0][2], iconimage, '', '', replay_chan_id=channelname) + else: + PlayStream(name, match[0][2], iconimage, '', '') + + +def AddAvailableLiveStreamsDirectory(name, channelname, iconimage, watch_from_start=False): + """Retrieves the available live streams for a channel + + Args: + name: only used for displaying the channel. + iconimage: only used for displaying the channel. + channelname: determines which channel is queried. + watch_from_start: True if the current programme is to be played from the start. + """ + streams = ParseLiveDASHStreams(channelname) + suppliers = ['', 'Akamai', 'Limelight', 'Bidi','Cloudfront'] + for supplier, bitrate, url, resolution in streams: + title = name + ' - [I][COLOR fff1f1f1]%s[/COLOR][/I]' % (suppliers[supplier]) + if watch_from_start: + AddMenuEntry(title, url, 201, iconimage, resume_time='0', replay_chan_id=channelname) + else: + AddMenuEntry(title, url, 201, iconimage, resume_time='0') + + +def GetJsonDataWithBBCid(url, retry=True): + html = OpenURL(url) + json_data = ScrapeJSON(html) + if not json_data: + xbmc.log(f"[ipwww_video] [Warning] No JSON data on page '{url}'.") + return + identity = json_data.get('id') or json_data['identity'] + if identity['signedIn']: + return json_data + + if retry: + if CheckLogin(): + return GetJsonDataWithBBCid(url, retry=False) + else: + CreateBaseDirectory('video') + else: + xbmc.log('[ipwww_video] [Error] GetJsonDataWithBBCid(): still not signed in at second attempt') + return + + +def ListWatching(): + url = "https://www.bbc.co.uk/iplayer/continue-watching" + data = GetJsonDataWithBBCid(url) + if not data: + return + + for watching_item in data['items']['elements']: + episode = watching_item['episode'] + programme = watching_item['programme'] + item_data = ParseEpisode(episode) + + # Lacking a field synopses, a watching item's description is empty. Since the + # remaining playtime is presented in the title instead of the usual episode name, + # place the original title/sub-title in the description. + item_data['description'] = item_data['name'] + remaining_seconds = watching_item.get('remaining') + if remaining_seconds: + total_seconds = int(remaining_seconds * 100 / (100 - watching_item.get('progress', 0))) + item_data['name'] = '{} - [I]{} min left[/I]'.format(episode.get('title', ''), int(remaining_seconds / 60)) + # Resume a little bit earlier, so it's easier to recognise where you've left off. + item_data['resume_time'] = str(max(total_seconds - remaining_seconds - 10, 0)) + item_data['total_time'] = str(total_seconds) + else: + item_data['name'] = '{} - [I]next episode[/I]'.format(episode.get('title', '')) + + item_data['context_mnu'] = ct_menus = [] + programme_id = episode.get('tleo_id') + + if episode.get('id') != programme_id: + # A programme with multiple episodes; add a 'View all episodes' context menu item. + all_episodes_link = 'https://www.bbc.co.uk/iplayer/episodes/' + programme['id'] + ct_menus.append((translation(30600), + f'Container.Update(plugin://plugin.video.iplayerwww/?mode=128&url={all_episodes_link})')) + + if programme_id: + # Add a context menu item 'Remove' + ct_menus.append((translation(30601), + f'RunPlugin(plugin://plugin.video.iplayerwww?mode=301&episode_id={programme_id}&url=url)')) + + CheckAutoplay(**item_data) + + +def RemoveWatching(episode_id): + """Remove an item from the 'Continue Watching' list. + Handler for the context menu option 'Remove' on list items in 'Continue watching'. + + """ + PostJson('https://user.ibl.api.bbc.co.uk/ibl/v1/user/hides', + {'id': episode_id}) + xbmc.executebuiltin('Container.Refresh') + + +def ListFavourites(): + """AKA 'Watchlist', FKA 'Added'.""" + data = GetJsonDataWithBBCid("https://www.bbc.co.uk/iplayer/watchlist") + if not data: + return + + for added_item in data['items']['elements']: + programme = added_item['programme'] + ct_mnu = [('Remove', + f'RunPlugin(plugin://plugin.video.iplayerwww?mode=302&episode_id={programme["id"]}&url=url)')] + if programme['count'] == 1: + CheckAutoplay(context_mnu=ct_mnu, **ParseProgramme(programme, playable=True)) + else: + AddMenuEntry(mode=128, subtitles_url='', context_mnu=ct_mnu, **ParseProgramme(programme)) + SetSortMethods() + + +def RemoveFavourite(programme_id): + """Remove an item from the Watchlist. + Handler for the context menu option 'Remove' on list items in 'Watchlist'. + + Delete will never fail, even if programme_id is not on the list, or does not exist at all. + """ + DeleteUrl('https://user.ibl.api.bbc.co.uk/ibl/v1/user/adds/' + programme_id) + xbmc.executebuiltin('Container.Refresh') + + +def ListRecommendations(item_id=None): + data = GetJsonDataWithBBCid('https://www.bbc.co.uk/iplayer') + if not data: + return + + if item_id and item_id != 'url': + # find the bundle and return its content from the main page: + for bundle in data['bundles']: + if bundle['id'] == item_id: + for recommended_item in bundle['entities']: + episode = recommended_item['episode'] + item_data = ParseEpisode(episode) + if not item_data: + continue + tleo_id = episode.get('tleo', {}).get('id') + if tleo_id and tleo_id != episode['id']: + all_episodes_link = 'https://www.bbc.co.uk/iplayer/episodes/' + tleo_id + item_data['context_mnu'] = [ + (translation(30600), # View all episodes + f'Container.Update(plugin://plugin.video.iplayerwww/?mode=128&url={all_episodes_link})')] + CheckAutoplay(**item_data) + SetSortMethods(xbmcplugin.SORT_METHOD_DATE) + return + else: + # Present the various rails of recommendations on iplayer's home page as folders in Kodi + for bundle in data['bundles']: + bundle_id = bundle.get('id', '') + if bundle_id in ('recommendations', 'if-you-liked'): + AddMenuEntry(bundle['title']['default'], bundle_id, 198, + SelectImage(bundle.get('image')), SelectSynopsis(bundle.get('synopses'))) + + + +def PlayStream(name, url, iconimage, description='', subtitles_url='', episode_id=None, stream_id=None, replay_chan_id=''): + if iconimage == '': + iconimage = 'DefaultVideo.png' + html = OpenURL(url) + check_geo = re.search( + '

    Access Denied

    ', html) + if check_geo or not html: + # print "Geoblock detected, raising error message" + raise GeoBlockedError(translation(30401)) + liz = xbmcgui.ListItem(name) + liz.setArt({'icon':'DefaultVideo.png', 'thumb':iconimage}) + liz.setInfo(type='Video', infoLabels={'Title': name}) + liz.setProperty("IsPlayable", "true") + liz.setPath(url) + liz.setProperty('inputstream', 'inputstream.adaptive') + liz.setProperty('inputstream.adaptive.manifest_type', 'mpd') + if subtitles_url and ADDON.getSetting('subtitles') == 'true': + # print "Downloading subtitles" + subtitles_file = download_subtitles(subtitles_url) + liz.setSubtitles([subtitles_file]) + if replay_chan_id: + resume_point = GetLiveStartPosition(replay_chan_id) + if resume_point is not None: + liz.setProperties({'ResumeTime': str(resume_point), + 'TotalTime': '7200', + 'inputstream.adaptive.play_timeshift_buffer': 'true'}) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, liz) + ipwww_progress.monitor_progress(episode_id, stream_id) + + +def GetLiveStartPosition(chan_id): + """Return the start position of the current programme relative to the beginning of the stream. + + :returns: The start position in seconds, or None if the start position is not available. + """ + if not chan_id: + return + + from datetime import datetime, timezone + + # Schedules from HD channels must be requested by their non-HD counterpart. + if chan_id.endswith('_hd'): + chan_id = chan_id[:-3] + if chan_id == 'bbc_one': + chan_id = 'bbc_one_london' + + now = datetime.now(timezone.utc) + resp = None + try: + # Get schedules of the current channel to obtain the start time of the current programme. + url = (f'https://ibl.api.bbc.co.uk/ibl/v1/channels/{chan_id}/broadcasts?per_page=2&from_date=' + + now.strftime('%Y-%m-%dT%H:%M')) + resp = OpenRequest('get', url) + data = json.loads(resp) + cur_broadcast = data['broadcasts']['elements'][0] + # transmission_start is more accurate, but not always available. + start = cur_broadcast.get('transmission_start') or cur_broadcast['scheduled_start'] + start_dt = strptime(start, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc) + except Exception as e: + xbmcgui.Dialog().ok(translation(30400), translation(30415)) # Error msg start time not available. + xbmc.log(f'[ipwww_video] [Error] Failed to get live resume point: {e!r}\n{resp}') + return + # Need to get the start of the current programme relative to the start of the stream. + # Since a live stream has a 2 hrs timeshift window, the live edge is at 7200 seconds from the start. + start_position = 7200 - (now - start_dt).total_seconds() + if start_position < 0: + if xbmcgui.Dialog().yesno( + translation(30405), # warning + translation(30416), # msg program started too long ago + yeslabel=translation(30417), # play from 2hrs back + nolabel=translation(30418)): # play live + start_position = 0 + else: + return + return start_position + + +def AddAvailableStreamsDirectory(name, stream_id, iconimage, description, episode_id, resume_time="", total_time=""): + """Will create one menu entry for each available stream of a particular stream_id""" + # print("Stream ID: %s"%stream_id) + streams = ParseStreamsHLSDASH(stream_id) + # print(streams) + if streams[1]: + # print("Setting subtitles URL") + subtitles_url = streams[1][0][1] + # print(subtitles_url) + else: + subtitles_url = '' + suppliers = ['', 'Akamai', 'Limelight', 'Bidi','Cloudfront'] + for supplier, bitrate, url, resolution, protocol in streams[0]: + title = name + ' - [I][COLOR ffd3d3d3]%s[/COLOR][/I]' % (suppliers[supplier]) + AddMenuEntry(title, url, 201, iconimage, description, subtitles_url, resolution=resolution, + episode_id=episode_id, stream_id=stream_id, resume_time=resume_time, total_time=total_time) + + +def ParseMediaselector(stream_id): + streams = [] + subtitles = [] + # print("Parsing streams for PID: %s"%stream_id) + # Open the page with the actual strem information and display the various available streams. + NEW_URL = 'https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/pc/vpid/%s/format/json/cors/1' % stream_id + html = OpenURL(NEW_URL) + json_data = json.loads(html) + if json_data: + # print(json.dumps(json_data, sort_keys=True, indent=2)) + if 'media' in json_data: + for media in json_data['media']: + if 'kind' in media: + if media['kind'] == 'captions': + if 'connection' in media: + for connection in media['connection']: + href = '' + protocol = '' + supplier = '' + if 'href' in connection: + href = connection['href'] + if 'protocol' in connection: + protocol = connection['protocol'] + if 'supplier' in connection: + supplier = connection['supplier'] + if protocol == 'https': + subtitles.append((href, protocol, supplier)) + elif media['kind'].startswith('video'): + if 'connection' in media: + for connection in media['connection']: + href = '' + protocol = '' + supplier = '' + transfer_format = '' + if 'href' in connection: + href = connection['href'] + if 'protocol' in connection: + protocol = connection['protocol'] + if 'supplier' in connection: + supplier = connection['supplier'] + if 'transferFormat' in connection: + transfer_format = connection['transferFormat'] + if protocol == 'https': + streams.append((href, protocol, supplier, transfer_format)) + elif 'result' in json_data: + if json_data['result'] == 'geolocation': + # print "Geoblock detected, raising error message" + raise GeoBlockedError(translation(30401)) + # print("Found streams:") + # print(streams) + # print(subtitles) + return streams, subtitles + + +def ParseStreamsHLSDASH(stream_id): + return ParseDASHStreams(stream_id) + + +def ParseDASHStreams(stream_id): + streams = [] + subtitles = [] + # print "Parsing streams for PID: %s"%stream_id + mediaselector = ParseMediaselector(stream_id) + # Open the page with the actual strem information and display the various available streams. + source = int(ADDON.getSetting('catchup_source')) + for url, protocol, supplier, transfer_format in mediaselector[0]: + tmp_sup = 0 + tmp_br = 0 + if transfer_format == 'dash': + if 'akamai' in supplier and source in [0,1]: + tmp_sup = 1 + elif 'limelight' in supplier and source in [0,2]: + tmp_sup = 2 + elif 'bidi' in supplier and source in [0,3]: + tmp_sup = 3 + elif 'cloudfront' in supplier and source in [0,4]: + tmp_sup = 4 + else: + continue + streams.append((tmp_sup, 1, url, '1280x720', protocol)) + source = int(ADDON.getSetting('subtitle_source')) + for href, protocol, supplier in mediaselector[1]: + if '$Number$' in href: + # Found in live and red-button streams and requires `$Number$` to be replaced by + # a DASH segment id. + # Since DASH is handled by Inputstream Adaptive, live subtitles are not supported. + continue + if 'akamai' in supplier and source in [0,1]: + tmp_sup = 1 + elif 'limelight' in supplier and source in [0,2]: + tmp_sup = 2 + elif 'bidi' in supplier and source in [0,3]: + tmp_sup = 3 + elif 'cloudfront' in supplier and source in [0,4]: + tmp_sup = 4 + else: + continue + subtitles.append((tmp_sup, href, protocol)) + # print streams + # print subtitles + return streams, subtitles + + +def ParseLiveDASHStreams(channelname): + streams = [] + mediaselector = ParseMediaselector(channelname) + # print mediaselector + for provider_url, protocol, provider_name, transfer_format in mediaselector[0]: + if transfer_format == 'dash': + tmp_sup = '' + if 'akamai' in provider_name: + tmp_sup = 1 + elif 'll' in provider_name or 'limelight' in provider_name: + tmp_sup = 2 + elif 'bidi' in provider_name: + tmp_sup = 3 + elif 'cloudfront' in provider_name: + tmp_sup = 4 + else: + continue + streams.append((tmp_sup, 1, provider_url, '1280x720')) + # print streams + return streams + + +def ScrapeAvailableStreams(url): + # Open page and retrieve the stream ID + html = OpenURL(url) + name = None + image = None + description = None + stream_id_st = [] + stream_id_sl = [] + stream_id_ad = [] + + json_data = ScrapeJSON(html) + # print(json.dumps(json_data, indent=2, sort_keys=True)) + if json_data: + if 'title' in json_data['episode']: + name = json_data['episode']['title'] + if 'synopses' in json_data['episode']: + synopses = json_data['episode']['synopses'] + if 'large' in synopses: + description = synopses['large'] + elif 'medium' in synopses: + description = synopses['medium'] + elif 'small' in synopses: + description = synopses['small'] + elif 'editorial' in synopses: + description = synopses['editorial'] + if 'standard' in json_data['episode']['images']: + image = json_data['episode']['images']['standard'].replace('{recipe}','832x468') + st = [] + ty = [] + for stream in json_data['versions']: + if (stream['kind'] == 'original'): + st.append(stream['id']) + ty.append(1) + elif (stream['kind'] == 'iplayer-version'): + st.append(stream['id']) + ty.append(2) + elif (stream['kind'] == 'technical-replacement'): + st.append(stream['id']) + ty.append(0) + elif (stream['kind'] == 'editorial'): + st.append(stream['id']) + ty.append(2) + elif (stream['kind'] == 'shortened'): + st.append(stream['id']) + ty.append(3) + elif (stream['kind'] == 'webcast'): + st.append(stream['id']) + ty.append(2) + elif (stream['kind'] == 'simulcast'): + st.append(stream['serviceId']) + ty.append(0) + elif (stream['kind'] == 'signed'): + stream_id_sl = stream['id'] + elif (stream['kind'] == 'audio-described'): + stream_id_ad = stream['id'] + else: + xbmc.log("iPlayer WWW warning: New stream kind: %s" % stream['kind']) + stream_id_st = stream['id'] + + if st: + st_st = [x for _,x in sorted(zip(ty,st))] + stream_id_st = st_st[0] + + return {'stream_id_st': stream_id_st, 'stream_id_sl': stream_id_sl, 'stream_id_ad': stream_id_ad, + 'name': name, 'image':image, 'description': description, 'episode_id': json_data['episode'].get('id', '')} + + +def ScrapeJSON(html): + json_data = None + format = 1 + match = re.search(r'window\.mediatorDefer\=page\(document\.getElementById\(\"tviplayer\"\),(.*?)\);', html, re.DOTALL) + if not match: + format = 2 + match = re.search(r'window.__IPLAYER_REDUX_STATE__ = (.*?);\s*', html, re.DOTALL) + if match: + data = match.group(1) + json_data = json.loads(data) + if format == 1: + if 'appStoreState' in json_data: + json_data = json_data.get('appStoreState') + elif 'initialState' in json_data: + json_data = json_data.get('initialState') + # print json.dumps(json_data, indent=2, sort_keys=True) + return json_data + + +def CheckAutoplay(name, url, iconimage, description, aired=None, resume_time="", total_time="", context_mnu=None): + if ADDON.getSetting('streams_autoplay') == 'true': + mode = 202 + else: + mode = 122 + AddMenuEntry(name, url, mode, iconimage, description, '', aired=aired, + resume_time=resume_time, total_time=total_time, context_mnu=context_mnu) + + +def GetSchedules(channel_list): + """Obtain the schedule for each channel in channel_list. + + :param channel_list: A list of tuples like defined in ListLive(). + The third item of each tuple is to be the channel_id used to obtain the schedule. + :returns: A mapping of channel_id's to schedules. + + The schedules of the regional BBC one channels only differ in the title of the + regional news. This function renames this titel of BBC One London to a more + generic "BBC News where you are". This way the schedules of BBC One London can be + used for all regional BBC One channels, which will greatly reduce the number of + HTTP request. + + Schedules for each channel is a tuple with two elements. The first is the title of the + programme that is now on, the second is single multi-line string, with the + time and name of each programme on a separate line. These strings are intended + to be used in the info fields of the ListItems of live channels. + + """ + import pytz + from concurrent import futures + + utc_tz = pytz.utc + utc_now = datetime.datetime.now(utc_tz) + utc_tomorrow = utc_now + timedelta(hours=20) + + try: + # Get local timezone from Kodi's settings + cmd_str = '{"jsonrpc": "2.0", "method": "Settings.GetSettingValue", "params": ["locale.timezone"], "id": 1}' + resp_data = json.loads(xbmc.executeJSONRPC(cmd_str)) + local_tz = pytz.timezone(resp_data['result']['value']) + except(KeyError, json.JSONDecodeError): + # Get from tzlocal as fallback if something fails. + from tzlocal import get_localzone + local_tz = get_localzone() + + # Use the user's local time format without seconds. Fix weird kodi formatting for 12-hour clock. + local_time_format = xbmc.getRegion('time').replace(':%S', '').replace('%I%I:', '%I:') + + def get_schedule(channel): + try: + url = ''.join(('https://ibl.api.bbc.co.uk/ibl/v1/channels/', + channel, + '/broadcasts?per_page=12&from_date=', + utc_now.strftime('%Y-%m-%dT%H:%M'))) + resp = OpenRequest('get', url) + schedule_list = json.loads(resp)['broadcasts']['elements'] + text_items = [] + now_on = schedule_list[0]['episode']['title'] + for item in schedule_list: + start_t = utc_tz.localize(strptime(item['scheduled_start'], '%Y-%m-%dT%H:%M:%S.%fZ')) + if start_t >= utc_tomorrow: + break + title = item['episode']['title'] + subtitle = item['episode'].get('subtitle', '') + if title == 'BBC London' and 'News' in subtitle: + title = 'BBC News where You Are' + text_items.append(' - '.join((start_t.astimezone(local_tz).strftime(local_time_format), title))) + return now_on, '\n'.join(text_items) + except Exception as e: + xbmc.log(f"Failed to get schedule of channel {channel}: {e!r}") + return '', '' + + schedule_channels = list({item[2] for item in channel_list}) + + with futures.ThreadPoolExecutor(max_workers=16) as executor: + res = [executor.submit(get_schedule, chan) for chan in schedule_channels] + futures.wait(res) + return dict(zip(schedule_channels, (r.result() for r in res))) diff --git a/plugin.video.iplayerwww/resources/settings.xml b/plugin.video.iplayerwww/resources/settings.xml new file mode 100644 index 0000000000..c92f37f0fd --- /dev/null +++ b/plugin.video.iplayerwww/resources/settings.xml @@ -0,0 +1,331 @@ + + +
    + + + + + + + 0 + false + + + + + + true + + + 30141 + + + true + + + + + + true + + + 30142 + true + + + + + true + + + + + + + 0 + false + + + + 0 + XBMC.RunPlugin(plugin://plugin.video.iplayerwww?mode=1) + + true + + + + + + 0 + false + + + + 0 + false + + + + 0 + false + + + + + + + + 0 + false + + + + 0 + 0 + + + + + + + + + + 0 + 1 + + + + + + + + + + 0 + true + + + + + + 0 + 1 + + + + + + + + + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + 0 + true + + + + + + + + 0 + true + + + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + true + + + + 0 + 0 + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + true + + + + 0 + 0 + + + + + + + + + true + + + + 0 + 3 + + + + + + + + + + + true + + + + +
    +
    diff --git a/plugin.video.iranintl/LICENSE.txt b/plugin.video.iranintl/LICENSE.txt new file mode 100644 index 0000000000..4f8e8eb30c --- /dev/null +++ b/plugin.video.iranintl/LICENSE.txt @@ -0,0 +1,282 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------- diff --git a/plugin.video.iranintl/addon.xml b/plugin.video.iranintl/addon.xml new file mode 100644 index 0000000000..3d68e7a219 --- /dev/null +++ b/plugin.video.iranintl/addon.xml @@ -0,0 +1,25 @@ + + + + + + + video + + + Iran International TV + Launched in May 2017, Iran International TV is a mix of rolling news, current affairs and documentaries, sport, lifestyle, health and technology, programmed to attract a wide audience, especially younger people. Iran International is a privately-owned UK entity. + all + fa + GPL-2.0-or-later + https://iranintl.com/ + babak@habibis.net + https://github.com/bbk79/xbmc-addons + + icon.png + + + diff --git a/plugin.video.iranintl/default.py b/plugin.video.iranintl/default.py new file mode 100644 index 0000000000..8c93f1df98 --- /dev/null +++ b/plugin.video.iranintl/default.py @@ -0,0 +1,68 @@ +from builtins import next +import xbmcvfs, xbmcgui, os, xbmcaddon, xbmcplugin, urllib.request, urllib.error, urllib.parse, re + +def main(): + __settings__ = xbmcaddon.Addon() + home = __settings__.getAddonInfo('path') + addon_handle = int(sys.argv[1]) + icon = xbmcvfs.translatePath(os.path.join(home, 'icon.png')) + livePageUrl = 'https://iranintl.com/live' + request = urllib.request.Request(livePageUrl, headers={'User-Agent': 'Kodi'}) + + try: + response = urllib.request.urlopen(request) + html = response.read().decode('utf-8') + + liveVideoUrls = [urllib.parse.unquote(url) for url in re.findall('https%3A%2F%2F.*?.m3u8', html)] + + if len(liveVideoUrls) > 0: + base_url = liveVideoUrls[0].rsplit('/', 1)[0]+'/' + streams = getStreamsFromPlayList(base_url, liveVideoUrls[0]) + + bitrates = list(streams.keys()) + bitrates.sort() + bitrates.reverse() + + for bitrate in bitrates: + li = xbmcgui.ListItem(streams[bitrate][0]) + li.setArt({'thumb': icon}) + xbmcplugin.addDirectoryItem(handle=addon_handle, url=streams[bitrate][1], listitem=li, isFolder=False) + + except urllib.error.URLError as e: + if hasattr(e, 'reason'): + xbmc.log('We failed to reach a server. Reason:', e.reason) + elif hasattr(e, 'code'): + xbmc.log('The server couldn\'t fulfill the request. Error code:', e.code) + + xbmcplugin.endOfDirectory(addon_handle) + +def getStreamsFromPlayList(base_url, url): + try: + resp = urllib.request.urlopen(url) + except urllib.error.URLError: + return None + except urllib.error.HTTPError: + return None + + lines = resp.read().decode('utf-8').split('\n') + + streams = {} + lines_iter = iter(lines) + for line in lines_iter: + if (line.startswith('#EXT-X-STREAM-INF')): + m = re.search("RESOLUTION=(\d+)x(\d+)", line) + if m: + xRes = m.group(1) + yRes = m.group(2) + m = re.search("BANDWIDTH=(\d+)", line) + if m: + bandwidth = int(m.group(1)) + title = "{yRes}p - {:.2f} Mbps".format(bandwidth / 1000000.0, xRes=xRes, yRes=yRes) + itemUrl = "{base_url}{item}".format(base_url=base_url, item=next(lines_iter)) + streams[bandwidth] = (title, itemUrl) + + streams[max(streams.keys()) + 1] = ('Iran International TV (Auto)', url) + return streams + +if __name__ == '__main__': + main() diff --git a/plugin.video.iranintl/icon.png b/plugin.video.iranintl/icon.png new file mode 100644 index 0000000000..049cb491fc Binary files /dev/null and b/plugin.video.iranintl/icon.png differ diff --git a/plugin.video.ispot.tv/addon.xml b/plugin.video.ispot.tv/addon.xml new file mode 100644 index 0000000000..ffe563b492 --- /dev/null +++ b/plugin.video.ispot.tv/addon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + video + + + + true + all + GPL-3.0-or-later + https://github.com/Lunatixz/KODI_Addons/tree/master/plugin.video.ispot.tv + https://www.ispot.tv/ + https://forum.kodi.tv/showthread.php?tid=311057 + v.2.0.9-Added Download Folder to menu + + resources/images/icon.png + resources/images/fanart.jpg + + iSpot.tv internet's most comprehensive database of nationally airing TV Commercials. + Welcome to the Internet's most comprehensive database of nationally airing TV Commercials. Explore the actors, songs and offers in the spots you see on TV. + + \ No newline at end of file diff --git a/plugin.video.ispot.tv/default.py b/plugin.video.ispot.tv/default.py new file mode 100644 index 0000000000..2afb8304f3 --- /dev/null +++ b/plugin.video.ispot.tv/default.py @@ -0,0 +1,24 @@ +# Copyright (C) 2024 Lunatixz +# +# +# This file is part of iSpot.tv. +# +# iSpot.tv 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. +# +# iSpot.tv 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 iSpot.tv. If not, see . + +# -*- coding: utf-8 -*- + +#entrypoint +import sys +from resources.lib import ispot +if __name__ == '__main__': ispot.iSpotTV(sys.argv).run() \ No newline at end of file diff --git a/plugin.video.ispot.tv/resources/images/fanart.jpg b/plugin.video.ispot.tv/resources/images/fanart.jpg new file mode 100644 index 0000000000..0f02b69fdb Binary files /dev/null and b/plugin.video.ispot.tv/resources/images/fanart.jpg differ diff --git a/plugin.video.ispot.tv/resources/images/icon.png b/plugin.video.ispot.tv/resources/images/icon.png new file mode 100644 index 0000000000..cad71a2a09 Binary files /dev/null and b/plugin.video.ispot.tv/resources/images/icon.png differ diff --git a/plugin.video.ispot.tv/resources/language/resource.language.en_gb/strings.po b/plugin.video.ispot.tv/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..f18499cc67 --- /dev/null +++ b/plugin.video.ispot.tv/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,133 @@ +# Kodi Media Center language file +# Addon Name: iSpot.tv +# Addon id: plugin.video.iSpot.tv +# Addon Provider: Lunatixz +msgid "" +msgstr "" +"Project-Id-Version: plugin.video.iSpot.tv\n" +"Report-Msgid-Bugs-To: http://forum.kodi.tv/showthread.php?tid=311057\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Lunatixz Translation Team\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "Addon Summary" +msgid "iSpot.tv internet's most comprehensive database of nationally airing TV Commercials." +msgstr "" + +msgctxt "Addon Description" +msgid "Welcome to the Internet's most comprehensive database of nationally airing TV Commercials. Explore the actors, songs and offers in the spots you see on TV." +msgstr "" + +msgctxt "#30000" +msgid "Enable Debugging [Log errors]" +msgstr "" + +msgctxt "#30001" +msgid "General" +msgstr "" + +msgctxt "#30002" +msgid "Download location" +msgstr "" + +msgctxt "#30008" +msgid "Install Log Uploader" +msgstr "" + +msgctxt "#30009" +msgid "Submit Log" +msgstr "" + +msgctxt "#30010" +msgid "Enable Download" +msgstr "" + +msgctxt "#30011" +msgid "Download Folder" +msgstr "" + +msgctxt "#30012" +msgid "Downloading (Finished!)" +msgstr "" + +msgctxt "#30013" +msgid "Downloading Adverts (%s%%)" +msgstr "" + +msgctxt "#30014" +msgid "Preparing Download..." +msgstr "" + +msgctxt "#30014" +msgid "Preparing Download..." +msgstr "" + +msgctxt "#30015" +msgid "Apparel, Footwear & Accessories" +msgstr "" + +msgctxt "#30016" +msgid "Business & Legal" +msgstr "" + +msgctxt "#30017" +msgid "Education" +msgstr "" + +msgctxt "#30018" +msgid "Electronics & Communication" +msgstr "" + +msgctxt "#30019" +msgid "Food & Beverage" +msgstr "" + +msgctxt "#30020" +msgid "Health & Beauty" +msgstr "" + +msgctxt "#30021" +msgid "Home & Real Estate" +msgstr "" + +msgctxt "#30022" +msgid "Insurance" +msgstr "" + +msgctxt "#30023" +msgid "Life & Entertainment" +msgstr "" + +msgctxt "#30024" +msgid "Pharmaceutical & Medical" +msgstr "" + +msgctxt "#30025" +msgid "Politics, Government & Organizations" +msgstr "" + +msgctxt "#30026" +msgid "Restaurants" +msgstr "" + +msgctxt "#30027" +msgid "Retail Stores" +msgstr "" + +msgctxt "#30028" +msgid "Travel" +msgstr "" + +msgctxt "#30029" +msgid "Vehicles" +msgstr "" + +msgctxt "#30030" +msgid "Include Espanol" +msgstr "" diff --git a/plugin.video.ispot.tv/resources/lib/ispot.py b/plugin.video.ispot.tv/resources/lib/ispot.py new file mode 100644 index 0000000000..ae64d0d353 --- /dev/null +++ b/plugin.video.ispot.tv/resources/lib/ispot.py @@ -0,0 +1,354 @@ +# Copyright (C) 2024 Lunatixz +# +# +# This file is part of iSpot.tv. +# +# iSpot.tv 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. +# +# iSpot.tv 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 iSpot.tv. If not, see . + +# -*- coding: utf-8 -*- + +import os, re, sys, routing, traceback, datetime +import json, requests, base64, time + +from six.moves import urllib +from youtube_dl import YoutubeDL +from bs4 import BeautifulSoup +from simplecache import SimpleCache, use_cache +from kodi_six import xbmc, xbmcaddon, xbmcplugin, xbmcgui, xbmcvfs, py2_encode, py2_decode +from infotagger.listitem import ListItemInfoTag + +try: + from multiprocessing.pool import ThreadPool + SUPPORTS_POOL = True +except Exception: + SUPPORTS_POOL = False + +# Plugin Info +ADDON_ID = 'plugin.video.ispot.tv' +REAL_SETTINGS = xbmcaddon.Addon(id=ADDON_ID) +ADDON_NAME = REAL_SETTINGS.getAddonInfo('name') +SETTINGS_LOC = REAL_SETTINGS.getAddonInfo('profile') +ADDON_PATH = REAL_SETTINGS.getAddonInfo('path') +ADDON_VERSION = REAL_SETTINGS.getAddonInfo('version') +ICON = REAL_SETTINGS.getAddonInfo('icon') +LOGO = os.path.join('special://home/addons/%s/'%(ADDON_ID),'resources','images','logo.png') +FANART = REAL_SETTINGS.getAddonInfo('fanart') +LANGUAGE = REAL_SETTINGS.getLocalizedString +ROUTER = routing.Plugin() +CONTENT_TYPE = 'episodes' +DISC_CACHE = False +DEBUG_ENABLED = REAL_SETTINGS.getSetting('Enable_Debugging').lower() == 'true' +ENABLE_DOWNLOAD = REAL_SETTINGS.getSetting('Enable_Download').lower() == 'true' +ENABLE_SAP = REAL_SETTINGS.getSetting('Enable_SAP').lower() == 'true' +DOWNLOAD_PATH = os.path.join(REAL_SETTINGS.getSetting('Download_Folder'),'resources','').replace('/resources/resources','/resources').replace('\\','/') +DEFAULT_ENCODING = "utf-8" +HEADER = {'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246"} +BASE_URL = 'https://www.ispot.tv' + +MENU = {LANGUAGE(30015) : "/browse/k/apparel-footwear-and-accessories", + LANGUAGE(30016) : "/browse/Y/business-and-legal", + LANGUAGE(30017) : "/browse/7/education", + LANGUAGE(30018) : "/browse/A/electronics-and-communication", + LANGUAGE(30019) : "/browse/d/food-and-beverage", + LANGUAGE(30020) : "/browse/I/health-and-beauty", + LANGUAGE(30021) : "/browse/o/home-and-real-estate", + LANGUAGE(30022) : "/browse/Z/insurance", + LANGUAGE(30023) : "/browse/w/life-and-entertainment", + LANGUAGE(30024) : "/browse/7k/pharmaceutical-and-medical", + LANGUAGE(30025) : "/browse/q/politics-government-and-organizations", + LANGUAGE(30026) : "/browse/b/restaurants", + LANGUAGE(30027) : "/browse/2/retail-stores", + LANGUAGE(30028) : "/browse/5/travel", + LANGUAGE(30029) : "/browse/L/vehicles"} + +#https://www.ispot.tv/events +def log(msg, level=xbmc.LOGDEBUG): + if not DEBUG_ENABLED and level != xbmc.LOGERROR: return + try: xbmc.log('%s-%s-%s'%(ADDON_ID,ADDON_VERSION,msg),level) + except Exception as e: 'log failed! %s'%(e) + +def chunkLst(lst, n): + for i in range(0, len(lst), n): + yield lst[i:i + n] + +def slugify(s, lowercase=False): + if lowercase: s = s.lower() + s = s.strip() + s = re.sub(r'[^\w\s-]', '', s) + s = re.sub(r'[\s_-]+', '_', s) + s = re.sub(r'^-+|-+$', '', s) + return s + +def unquoteString(text): + return urllib.parse.unquote(text) + +def quoteString(text): + return urllib.parse.quote(text) + +def encodeString(text): + base64_bytes = base64.b64encode(text.encode(DEFAULT_ENCODING)) + return base64_bytes.decode(DEFAULT_ENCODING) + +def decodeString(base64_bytes): + try: + message_bytes = base64.b64decode(base64_bytes.encode(DEFAULT_ENCODING)) + return message_bytes.decode(DEFAULT_ENCODING) + except: pass + +def poolit(func, items): + results = [] + if SUPPORTS_POOL: + pool = ThreadPool() + try: results = pool.map(func, items) + except Exception: pass + pool.close() + pool.join() + else: + results = [func(item) for item in items] + results = filter(None, results) + return results + +@ROUTER.route('/') +def buildMenu(): + iSpotTV().buildMenu() + +@ROUTER.route('/downloads') +def getDownloaded(): + iSpotTV().buildDownloaded() + +@ROUTER.route('/browse/') +def getCategory(url): + iSpotTV().buildCategory(decodeString(url)) + +@ROUTER.route('/play/') +def playVideo(meta): + iSpotTV().playVideo(meta.split('|')[0],decodeString(meta.split('|')[1])) + +class iSpotTV(object): + def __init__(self, sysARG=sys.argv): + log('__init__, sysARG = %s'%(sysARG)) + self.sysARG = sysARG + self.cache = SimpleCache() + self.myMonitor = xbmc.Monitor() + + + @use_cache(1) + def getURL(self, url): + log('getURL, url = %s'%(url)) + try: return requests.get(url, headers=HEADER).content + except Exception as e: log('getURL Failed! %s'%(e)) + + + def getSoup(self, url): + log('getSoup, url = %s'%(url)) + return BeautifulSoup(self.getURL(url), 'html.parser') + + + def buildMenu(self): + log('buildMenu') + if ENABLE_DOWNLOAD: self.addDir('- %s'%(LANGUAGE(30011)),uri=(getDownloaded,)) + for name, url in list(MENU.items()): self.addDir(name,uri=(getCategory,encodeString(url))) + + + def buildDownloaded(self): + def _buildFile(item): + try: self.addLink(item.get('label'),(playVideo,'%s|%s'%(item.get('label'),encodeString(item.get('file')))),info={'label':item.get('label'),'label2':os.path.join(DOWNLOAD_PATH,item.get('label')),'title':item.get('title')},art={"thumb":ICON,"poster":ICON,"fanart":FANART,"icon":LOGO,"logo":LOGO}) + except: pass + poolit(_buildFile,self.getDirectory(DOWNLOAD_PATH)) + + + def buildCategory(self, url): + """ < div + class ="mb-0" > + < a adname = "FootJoy Pro/SLX TV Spot, 'Joy Ride' Featuring Max Homa, Danielle Kang, Song by 10cc - 16 airings" href = "/ad/6K6T/footjoy-pro-slx-joy-ride" > + < img alt = "FootJoy Pro/SLX TV Spot, 'Joy Ride' Featuring Max Homa, Danielle Kang, Song by 10cc" class ="img-16x9" loading="lazy" src="https://images-cdn.ispot.tv/ad/6K6T/default-large.jpg" width="100%" / >< / a > + < / div >""" + def _buildDir(sub): + try: + if not sub.a['href'].startswith(('/brands/','/products/')): + self.addDir('- %s'%(sub.string),uri=(getCategory,encodeString(sub.a['href']))) + except: pass + + def _buildFile(row): + try: + label, label2 = row.a['adname'].split(' - ') + if label.lower().endswith('[spanish]') and not ENABLE_SAP: return + uris.append(row.a['href']) + self.addLink(label,(playVideo,'%s|%s'%(row.a['adname'],encodeString(row.a['href']))),info={'label':label,'label2':label2,'title':label},art={"thumb":row.img['src'],"poster":row.img['src'],"fanart":FANART,"icon":LOGO,"logo":LOGO}) + except: pass + + try: + uris = [] + log('buildCategory, url = %s'%(url)) + soup = self.getSoup('%s%s'%(BASE_URL,url)) + poolit(_buildDir,soup.find_all('div', {'class': 'list-grid__item'})) + poolit(_buildFile,soup.find_all('div', {'class': 'mb-0'})) + if ENABLE_DOWNLOAD: self.queDownload(uris) + except Exception as e: log('buildCategory Failed! %s'%(e)) + + + def addLink(self, name, uri=(''), info={}, art={}, media='video', total=0): + log('addLink, name = %s'%name) + if not info: info = {"label":name,"label2":"","title":name} + if not art: art = {"thumb":ICON,"poster":ICON,"fanart":FANART,"icon":LOGO,"logo":LOGO} + info["mediatype"] = media + liz = self.getListItem(info.pop('label'), info.pop('label2'), ROUTER.url_for(*uri)) + liz.setArt(art) + liz.setProperty('IsPlayable','true') + infoTag = ListItemInfoTag(liz, media) + infoTag.set_info(info) + xbmcplugin.addDirectoryItem(ROUTER.handle, ROUTER.url_for(*uri), liz, isFolder=False, totalItems=total) + + + def addDir(self, name, uri=(''), info={}, art={}, media='video'): + log('addDir, name = %s'%name) + if not info: info = {"label":name,"label2":"","title":name} + if not art: art = {"thumb":ICON,"poster":ICON,"fanart":FANART,"icon":LOGO,"logo":LOGO} + info["mediatype"] = media + liz = self.getListItem(info.pop('label'), info.pop('label2'), ROUTER.url_for(*uri)) + liz.setArt(art) + liz.setProperty('IsPlayable','false') + infoTag = ListItemInfoTag(liz, media) + infoTag.set_info(info) + xbmcplugin.addDirectoryItem(ROUTER.handle, ROUTER.url_for(*uri), liz, isFolder=True) + + + def getListItem(self, label='', label2='', path='', offscreen=False): + return xbmcgui.ListItem(label,label2,path,offscreen) + + + @use_cache(1) + def getVideo(self, url): + # {'id': '5Gwt-video-sm', 'title': '5Gwt-video-sm', 'timestamp': 1697567715.0, 'direct': True, + # 'formats': [{'format_id': 'mp4', 'url': 'https://videos-cdn.ispot.tv/ad/d0c1/5Gwt-video-sm.mp4', + # 'vcodec': None, 'ext': 'mp4', 'format': 'mp4 - unknown', 'protocol': 'https', + # 'http_headers':{}}], 'extractor': 'generic', 'webpage_url': 'https://videos-cdn.ispot.tv/ad/d0c1/5Gwt-video-sm.mp4', + # 'webpage_url_basename': '5Gwt-video-sm.mp4', 'extractor_key': 'Generic', 'playlist': None, 'playlist_index': None, + # 'display_id': '5Gwt-video-sm', 'upload_date': '20231017', 'requested_subtitles': None, 'format_id': 'mp4', + # 'url': 'https://videos-cdn.ispot.tv/ad/d0c1/5Gwt-video-sm.mp4', 'vcodec': None, 'ext': 'mp4', 'format': 'mp4 - unknown', + # 'protocol': 'https', 'http_headers': {}} + log('getVideo, url = %s'%url) + ydl = YoutubeDL({'no_color': True, 'format': 'best', 'outtmpl': '%(id)s.%(ext)s', 'no-mtime': True, 'add-header': HEADER}) + with ydl: + result = ydl.extract_info(url, download=False) + if 'entries' in result: return result['entries'][0] #playlist + else: return result + + + def playVideo(self, name, uri): + file = uri + exists = True + found = True + if uri.startswith('/'): + file, exists = self.getFile(uri,que=True) + if not exists: + video = self.getVideo('%s%s'%(BASE_URL,uri)) + if video: file = video['url'].replace('https://','http://') + else: found = False + log('playVideo, file = %s, found = %s'%(file,found)) + xbmcplugin.setResolvedUrl(ROUTER.handle, found, self.getListItem(name, path=file)) + + + def getFile(self, uri, que=False): + file = xbmcvfs.translatePath(os.path.join(DOWNLOAD_PATH,'%s.mp4'%(slugify(uri)))).replace('\\','/') + exists = xbmcvfs.exists(file) + if que and not exists and ENABLE_DOWNLOAD: self.queDownload([uri]) + log('getFile, uri = %s, file = %s'%(uri,file)) + return file, exists + + + def getDirectory(self, path): + items = list() + chks = list() + dirs = [path] + for idx, dir in enumerate(dirs): + if self.myMonitor.waitForAbort(0.001): break + else: + log('getDirectory, walking %s/%s directory'%(idx,len(dirs))) + if len(dirs) > 0: dir = dirs.pop(dirs.index(dir)) + if dir in chks: continue + else: chks.append(dir) + for item in json.loads(xbmc.executeJSONRPC(json.dumps({"jsonrpc":"2.0","id":ADDON_ID,"method":"Files.GetDirectory","params":{"directory":dir,"media":"files"}}))).get('result', {}).get('files',[]): + if self.myMonitor.waitForAbort(0.001): break + if item.get('filetype') == 'directory': dirs.append(item.get('file')) + else: items.append(item) + log('getDirectory, returning %s items'%(len(items))) + return items + + + def queDownload(self, uris): + log('queDownload, uris = %s'%(len(uris))) + queuePool = (self.cache.get('queuePool', json_data=True) or {}) + queuePool.setdefault('uri',[]).extend(uris) + if len(queuePool['uri']) > 0: queuePool['uri'] = list(set(queuePool['uri'])) + self.cache.set('queuePool', queuePool, json_data=True, expiration=datetime.timedelta(days=28)) + + + def queDownloads(self): + if ENABLE_DOWNLOAD: [self.addDir(name,uri=(getCategory,encodeString(url))) for name, url in list(MENU.items())] + + + def getDownloads(self): + if ENABLE_DOWNLOAD: + if not xbmcvfs.exists(DOWNLOAD_PATH): xbmcvfs.mkdir(DOWNLOAD_PATH) + queuePool = (self.cache.get('queuePool', json_data=True) or {}) + uris = queuePool.get('uri',[]) + dia = self.progressBGDialog(message=LANGUAGE(30014)) + dluris = (list(chunkLst(uris,5)) or [[]])[0] + + for idx, uri in enumerate(dluris): + video = None + diact = int(idx*100//len(dluris)) + dia = self.progressBGDialog(diact, dia, message=LANGUAGE(30013)%(diact)) + dest, exists = self.getFile(uri) + if not exists: + try: + video = self.getVideo('%s%s'%(BASE_URL,uri)) + if video: + log('getDownloads, url = %s, dest = %s'%(video['url'],dest)) + req = urllib.request.Request(video['url'], headers=HEADER) + response = urllib.request.urlopen(req) + with xbmcvfs.File(dest,'wb') as fle: + fle.write(response.read()) + if self.myMonitor.waitForAbort(5): break #avoid DDoS + uris.pop(uris.index(uri)) + except Exception as e: log('getDownloads Failed! %s\nurl = %s\nvideo = %s'%(e,'%s%s'%(BASE_URL,uri),video)) + self.progressBGDialog(100, dia, message=LANGUAGE(30012)) + queuePool['uri'] = uris + log('getDownloads, remaining urls = %s'%(len(uris))) + self.cache.set('queuePool', queuePool, json_data=True, expiration=datetime.timedelta(days=28)) + + + def progressBGDialog(self, percent=0, control=None, message='', header=ADDON_NAME, silent=None, wait=None): + if control is None and int(percent) == 0: + control = xbmcgui.DialogProgressBG() + control.create(header, message) + elif control: + if int(percent) == 100 or control.isFinished(): + if hasattr(control, 'close'): + control.close() + return None + elif hasattr(control, 'update'): control.update(int(percent), header, message) + if wait: self.myMonitor.waitForAbort(wait/1000) + return control + + + def run(self): + ROUTER.run() + xbmcplugin.setContent(ROUTER.handle ,CONTENT_TYPE) + xbmcplugin.addSortMethod(ROUTER.handle ,xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(ROUTER.handle ,xbmcplugin.SORT_METHOD_NONE) + xbmcplugin.addSortMethod(ROUTER.handle ,xbmcplugin.SORT_METHOD_LABEL) + xbmcplugin.addSortMethod(ROUTER.handle ,xbmcplugin.SORT_METHOD_TITLE) + xbmcplugin.endOfDirectory(ROUTER.handle ,cacheToDisc=DISC_CACHE) \ No newline at end of file diff --git a/plugin.video.ispot.tv/resources/settings.xml b/plugin.video.ispot.tv/resources/settings.xml new file mode 100644 index 0000000000..91ebe4463e --- /dev/null +++ b/plugin.video.ispot.tv/resources/settings.xml @@ -0,0 +1,89 @@ + + +
    + + + + 1 + false + + + + 1 + false + + + + 0 + + + true + + + + + true + !System.HasAddon(script.kodi.loguploader) + + + + + InstallAddon(script.kodi.loguploader) + true + + + + 1 + + + true + + + + + true + System.HasAddon(script.kodi.loguploader) + System.AddonIsEnabled(script.kodi.loguploader) + + + + + RunAddon(script.kodi.loguploader) + true + + + + 1 + false + + + + System.HasAddon(plugin.video.pseudotv.live) + + + + + 1 + special://profile/addon_data/plugin.video.ispot.tv/resources/ + + + files + + true + + + + + true + System.HasAddon(plugin.video.pseudotv.live) + + + + + 30002 + + + + +
    +
    \ No newline at end of file diff --git a/plugin.video.ispot.tv/service.py b/plugin.video.ispot.tv/service.py new file mode 100644 index 0000000000..c90faeb6ab --- /dev/null +++ b/plugin.video.ispot.tv/service.py @@ -0,0 +1,57 @@ +# Copyright (C) 2024 Lunatixz +# +# +# This file is part of iSpot.tv. +# +# iSpot.tv 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. +# +# iSpot.tv 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 iSpot.tv. If not, see . + +# -*- coding: utf-8 -*- + +import sys, time + +from resources.lib import ispot +from kodi_six import xbmc, xbmcaddon, xbmcplugin, xbmcgui, xbmcvfs, py2_encode, py2_decode + +# Plugin Info +ADDON_ID = 'plugin.video.ispot.tv' +REAL_SETTINGS = xbmcaddon.Addon(id=ADDON_ID) + +class Service(object): + def __init__(self): + self.myMonitor = xbmc.Monitor() + self.running = False + self._start() + + + def _check(self, key, runEvery=900): + epoch = int(time.time()) + next = int(xbmcgui.Window(10000).getProperty(key) or '0') + if (epoch >= next): + xbmcgui.Window(10000).setProperty(key,str(epoch+runEvery)) + return True + return False + + + def _start(self): + while not self.myMonitor.abortRequested(): + if self.myMonitor.waitForAbort(5): break + elif not self.running: + self.running = True + iservice = ispot.iSpotTV(sys.argv) + if self._check('queue',86400): iservice.queDownloads() + elif self._check('download',900): iservice.getDownloads() + del iservice + self.running = False + +if __name__ == '__main__': Service() \ No newline at end of file diff --git a/plugin.video.jpcandioti.5rtv/LICENSE.txt b/plugin.video.jpcandioti.5rtv/LICENSE.txt new file mode 100644 index 0000000000..74d6ed9870 --- /dev/null +++ b/plugin.video.jpcandioti.5rtv/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright 2020 Juan Pablo Candioti + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/plugin.video.jpcandioti.5rtv/README.md b/plugin.video.jpcandioti.5rtv/README.md new file mode 100644 index 0000000000..8ab715c313 --- /dev/null +++ b/plugin.video.jpcandioti.5rtv/README.md @@ -0,0 +1,21 @@ +# 5RTV Addon + +Addon para Kodi de 5RTV, el canal público de la Provincia de Santa Fe, Argentina. + +## Licencia + +__5RTV Addon__ está licenciado bajo [Apache License Version 2.0] + + Copyright 2020 Juan Pablo Candioti + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugin.video.jpcandioti.5rtv/addon.xml b/plugin.video.jpcandioti.5rtv/addon.xml new file mode 100644 index 0000000000..c7fd8a821b --- /dev/null +++ b/plugin.video.jpcandioti.5rtv/addon.xml @@ -0,0 +1,33 @@ + + + + + + + + + video + + + 5RTV is the Public Channel of the Province of Santa Fe, Argentina. + 5RTV es el Canal Público de la Provincia de Santa Fe, Argentina. + 5RTV es un canal de los llamados generalistas. +No es un canal temático, ni es un canal “cultural”. +No es tampoco una productora de contenidos. +Es un medio de comunicación público de alcance provincial. +Esto significa que su programación no está basada en documentales unitarios sino en ciclos de programas de interés general que abarcan distintos rubros como Noticias, Política, Información de Municipios, Salud, Tecnología, Deportes, Gastronomía, Tradiciones, Ruralidad, Música, Humor, Empresariales, Entretenimiento, Arte, Infantiles, Ficción, Dibujos Animados, Clips de Oficios, Efemérides, Break News locales, provinciales, nacionales e internacionales etcétera. +El tratamiento de estos temas se realiza siempre preservando los principios esenciales que motivaron la creación de este medio de comunicación: TERRITORIO, IDENTIDAD y PLURALISMO, con la CALIDAD como paradigma innegociable y la CREATIVIDAD como filosofía de trabajo. +Se apuesta al dinamismo que propone el formato de media hora para la mayoría de los programas con unas pocas excepciones que por su trascendencia lo justifiquen. + + + icon.png + fanart.jpg + + all + Apache License Version 2.0 + https://github.com/jpcandioti/plugin.video.jpcandioti.5rtv + + diff --git a/plugin.video.jpcandioti.5rtv/changelog.txt b/plugin.video.jpcandioti.5rtv/changelog.txt new file mode 100644 index 0000000000..0bbddb7663 --- /dev/null +++ b/plugin.video.jpcandioti.5rtv/changelog.txt @@ -0,0 +1,11 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [1.1.0] - 2021-06-23 +### Fixed +- Correcciones necesarias para la versión Matrix. + +## [1.0.0] - 2020-04-23 +### Added +- Permite ver la transmisión de 5RTV en vivo. +- Acceso a la lista de programas emitidos. diff --git a/plugin.video.jpcandioti.5rtv/fanart.jpg b/plugin.video.jpcandioti.5rtv/fanart.jpg new file mode 100644 index 0000000000..ed95a1b1f8 Binary files /dev/null and b/plugin.video.jpcandioti.5rtv/fanart.jpg differ diff --git a/plugin.video.jpcandioti.5rtv/icon.png b/plugin.video.jpcandioti.5rtv/icon.png new file mode 100644 index 0000000000..4214129ecf Binary files /dev/null and b/plugin.video.jpcandioti.5rtv/icon.png differ diff --git a/plugin.video.jpcandioti.5rtv/main.py b/plugin.video.jpcandioti.5rtv/main.py new file mode 100644 index 0000000000..eae95c5cc5 --- /dev/null +++ b/plugin.video.jpcandioti.5rtv/main.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Module: plugin.video.jpcandioti.5rtv +# Author: Juan Pablo Candioti - @JPCandioti +# Site: https://github.com/jpcandioti/plugin.video.jpcandioti.5rtv +# Created on: 2020-03-21 + +import sys +import xbmcgui +import xbmcplugin +import xbmcaddon + +ADDON = xbmcaddon.Addon() +LANGUAGE = ADDON.getLocalizedString + +_HANDLE = int(sys.argv[1]) + +LIVESTREAM_CHANNEL = '22636012' +LIVESTREAM_EVENT = '8242619' +YOUTUBE_CHANNEL = 'UCCvR_NFSKLkPM-7zbGV2Ckg' + +def mainlist(): + listing = [] + + list_item = xbmcgui.ListItem(label=LANGUAGE(32001)) + list_item.setArt({'icon': 'DefaultTVShows.png'}) + url = 'plugin://plugin.video.livestream/?url=%2Flive_now&mode=104&event_id=' + LIVESTREAM_EVENT + '&owner_id=' + LIVESTREAM_CHANNEL + '&video_id=LIVE' + list_item.setProperty('IsPlayable', 'true') + list_item.setInfo(type='video', infoLabels={'title': LANGUAGE(32003)}) + is_folder = False + listing.append((url, list_item, is_folder)) + + list_item = xbmcgui.ListItem(label=LANGUAGE(32002)) + list_item.setArt({'icon': 'DefaultVideoPlaylists.png'}) + url = 'plugin://plugin.video.youtube/channel/' + YOUTUBE_CHANNEL + '/playlists/' + is_folder = True + listing.append((url, list_item, is_folder)) + + list_items(listing) + + +def list_items(listing): + xbmcplugin.addDirectoryItems(_HANDLE, listing, len(listing)) + xbmcplugin.endOfDirectory(_HANDLE) + + +if __name__ == '__main__': + mainlist() diff --git a/plugin.video.jpcandioti.5rtv/resources/language/resource.language.en_gb/strings.po b/plugin.video.jpcandioti.5rtv/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..2497a5ce37 --- /dev/null +++ b/plugin.video.jpcandioti.5rtv/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,23 @@ +# Kodi Media Center language file +# Addon Name: 5RTV Addon +# Addon id: plugin.video.jpcandioti.5rtv +# Addon Provider: @JPCandioti +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" + +msgctxt "#32001" +msgid "Live" +msgstr "Live" + +msgctxt "#32002" +msgid "Contents on demand" +msgstr "Contents on demand" + +msgctxt "#32003" +msgid "5RTV live" +msgstr "5RTV live" + diff --git a/plugin.video.jpcandioti.5rtv/resources/language/resource.language.es_ar/strings.po b/plugin.video.jpcandioti.5rtv/resources/language/resource.language.es_ar/strings.po new file mode 100644 index 0000000000..1b96d79eb7 --- /dev/null +++ b/plugin.video.jpcandioti.5rtv/resources/language/resource.language.es_ar/strings.po @@ -0,0 +1,23 @@ +# Kodi Media Center language file +# Addon Name: 5RTV Addon +# Addon id: plugin.video.jpcandioti.5rtv +# Addon Provider: @JPCandioti +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" + +msgctxt "#32001" +msgid "Live" +msgstr "En vivo" + +msgctxt "#32002" +msgid "Contents on demand" +msgstr "Contenidos disponibles" + +msgctxt "#32003" +msgid "5RTV live" +msgstr "5RTV en vivo" + diff --git a/plugin.video.lacapitv/LICENSE b/plugin.video.lacapitv/LICENSE new file mode 100644 index 0000000000..d8577f551a --- /dev/null +++ b/plugin.video.lacapitv/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + plugin.video.lacapitv Copyright (C) 2022 Ernesto Bazzano + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugin.video.lacapitv/README.md b/plugin.video.lacapitv/README.md new file mode 100644 index 0000000000..5e252e8d1c --- /dev/null +++ b/plugin.video.lacapitv/README.md @@ -0,0 +1,3 @@ +#Kodi La Capi TV + +Un addon para Kodi que reproduce los videos de [play.Lacapi.tv](https://play.Lacapi.tv) diff --git a/plugin.video.lacapitv/addon.xml b/plugin.video.lacapitv/addon.xml new file mode 100644 index 0000000000..99e9fc4e86 --- /dev/null +++ b/plugin.video.lacapitv/addon.xml @@ -0,0 +1,33 @@ + + + + + + + + + video + + + all + Interspace television + An intergalactic streaming service where you can find La Capi animations among many other series and small animation experiments. + + Una televisión interespacial + Un servicio de streaming intergaláctico donde podés encontrar animaciones de La Capi entre tantas otras series y pequeños experimentos de animación. + + GPL-3.0-or-later + es + https://git.tuxfamily.org/4232/kodi_plugin_video_lacapitv.git/ + https://lacapi.tv + info@lacapi.tv + + resources/icon.png + resources/fanart.jpg + resources/01.jpg + resources/02.jpg + resources/03.jpg + + Start the project + + diff --git a/plugin.video.lacapitv/default.py b/plugin.video.lacapitv/default.py new file mode 100644 index 0000000000..b3e147d433 --- /dev/null +++ b/plugin.video.lacapitv/default.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# KodiAddon +# +from resources.lib.scraper import myAddon +import re +import sys + +# Start of Module + +myAddon("lacapitv").processAddonEvent() diff --git a/plugin.video.lacapitv/resources/01.jpg b/plugin.video.lacapitv/resources/01.jpg new file mode 100644 index 0000000000..bd75937166 Binary files /dev/null and b/plugin.video.lacapitv/resources/01.jpg differ diff --git a/plugin.video.lacapitv/resources/02.jpg b/plugin.video.lacapitv/resources/02.jpg new file mode 100644 index 0000000000..d1d5ba8ed4 Binary files /dev/null and b/plugin.video.lacapitv/resources/02.jpg differ diff --git a/plugin.video.lacapitv/resources/03.jpg b/plugin.video.lacapitv/resources/03.jpg new file mode 100644 index 0000000000..a5c7c673a5 Binary files /dev/null and b/plugin.video.lacapitv/resources/03.jpg differ diff --git a/plugin.video.lacapitv/resources/fanart.jpg b/plugin.video.lacapitv/resources/fanart.jpg new file mode 100644 index 0000000000..ac7534dd4e Binary files /dev/null and b/plugin.video.lacapitv/resources/fanart.jpg differ diff --git a/plugin.video.lacapitv/resources/icon.png b/plugin.video.lacapitv/resources/icon.png new file mode 100644 index 0000000000..553eacf0ee Binary files /dev/null and b/plugin.video.lacapitv/resources/icon.png differ diff --git a/plugin.video.lacapitv/resources/language/resource.language.en_gb/strings.po b/plugin.video.lacapitv/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..a9531c1d3b --- /dev/null +++ b/plugin.video.lacapitv/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,32 @@ +# Kodi Media Center language file +# Addon Name: La Capi TV +# Addon id: plugin.video.lacapitv +# Addon Provider: Ernesto Bazzano +msgid "" +msgstr "" +"Report-Msgid-Bugs-To: info@lacapi.tv\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 "#30001" +msgid "Enter your e-mail address" +msgstr "" + +msgctxt "#30002" +msgid "Enter your password" +msgstr "" + +msgctxt "#30003" +msgid "Login" +msgstr "" + +msgctxt "#30004" +msgid "Active" +msgstr "" + +msgctxt "#30005" +msgid "To view the complete video creates an account in play.lacapi.tv" +msgstr "" \ No newline at end of file diff --git a/plugin.video.lacapitv/resources/language/resource.language.es_ar/strings.po b/plugin.video.lacapitv/resources/language/resource.language.es_ar/strings.po new file mode 100644 index 0000000000..7ad3f0821a --- /dev/null +++ b/plugin.video.lacapitv/resources/language/resource.language.es_ar/strings.po @@ -0,0 +1,32 @@ +# Kodi Media Center language file +# Addon Name: La Capi TV +# Addon id: plugin.video.lacapitv +# Addon Provider: Ernesto Bazzano +msgid "" +msgstr "" +"Report-Msgid-Bugs-To: info@lacapi.tv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30001" +msgid "Enter your e-mail address" +msgstr "Ingresá tu dirección de correo electrónico" + +msgctxt "#30002" +msgid "Enter your password" +msgstr "Ingresá tu contraseña" + +msgctxt "#30003" +msgid "Login" +msgstr "Inicio de sesión" + +msgctxt "#30004" +msgid "Active" +msgstr "Activar" + +msgctxt "#30005" +msgid "To view the complete video creates an account in play.lacapi.tv" +msgstr "Para ver el video completo cree una cuenta en play.lacapi.tv" \ No newline at end of file diff --git a/plugin.video.lacapitv/resources/lib/scraper.py b/plugin.video.lacapitv/resources/lib/scraper.py new file mode 100644 index 0000000000..ddf50a0752 --- /dev/null +++ b/plugin.video.lacapitv/resources/lib/scraper.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# KodiAddon (La Capi TV) +# +from t1mlib import t1mAddon +import json +import re +import xbmcplugin, xbmcaddon +import xbmcgui +import sys +import xbmc +import requests + +settings = xbmcaddon.Addon(id="plugin.video.lacapitv") +cuenta = "" +path = "" +if settings.getSetting('activa') == "true": + path ="p/" + cuenta = settings.getSetting('username') + ":" + settings.getSetting('password') + "@" +html = requests.get('https://'+cuenta+'play.lacapi.tv/'+path+'show.json').text +translation = settings.getLocalizedString + +class myAddon(t1mAddon): + def getAddonMenu(self,url,ilist): + a = json.loads(html) + for b in a['shows']: + name = b['titulo'] + url = b['slug'] + thumb = b['poster'] + fanart = thumb + infoList = {'mediatype':'tvshow', + 'TVShowTitle': name, + 'Title': name, + 'Plot': b['descripcion']} + ilist = self.addMenuItem(name,'GE', ilist, url, thumb, fanart, infoList, isFolder=True) + return(ilist) + + def getAddonEpisodes(self,url,ilist): + a = json.loads(html) + for c in a['shows']: + if c['slug'] != url: + continue + for b in c["capitulo"]: + name = b['titulo'] + thumb = b['poster'] + fanart = thumb + url = b['video'] + mensaje = "" + if (b['privado']) and path == "" : + mensaje="\r\n\r\n" + translation(30005) + infoList = {'mediatype':'episode', + 'Title': name, + 'TVShowTitle': xbmc.getInfoLabel('ListItem.TVShowTitle'), + 'Plot': b['descripcion'] + mensaje, + 'Duration': 0, + 'Episode': 0 , + 'Premiered': b['privado'] } + ilist = self.addMenuItem(name,'GV', ilist, url, thumb, fanart, infoList, isFolder=False) + return(ilist) + + def getAddonVideo(self,url): + liz = xbmcgui.ListItem(path = url, offscreen=True) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, liz) diff --git a/plugin.video.lacapitv/resources/settings.xml b/plugin.video.lacapitv/resources/settings.xml new file mode 100644 index 0000000000..2ea76afbfb --- /dev/null +++ b/plugin.video.lacapitv/resources/settings.xml @@ -0,0 +1,40 @@ + + +
    + + + + 0 + false + + + + 0 + + + true + + + 30001 + + + + 0 + + + true + + + + + + + + 30002 + true + + + + +
    +
    diff --git a/plugin.video.lbry/LICENSE.txt b/plugin.video.lbry/LICENSE.txt new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/plugin.video.lbry/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugin.video.lbry/README.md b/plugin.video.lbry/README.md new file mode 100644 index 0000000000..12edd5a0ba --- /dev/null +++ b/plugin.video.lbry/README.md @@ -0,0 +1,9 @@ +# Kodi/XBMC plugin for LBRY + +This is a basic plugin for accessing [LBRY](https://lbry.com) content (video's only, for now). + +By default you don't need to install any extra software to start using this LBRY plugin, the plugin uses the API server provided by lbry.tv (https://api.lbry.tv/api/v1/proxy). + +Alternatively, you can run your own API server and contribute to the LBRY network by hosting content data of videos you watched. This enables the 'Download' feature in the plugin, so you can watch videos uninterrupted or save the video to a local file. Also this enables wallet features like watching paid videos or tipping authors. + +You will need to run `lbrynet` client (installation described here: https://github.com/lbryio/lbry-sdk) and have a bit of storage space available. diff --git a/plugin.video.lbry/addon.xml b/plugin.video.lbry/addon.xml new file mode 100644 index 0000000000..d214f25f92 --- /dev/null +++ b/plugin.video.lbry/addon.xml @@ -0,0 +1,22 @@ + + + + + + + + + video + + + Plugin for LBRY + LBRY is a secure, open, and community-run digital marketplace + all + GPL-2.0-or-later + https://github.com/accumulator/plugin.video.lbry + + resources/assets/256x256.png + resources/assets/fanart.jpg + + + diff --git a/plugin.video.lbry/lbry.py b/plugin.video.lbry/lbry.py new file mode 100644 index 0000000000..8797a8d7c6 --- /dev/null +++ b/plugin.video.lbry/lbry.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +from resources.lib import plugin + +plugin.run() diff --git a/plugin.video.lbry/resources/assets/256x256.png b/plugin.video.lbry/resources/assets/256x256.png new file mode 100644 index 0000000000..c477c41ae5 Binary files /dev/null and b/plugin.video.lbry/resources/assets/256x256.png differ diff --git a/plugin.video.lbry/resources/assets/fanart.jpg b/plugin.video.lbry/resources/assets/fanart.jpg new file mode 100644 index 0000000000..befaef8eb7 Binary files /dev/null and b/plugin.video.lbry/resources/assets/fanart.jpg differ diff --git a/plugin.video.lbry/resources/language/resource.language.en_gb/strings.po b/plugin.video.lbry/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..88f1b2f2e8 --- /dev/null +++ b/plugin.video.lbry/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,265 @@ +msgid "" +msgstr "" +"Project-Id-Version: plugin.video.lbry\n" +"POT-Creation-Date: 2020-12-01 12:53+0100\n" +"PO-Revision-Date: 2021-06-14 10:03+0800\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.2\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# settings 300xx +msgctxt "#30000" +msgid "API Server" +msgstr "" + +msgctxt "#30001" +msgid "Show NSFW" +msgstr "" + +msgctxt "#30002" +msgid "Items per page" +msgstr "" + +msgctxt "#30003" +msgid "Kludges" +msgstr "" + +msgctxt "#30004" +msgid "Workaround server filter time out" +msgstr "" + +msgctxt "#30005" +msgid "Options" +msgstr "" + +# errors 301xx +msgctxt "#30100" +msgid "Error" +msgstr "" + +msgctxt "#30101" +msgid "Server Error" +msgstr "" + +msgctxt "#30102" +msgid "API Error" +msgstr "" + +msgctxt "#30103" +msgid "Not implemented yet" +msgstr "" + +msgctxt "#30104" +msgid "Save error" +msgstr "" + +msgctxt "#30105" +msgid "Connection failed" +msgstr "" + +msgctxt "#30106" +msgid "API Server unreachable" +msgstr "" + +msgctxt "#30107" +msgid "Comment API Error" +msgstr "" + +msgctxt "#30108" +msgid "Comment API Server unreachable" +msgstr "" + +# generic UI 302xx +msgctxt "#30200" +msgid "Followed Channels" +msgstr "" + +msgctxt "#30201" +msgid "Search" +msgstr "" + +msgctxt "#30202" +msgid "New" +msgstr "" + +msgctxt "#30203" +msgid "Next Page" +msgstr "" + +msgctxt "#30204" +msgid "Payment required" +msgstr "" + +msgctxt "#30205" +msgid "Follow %s" +msgstr "" + +msgctxt "#30206" +msgid "Unfollow %s" +msgstr "" + +msgctxt "#30207" +msgid "Go to %s" +msgstr "" + +msgctxt "#30208" +msgid "Download" +msgstr "" + +msgctxt "#30209" +msgid "Enter search terms" +msgstr "" + +msgctxt "#30210" +msgid "Playlists" +msgstr "" + +msgctxt "#30211" +msgid "Watch Later" +msgstr "" + +msgctxt "#30212" +msgid "Add to %s" +msgstr "" + +msgctxt "#30213" +msgid "Remove from %s" +msgstr "" + +msgctxt "#30214" +msgid "Pay %.2f %s for this video?" +msgstr "" + +msgctxt "#30215" +msgid "Current balance: %.2f %s" +msgstr "" + +msgctxt "#30216" +msgid "Repost" +msgstr "" + +msgctxt "#30217" +msgid "Repost in %s" +msgstr "" + +msgctxt "#30218" +msgid "Recent" +msgstr "" + +msgctxt "#30219" +msgid "Loading comments..." +msgstr "" + +msgctxt "#30220" +msgid "Fetching Page" +msgstr "" + +msgctxt "#30221" +msgid "New Comment" +msgstr "" + +msgctxt "#30222" +msgid "Reply" +msgstr "" + +msgctxt "#30223" +msgid "Edit" +msgstr "" + +msgctxt "#30224" +msgid "Remove" +msgstr "" + +msgctxt "#30225" +msgid "Change User" +msgstr "" + +msgctxt "#30226" +msgid "Like" +msgstr "" + +msgctxt "#30227" +msgid "Dislike" +msgstr "" + +msgctxt "#30228" +msgid "Clear Vote" +msgstr "" + +msgctxt "#30229" +msgid "Select User Channel" +msgstr "" + +msgctxt "#30230" +msgid "No Comments" +msgstr "" + +msgctxt "#30231" +msgid "Loading user channel list..." +msgstr "" + +msgctxt "#30232" +msgid "No owned channels found." +msgstr "" + +msgctxt "#30233" +msgid "Found user." +msgstr "" + +msgctxt "#30234" +msgid "Multiple users found. Select a user." +msgstr "" + +msgctxt "#30235" +msgid "Verifying user channel..." +msgstr "" + +msgctxt "#30236" +msgid "User verified!" +msgstr "" + +msgctxt "#30237" +msgid "User could not be verified!" +msgstr "" + +msgctxt "#30238" +msgid "Comments" +msgstr "" + +msgctxt "#30239" +msgid "Post As" +msgstr "" + +msgctxt "#30240" +msgid "Refresh" +msgstr "" + +msgctxt "#30241" +msgid "Posting" +msgstr "" + +msgctxt "#30242" +msgid "Waiting for response..." +msgstr "" + +msgctxt "#30243" +msgid "Select channel" +msgstr "" + +msgctxt "#30244" +msgid "Channel: " +msgstr "" + +msgctxt "#30245" +msgid "Clear channel" +msgstr "" + +msgctxt "#30246" +msgid "Login" +msgstr "" diff --git a/plugin.video.lbry/resources/lib/exception.py b/plugin.video.lbry/resources/lib/exception.py new file mode 100644 index 0000000000..b8fdb9b0d0 --- /dev/null +++ b/plugin.video.lbry/resources/lib/exception.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +class PluginException(Exception): + pass diff --git a/plugin.video.lbry/resources/lib/local.py b/plugin.video.lbry/resources/lib/local.py new file mode 100644 index 0000000000..258a93a6d4 --- /dev/null +++ b/plugin.video.lbry/resources/lib/local.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import xbmc +import xbmcaddon +import xbmcvfs +import xbmcgui + +ADDON = xbmcaddon.Addon() +tr = ADDON.getLocalizedString + +def get_profile_path(rpath): + return xbmcvfs.translatePath(ADDON.getAddonInfo('profile') + rpath) + +def load_channel_subs(): + channels = [] + try: + f = xbmcvfs.File(get_profile_path('channel_subs'), 'r') + lines = f.readBytes() + f.close() + except Exception as e: + pass + lines = lines.decode('utf-8') + for line in lines.split('\n'): + items = line.split('#') + if len(items) < 2: + continue + channels.append((items[0],items[1])) + return channels + +def save_channel_subs(channels): + try: + with xbmcvfs.File(get_profile_path('channel_subs'), 'w') as f: + for (name, claim_id) in channels: + f.write(bytearray(name.encode('utf-8'))) + f.write('#') + f.write(bytearray(claim_id.encode('utf-8'))) + f.write('\n') + except Exception as e: + xbmcgui.Dialog().notification(tr(30104), str(e), xbmcgui.NOTIFICATION_ERROR) + +def load_playlist(name): + items = [] + try: + with xbmcvfs.File(get_profile_path(name + '.list'), 'r') as f: + lines = f.readBytes() + except Exception as e: + pass + lines = lines.decode('utf-8') + for line in lines.split('\n'): + if line != '': + items.append(line) + return items + +def save_playlist(name, items): + try: + with xbmcvfs.File(get_profile_path(name + '.list'), 'w') as f: + for item in items: + f.write(bytearray(item.encode('utf-8'))) + f.write('\n') + except Exception as e: + xbmcgui.Dialog().notification(tr(30104), str(e), xbmcgui.NOTIFICATION_ERROR) diff --git a/plugin.video.lbry/resources/lib/plugin.py b/plugin.video.lbry/resources/lib/plugin.py new file mode 100644 index 0000000000..45b5685662 --- /dev/null +++ b/plugin.video.lbry/resources/lib/plugin.py @@ -0,0 +1,973 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import xbmc +import xbmcaddon +from xbmcgui import ListItem, Dialog, NOTIFICATION_ERROR, Window, WindowXML +from xbmcplugin import addDirectoryItem, addDirectoryItems, endOfDirectory, setContent, setResolvedUrl + +import routing +import requests +import time + +from urllib.parse import quote,unquote,quote_plus,unquote_plus + +from resources.lib.local import * +from resources.lib.exception import * + +ADDON = xbmcaddon.Addon() +tr = ADDON.getLocalizedString +lbry_api_url = unquote(ADDON.getSetting('lbry_api_url')) +if lbry_api_url == '': + raise Exception('Lbry API URL is undefined.') +using_lbry_proxy = lbry_api_url.find('api.lbry.tv') != -1 + +odysee_comment_api_url = 'https://comments.odysee.com/api/v2' + +# assure profile directory exists +profile_path = ADDON.getAddonInfo('profile') +if not xbmcvfs.exists(profile_path): + xbmcvfs.mkdir(profile_path) + +items_per_page = ADDON.getSettingInt('items_per_page') +nsfw = ADDON.getSettingBool('nsfw') + +plugin = routing.Plugin() +ph = plugin.handle +setContent(ph, 'videos') +dialog = Dialog() + +def call_rpc(method, params={}, errdialog=True): + try: + xbmc.log('call_rpc: url=' + lbry_api_url + ', method=' + method + ', params=' + str(params)) + headers = {'content-type' : 'application/json'} + json = { 'jsonrpc' : '2.0', 'id' : 1, 'method': method, 'params': params } + result = requests.post(lbry_api_url, headers=headers, json=json) + result.raise_for_status() + rjson = result.json() + if 'error' in rjson: + raise PluginException(rjson['error']['message']) + return result.json()['result'] + except requests.exceptions.ConnectionError as e: + if errdialog: + dialog.notification(tr(30105), tr(30106), NOTIFICATION_ERROR) + raise PluginException(e) + except requests.exceptions.HTTPError as e: + if errdialog: + dialog.notification(tr(30101), str(e), NOTIFICATION_ERROR) + raise PluginException(e) + except PluginException as e: + if errdialog: + dialog.notification(tr(30102), str(e), NOTIFICATION_ERROR) + raise e + except Exception as e: + xbmc.log('call_rpc exception:' + str(e)) + raise e + +def call_comment_rpc(method, params={}, errdialog=True): + try: + xbmc.log('call_comment_rpc: url=' + odysee_comment_api_url + ', method=' + method + ', params=' + str(params)) + headers = {'content-type' : 'application/json'} + json = { 'jsonrpc' : '2.0', 'id' : 1, 'method': method, 'params': params } + result = requests.post(odysee_comment_api_url, headers=headers, json=json) + result.raise_for_status() + rjson = result.json() + if 'error' in rjson: + raise PluginException(rjson['error']['message']) + return result.json()['result'] + except requests.exceptions.ConnectionError as e: + if errdialog: + dialog.notification(tr(30105), tr(30108), NOTIFICATION_ERROR) + raise PluginException(e) + except requests.exceptions.HTTPError as e: + if errdialog: + dialog.notification(tr(30101), str(e), NOTIFICATION_ERROR) + raise PluginException(e) + except PluginException as e: + if errdialog: + dialog.notification(tr(30107), str(e), NOTIFICATION_ERROR) + raise e + except Exception as e: + xbmc.log('call_comment_rpc exception:' + str(e)) + raise e + +# Sign data if a user channel is selected +def sign(data): + user_channel = get_user_channel() + if not user_channel: + return None + + # assume data type is str + if type(data) is not str: + raise Exception('attempt to sign non-str type') + + bdata = bytes(data, 'utf-8') + + toHex = lambda x : "".join([format(c,'02x') for c in x]) + + return call_rpc('channel_sign', params={'channel_id': user_channel[1], 'hexdata': toHex(bdata)}) + + +def serialize_uri(item): + # all uris passed via kodi's routing system must be urlquoted + if type(item) is dict: + return quote(item['name'] + '#' + item['claim_id']) + else: + return quote(item) + +def deserialize_uri(item): + # all uris passed via kodi's routing system must be urlquoted + return unquote(item) + +def to_video_listitem(item, playlist='', channel='', repost=None): + li = ListItem(item['value']['title'] if 'title' in item['value'] else item['file_name'] if 'file_name' in item else '') + li.setProperty('IsPlayable', 'true') + if 'thumbnail' in item['value'] and 'url' in item['value']['thumbnail']: + li.setArt({ + 'thumb': item['value']['thumbnail']['url'], + 'poster': item['value']['thumbnail']['url'], + 'fanart': item['value']['thumbnail']['url'] + }) + + infoLabels = {} + menu = [] + plot = '' + if 'description' in item['value']: + plot = item['value']['description'] + if 'author' in item['value']: + infoLabels['writer'] = item['value']['author'] + elif 'channel_name' in item: + infoLabels['writer'] = item['channel_name'] + if 'timestamp' in item: + timestamp = time.localtime(item['timestamp']) + infoLabels['year'] = timestamp.tm_year + infoLabels['premiered'] = time.strftime('%Y-%m-%d',timestamp) + if 'video' in item['value'] and 'duration' in item['value']['video']: + infoLabels['duration'] = str(item['value']['video']['duration']) + + if playlist == '': + if 'signing_channel' in item and 'name' in item['signing_channel']: + comment_uri = item['signing_channel']['name'] + '#' + item['signing_channel']['claim_id'] + '#' + item['claim_id'] + menu.append(( + tr(30238), 'RunPlugin(%s)' % plugin.url_for(plugin_comment_show, uri=serialize_uri(comment_uri)) + )) + + menu.append(( + tr(30212) % tr(30211), 'RunPlugin(%s)' % plugin.url_for(plugin_playlist_add, name=quote(tr(30211)), uri=serialize_uri(item)) + )) + else: + menu.append(( + tr(30213) % tr(30211), 'RunPlugin(%s)' % plugin.url_for(plugin_playlist_del, name=quote(tr(30211)), uri=serialize_uri(item)) + )) + + menu.append(( + tr(30208), 'RunPlugin(%s)' % plugin.url_for(claim_download, uri=serialize_uri(item)) + )) + + if 'signing_channel' in item and 'name' in item['signing_channel']: + ch_name = item['signing_channel']['name'] + ch_claim = item['signing_channel']['claim_id'] + ch_title = '' + if 'value' in item['signing_channel'] and 'title' in item['signing_channel']['value']: + ch_title = item['signing_channel']['value']['title'] + + plot = '[B]' + (ch_title if ch_title.strip() != '' else ch_name) + '[/B]\n' + plot + + infoLabels['studio'] = ch_name + + if channel == '': + menu.append(( + tr(30207) % ch_name, 'Container.Update(%s)' % plugin.url_for(lbry_channel, uri=serialize_uri(item['signing_channel']),page=1) + )) + menu.append(( + tr(30205) % ch_name, 'RunPlugin(%s)' % plugin.url_for(plugin_follow, uri=serialize_uri(item['signing_channel'])) + )) + + if repost != None: + if 'signing_channel' in repost and 'name' in repost['signing_channel']: + plot = (('[COLOR yellow]%s[/COLOR]\n' % tr(30217)) % repost['signing_channel']['name']) + plot + else: + plot = ('[COLOR yellow]%s[/COLOR]\n' % tr(30216)) + plot + + infoLabels['plot'] = plot + li.setInfo('video', infoLabels) + li.addContextMenuItems(menu) + + return li + +def result_to_itemlist(result, playlist='', channel=''): + items = [] + for item in result: + if not 'value_type' in item: + xbmc.log(str(item)) + continue + if item['value_type'] == 'stream' and 'stream_type' in item['value'] and item['value']['stream_type'] == 'video': + # nsfw? + if 'tags' in item['value']: + if 'mature' in item['value']['tags'] and not nsfw: + continue + + li = to_video_listitem(item, playlist, channel) + url = plugin.url_for(claim_play, uri=serialize_uri(item)) + + items.append((url, li)) + elif item['value_type'] == 'repost' and 'reposted_claim' in item and item['reposted_claim']['value_type'] == 'stream' and item['reposted_claim']['value']['stream_type'] == 'video': + stream_item = item['reposted_claim'] + # nsfw? + if 'tags' in stream_item['value']: + if 'mature' in stream_item['value']['tags'] and not nsfw: + continue + + li = to_video_listitem(stream_item, playlist, channel, repost=item) + url = plugin.url_for(claim_play, uri=serialize_uri(stream_item)) + + items.append((url, li)) + elif item['value_type'] == 'channel': + li = ListItem('[B]%s[/B] [I]#%s[/I]' % (item['name'], item['claim_id'][0:4])) + li.setProperty('IsFolder','true') + if 'thumbnail' in item['value'] and 'url' in item['value']['thumbnail']: + li.setArt({ + 'thumb': item['value']['thumbnail']['url'], + 'poster': item['value']['thumbnail']['url'], + 'fanart': item['value']['thumbnail']['url'] + }) + url = plugin.url_for(lbry_channel, uri=serialize_uri(item),page=1) + + menu = [] + ch_name = item['name'] + menu.append(( + tr(30205) % ch_name, 'RunPlugin(%s)' % plugin.url_for(plugin_follow, uri=serialize_uri(item)) + )) + li.addContextMenuItems(menu) + + items.append((url, li, True)) + else: + xbmc.log('ignored item, value_type=' + item['value_type']) + xbmc.log('item name=' + item['name']) + + return items + +def get_user_channel(): + user_channel_str = ADDON.getSettingString('user_channel') + if user_channel_str: + toks = user_channel_str.split('#') + if len(toks) == 2: + return (toks[0], toks[1]) + return None + +def set_user_channel(channel_name, channel_id): + ADDON.setSettingString('user_channel', "%s#%s" % (channel_name, channel_id)) + ADDON.setSettingString('user_channel_vis', "%s#%s" % (channel_name, channel_id[:5])) + +@plugin.route('/clear_user_channel') +def clear_user_channel(): + ADDON.setSettingString('user_channel', '') + ADDON.setSettingString('user_channel_vis', '') + +@plugin.route('/select_user_channel') +def select_user_channel(): + progressDialog = xbmcgui.DialogProgress() + progressDialog.create(tr(30231)) + + page = 1 + total_pages = 1 + items = [] + while page <= total_pages: + if progressDialog.iscanceled(): + break + + try: + params = {'page' : page} + result = call_rpc('channel_list', params, errdialog=not using_lbry_proxy) + total_pages = max(result['total_pages'], 1) # Total pages returns 0 if empty + if 'items' in result: + items += result['items'] + else: + break + except: + pass + + page = page + 1 + progressDialog.update(int(100.0*page/total_pages), tr(30220) + ' %s/%s' % (page, total_pages)) + + selected_item = None + + if len(items) == 0: + progressDialog.update(100, tr(30232)) # No owned channels found + xbmc.sleep(1000) + progressDialog.close() + return + elif len(items) == 1: + progressDialog.update(100, tr(30233)) # Found single user + xbmc.sleep(1000) + progressDialog.close() + + selected_item = items[0] + else: + progressDialog.update(100, tr(30234)) # Multiple users found + xbmc.sleep(1000) + progressDialog.close() + + names = [] + for item in items: + names.append(item['name']) + + selected_name_index = dialog.select(tr(30239), names) # Post As + + if selected_name_index >= 0: # If not cancelled + selected_item = items[selected_name_index] + + if selected_item: + set_user_channel(selected_item['name'], selected_item['claim_id']) + +class CommentWindow(WindowXML): + def __init__(self, *args, **kwargs): + self.channel_name = kwargs['channel_name'] + self.channel_id = kwargs['channel_id'] + self.claim_id = kwargs['claim_id'] + self.last_selected_position = -1 + WindowXML.__init__(self, args, kwargs) + + def onInit(self): + self.refresh() + + def onAction(self, action): + if action == xbmcgui.ACTION_CONTEXT_MENU: + # Commenting is not supported + if using_lbry_proxy: + ret = dialog.contextmenu([tr(30240)]) # Only allow refreshing + if ret == 0: + self.refresh() + return + + # No user channel. Allow user to select an account or refresh. + if not get_user_channel(): + ret = dialog.contextmenu([tr(30240)]) + if ret == 0: + self.refresh() + return + + # User channel selected. Allow comment manipulation. + user_channel = get_user_channel() + if get_user_channel(): + ccl = self.get_comment_control_list() + selected_pos = ccl.getSelectedPosition() + item = ccl.getSelectedItem() + + menu = [] + offsets = [] + offset = 0 + invalid_offset = 10000 + if item: + comment_id = item.getProperty('id') + + menu.append(tr(30226)) # Like + offsets.append(0) + + menu.append(tr(30227)) # Dislike + offsets.append(1) + + menu.append(tr(30228)) # Clear Vote + offsets.append(2) + + offset = 3 + else: + offsets.append(invalid_offset) + offsets.append(invalid_offset) + offsets.append(invalid_offset) + offset = 0 + + menu.append(tr(30221)) # New comment + offsets.append(offset) + offset = offset + 1 + + if item: + menu.append(tr(30222)) # Reply + offsets.append(offset) + offset = offset + 1 + + if item.getProperty('channel_id') == get_user_channel()[1]: + + menu.append(tr(30223)) # Edit + offsets.append(offset) + offset = offset + 1 + + menu.append(tr(30224)) # Remove + offsets.append(offset) + offset = offset + 1 + else: + offsets.append(invalid_offset) + offsets.append(invalid_offset) + else: + offsets.append(invalid_offset) + offsets.append(invalid_offset) + offsets.append(invalid_offset) + + menu.append(tr(30240)) # Refresh + offsets.append(offset) + + ret = dialog.contextmenu(menu) + + if ret == offsets[0]: # Like + self.like(comment_id) + item.setProperty('my_vote', str(1)) + self.refresh_label(item) + + elif ret == offsets[1]: # Dislike + self.dislike(comment_id) + item.setProperty('my_vote', str(-1)) + self.refresh_label(item) + + elif ret == offsets[2]: # Clear Vote + self.neutral(comment_id, item.getProperty('my_vote')) + item.setProperty('my_vote', str(0)) + self.refresh_label(item) + + elif ret == offsets[3]: # New Comment + comment = dialog.input(tr(30221), type=xbmcgui.INPUT_ALPHANUM) + if comment: + comment_id = self.create_comment(comment) + + # Remove 'No Comments' item + if ccl.size() == 1 and ccl.getListItem(0).getLabel() == tr(30230): + ccl.reset() + + # Add new comment item + ccl.addItem(self.create_list_item(comment_id, user_channel[0], user_channel[1], 0, 0, comment, 0, 1)) + ccl.selectItem(ccl.size()-1) + + elif ret == offsets[4]: # Reply + comment = dialog.input(tr(30222), type=xbmcgui.INPUT_ALPHANUM) + if comment: + comment_id = self.create_comment(comment, comment_id) + + # Insert new item by copying the list (no XMBC method to allow a fast insertion). + newItems = [] + for i in range(selected_pos+1): + newItems.append(self.copy_list_item(ccl.getListItem(i))) + newItems.append(self.create_list_item(comment_id, user_channel[0], user_channel[1], 0, 0, comment, int(item.getProperty('indent'))+1, 1)) + for i in range(selected_pos+1, ccl.size()): + newItems.append(self.copy_list_item(ccl.getListItem(i))) + + ccl.reset() + ccl.addItems(newItems) + ccl.selectItem(selected_pos+1) + + elif ret == offsets[5]: # Edit + id = item.getProperty('id'); + comment = item.getProperty('comment') + comment = dialog.input(tr(30223), type=xbmcgui.INPUT_ALPHANUM, defaultt=comment) + if comment: + self.edit_comment(id, comment) + item.setProperty('comment', comment) + self.refresh_label(item) + + elif ret == offsets[6]: # Change User + indentRemoved = item.getProperty('indent') + self.remove_comment(comment_id) + ccl.removeItem(selected_pos) + + while True: + if selected_pos == ccl.size(): + break + indent = ccl.getListItem(selected_pos).getProperty('indent') + if indent <= indentRemoved: + break + ccl.removeItem(selected_pos) + + if selected_pos > 0: + ccl.selectItem(selected_pos-1) + + if ccl.size() == 0: + ccl.addItem(ListItem(label=tr(30230))) + + elif ret == offsets[7]: # Refresh + self.refresh() + + else: + WindowXML.onAction(self, action) + + # If an action changes the selected item position refresh the label + ccl = self.get_comment_control_list() + if self.last_selected_position != ccl.getSelectedPosition(): + if self.last_selected_position >= 0 and self.last_selected_position < ccl.size(): + oldItem = ccl.getListItem(self.last_selected_position) + if oldItem: + self.refresh_label(oldItem, False) + newItem = ccl.getSelectedItem() + if newItem: + self.refresh_label(newItem, True) + self.last_selected_position = ccl.getSelectedPosition() + + def fetch_comment_list(self, page): + return call_comment_rpc('comment.List', params={"page":page,"page_size":50,'include_replies':True,'visible':False,'hidden':False,'top_level':False,'channel_name':self.channel_name,'channel_id':self.channel_id,'claim_id':self.claim_id,'sort_by':0}) + + def fetch_react_list(self, comment_ids): + user_channel = get_user_channel() + params = {'comment_ids' : comment_ids } + if user_channel: + params['channel_name'] = user_channel[0] + params['channel_id'] = user_channel[1] + self.sign(user_channel[0], params) + return call_comment_rpc('reaction.List', params=params) + + def refresh(self): + self.last_selected_position = -1 + progressDialog = xbmcgui.DialogProgress() + progressDialog.create(tr(30219), tr(30220) + ' 1') + + ccl = self.get_comment_control_list() + + page = 1 + result = self.fetch_comment_list(page) + total_pages = result['total_pages'] + + while page < total_pages: + if progressDialog.iscanceled(): + break + progressDialog.update(int(100.0*page/total_pages), tr(30220) + " %s/%s" % (page + 1, total_pages)) + page = page+1 + result['items'] += self.fetch_comment_list(page)['items'] + + if 'items' in result: + ccl.reset() + items = result['items'] + + # Grab the likes and dislikes. + comment_ids = '' + for item in items: + comment_ids += item['comment_id'] + ',' + result = self.fetch_react_list(comment_ids) + others_reactions = result['others_reactions'] + + # Items are returned newest to oldest which implies that child comments are always before their parents. + # Iterate from oldest to newest comments building up a pre-order traversal ordering of the comment tree. Order + # the tree roots by decreasing score (likes-dislikes). + sort_indices = [] + i = len(items)-1 + while i >= 0: + item = items[i] + comment_id = item['comment_id'] + if 'parent_id' in item and item['parent_id'] != 0: + for j in range(len(sort_indices)): # search for the parent in the sorted index list + sorted_item = items[sort_indices[j][0]] + indent = sort_indices[j][1] + if sorted_item['comment_id'] == item['parent_id']: # found the parent + # Insert at the end of the subtree of the parent. Use the indentation to figure + # out where the end is. + while j+1 < len(sort_indices): + if sort_indices[j+1][1] > indent: # Item with index j+1 is in the child subtree + j = j+1 + else: # Item with index j+1 is not in the child subtree. Break and insert before this item. + break + sort_indices.insert(j+1, (i, indent+1, 0)) + break + else: + reaction = others_reactions[comment_id] + likes = reaction['like'] + dislikes = reaction['dislike'] + score = likes-dislikes + + j = 0 + insert_index = len(sort_indices) + while j < len(sort_indices): + if sort_indices[j][1] == 0 and score > sort_indices[j][2]: + insert_index = j + break + j = j+1 + + sort_indices.insert(insert_index, (i, 0, score)) + + i -= 1 + + for (index,indent,score) in sort_indices: + item = items[index] + channel_name = item['channel_name'] + channel_id = item['channel_id'] + comment = item['comment'] + comment_id = item['comment_id'] + reaction = result['others_reactions'][comment_id] + likes = reaction['like'] + dislikes = reaction['dislike'] + + if 'my_reactions' in result: + my_reaction = result['my_reactions'][comment_id] + my_vote = my_reaction['like'] - my_reaction['dislike'] + else: + my_vote = 0 + + ccl.addItem(self.create_list_item(comment_id, channel_name, channel_id, likes, dislikes, comment, indent, my_vote)) + else: + if ccl.size() == 0: + ccl.addItem(ListItem(label=tr(30230))) # No Comments + + progressDialog.update(100) + progressDialog.close() + + def get_comment_control_list(self): + return self.getControl(1) + + def create_list_item(self, comment_id, channel_name, channel_id, likes, dislikes, comment, indent, my_vote): + li = ListItem(label=self.create_label(channel_name, channel_id, likes, dislikes, comment, indent, my_vote)) + li.setProperty('id', comment_id) + li.setProperty('channel_name', channel_name) + li.setProperty('channel_id', channel_id) + li.setProperty('likes', str(likes)) + li.setProperty('dislikes', str(dislikes)) + li.setProperty('comment', comment) + li.setProperty('indent', str(indent)) + li.setProperty('my_vote', str(my_vote)) + return li + + def copy_list_item(self, li): + li_copy = ListItem(label=li.getLabel()) + li_copy.setProperty('id', li.getProperty('id')) + li_copy.setProperty('channel_name', li.getProperty('channel_name')) + li_copy.setProperty('channel_id', li.getProperty('channel_id')) + li_copy.setProperty('likes', li.getProperty('likes')) + li_copy.setProperty('dislikes', li.getProperty('dislikes')) + li_copy.setProperty('comment', li.getProperty('comment')) + li_copy.setProperty('indent', li.getProperty('indent')) + li_copy.setProperty('my_vote', li.getProperty('my_vote')) + return li_copy + + def refresh_label(self, li, selected=True): + li.getProperty('id'); + channel_name = li.getProperty('channel_name') + channel_id = li.getProperty('channel_id') + likes = int(li.getProperty('likes')) + dislikes = int(li.getProperty('dislikes')) + comment = li.getProperty('comment') + indent = int(li.getProperty('indent')) + my_vote = int(li.getProperty('my_vote')) + li.setLabel(self.create_label(channel_name, channel_id, likes, dislikes, comment, indent, my_vote, selected)) + + def create_label(self, channel_name, channel_id, likes, dislikes, comment, indent, my_vote, selected=False): + user_channel = get_user_channel() + if user_channel and user_channel[1] == channel_id: + color = 'red' if selected else 'green' + channel_name = '[COLOR ' + color + ']' + channel_name + '[/COLOR]' + + if my_vote == 1: + likes = '[COLOR green]' + str(likes+1) + '[/COLOR]' + dislikes = str(dislikes) + elif my_vote == -1: + likes = str(likes) + dislikes = '[COLOR green]' + str(dislikes+1) + '[/COLOR]' + else: + likes = str(likes) + dislikes = str(dislikes) + + lilabel = channel_name + ' [COLOR orange]' + likes + '/' + dislikes + '[/COLOR] [COLOR white]' + comment + '[/COLOR]' + + padding = '' + for i in range(indent): + padding += ' ' + lilabel = padding + lilabel + + return lilabel + + def sign(self, data, params): + res = sign(data) + params['signature'] = res['signature'] + params['signing_ts'] = res['signing_ts'] + + def create_comment(self, comment, parent_id=None): + user_channel = get_user_channel() + progressDialog = xbmcgui.DialogProgress() + progressDialog.create(tr(30241), tr(30242)) + params = { 'claim_id' : self.claim_id, 'comment' : comment, 'channel_id' : user_channel[1] } + if parent_id: + params['parent_id'] = parent_id + self.sign(comment, params) + res = call_comment_rpc('comment.Create', params) + self.like(res['comment_id']) + progressDialog.close() + return res['comment_id'] + + def edit_comment(self, comment_id, comment): + user_channel = get_user_channel() + params = { 'comment_id' : comment_id, 'comment' : comment } + self.sign(comment, params) + return call_comment_rpc('comment.Edit', params) + + def remove_comment(self, comment_id): + params = { 'comment_id' : comment_id } + self.sign(comment_id, params) + call_comment_rpc('comment.Abandon', params) + + def react(self, comment_id, current_vote=0, type=None): + # No vote to clear + if current_vote == '0' and type == None: + return + + user_channel = get_user_channel() + params = { 'comment_ids' : comment_id, + 'channel_name' : user_channel[0], + 'channel_id' : user_channel[1] + } + if type == 'like': + params['clear_types'] = 'dislike' + params['type'] = 'like' + elif type == 'dislike': + params['clear_types'] = 'like' + params['type'] = 'dislike' + else: + params['remove'] = True + params['type'] = 'dislike' if current_vote == '-1' else 'like' + + self.sign(user_channel[0], params) + call_comment_rpc('reaction.React', params) + + def like(self, comment_id): + self.react(comment_id, type='like') + + def dislike(self, comment_id): + self.react(comment_id, type='dislike') + + def neutral(self, comment_id, current_vote): + self.react(comment_id, current_vote=current_vote) + +@plugin.route('/') +def lbry_root(): + addDirectoryItem(ph, plugin.url_for(plugin_follows), ListItem(tr(30200)), True) + addDirectoryItem(ph, plugin.url_for(plugin_recent, page=1), ListItem(tr(30218)), True) + #addDirectoryItem(ph, plugin.url_for(plugin_playlists), ListItem(tr(30210)), True) + addDirectoryItem(ph, plugin.url_for(plugin_playlist, name=quote_plus(tr(30211))), ListItem(tr(30211)), True) + #addDirectoryItem(ph, plugin.url_for(lbry_new, page=1), ListItem(tr(30202)), True) + addDirectoryItem(ph, plugin.url_for(lbry_search), ListItem(tr(30201)), True) + endOfDirectory(ph) + +#@plugin.route('/playlists') +#def plugin_playlists(): +# addDirectoryItem(ph, plugin.url_for(plugin_playlist, name=quote_plus(tr(30211))), ListItem(tr(30211)), True) +# endOfDirectory(ph) + +@plugin.route('/playlist/list/') +def plugin_playlist(name): + name = unquote_plus(name) + uris = load_playlist(name) + claim_info = call_rpc('resolve', {'urls': uris}) + items = [] + for uri in uris: + items.append(claim_info[uri]) + items = result_to_itemlist(items, playlist=name) + addDirectoryItems(ph, items, items_per_page) + endOfDirectory(ph) + +@plugin.route('/playlist/add//') +def plugin_playlist_add(name,uri): + name = unquote_plus(name) + uri = deserialize_uri(uri) + items = load_playlist(name) + if not uri in items: + items.append(uri) + save_playlist(name, items) + +@plugin.route('/playlist/del//') +def plugin_playlist_del(name,uri): + name = unquote_plus(name) + uri = deserialize_uri(uri) + items = load_playlist(name) + items.remove(uri) + save_playlist(name, items) + xbmc.executebuiltin('Container.Refresh') + +@plugin.route('/follows') +def plugin_follows(): + channels = load_channel_subs() + resolve_uris = [] + for (name,claim_id) in channels: + resolve_uris.append(name+'#'+claim_id) + channel_infos = call_rpc('resolve', {'urls': resolve_uris}) + + for (name,claim_id) in channels: + uri = name+'#'+claim_id + channel_info = channel_infos[uri] + li = ListItem(name) + if not 'error' in channel_info: + plot = '' + if 'title' in channel_info['value'] and channel_info['value']['title'].strip() != '': + plot = '[B]%s[/B]\n' % channel_info['value']['title'] + else: + plot = '[B]%s[/B]\n' % channel_info['name'] + if 'description' in channel_info['value']: + plot = plot + channel_info['value']['description'] + infoLabels = { 'plot': plot } + li.setInfo('video', infoLabels) + + if 'thumbnail' in channel_info['value'] and 'url' in channel_info['value']['thumbnail']: + li.setArt({ + 'thumb': channel_info['value']['thumbnail']['url'], + 'poster': channel_info['value']['thumbnail']['url'], + 'fanart': channel_info['value']['thumbnail']['url'] + }) + menu = [] + menu.append(( + tr(30206) % name, 'RunPlugin(%s)' % plugin.url_for(plugin_unfollow, uri=serialize_uri(uri)) + )) + li.addContextMenuItems(menu) + addDirectoryItem(ph, plugin.url_for(lbry_channel, uri=serialize_uri(uri), page=1), li, True) + endOfDirectory(ph) + +@plugin.route('/recent/') +def plugin_recent(page): + page = int(page) + channels = load_channel_subs() + channel_ids = [] + for (name,claim_id) in channels: + channel_ids.append(claim_id) + query = {'page': page, 'page_size': items_per_page, 'order_by': 'release_time', 'channel_ids': channel_ids} + if not ADDON.getSettingBool('server_filter_disable'): + query['stream_types'] = ['video'] + result = call_rpc('claim_search', query) + items = result_to_itemlist(result['items']) + addDirectoryItems(ph, items, result['page_size']) + total_pages = int(result['total_pages']) + if total_pages > 1 and page < total_pages: + addDirectoryItem(ph, plugin.url_for(plugin_recent, page=page+1), ListItem(tr(30203)), True) + endOfDirectory(ph) + +@plugin.route('/comments/show/') +def plugin_comment_show(uri): + params = deserialize_uri(uri).split('#') + win = CommentWindow('addon-lbry-comments.xml', xbmcaddon.Addon().getAddonInfo('path'), 'Default', channel_name=params[0], channel_id=params[1], claim_id=params[2]) + win.doModal() + del win + +@plugin.route('/follows/add/') +def plugin_follow(uri): + uri = deserialize_uri(uri) + channels = load_channel_subs() + channel = (uri.split('#')[0],uri.split('#')[1]) + if not channel in channels: + channels.append(channel) + save_channel_subs(channels) + +@plugin.route('/follows/del/') +def plugin_unfollow(uri): + uri = deserialize_uri(uri) + channels = load_channel_subs() + channels.remove((uri.split('#')[0],uri.split('#')[1])) + save_channel_subs(channels) + xbmc.executebuiltin('Container.Refresh') + +@plugin.route('/new/') +def lbry_new(page): + page = int(page) + query = {'page': page, 'page_size': items_per_page, 'order_by': 'release_time'} + if not ADDON.getSettingBool('server_filter_disable'): + query['stream_types'] = ['video'] + result = call_rpc('claim_search', query) + items = result_to_itemlist(result['items']) + addDirectoryItems(ph, items, result['page_size']) + total_pages = int(result['total_pages']) + if total_pages > 1 and page < total_pages: + addDirectoryItem(ph, plugin.url_for(lbry_new, page=page+1), ListItem(tr(30203)), True) + endOfDirectory(ph) + +@plugin.route('/channel/') +def lbry_channel_landing(uri): + lbry_channel(uri,1) + +@plugin.route('/channel//') +def lbry_channel(uri,page): + uri = deserialize_uri(uri) + page = int(page) + query = {'page': page, 'page_size': items_per_page, 'order_by': 'release_time', 'channel': uri} + if not ADDON.getSettingBool('server_filter_disable'): + query['stream_types'] = ['video'] + result = call_rpc('claim_search', query) + items = result_to_itemlist(result['items'], channel=uri) + addDirectoryItems(ph, items, result['page_size']) + total_pages = int(result['total_pages']) + if total_pages > 1 and page < total_pages: + addDirectoryItem(ph, plugin.url_for(lbry_channel, uri=serialize_uri(uri), page=page+1), ListItem(tr(30203)), True) + endOfDirectory(ph) + +@plugin.route('/search') +def lbry_search(): + query = dialog.input(tr(30209)) + lbry_search_pager(quote_plus(query), 1) + +@plugin.route('/search//') +def lbry_search_pager(query, page): + query = unquote_plus(query) + page = int(page) + if query != '': + params = {'text': query, 'page': page, 'page_size': items_per_page, 'order_by': 'release_time'} + #always times out on server :( + #if not ADDON.getSettingBool('server_filter_disable'): + # params['stream_types'] = ['video'] + result = call_rpc('claim_search', params) + items = result_to_itemlist(result['items']) + addDirectoryItems(ph, items, result['page_size']) + total_pages = int(result['total_pages']) + if total_pages > 1 and page < total_pages: + addDirectoryItem(ph, plugin.url_for(lbry_search_pager, query=quote_plus(query), page=page+1), ListItem(tr(30203)), True) + endOfDirectory(ph) + else: + endOfDirectory(ph, False) + +def user_payment_confirmed(claim_info): + # paid for claim already? + purchase_info = call_rpc('purchase_list', {'claim_id': claim_info['claim_id']}) + if len(purchase_info['items']) > 0: + return True + + account_list = call_rpc('account_list') + for account in account_list['items']: + if account['is_default']: + balance = float(str(account['satoshis'])[:-6]) / float(100) + dtext = tr(30214) % (float(claim_info['value']['fee']['amount']), str(claim_info['value']['fee']['currency'])) + dtext = dtext + '\n\n' + tr(30215) % (balance, str(claim_info['value']['fee']['currency'])) + return dialog.yesno(tr(30204), dtext) + +@plugin.route('/play/') +def claim_play(uri): + uri = deserialize_uri(uri) + + claim_info = call_rpc('resolve', {'urls': uri})[uri] + if 'error' in claim_info: + dialog.notification(tr(30102), claim_info['error']['name'], NOTIFICATION_ERROR) + return + + if 'fee' in claim_info['value']: + if claim_info['value']['fee']['currency'] != 'LBC': + dialog.notification(tr(30204), tr(30103), NOTIFICATION_ERROR) + return + + if not user_payment_confirmed(claim_info): + return + + result = call_rpc('get', {'uri': uri, 'save_file': False}) + stream_url = result['streaming_url'].replace('0.0.0.0','127.0.0.1') + + (url,li) = result_to_itemlist([claim_info])[0] + li.setPath(stream_url) + setResolvedUrl(ph, True, li) + +@plugin.route('/download/') +def claim_download(uri): + uri = deserialize_uri(uri) + + claim_info = call_rpc('resolve', {'urls': uri})[uri] + if 'error' in claim_info: + dialog.notification(tr(30102), claim_info['error']['name'], NOTIFICATION_ERROR) + return + + if 'fee' in claim_info['value']: + if claim_info['value']['fee']['currency'] != 'LBC': + dialog.notification(tr(30204), tr(30103), NOTIFICATION_ERROR) + return + + if not user_payment_confirmed(claim_info): + return + + call_rpc('get', {'uri': uri, 'save_file': True}) + +def run(): + try: + plugin.run() + except PluginException as e: + xbmc.log("PluginException: " + str(e)) diff --git a/plugin.video.lbry/resources/settings.xml b/plugin.video.lbry/resources/settings.xml new file mode 100644 index 0000000000..a45966eff1 --- /dev/null +++ b/plugin.video.lbry/resources/settings.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/plugin.video.lbry/resources/skins/Default/720p/addon-lbry-comments.xml b/plugin.video.lbry/resources/skins/Default/720p/addon-lbry-comments.xml new file mode 100644 index 0000000000..50c3337d4e --- /dev/null +++ b/plugin.video.lbry/resources/skins/Default/720p/addon-lbry-comments.xml @@ -0,0 +1,48 @@ + + + 1 + 1 + 0xff000000 + WindowOpen + WindowClose + 1 + + 0 + 0 + + + + Comment list + 0 + 0 + true + list + vertical + 25 + + + 0 + 24 + font14 + center + blue + left + + + + + + 0 + 24 + font14 + center + red + left + + true + 250 + + + + + diff --git a/plugin.video.lighttv/LICENSE.txt b/plugin.video.lighttv/LICENSE.txt new file mode 100644 index 0000000000..4f8e8eb30c --- /dev/null +++ b/plugin.video.lighttv/LICENSE.txt @@ -0,0 +1,282 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------- diff --git a/plugin.video.lighttv/README.txt b/plugin.video.lighttv/README.txt new file mode 100644 index 0000000000..536ae91eaf --- /dev/null +++ b/plugin.video.lighttv/README.txt @@ -0,0 +1,8 @@ +plugin.video.lighttv +================ + +Kodi Video Addon for LightTV Live +For Kodi Matrix and above releases + +Version 4.0.0 Initial Release for Matrix + diff --git a/plugin.video.lighttv/addon.xml b/plugin.video.lighttv/addon.xml new file mode 100644 index 0000000000..e595f77440 --- /dev/null +++ b/plugin.video.lighttv/addon.xml @@ -0,0 +1,31 @@ + + + + + + + + video + + + LIGHTtv Live + LIGHTtv is a new free broadcast network featuring family programming including movies, series and entertainment. + Feel free to use this script. For information visit the wiki. + all + en + GPL-2.0-or-later + https://forum.kodi.tv/showthread.php?tid=355675 + lighttv.com + https://github.com/learningit/repo-plugins + Version 4.0.0 Initial release for Matrix + + + resources/icon.png + resources/fanart.jpg + + + diff --git a/plugin.video.lighttv/default.py b/plugin.video.lighttv/default.py new file mode 100644 index 0000000000..0b0cbd4fce --- /dev/null +++ b/plugin.video.lighttv/default.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# KodiAddon +# +from resources.lib.scraper import myAddon +import re +import sys + +# Start of Module + +addonName = re.search('plugin\://plugin.video.(.+?)/',str(sys.argv[0])).group(1) +ma = myAddon(addonName) +ma.processAddonEvent() + diff --git a/plugin.video.lighttv/resources/fanart.jpg b/plugin.video.lighttv/resources/fanart.jpg new file mode 100644 index 0000000000..d6d446df1a Binary files /dev/null and b/plugin.video.lighttv/resources/fanart.jpg differ diff --git a/plugin.video.lighttv/resources/icon.png b/plugin.video.lighttv/resources/icon.png new file mode 100644 index 0000000000..b16548bb03 Binary files /dev/null and b/plugin.video.lighttv/resources/icon.png differ diff --git a/plugin.video.lighttv/resources/lib/scraper.py b/plugin.video.lighttv/resources/lib/scraper.py new file mode 100644 index 0000000000..9c95192aef --- /dev/null +++ b/plugin.video.lighttv/resources/lib/scraper.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# KodiAddon (LIGHTtv Live) +# +from t1mlib import t1mAddon +import re +import requests + +class myAddon(t1mAddon): + + def getAddonMenu(self,url,ilist): + html = requests.get('https://lighttv.com/watch-now/', headers=self.defaultHeaders).text + url = re.compile(' + # look for http youtube + video_urls = soup.findAll('iframe', attrs={'src': re.compile("^http://www.youtube.com/embed")}, limit=1) + if len(video_urls) == 0: + # look for https youtube + video_urls = soup.findAll('iframe', attrs={'src': re.compile("^https://www.youtube.com/embed")}, limit=1) + if len(video_urls) == 0: + unplayable_media_file = True + else: + video_url = video_urls[0]['src'] + + log("video_url", video_url) + + # make youtube plugin url + pos_of_last_question_mark = video_url.rfind("?") + video_url = video_url[0: pos_of_last_question_mark] + video_url_len = len(video_url) + youtubeID = video_url[len("https://www.youtube.com/embed/"):video_url_len] + youtube_url = 'plugin://plugin.video.youtube/play/?video_id=%s' % youtubeID + have_valid_url = True + video_url = youtube_url + else: + video_url = video_urls[0]['src'] + + log("video_url", video_url) + + # make youtube plugin url + pos_of_last_question_mark = video_url.rfind("?") + video_url = video_url[0: pos_of_last_question_mark] + video_url_len = len(video_url) + youtubeID = video_url[len("http://www.youtube.com/embed/"):video_url_len] + youtube_url = 'plugin://plugin.video.youtube/play/?video_id=%s' % youtubeID + have_valid_url = True + video_url = youtube_url + else: + unplayable_media_file = True + + log("have_valid_url", have_valid_url) + + log("video_url", video_url) + + if have_valid_url: + list_item = xbmcgui.ListItem(path=video_url) + xbmcplugin.setResolvedUrl(self.plugin_handle, True, list_item) + + # + # Alert user + # + elif unplayable_media_file: + xbmcgui.Dialog().ok(LANGUAGE(30000), LANGUAGE(30506)) \ No newline at end of file diff --git a/plugin.video.worldstarhiphop/resources/lib/worldstarhiphop_search.py b/plugin.video.worldstarhiphop/resources/lib/worldstarhiphop_search.py new file mode 100644 index 0000000000..532030eaa0 --- /dev/null +++ b/plugin.video.worldstarhiphop/resources/lib/worldstarhiphop_search.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# +# Imports +# +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import object +import requests +import os +import re +import sys +import urllib.request, urllib.parse, urllib.error +import xbmc +import xbmcgui +import xbmcplugin + +from resources.lib.worldstarhiphop_const import ADDON, SETTINGS, LANGUAGE, IMAGES_PATH, DATE, VERSION, HEADERS, BASEURLWSHH, convertToUnicodeString, log, getSoup +# +# Main class +# +class Main(object): + # + # Init + # + def __init__(self): + # Get the command line arguments + # Get the plugin url in plugin:// notation + self.plugin_url = sys.argv[0] + # Get the plugin handle as an integer number + self.plugin_handle = int(sys.argv[1]) + + # Get plugin settings + self.VIDEO = SETTINGS.getSetting('video') + + log("ARGV", repr(sys.argv)) + + try: + self.plugin_category = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['plugin_category'][0] + self.video_list_page_url = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['url'][0] + self.next_page_possible = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[2]).query)['next_page_possible'][0] + except: + self.plugin_category = LANGUAGE(30000) + self.next_page_possible = "True" + # Get the search-string from the user + keyboard = xbmc.Keyboard('', LANGUAGE(30103)) + keyboard.doModal() + if keyboard.isConfirmed(): + self.search_string = keyboard.getText() + self.video_list_page_url = BASEURLWSHH + "/videos/search.php?s=%s&start=001" % ( + self.search_string) + + if self.next_page_possible == 'True': + # Determine current item number, next item number, next_url + pos_of_page = self.video_list_page_url.rfind('&start=') + if pos_of_page >= 0: + page_number_str = str( + self.video_list_page_url[pos_of_page + len('&start='):pos_of_page + len('&start=') + len('000')]) + page_number = int(page_number_str) + page_number_next = page_number + 1 + if page_number_next >= 100: + page_number_next_str = str(page_number_next) + elif page_number_next >= 10: + page_number_next_str = '0' + str(page_number_next) + else: + page_number_next_str = '00' + str(page_number_next) + self.next_url = str(self.video_list_page_url).replace(page_number_str, page_number_next_str) + + log("self.next_url", self.next_url) + + # + # Get the videos... + # + self.getVideos() + + # + # Get videos... + # + def getVideos(self): + # + # Init + # + is_folder = False + # Create a list for our items. + listing = [] + + # + # Get HTML page + # + response = requests.get(self.video_list_page_url, headers=HEADERS) + + html_source = response.text + html_source = convertToUnicodeString(html_source) + + # Parse response + soup = getSoup(html_source) + + # + + items = soup.findAll('a', attrs={'class': re.compile("^video-box")}) + + log("len(items)", len(items)) + + for item in items: + try: + video_page_url = str(item['href']) + except: + # skip the item if it does not have a href + + log("skipping item without href", item) + + continue + + log("video_page_url", video_page_url) + + # # skip the item if the video page url isn't a real video page url + if str(video_page_url).find('/videos/') == -1: + + log("skipping item because no video could be found", video_page_url) + + continue + + try: + thumbnail_url = item.img['data-original'] + except: + thumbnail_url = item.img['src'] + + log("thumbnail_url", thumbnail_url) + + title = item.img['alt'] + + title = title.replace('-', ' ') + title = title.replace('/', ' ') + title = title.replace(' i ', ' I ') + title = title.replace(' ii ', ' II ') + title = title.replace(' iii ', ' III ') + title = title.replace(' iv ', ' IV ') + title = title.replace(' v ', ' V ') + title = title.replace(' vi ', ' VI ') + title = title.replace(' vii ', ' VII ') + title = title.replace(' viii ', ' VIII ') + title = title.replace(' ix ', ' IX ') + title = title.replace(' x ', ' X ') + title = title.replace(' xi ', ' XI ') + title = title.replace(' xii ', ' XII ') + title = title.replace(' xiii ', ' XIII ') + title = title.replace(' xiv ', ' XIV ') + title = title.replace(' xv ', ' XV ') + title = title.replace(' xvi ', ' XVI ') + title = title.replace(' xvii ', ' XVII ') + title = title.replace(' xviii ', ' XVIII ') + title = title.replace(' xix ', ' XIX ') + title = title.replace(' xx ', ' XXX ') + title = title.replace(' xxi ', ' XXI ') + title = title.replace(' xxii ', ' XXII ') + title = title.replace(' xxiii ', ' XXIII ') + title = title.replace(' xxiv ', ' XXIV ') + title = title.replace(' xxv ', ' XXV ') + title = title.replace(' xxvi ', ' XXVI ') + title = title.replace(' xxvii ', ' XXVII ') + title = title.replace(' xxviii ', ' XXVIII ') + title = title.replace(' xxix ', ' XXIX ') + title = title.replace(' xxx ', ' XXX ') + title = title.replace(' ', ' ') + title = title.replace(' ', ' ') + # welcome to unescaping-hell + title = title.replace('£', "Pound Sign") + title = title.replace('&#039;', "'") + title = title.replace('&#39;', "'") + title = title.replace('&quot;', '"') + title = title.replace("'", "'") + title = title.replace("'", "'") + title = title.replace('&amp;', '&') + title = title.replace('&', '&') + title = title.replace('"', '"') + title = title.replace('“', '"') + title = title.replace('”', '"') + title = title.replace('’', "'") + title = title.replace('–', "/") + + log("title", title) + + # Add to list... + list_item = xbmcgui.ListItem(title) + list_item.setInfo("video", {"title": title, "studio": ADDON}) + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + list_item.setProperty('IsPlayable', 'true') + parameters = {"action": "play", "video_page_url": video_page_url, "title": title} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + is_folder = False + # Add refresh option to context menu + list_item.addContextMenuItems([('Refresh', 'Container.Refresh')]) + # Add our item to the listing as a 3-element tuple. + listing.append((url, list_item, is_folder)) + + # Next page entry + if self.next_page_possible == 'True': + thumbnail_url = os.path.join(IMAGES_PATH, 'next-page.png') + list_item = xbmcgui.ListItem(LANGUAGE(30503)) + list_item.setArt({'thumb': thumbnail_url, 'icon': thumbnail_url, + 'fanart': os.path.join(IMAGES_PATH, 'fanart-blur.jpg')}) + list_item.setProperty('IsPlayable', 'false') + parameters = {"action": "list", "plugin_category": self.plugin_category, "url": str(self.next_url), + "next_page_possible": self.next_page_possible} + url = self.plugin_url + '?' + urllib.parse.urlencode(parameters) + is_folder = True + # Add refresh option to context menu + list_item.addContextMenuItems([('Refresh', 'Container.Refresh')]) + # Add our item to the listing as a 3-element tuple. + listing.append((url, list_item, is_folder)) + + # Add our listing to Kodi. + # Large lists and/or slower systems benefit from adding all items at once via addDirectoryItems + # instead of adding one by ove via addDirectoryItem. + xbmcplugin.addDirectoryItems(self.plugin_handle, listing, len(listing)) + # Disable sorting + xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE) + # Finish creating a virtual folder. + xbmcplugin.endOfDirectory(self.plugin_handle) diff --git a/plugin.video.worldstarhiphop/resources/next-page.png b/plugin.video.worldstarhiphop/resources/next-page.png new file mode 100644 index 0000000000..b12e695539 Binary files /dev/null and b/plugin.video.worldstarhiphop/resources/next-page.png differ diff --git a/plugin.video.worldstarhiphop/resources/settings.xml b/plugin.video.worldstarhiphop/resources/settings.xml new file mode 100644 index 0000000000..40c76e1fa2 --- /dev/null +++ b/plugin.video.worldstarhiphop/resources/settings.xml @@ -0,0 +1,22 @@ + + +
    + + + + 0 + 2 + + + + + + + + + + + + +
    +
    diff --git a/plugin.video.wrallocal/LICENSE.txt b/plugin.video.wrallocal/LICENSE.txt new file mode 100644 index 0000000000..4f8e8eb30c --- /dev/null +++ b/plugin.video.wrallocal/LICENSE.txt @@ -0,0 +1,282 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------- diff --git a/plugin.video.wrallocal/addon.xml b/plugin.video.wrallocal/addon.xml new file mode 100644 index 0000000000..a325820266 --- /dev/null +++ b/plugin.video.wrallocal/addon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + video + + + WRAL Local Weather and News + Get current WRAL News and Weather. + Feel free to use this script. For information visit the wiki. + all + en + http://forum.kodi.tv/showthread.php?tid=222072 + www.wral.com + GPL-2.0-or-later + https://github.com/learningit/repo-plugins + Version 4.0.1 Fix for missing date on news item + + + resources/icon.png + resources/fanart.jpg + + + diff --git a/plugin.video.wrallocal/default.py b/plugin.video.wrallocal/default.py new file mode 100644 index 0000000000..414fdefc95 --- /dev/null +++ b/plugin.video.wrallocal/default.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# KodiAddon +# +from resources.lib.scraper import myAddon +import re +import sys + +# Start of Module + +addonName = re.search('plugin\://plugin.video.(.+?)/',str(sys.argv[0])).group(1) +ma = myAddon(addonName) +ma.processAddonEvent() + diff --git a/plugin.video.wrallocal/readme.txt b/plugin.video.wrallocal/readme.txt new file mode 100644 index 0000000000..fda7590927 --- /dev/null +++ b/plugin.video.wrallocal/readme.txt @@ -0,0 +1,8 @@ +plugin.video.wrallocal +===================== +Kodi Addon for WRAL news and weather website + +Change Log +===================== +Version 4.0.0 Matrix version +Version 4.0.1 Fix for missing date on news item \ No newline at end of file diff --git a/plugin.video.wrallocal/resources/fanart.jpg b/plugin.video.wrallocal/resources/fanart.jpg new file mode 100644 index 0000000000..47cb94c697 Binary files /dev/null and b/plugin.video.wrallocal/resources/fanart.jpg differ diff --git a/plugin.video.wrallocal/resources/icon.png b/plugin.video.wrallocal/resources/icon.png new file mode 100644 index 0000000000..7d2a316edb Binary files /dev/null and b/plugin.video.wrallocal/resources/icon.png differ diff --git a/plugin.video.wrallocal/resources/language/resource.language.en_gb/strings.po b/plugin.video.wrallocal/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..cfe472e8f9 --- /dev/null +++ b/plugin.video.wrallocal/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,39 @@ +# Kodi Media Center language file +# Addon Name: cbcnews +# Addon id: plugin.video.wrallocal +# Addon Provider: t1m +msgid "" +msgstr "" +"Project-Id-Version: XBMC Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: Amharic (http://www.transifex.com/projects/p/xbmc-addons/language/am/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: am\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgctxt "#30001" +msgid "WRAL News On Demand" +msgstr "" + +msgctxt "#30002" +msgid "WRAL News Stream Live" +msgstr "" + +msgctxt "#30003" +msgid "WRAL WeatherCenter Forecast Stream" +msgstr "" + +msgctxt "#30004" +msgid "WRAL Live Stream Dual Doppler5000" +msgstr "" + +msgctxt "#30005" +msgid "Next Page" +msgstr "" + + diff --git a/plugin.video.wrallocal/resources/lib/scraper.py b/plugin.video.wrallocal/resources/lib/scraper.py new file mode 100644 index 0000000000..334f91a48a --- /dev/null +++ b/plugin.video.wrallocal/resources/lib/scraper.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# WRAL Kodi Video Addon +# +from t1mlib import t1mAddon +import re +import os +from datetime import datetime +import time +import xbmc +import xbmcplugin +import xbmcgui +import html.parser +import sys +import requests + +UNESCAPE = html.parser.HTMLParser().unescape +WRALBASE = 'https://www.wral.com' + + +class myAddon(t1mAddon): + + def getAddonMenu(self,url,ilist): + ALS = self.addon.getLocalizedString + shows = [] + infoList = {} + fanart = self.addonFanart + shows = [(''.join([WRALBASE,'/news/asset_gallery/13130437/?s=0']), ALS(30001)), + (''.join([WRALBASE,'/news/video/1082826/']), ALS(30002)), + (''.join([WRALBASE,'/wral-weathercenter-forecast/1076424/']), ALS(30003)), + (''.join([WRALBASE,'/live_dualdoppler5000_radar/1068935/']), ALS(30004))] + for url, name in shows: + infoList = {'mediatype':'tvshow', + 'Title': name, + 'Plot': name, + 'TVShowTitle': name} + if 'Stream' in name: + ilist = self.addMenuItem(name, 'GV', ilist, url, self.addonIcon, self.addonFanart, infoList, isFolder=False) + else: + ilist = self.addMenuItem(name, 'GE', ilist, url, self.addonIcon, self.addonFanart, infoList, isFolder=True) + return(ilist) + + + def getAddonEpisodes(self,url,ilist): + ALS = self.addon.getLocalizedString + html = requests.get(url, headers=self.defaultHeaders).text + nexturl = re.compile('page btn control next" href="(.+?)"', re.DOTALL).search(html) + html = re.compile('