summaryrefslogtreecommitdiff
path: root/plugin.video.catchuptvandmore/resources
diff options
context:
space:
mode:
Diffstat (limited to 'plugin.video.catchuptvandmore/resources')
-rwxr-xr-xplugin.video.catchuptvandmore/resources/__init__.py0
-rwxr-xr-xplugin.video.catchuptvandmore/resources/language/resource.language.en_gb/strings.po137
-rwxr-xr-xplugin.video.catchuptvandmore/resources/language/resource.language.fr_fr/strings.po140
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/__init__.py0
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/__init__.py0
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/6play.py381
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/__init__.py0
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/arte.py260
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/bfmtv.py224
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/c.py210
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/canalplus.py318
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/gulli.py203
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/itele.py192
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/pluzz.py282
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/channels/fr/tf1.py238
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/common.py30
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/simpleplugin.py923
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/skeleton.py54
-rwxr-xr-xplugin.video.catchuptvandmore/resources/lib/utils.py172
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/arte.pngbin0 -> 23700 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/bfmtv.pngbin0 -> 35775 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/bfmtv_fanart.pngbin0 -> 483976 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/c8.pngbin0 -> 85413 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/c8_fanart.pngbin0 -> 201320 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/canalplus.pngbin0 -> 23038 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/canalplus_fanart.pngbin0 -> 104838 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/cstar.pngbin0 -> 36243 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/cstar_fanart.pngbin0 -> 128048 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/france2.pngbin0 -> 53728 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/france2_fanart.pngbin0 -> 333122 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/france3.pngbin0 -> 31569 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/france3_fanart.pngbin0 -> 176208 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/france4.pngbin0 -> 78093 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/france5.pngbin0 -> 51222 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/france5_fanart.pngbin0 -> 157085 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/gulli.pngbin0 -> 162929 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/gulli_fanart.pngbin0 -> 318305 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/itele.pngbin0 -> 26801 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/itele_fanart.pngbin0 -> 85354 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/m6.pngbin0 -> 125343 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/m6_fanart.pngbin0 -> 569115 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/nrj12.pngbin0 -> 73550 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/nrj12_fanart.pngbin0 -> 340086 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/nt1.pngbin0 -> 178003 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/nt1_fanart.pngbin0 -> 339900 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/tf1.pngbin0 -> 38203 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/tf1_fanart.pngbin0 -> 694530 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/tmc.pngbin0 -> 25166 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/tmc_fanart.pngbin0 -> 345299 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/media/channels/fr/w9.pngbin0 -> 115591 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/screenshots/screenshot-01.jpgbin0 -> 534015 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/screenshots/screenshot-02.jpgbin0 -> 543386 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/screenshots/screenshot-03.jpgbin0 -> 532567 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/screenshots/screenshot-04.jpgbin0 -> 809400 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/screenshots/screenshot-05.jpgbin0 -> 849740 bytes
-rwxr-xr-xplugin.video.catchuptvandmore/resources/settings.xml122
56 files changed, 3886 insertions, 0 deletions
diff --git a/plugin.video.catchuptvandmore/resources/__init__.py b/plugin.video.catchuptvandmore/resources/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/__init__.py
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 100755
index 0000000..abd4300
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/language/resource.language.en_gb/strings.po
@@ -0,0 +1,137 @@
+# Kodi Media Center language file
+# Addon Name: Catch-up TV & More
+# Addon id: plugin.video.catchuptvandmore
+# Addon Provider: SylvainCecchetto
+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: SylvainCecchetto\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+# Settings categories (from 30000 to 30009)
+msgctxt "#30000"
+msgid "Main menu"
+msgstr "Menu principal"
+
+msgctxt "#30001"
+msgid "Channels"
+msgstr ""
+
+msgctxt "#30002"
+msgid "Quality and content"
+msgstr ""
+
+# Settings line separators (from 30010 to 30019)
+msgctxt "#30010"
+msgid "Hide main menu categories"
+msgstr ""
+
+msgctxt "#30011"
+msgid "Hide channels from categories"
+msgstr ""
+
+
+# Main categories (from 30020 to 30039)
+msgctxt "#30020"
+msgid "French channels"
+msgstr ""
+
+msgctxt "#30021"
+msgid "Belgian channels"
+msgstr ""
+
+
+# Context menu (from 30040 to 30049)
+msgctxt "#30040"
+msgid "Move down"
+msgstr ""
+
+msgctxt "#30041"
+msgid "Move up"
+msgstr ""
+
+msgctxt "#30042"
+msgid "Hide"
+msgstr ""
+
+
+# Dialog boxes (from 30050 to 30069)
+msgctxt "#30050"
+msgid "Information"
+msgstr ""
+
+msgctxt "#30051"
+msgid "To re-enable hidden items go to the plugin settings"
+msgstr ""
+
+
+# Settings quality and content (from 30070 to 30099)
+msgctxt "#30070"
+msgid "TF1: Show bonus videos"
+msgstr ""
+
+msgctxt "#30071"
+msgid "France 2: Video quality"
+msgstr ""
+
+msgctxt "#30072"
+msgid "France 3: Video quality"
+msgstr ""
+
+msgctxt "#30073"
+msgid "France 4: Video quality"
+msgstr ""
+
+msgctxt "#30074"
+msgid "France 5: Video quality"
+msgstr ""
+
+msgctxt "#30075"
+msgid "France Ô: Video quality"
+msgstr ""
+
+msgctxt "#30076"
+msgid "M6: Video quality"
+msgstr ""
+
+msgctxt "#30077"
+msgid "W9: Video quality"
+msgstr ""
+
+msgctxt "#30078"
+msgid "6ter: Video quality"
+msgstr ""
+
+msgctxt "#30079"
+msgid "Arte: Video quality"
+msgstr ""
+
+msgctxt "#30080"
+msgid "Arte: Language"
+msgstr ""
+
+msgctxt "#30081"
+msgid "BFM TV: Video quality"
+msgstr ""
+
+
+# Others (from 30100 to 30140)
+msgctxt "#30100"
+msgid "More videos..."
+msgstr ""
+
+msgctxt "#30101"
+msgid "All videos"
+msgstr ""
+
+msgctxt "#30102"
+msgid "DRM protected video"
+msgstr "Vidéo protégée par DRM" \ No newline at end of file
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 100755
index 0000000..00433d9
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/language/resource.language.fr_fr/strings.po
@@ -0,0 +1,140 @@
+# Kodi Media Center language file
+# Addon Name: Catch-up TV & More
+# Addon id: plugin.video.catchuptvandmore
+# Addon Provider: SylvainCecchetto
+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: SylvainCecchetto\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+# Settings categories (from 30000 to 30009)
+msgctxt "#30000"
+msgid "Main menu"
+msgstr "Menu principal"
+
+msgctxt "#30001"
+msgid "Channels"
+msgstr "Chaînes"
+
+msgctxt "#30002"
+msgid "Quality and content"
+msgstr "Qualité et contenu"
+
+
+# Settings line separators (from 30010 to 30019)
+msgctxt "#30010"
+msgid "Hide main menu categories"
+msgstr "Masquer des catégories du menu principal"
+
+msgctxt "#30011"
+msgid "Hide channels from categories"
+msgstr "Masquer des chaînes des catégories"
+
+
+# Main categories (from 30020 to 30039)
+msgctxt "#30020"
+msgid "French channels"
+msgstr "Chaînes françaises"
+
+msgctxt "#30021"
+msgid "Belgian channels"
+msgstr "Chaînes belges"
+
+
+# Context menu (from 30040 to 30049)
+msgctxt "#30040"
+msgid "Move down"
+msgstr "Descendre"
+
+msgctxt "#30041"
+msgid "Move up"
+msgstr "Monter"
+
+msgctxt "#30042"
+msgid "Hide"
+msgstr "Masquer"
+
+
+# Dialog boxes (from 30050 to 30069)
+msgctxt "#30050"
+msgid "Information"
+msgstr "Information"
+
+msgctxt "#30051"
+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"
+
+
+# Settings quality and content (from 30070 to 30099)
+msgctxt "#30070"
+msgid "TF1: Show bonus videos"
+msgstr "TF1 : Afficher les vidéos bonus"
+
+msgctxt "#30071"
+msgid "France 2: Video quality"
+msgstr "France 2 : Qualité vidéo"
+
+msgctxt "#30072"
+msgid "France 3: Video quality"
+msgstr "France 3 : Qualité vidéo"
+
+msgctxt "#30073"
+msgid "France 4: Video quality"
+msgstr "France 4 : Qualité vidéo"
+
+msgctxt "#30074"
+msgid "France 5: Video quality"
+msgstr "France 5 : Qualité vidéo"
+
+msgctxt "#30075"
+msgid "France Ô: Video quality"
+msgstr "France Ô : Qualité vidéo"
+
+msgctxt "#30076"
+msgid "M6: Video quality"
+msgstr "M6 : Qualité vidéo"
+
+msgctxt "#30077"
+msgid "W9: Video quality"
+msgstr "W9 : Qualité vidéo"
+
+msgctxt "#30078"
+msgid "6ter: Video quality"
+msgstr "6ter : Qualité vidéo"
+
+msgctxt "#30079"
+msgid "Arte: Video quality"
+msgstr "Arte : Qualité vidéo"
+
+msgctxt "#30080"
+msgid "Arte: Language"
+msgstr "Arte : Langue"
+
+msgctxt "#30081"
+msgid "BFM TV: Video quality"
+msgstr "BFM TV : Qualité vidéo"
+
+
+# Others (from 30100 to 30140)
+msgctxt "#30100"
+msgid "More videos..."
+msgstr "Plus de vidéos..."
+
+msgctxt "#30101"
+msgid "All videos"
+msgstr "Toutes les vidéos"
+
+msgctxt "#30102"
+msgid "DRM protected video"
+msgstr "Vidéo protégée par DRM"
+
+
diff --git a/plugin.video.catchuptvandmore/resources/lib/__init__.py b/plugin.video.catchuptvandmore/resources/lib/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/__init__.py
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/__init__.py b/plugin.video.catchuptvandmore/resources/lib/channels/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/__init__.py
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/6play.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/6play.py
new file mode 100755
index 0000000..d60ebd0
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/6play.py
@@ -0,0 +1,381 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Original work (C) JUL1EN094, SPM, SylvainCecchetto
+ Copyright (C) 2016 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+
+import json
+from resources.lib import utils
+from resources.lib import common
+
+
+# Url to get channel's categories
+# e.g. Info, Divertissement, Séries, ...
+# We get an id by category
+url_root = 'http://pc.middleware.6play.fr/6play/v2/platforms/' \
+ 'm6group_web/services/%sreplay/folders?limit=999&offset=0'
+
+# Url to get catgory's programs
+# e.g. Le meilleur patissier, La france à un incroyable talent, ...
+# We get an id by program
+url_category = 'http://pc.middleware.6play.fr/6play/v2/platforms/' \
+ 'm6group_web/services/6play/folders/%s/programs' \
+ '?limit=999&offset=0&csa=9&with=parentcontext'
+
+# Url to get program's subfolders
+# e.g. Saison 5, Les meilleurs moments, les recettes pas à pas, ...
+# We get an id by subfolder
+url_subcategory = 'http://pc.middleware.6play.fr/6play/v2/platforms/' \
+ 'm6group_web/services/6play/programs/%s' \
+ '?with=links,subcats,rights'
+
+
+# Url to get shows list
+# e.g. Episode 1, Episode 2, ...
+url_videos = 'http://pc.middleware.6play.fr/6play/v2/platforms/' \
+ 'm6group_web/services/6play/programs/%s/videos?' \
+ 'csa=6&with=clips,freemiumpacks&type=vi,vc,playlist&limit=999'\
+ '&offset=0&subcat=%s&sort=subcat'
+
+url_videos2 = 'https://pc.middleware.6play.fr/6play/v2/platforms/' \
+ 'm6group_web/services/6play/programs/%s/videos?' \
+ 'csa=6&with=clips,freemiumpacks&type=vi&limit=999&offset=0'
+
+
+url_json_video = 'https://pc.middleware.6play.fr/6play/v2/platforms/' \
+ 'm6group_web/services/6play/videos/%s'\
+ '?csa=9&with=clips,freemiumpacks'
+
+
+url_img = 'https://images.6play.fr/v1/images/%s/raw'
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_URL(params)
+
+
+@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ shows = []
+
+ if params.next == 'list_shows_1':
+ file_path = utils.download_catalog(
+ url_root % (params.channel_name),
+ '%s.json' % (params.channel_name),
+ random_ua=True)
+ file_prgm = open(file_path).read()
+ json_parser = json.loads(file_prgm)
+
+ # do not cache failed catalog fetch
+ # the error format is:
+ # {"error":{"code":403,"message":"Forbidden"}}
+ if isinstance(json_parser, dict) and \
+ 'error' in json_parser.keys():
+ utils.os.remove(file_path)
+ raise Exception('Failed to fetch the 6play catalog')
+
+ for array in json_parser:
+ category_id = str(array['id'])
+ category_name = array['name'].encode('utf-8')
+ shows.append({
+ 'label': category_name,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category_id=category_id,
+ next='list_shows_2',
+ title=category_name
+ )
+ })
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ elif params.next == 'list_shows_2':
+ file_prgm = utils.get_webcontent(
+ url_category % (params.category_id),
+ random_ua=True)
+ json_parser = json.loads(file_prgm)
+
+ for array in json_parser:
+ program_title = array['title'].encode('utf-8')
+ program_id = str(array['id'])
+ program_desc = array['description'].encode('utf-8')
+ program_imgs = array['images']
+ program_img = ''
+ for img in program_imgs:
+ if img['role'].encode('utf-8') == 'vignette':
+ external_key = img['external_key'].encode('utf-8')
+ program_img = url_img % (external_key)
+ elif img['role'].encode('utf-8') == 'carousel':
+ external_key = img['external_key'].encode('utf-8')
+ program_fanart = url_img % (external_key)
+
+ info = {
+ 'video': {
+ 'title': program_title,
+ 'plot': program_desc
+ }
+ }
+ shows.append({
+ 'label': program_title,
+ 'thumb': program_img,
+ 'fanart': program_fanart,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='list_shows_3',
+ program_id=program_id,
+ program_img=program_img,
+ program_fanart=program_fanart,
+ program_desc=program_desc,
+ title=program_title
+ ),
+ 'info': info
+ })
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ elif params.next == 'list_shows_3':
+ program_json = utils.get_webcontent(
+ url_subcategory % (params.program_id),
+ random_ua=True)
+
+ json_parser = json.loads(program_json)
+ for sub_category in json_parser['program_subcats']:
+ sub_category_id = str(sub_category['id'])
+ sub_category_title = sub_category['title'].encode('utf-8')
+
+ info = {
+ 'video': {
+ 'title': sub_category_title,
+ 'plot': params.program_desc
+ }
+ }
+
+ shows.append({
+ 'label': sub_category_title,
+ 'thumb': params.program_img,
+ 'fanart': params.program_fanart,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='list_videos',
+ program_id=params.program_id,
+ sub_category_id=sub_category_id
+ ),
+ 'info': info
+ })
+
+ info = {
+ 'video': {
+ 'title': common.addon.get_localized_string(30101),
+ 'plot': params.program_desc
+ }
+ }
+ shows.append({
+ 'label': common.addon.get_localized_string(30101),
+ 'thumb': params.program_img,
+ 'fanart': params.program_fanart,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='list_videos',
+ program_id=params.program_id,
+ sub_category_id='null'
+
+ ),
+ 'info': info
+ })
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ return shows
+
+
+@common.plugin.cached(common.cache_time)
+def list_videos(params):
+ videos = []
+
+ if params.sub_category_id == 'null':
+ url = url_videos2 % params.program_id
+ else:
+ url = url_videos % (params.program_id, params.sub_category_id)
+ program_json = utils.get_webcontent(
+ url,
+ random_ua=True)
+ json_parser = json.loads(program_json)
+
+ for video in json_parser:
+ video_id = str(video['id'])
+
+ title = video['title'].encode('utf-8')
+ duration = video['clips'][0]['duration']
+ description = video['description'].encode('utf-8')
+ try:
+ aired = video['clips'][0]['product']['last_diffusion']
+ aired = aired.encode('utf-8')
+ aired = aired[:10]
+ year = aired[:4]
+ # date : string (%d.%m.%Y / 01.01.2009)
+ # aired : string (2008-12-07)
+ day = aired.split('-')[2]
+ mounth = aired.split('-')[1]
+ year = aired.split('-')[0]
+ date = '.'.join((day, mounth, year))
+
+ except:
+ aired = ''
+ year = ''
+ date = ''
+ img = ''
+
+ program_imgs = video['clips'][0]['images']
+ program_img = ''
+ for img in program_imgs:
+ if img['role'].encode('utf-8') == 'vignette':
+ external_key = img['external_key'].encode('utf-8')
+ program_img = url_img % (external_key)
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': description,
+ 'aired': aired,
+ 'date': date,
+ 'duration': duration,
+ 'year': year,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'thumb': program_img,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ video_id=video_id,
+ ),
+ 'is_playable': True,
+ 'info': info
+ })
+
+ return common.plugin.create_listing(
+ videos,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_DATE,
+ common.sp.xbmcplugin.SORT_METHOD_DURATION,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE,
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED
+ ),
+ content='tvshows')
+
+
+@common.plugin.cached(common.cache_time)
+def get_video_URL(params):
+ video_json = utils.get_webcontent(
+ url_json_video % (params.video_id),
+ random_ua=True)
+ json_parser = json.loads(video_json)
+
+ video_assets = json_parser['clips'][0]['assets']
+ url = ''
+ url2 = ''
+ url3 = ''
+ for asset in video_assets:
+ if 'ism' in asset['video_container'].encode('utf-8'):
+ url = asset['full_physical_path'].encode('utf-8')
+ if 'mp4' in asset['video_container'].encode('utf-8'):
+ if 'hd' in asset['video_quality'].encode('utf-8'):
+ url2 = asset['full_physical_path'].encode('utf-8')
+ else:
+ url3 = asset['full_physical_path'].encode('utf-8')
+ manifest_url = ''
+ if url:
+ manifest_url = url
+ elif url2:
+ manifest_url = url2
+ else:
+ manifest_url = url3
+
+ manifest = utils.get_webcontent(
+ manifest_url,
+ random_ua=True)
+ if 'drm' in manifest:
+ utils.send_notification(common.addon.get_localized_string(30102))
+ return ''
+
+ desired_quality = common.plugin.get_setting(
+ params.channel_id + '.quality')
+
+ if desired_quality == 'Auto':
+ return manifest_url
+
+ root = common.os.path.dirname(manifest_url)
+
+ url_sd = ''
+ url_hd = ''
+ url_ultra_sd = ''
+ url_ultra_hd = ''
+
+ lines = manifest.splitlines()
+ for k in range(0, len(lines) - 1):
+ if 'RESOLUTION=400' in lines[k]:
+ url_ultra_sd = root + '/' + lines[k + 1]
+ elif 'RESOLUTION=640' in lines[k]:
+ url_sd = root + '/' + lines[k + 1]
+ elif 'RESOLUTION=720' in lines[k]:
+ url_hd = root + '/' + lines[k + 1]
+ elif 'RESOLUTION=1080' in lines[k]:
+ url_ultra_hd = root + '/' + lines[k + 1]
+
+ if desired_quality == 'Force HD':
+ if url_ultra_hd:
+ return url_ultra_hd
+ elif url_hd:
+ return url_hd
+ return manifest_url
+
+ elif desired_quality == 'Force SD':
+ if url_ultra_sd:
+ return url_ultra_sd
+ elif url_sd:
+ return url_sd
+ return manifest_url
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/__init__.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/__init__.py
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/arte.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/arte.py
new file mode 100755
index 0000000..f2b7a2b
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/arte.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Copyright (C) 2016 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+import json
+from resources.lib import utils
+from resources.lib import common
+
+authorization_key = 'Bearer OTE3NjJhOTYwNzQzNWY0MGE0OGI5MGQ0YmVm' \
+ 'MWY2Y2JiYzc5NDQzY2IxMmYxYjQ0NDVlYmEyOTBmYjVkMDg3OQ'
+
+headers = {'Authorization': authorization_key}
+
+url_categories = 'https://api-cdn.arte.tv/api/opa/v2/categories?' \
+ 'language=%s&limit=100&sort=order'
+# Valid languages list : fr|de|en|es|pl
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_URL(params)
+
+
+@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ shows = []
+
+ disered_language = common.plugin.get_setting(
+ params.channel_id + '.language')
+
+ if disered_language == 'Auto':
+ disered_language = params.channel_country
+
+ file_path = utils.download_catalog(
+ url_categories % disered_language,
+ '%s.json' % params.channel_name,
+ specific_headers=headers)
+ file_categories = open(file_path).read()
+ json_parser = json.loads(file_categories)
+
+ for category in json_parser['categories']:
+ label = category['label'].encode('utf-8')
+ desc = category['description'].encode('utf-8')
+ href = category['links']['videos']['href'].encode('utf-8')
+ code = category['code'].encode('utf-8')
+
+ info = {
+ 'video': {
+ 'title': label,
+ 'plot': desc
+ }
+ }
+
+ shows.append({
+ 'label': label,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ code=code,
+ href=href,
+ next='list_videos',
+ title=label
+ ),
+ 'info': info
+ })
+
+ return common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+
+@common.plugin.cached(common.cache_time)
+def list_videos(params):
+ videos = []
+
+ params_url = {
+ 'geoblockingZone': 'EUR_DE_FR,ALL,SAT,DE_FR',
+ 'imageSize': '1920x1080,625x224,940x530,720x406,400x225',
+ 'kind': 'SHOW',
+ 'limit': '100',
+ 'platform': 'ARTEPLUS7',
+ 'sort': '-broadcastBegin',
+ 'videoLibrary': 'true'
+ }
+ file_path = utils.download_catalog(
+ params.href,
+ '%s.json' % (params.channel_name + params.code),
+ specific_headers=headers,
+ params=params_url)
+ file_shows = open(file_path).read()
+ json_parser = json.loads(file_shows)
+
+ for video in json_parser['videos']:
+ title = video['title'].encode('utf-8')
+ subtitle = ''
+ if video['subtitle'] is not None:
+ subtitle = video['subtitle'].encode('utf-8')
+
+ original_title = video['originalTitle'].encode('utf-8')
+ plotoutline = ''
+ if video['shortDescription'] is not None:
+ plotoutline = video['shortDescription'].encode('utf-8')
+ plot = ''
+ if video['fullDescription'] is not None:
+ plot = video['fullDescription'].encode('utf-8')
+ duration = video['durationSeconds']
+ year_prod = video['productionYear']
+ genre = video['genrePresse'].encode('utf-8')
+ season = video['season']
+ episode = video['episode']
+ total_episodes = video['totalEpisodes']
+ href = video['links']['videoStreams']['href'].encode('utf-8')
+ views = video['views']
+ director = video['director']
+ aired = video['arteSchedulingDay'] # year-mounth-day
+ day = aired.split('-')[2]
+ mounth = aired.split('-')[1]
+ year = aired.split('-')[0]
+ date = '.'.join((day, mounth, year))
+ fanart = video['mainImage']['url'].encode('utf-8')
+ thumb = video['mainImage']['alternateResolutions'][1]['url'].encode('utf-8')
+
+ if subtitle:
+ title = title + ' - [I]' + subtitle + '[/I]'
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'originaltitle': original_title,
+ 'plot': plot,
+ 'plotoutline': plotoutline,
+ 'aired': aired,
+ 'date': date,
+ 'duration': duration,
+ 'year': year_prod,
+ 'genre': genre,
+ 'season': season,
+ 'episode': episode,
+ 'playcount': views,
+ 'director': director,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'fanart': fanart,
+ 'thumb': thumb,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ href=href,
+ ),
+ 'is_playable': True,
+ 'info': info
+ })
+
+ return common.plugin.create_listing(
+ videos,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_DATE,
+ common.sp.xbmcplugin.SORT_METHOD_DURATION,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE,
+ common.sp.xbmcplugin.SORT_METHOD_GENRE,
+ common.sp.xbmcplugin.SORT_METHOD_PLAYCOUNT,
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED
+ ),
+ content='tvshows')
+
+
+@common.plugin.cached(common.cache_time)
+def get_video_URL(params):
+ file_medias = utils.get_webcontent(
+ params.href,
+ specific_headers=headers)
+ json_parser = json.loads(file_medias)
+
+ url_auto = ''
+ url_hd_plus = ''
+ url_hd = ''
+ url_sd = ''
+ url_sd_minus = ''
+ for video_stream in json_parser['videoStreams']:
+ if video_stream['audioSlot'] == 1:
+ if video_stream['quality'] == 'AQ' or \
+ video_stream['quality'] == 'XQ':
+ url_auto = video_stream['url'].encode('utf-8')
+
+ elif video_stream['quality'] == 'SQ' and \
+ video_stream['mediaType'] == 'mp4' and \
+ video_stream['protocol'] == 'HTTP':
+ url_hd_plus = video_stream['url'].encode('utf-8')
+
+ elif video_stream['quality'] == 'EQ' and \
+ video_stream['mediaType'] == 'mp4' and \
+ video_stream['protocol'] == 'HTTP':
+ url_hd = video_stream['url'].encode('utf-8')
+
+ elif video_stream['quality'] == 'HQ' and \
+ video_stream['mediaType'] == 'mp4' and \
+ video_stream['protocol'] == 'HTTP':
+ url_sd = video_stream['url'].encode('utf-8')
+
+ elif video_stream['quality'] == 'MQ' and \
+ video_stream['mediaType'] == 'mp4' and \
+ video_stream['protocol'] == 'HTTP':
+ url_sd_minus = video_stream['url'].encode('utf-8')
+
+ desired_quality = common.plugin.get_setting(
+ params.channel_id + '.quality')
+
+ if desired_quality == 'Auto' and url_auto:
+ return url_auto
+
+ if desired_quality == 'HD+' and url_hd_plus:
+ return url_hd_plus
+ elif url_hd:
+ return url_hd
+
+ if desired_quality == 'HD' and url_hd:
+ return url_hd
+ elif url_hd_plus:
+ return url_hd_plus
+
+ if desired_quality == 'SD' and url_sd:
+ return url_sd
+ elif url_sd_minus:
+ return url_sd_minus
+
+ if desired_quality == 'SD-' and url_sd_minus:
+ return url_sd_minus
+ elif url_sd:
+ return url_sd
+
+ return url_auto
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/bfmtv.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/bfmtv.py
new file mode 100755
index 0000000..f88615b
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/bfmtv.py
@@ -0,0 +1,224 @@
+# -*- coding: utf-8 -*-
+'''
+ Catch-up TV & More
+ Copyright (C) 2017 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+'''
+
+import json
+from resources.lib import utils
+from resources.lib import common
+
+url_token = 'http://api.nextradiotv.com/bfmtv-applications/'
+
+url_menu = 'http://www.bfmtv.com/static/static-mobile/bfmtv/' \
+ 'ios-smartphone/v0/configuration.json'
+
+url_replay = 'http://api.nextradiotv.com/bfmtv-applications/%s/' \
+ 'getPage?pagename=replay'
+# token
+
+url_show = 'http://api.nextradiotv.com/bfmtv-applications/%s/' \
+ 'getVideosList?category=%s&count=100&page=%s'
+# token, category, page_number
+
+url_video = 'http://api.nextradiotv.com/bfmtv-applications/%s/' \
+ 'getVideo?idVideo=%s'
+# token, video_id
+
+
+@common.plugin.cached(common.cache_time)
+def get_token():
+ file_token = utils.get_webcontent(url_token)
+ token_json = json.loads(file_token)
+ return token_json['session']['token'].encode('utf-8')
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_URL(params)
+
+
+@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ # Create categories list
+ shows = []
+
+ if params.next == 'list_shows_1':
+ file_path = utils.download_catalog(
+ url_replay % get_token(),
+ '%s.json' % (params.channel_name))
+ file_categories = open(file_path).read()
+ json_categories = json.loads(file_categories)
+ json_categories = json_categories['page']['contents'][0]
+ json_categories = json_categories['elements'][0]['items']
+
+ for categories in json_categories:
+ title = categories['title'].encode('utf-8')
+ image_url = categories['image_url'].encode('utf-8')
+ category = categories['categories'].encode('utf-8')
+
+ shows.append({
+ 'label': title,
+ 'thumb': image_url,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category=category,
+ next='list_videos_1',
+ title=title,
+ page='1'
+ )
+ })
+
+ return common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+
+@common.plugin.cached(common.cache_time)
+def list_videos(params):
+ videos = []
+ if params.next == 'list_videos_1':
+ file_path = utils.download_catalog(
+ url_show % (
+ get_token(),
+ params.category,
+ params.page),
+ '%s_%s_%s.json' % (
+ params.channel_name,
+ params.category,
+ params.page))
+ file_show = open(file_path).read()
+ json_show = json.loads(file_show)
+
+ for video in json_show['videos']:
+ video_id = video['video'].encode('utf-8')
+ video_id_ext = video['id_ext'].encode('utf-8')
+ category = video['category'].encode('utf-8')
+ title = video['title'].encode('utf-8')
+ description = video['description'].encode('utf-8')
+ begin_date = video['begin_date'] # 1486725600,
+ image = video['image'].encode('utf-8')
+ duration = video['video_duration_ms'] / 1000
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': description,
+ #'aired': aired,
+ #'date': date,
+ 'duration': duration,
+ #'year': year,
+ 'genre': category,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'thumb': image,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ video_id=video_id,
+ video_id_ext=video_id_ext
+ ),
+ 'is_playable': True,
+ 'info': info
+ })
+
+ # More videos...
+ videos.append({
+ 'label': common.addon.get_localized_string(30100),
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category=params.category,
+ next='list_videos_1',
+ title=title,
+ page=str(int(params.page) + 1)
+ )
+
+ })
+
+ return common.plugin.create_listing(
+ videos,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_DURATION,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE,
+ common.sp.xbmcplugin.SORT_METHOD_GENRE,
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED
+ ),
+ content='tvshows')
+
+
+@common.plugin.cached(common.cache_time)
+def get_video_URL(params):
+ file_medias = utils.get_webcontent(
+ url_video % (get_token(), params.video_id))
+ json_parser = json.loads(file_medias)
+
+ url_hd_plus = ''
+ url_hd = ''
+ url_sd = ''
+ url_sd_minus = ''
+ url_default = ''
+
+ for media in json_parser['video']['medias']:
+ if media['frame_height'] == 270:
+ url_sd_minus = media['video_url'].encode('utf-8')
+ elif media['frame_height'] == 360:
+ url_sd = media['video_url'].encode('utf-8')
+ elif media['frame_height'] == 720:
+ url_hd = media['video_url'].encode('utf-8')
+ elif media['frame_height'] == 1080:
+ url_hd_plus = media['video_url'].encode('utf-8')
+ url_default = media['video_url'].encode('utf-8')
+
+ desired_quality = common.plugin.get_setting(
+ params.channel_id + '.quality')
+
+ if desired_quality == 'HD+' and url_hd_plus:
+ return url_hd_plus
+ elif url_hd:
+ return url_hd
+
+ if desired_quality == 'HD' and url_hd:
+ return url_hd
+ elif url_hd_plus:
+ return url_hd_plus
+
+ if desired_quality == 'SD' and url_sd:
+ return url_sd
+ elif url_sd_minus:
+ return url_sd_minus
+
+ if desired_quality == 'SD-' and url_sd_minus:
+ return url_sd_minus
+ elif url_sd:
+ return url_sd
+
+ return url_default
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/c.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/c.py
new file mode 100755
index 0000000..84d3b2d
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/c.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Copyright (C) 2017 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+import json
+from resources.lib import utils
+from resources.lib import common
+
+
+url_root = 'http://lab.canal-plus.pro/web/app_prod.php/api/replay/%s'
+# Channel id :
+# c8 : 1
+# cstar : 2
+
+url_shows = 'http://lab.canal-plus.pro/web/app_prod.php/api/pfv/list/%s/%s'
+# channel_id/show_id
+
+url_video = 'http://lab.canal-plus.pro/web/app_prod.php/api/pfv/video/%s/%s'
+# channel_id/video_id
+
+
+def get_channel_id(params):
+ if params.channel_name == 'c8':
+ return '1'
+ elif params.channel_name == 'cstar':
+ return '2'
+ else:
+ return '1'
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_URL(params)
+
+
+@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ # Create categories list
+ shows = []
+
+ if params.next == 'list_shows_1':
+ file_path = utils.download_catalog(
+ url_root % get_channel_id(params),
+ '%s.json' % (params.channel_name))
+ file_categories = open(file_path).read()
+ json_categories = json.loads(file_categories)
+
+ for categories in json_categories:
+ title = categories['title'].encode('utf-8')
+ slug = categories['slug'].encode('utf-8')
+
+ shows.append({
+ 'label': title,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ slug=slug,
+ next='list_shows_2',
+ title=title
+ )
+ })
+
+ return common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ elif params.next == 'list_shows_2':
+ # Create category's programs list
+ file_path = utils.download_catalog(
+ url_root % get_channel_id(params),
+ '%s_%s.json' % (params.channel_name, params.slug))
+ file_categories = open(file_path).read()
+ json_categories = json.loads(file_categories)
+
+ for categories in json_categories:
+ if categories['slug'].encode('utf-8') == params.slug:
+ for programs in categories['programs']:
+ id = str(programs['id'])
+ title = programs['title'].encode('utf-8')
+ slug = programs['slug'].encode('utf-8')
+ videos_recent = str(programs['videos_recent'])
+
+ shows.append({
+ 'label': title,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='list_videos',
+ id=id,
+ videos_recent=videos_recent,
+ slug=slug,
+ title=title
+ )
+ })
+
+ return common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+
+@common.plugin.cached(common.cache_time)
+def list_videos(params):
+ videos = []
+ file_path = utils.download_catalog(
+ url_shows % (get_channel_id(params), params.videos_recent),
+ '%s_%s.json' % (params.channel_name, params.videos_recent))
+ file_videos = open(file_path).read()
+ videos_json = json.loads(file_videos)
+
+ for video in videos_json:
+ id = video['ID'].encode('utf-8')
+ try:
+ duration = int(video['DURATION'].encode('utf-8'))
+ except:
+ duration = 0
+ description = video['INFOS']['DESCRIPTION'].encode('utf-8')
+ views = int(video['INFOS']['NB_VUES'].encode('utf-8'))
+ try:
+ date_video = video['INFOS']['DIFFUSION']['DATE'].encode('utf-8') # 31/12/2017
+ except:
+ date_video = "00/00/0000"
+ day = date_video.split('/')[0]
+ mounth = date_video.split('/')[1]
+ year = date_video.split('/')[2]
+ aired = '-'.join((day, mounth, year))
+ date = date_video.replace('/', '.')
+ title = video['INFOS']['TITRAGE']['TITRE'].encode('utf-8')
+ subtitle = video['INFOS']['TITRAGE']['SOUS_TITRE'].encode('utf-8')
+ thumb = video['MEDIA']['IMAGES']['GRAND'].encode('utf-8')
+ category = video['RUBRIQUAGE']['CATEGORIE'].encode('utf-8')
+
+ if subtitle:
+ title = title + ' - [I]' + subtitle + '[/I]'
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': description,
+ 'aired': aired,
+ 'date': date,
+ 'duration': duration,
+ 'year': year,
+ 'genre': category,
+ 'playcount': views,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'thumb': thumb,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ id=id,
+ ),
+ 'is_playable': True,
+ 'info': info
+ })
+
+ return common.plugin.create_listing(
+ videos,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_DATE,
+ common.sp.xbmcplugin.SORT_METHOD_DURATION,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE,
+ common.sp.xbmcplugin.SORT_METHOD_GENRE,
+ common.sp.xbmcplugin.SORT_METHOD_PLAYCOUNT,
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED
+ ),
+ content='tvshows')
+
+
+
+@common.plugin.cached(common.cache_time)
+def get_video_URL(params):
+ file_video = utils.get_webcontent(
+ url_video % (get_channel_id(params), params.id)
+ )
+ video_json = json.loads(file_video)
+ return video_json['main']['MEDIA']['VIDEOS']['HLS'].encode('utf-8')
+
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/canalplus.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/canalplus.py
new file mode 100755
index 0000000..12240ef
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/canalplus.py
@@ -0,0 +1,318 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Original work (C) JUL1EN094, SPM, SylvainCecchetto
+ Copyright (C) 2016 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+import json
+from resources.lib import utils
+from resources.lib import common
+
+
+url_auth = 'http://service.mycanal.fr/authenticate.json/iphone/' \
+ '1.6?highResolution=1&isActivated=0&isAuthenticated=0&paired=0'
+
+url_categories = 'http://service.mycanal.fr/page/%s/4578.json?' \
+ 'cache=60000&nbContent=96'
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_URL(params)
+
+
+def get_token():
+ token_json = utils.get_webcontent(url_auth)
+ token_json = json.loads(token_json)
+ token = token_json['token']
+ return token
+
+
+@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ # Create categories list
+ shows = []
+
+ if params.next == 'list_shows_1':
+ file_path = utils.download_catalog(
+ url_categories % get_token(),
+ '%s.json' % (params.channel_name))
+ file_categories = open(file_path).read()
+ json_categories = json.loads(file_categories)
+
+ for strate in json_categories['strates']:
+ if strate['type'] == 'textList':
+ for content in strate['contents']:
+ title = content['title'].encode('utf-8')
+ url_page = content['onClick']['URLPage'].encode('utf-8')
+
+ shows.append({
+ 'label': title,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ url_page=url_page,
+ next='list_shows_2',
+ title=title
+ )
+ })
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ elif params.next == 'list_shows_2':
+ # Create category's programs list
+ file_path = utils.download_catalog(
+ params.url_page,
+ '%s.json' % (params.title))
+ file_shows = open(file_path).read()
+ shows_json = json.loads(file_shows)
+
+ for strate in shows_json['strates']:
+ if strate['type'] == 'contentGrid':
+ for content in strate['contents']:
+ title = content['title'].encode('utf-8')
+ try:
+ subtitle = content['subtitle'].encode('utf-8')
+ except:
+ subtitle = ''
+ img = content['URLImage'].encode('utf-8')
+ url_page = content['onClick']['URLPage'].encode('utf-8')
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': subtitle,
+ }
+ }
+ shows.append({
+ 'label': title,
+ 'thumb': img,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='list_shows_3',
+ url_page=url_page,
+ title=title
+ ),
+ 'info': info
+ })
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ elif params.next == 'list_shows_3':
+ # Check if there is any folder for this program
+ file_path = utils.download_catalog(
+ params.url_page,
+ '%s.json' % (params.title))
+ file_shows = open(file_path).read()
+ shows_json = json.loads(file_shows)
+
+ if 'strates' not in shows_json or len(shows_json['strates']) <= 2:
+ params.next = 'list_videos'
+ params.title = 'none'
+ params.index_page = 1
+ return list_videos(params)
+ else:
+ fanart = ''
+ for strate in shows_json['strates']:
+ if strate['type'] == 'carrousel':
+ for content in strate['contents']:
+ fanart = content['URLImage'].encode('utf-8')
+ elif strate['type'] == 'contentRow':
+ title = strate['title'].encode('utf-8')
+
+ info = {
+ 'video': {
+ 'title': title,
+ }
+ }
+
+ shows.append({
+ 'label': title,
+ 'fanart': fanart,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='list_videos',
+ url_page=params.url_page,
+ title=title,
+ index_page=1
+ ),
+ 'info': info
+ })
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ return shows
+
+
+@common.plugin.cached(common.cache_time)
+def list_videos(params):
+ videos = []
+ file_path = utils.download_catalog(
+ params.url_page,
+ '%s.json' % (params.url_page))
+ file_videos = open(file_path).read()
+ videos_json = json.loads(file_videos)
+
+ more_videos = False
+ no_strates = False
+ fanart = ''
+
+ if 'strates' not in videos_json:
+ no_strates = True
+ else:
+ for strate in videos_json['strates']:
+ if strate['type'] == 'carrousel':
+ for content in strate['contents']:
+ fanart = content['URLImage'].encode('utf-8')
+ if strate['type'] == 'contentRow' or strate['type'] == 'contentGrid':
+ if strate['title'].encode('utf-8') == params.title or params.title == 'none':
+ if 'URLPage' in strate['paging']:
+ url = strate['paging']['URLPage'].encode('utf-8')
+ url = url + '&indexPage=' + params.index_page
+ params.index_page = int(params.index_page) + 1
+ more_videos = True
+ file_videos = utils.get_webcontent(url)
+ videos_json = json.loads(file_videos)
+
+ if more_videos or no_strates:
+ if len(videos_json['contents']) == 0:
+ more_videos = False
+ for content in videos_json['contents']:
+ title = content['title'].encode('utf-8')
+ try:
+ subtitle = content['subtitle'].encode('utf-8')
+ except:
+ subtitle = ''
+ img = content['URLImage'].encode('utf-8')
+ url_media = content['onClick']['URLPage'].encode('utf-8')
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': subtitle,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'thumb': img,
+ 'fanart': fanart,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ url_media=url_media,
+ url_page=params.url_page,
+ title=title,
+ index_page=params.index_page
+ ),
+ 'info': info,
+ 'is_playable': True
+ })
+ else:
+ for strate in videos_json['strates']:
+ if strate['type'] == 'contentRow' or strate['type'] == 'contentGrid':
+ if strate['title'].encode('utf-8') == params.title or params.title == 'none':
+ for content in strate['contents']:
+ title = content['title'].encode('utf-8')
+ try:
+ subtitle = content['subtitle'].encode('utf-8')
+ except:
+ subtitle = ''
+ img = content['URLImage'].encode('utf-8')
+ url_media = content['onClick']['URLPage'].encode('utf-8')
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': subtitle,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'thumb': img,
+ 'fanart': fanart,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ url_media=url_media,
+ url_page=params.url_page,
+ title=title,
+ index_page=params.index_page
+ ),
+ 'info': info,
+ 'is_playable': True
+ })
+
+ if more_videos:
+ videos.append({
+ 'label': common.addon.get_localized_string(30100),
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='list_videos',
+ url_media=url_media,
+ title=params.title,
+ url_page=params.url_page,
+ index_page=params.index_page
+ ),
+
+ })
+
+ videos = common.plugin.create_listing(
+ videos,
+ content='tvshows',
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ return videos
+
+
+@common.plugin.cached(common.cache_time)
+def get_video_URL(params):
+ file_path = utils.get_webcontent(params.url_media)
+ media_json = json.loads(file_path)
+ url = media_json['detail']['informations']['VoD']['videoURL'].encode('utf-8')
+ return url
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/gulli.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/gulli.py
new file mode 100755
index 0000000..b7ab8ee
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/gulli.py
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Copyright (C) 2017 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+import json
+from resources.lib import utils
+from resources.lib import common
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_URL(params)
+
+
+categories = {}
+
+categories['Dessins animés'] = 'http://sslreplay.gulli.fr/replay/api?' \
+ 'call=%7B%22api_key%22:%22iphoner_a140e' \
+ 'e8cb4b10fcd8b12a7fe688b34de%22,%22method' \
+ '%22:%22programme.getLatestEpisodes%22,%' \
+ '22params%22:%7B%22program_image_thumb%' \
+ '22:%5B310,230%5D,%22category_id%22:%22' \
+ 'dessins-animes%22%7D%7D'
+
+categories['Émissions'] = 'https://sslreplay.gulli.fr/replay/api?' \
+ 'call=%7B%22api_key%22:%22iphoner_a140e' \
+ 'e8cb4b10fcd8b12a7fe688b34de%22,%22method' \
+ '%22:%22programme.getLatestEpisodes%22,%' \
+ '22params%22:%7B%22program_image_thumb%' \
+ '22:%5B310,230%5D,%22category_id%22:%22' \
+ 'emissions%22%7D%7D'
+
+categories['Séries & films'] = 'https://sslreplay.gulli.fr/replay/api?' \
+ 'call=%7B%22api_key%22:%22iphoner_a140e' \
+ 'e8cb4b10fcd8b12a7fe688b34de%22,%22method' \
+ '%22:%22programme.getLatestEpisodes%22,%' \
+ '22params%22:%7B%22program_image_thumb%' \
+ '22:%5B310,230%5D,%22category_id%22:%22' \
+ 'series%22%7D%7D'
+
+url_list_show = 'https://sslreplay.gulli.fr/replay/api?call=%%7B%%22api_key' \
+ '%%22:%%22iphoner_a140ee8cb4b10fcd8b12a7fe688b34de%%22,%%22' \
+ 'method%%22:%%22programme.getEpisodesByProgramIds%%22,%%22' \
+ 'params%%22:%%7B%%22program_id_list%%22:%%5B%%22%s%%22%%5D' \
+ '%%7D%%7D'
+# program_id
+
+
+@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ # Create categories list
+ shows = []
+
+ if params.next == 'list_shows_1':
+ for category_title, category_url in categories.iteritems():
+ shows.append({
+ 'label': category_title,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category_url=category_url,
+ next='list_shows_cat',
+ title=category_title
+ )
+ })
+
+ return common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ elif params.next == 'list_shows_cat':
+ file_path = utils.download_catalog(
+ params.category_url,
+ '%s_%s.json' % (params.channel_name, params.title),
+ random_ua=True)
+ file = open(file_path).read()
+ json_category = json.loads(file)
+
+ for show in json_category['res']:
+ program_title = show['program_title'.encode('utf-8')]
+ program_id = show['program_id'].encode('utf-8')
+ fanart = show['program_image'].encode('utf-8')
+
+ shows.append({
+ 'label': program_title,
+ 'thumb': fanart,
+ 'fanart': fanart,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ program_id=program_id,
+ next='list_videos',
+ title=program_title
+ )
+ })
+
+ return common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+
+@common.plugin.cached(common.cache_time)
+def list_videos(params):
+ videos = []
+
+ file_path = utils.download_catalog(
+ url_list_show % params.program_id,
+ '%s_%s.json' % (params.channel_name, params.program_id))
+ file = open(file_path).read()
+ json_show = json.loads(file)
+
+ for show in json_show['res']:
+ # media_id = show['media_id'].encode('utf-8')
+ # program_title = show['program_title'.encode('utf-8')]
+ # cat_id = show['cat_id'].encode('utf-8')
+ # program_id = show['program_id'].encode('utf-8')
+ fanart = show['program_image'].encode('utf-8')
+ thumb = show['episode_image'].encode('utf-8')
+ episode_title = show['episode_title'].encode('utf-8')
+ episode_number = show['episode_number']
+ season_number = show['season_number']
+ # total_episodes_in_season = show['total_episodes_in_season']
+ url_ipad = show['url_ipad'].encode('utf-8')
+ # url_streaming = show['url_streaming'].encode('utf-8')
+ short_desc = show['short_desc'].encode('utf-8')
+ note = float(show['note'].encode('utf-8')) * 2
+ date_debut = show['date_debut'].encode('utf-8')
+ # "2017-02-03 00:00:00"
+ year = int(date_debut[:4])
+ day = date_debut[8:10]
+ month = date_debut[5:7]
+ date = '.'.join((day, month, str(year)))
+ aired = '-'.join((str(year), month, day))
+
+ info = {
+ 'video': {
+ 'title': episode_title,
+ 'plot': short_desc,
+ 'episode': episode_number,
+ 'season': season_number,
+ 'rating': note,
+ 'aired': aired,
+ 'date': date,
+ # 'duration': duration,
+ 'year': year,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': episode_title,
+ 'thumb': thumb,
+ 'fanart': fanart,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ url_ipad=url_ipad
+ ),
+ 'is_playable': True,
+ 'info': info
+ })
+
+ return common.plugin.create_listing(
+ videos,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_DATE,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE
+ ),
+ content='tvshows')
+
+
+@common.plugin.cached(common.cache_time)
+def get_video_URL(params):
+ return params.video_urlhd
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/itele.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/itele.py
new file mode 100755
index 0000000..0963fb3
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/itele.py
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Copyright (C) 2017 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+import json
+from resources.lib import utils
+from resources.lib import common
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_URL(params)
+
+
+url_category_query = 'http://service.itele.fr/iphone/categorie_news?query='
+
+
+categories = {
+ 'http://service.itele.fr/iphone/topnews': 'La Une',
+ url_category_query + 'FRANCE': 'France',
+ url_category_query + 'MONDE': 'Monde',
+ url_category_query + 'POLITIQUE': 'Politique',
+ url_category_query + 'JUSTICE': 'Justice',
+ url_category_query + 'ECONOMIE': 'Économie',
+ url_category_query + 'SPORT': 'Sport',
+ url_category_query + 'CULTURE': 'Culture',
+ url_category_query + 'INSOLITE': 'Insolite'
+}
+
+#@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ # Create categories list
+ shows = []
+
+ if params.next == 'list_shows_1':
+ for category_url, category_title in categories.iteritems():
+ shows.append({
+ 'label': category_title,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category_url=category_url,
+ next='list_videos_cat',
+ title=category_title
+ )
+ })
+
+ shows.append({
+ 'label': 'Les Émissions',
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category_url='emissions',
+ next='list_shows_emissions',
+ title='Les Émissions'
+ )
+ })
+
+ return common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+ elif params.next == 'list_shows_emissions':
+ shows.append({
+ 'label': 'À la Une',
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category_url='http://service.itele.fr/iphone/dernieres_emissions?query=',
+ next='list_videos_cat',
+ title='À la Une'
+ )
+ })
+
+ shows.append({
+ 'label': 'Magazines',
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category_url='http://service.itele.fr/iphone/emissions?query=magazines',
+ next='list_videos_cat',
+ title='Magazines'
+ )
+ })
+
+ shows.append({
+ 'label': 'Chroniques',
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category_url='http://service.itele.fr/iphone/emissions?query=chroniques',
+ next='list_videos_cat',
+ title='Chroniques'
+ )
+ })
+
+ return common.plugin.create_listing(
+ shows,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+ )
+
+
+def list_videos(params):
+ videos = []
+ if params.next == 'list_videos_cat':
+ file_path = utils.download_catalog(
+ params.category_url,
+ '%s_%s.json' % (params.channel_name, params.title))
+ file = open(file_path).read()
+ json_category = json.loads(file)
+
+ if 'news' in json_category:
+ json_category = json_category['news']
+ elif 'videos' in json_category:
+ json_category = json_category['videos']
+ elif 'topnews' in json_category:
+ json_category = json_category['topnews']
+ for video in json_category:
+ video_id = video['id_pfv'].encode('utf-8')
+ category = video['category'].encode('utf-8')
+ date_time = video['date'].encode('utf-8') # 2017-02-10 22:05:02
+ title = video['title'].encode('utf-8')
+ description = video['description'].encode('utf-8')
+ thumb = video['preview169'].encode('utf-8')
+ video_url = video['video_urlhd'].encode('utf-8')
+ if not video_url:
+ video_url = 'no_video'
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': description,
+ # 'aired': aired,
+ # 'date': date,
+ #'duration': duration,
+ #'year': year,
+ 'genre': category,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'thumb': thumb,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ video_id=video_id,
+ video_urlhd=video_url
+ ),
+ 'is_playable': True,
+ 'info': info
+ })
+
+ return common.plugin.create_listing(
+ videos,
+ sort_methods=(
+ common.sp.xbmcplugin.SORT_METHOD_DATE,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE,
+ common.sp.xbmcplugin.SORT_METHOD_GENRE,
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED
+ ),
+ content='tvshows')
+
+
+def get_video_URL(params):
+ return params.video_urlhd
+
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/pluzz.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/pluzz.py
new file mode 100755
index 0000000..2c1ddaf
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/pluzz.py
@@ -0,0 +1,282 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Original work (C) JUL1EN094, SPM, SylvainCecchetto
+ Copyright (C) 2016 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+import json
+from resources.lib import utils
+from resources.lib import common
+
+channelCatalog = 'http://pluzz.webservices.francetelevisions.fr/' \
+ 'pluzz/liste/type/replay/nb/10000/chaine/%s'
+
+showInfo = 'http://webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/' \
+ '?idDiffusion=%s&catalogue=Pluzz'
+
+imgURL = 'http://refonte.webservices.francetelevisions.fr%s'
+
+categories = {"france2": "France 2",
+ "france3": "France 3",
+ "france4": "France 4",
+ "france5": "France 5",
+ "franceo": "France Ô",
+ "guadeloupe": "Guadeloupe 1ère",
+ "guyane": "Guyane 1ère",
+ "martinique": "Martinique 1ère",
+ "mayotte": "Mayotte 1ère",
+ "nouvellecaledonie": "Nouvelle Calédonie 1ère",
+ "polynesie": "Polynésie 1ère",
+ "reunion": "Réunion 1ère",
+ "saintpierreetmiquelon": "St-Pierre et Miquelon 1ère",
+ "wallisetfutuna": "Wallis et Futuna 1ère",
+ "sport": "Sport",
+ "info": "Info",
+ "documentaire": "Documentaire",
+ "seriefiction": "Série & fiction",
+ "magazine": "Magazine",
+ "jeunesse": "Jeunesse",
+ "divertissement": "Divertissement",
+ "jeu": "Jeu",
+ "culture": "Culture"}
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_URL(params)
+
+
+@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ shows = []
+ uniqueItem = dict()
+
+ realChannel = params.channel_name
+ if params.channel_name == 'la_1ere':
+ realChannel = 'la_1ere_reunion%2C' \
+ 'la_1ere_guyane%2C' \
+ 'la_1ere_polynesie%2C' \
+ 'la_1ere_martinique%2C' \
+ 'la_1ere_mayotte%2C' \
+ 'la_1ere_nouvellecaledonie%2C' \
+ 'la_1ere_guadeloupe%2C' \
+ 'la_1ere_wallisetfutuna%2C' \
+ 'la_1ere_saintpierreetmiquelon'
+
+ url_json = channelCatalog % (realChannel)
+ filePath = utils.download_catalog(
+ url_json,
+ '%s.json' % (params.channel_name))
+ filPrgm = open(filePath).read()
+ jsonParser = json.loads(filPrgm)
+ emissions = jsonParser['reponse']['emissions']
+
+ if params.next == 'list_shows_1':
+ for emission in emissions:
+ rubrique = emission['rubrique'].encode('utf-8')
+ if rubrique not in uniqueItem:
+ uniqueItem[rubrique] = rubrique
+ rubrique_title = change_to_nicer_name(rubrique)
+
+ shows.append({
+ 'label': rubrique_title,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ rubrique=rubrique,
+ next='list_shows_2'
+ )
+ })
+
+ sort_methods = (
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=sort_methods
+ )
+
+ elif params.next == 'list_shows_2':
+ for emission in emissions:
+ rubrique = emission['rubrique'].encode('utf-8')
+ if rubrique == params.rubrique:
+ titre_programme = emission['titre_programme'].encode('utf-8')
+ if titre_programme != '':
+ id_programme = emission['id_programme'].encode('utf-8')
+ if id_programme == '':
+ id_programme = emission['id_emission'].encode('utf-8')
+ if id_programme not in uniqueItem:
+ uniqueItem[id_programme] = id_programme
+ icon = imgURL % (emission['image_large'])
+ genre = emission['genre']
+ accroche_programme = emission['accroche_programme']
+
+ info = {
+ 'video': {
+ 'title': titre_programme,
+ 'plot': accroche_programme,
+ 'genre': genre
+ }
+ }
+ shows.append({
+ 'label': titre_programme,
+ 'thumb': icon,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='list_videos_1',
+ id_programme=id_programme,
+ ),
+ 'info': info
+ })
+
+ sort_methods = (
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL
+ )
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=sort_methods
+ )
+
+ return shows
+
+
+@common.plugin.cached(common.cache_time)
+def change_to_nicer_name(original_name):
+ if original_name in categories:
+ return categories[original_name]
+ return original_name
+
+
+@common.plugin.cached(common.cache_time)
+def list_videos(params):
+ videos = []
+ filePath = utils.download_catalog(
+ channelCatalog % (params.channel_name),
+ '%s.json' % (params.channel_name)
+ )
+ filPrgm = open(filePath).read()
+ jsonParser = json.loads(filPrgm)
+ emissions = jsonParser['reponse']['emissions']
+ for emission in emissions:
+ id_programme = emission['id_programme'].encode('utf-8')
+ if id_programme == '':
+ id_programme = emission['id_emission'].encode('utf-8')
+ if id_programme == params.id_programme:
+ title = ''
+ plot = ''
+ duration = 0
+ date = ''
+ genre = ''
+ id_diffusion = emission['id_diffusion']
+ filPrgm = utils.get_webcontent(
+ showInfo % (emission['id_diffusion']))
+ if(filPrgm != ''):
+ jsonParserShow = json.loads(filPrgm)
+ if jsonParserShow['synopsis']:
+ plot = jsonParserShow['synopsis'].encode('utf-8')
+ if jsonParserShow['diffusion']['date_debut']:
+ date = jsonParserShow['diffusion']['date_debut']
+ date = date.encode('utf-8')
+ if jsonParserShow['real_duration']:
+ duration = int(jsonParserShow['real_duration'])
+ if jsonParserShow['titre']:
+ title = jsonParserShow['titre'].encode('utf-8')
+ if jsonParserShow['sous_titre']:
+ title = ' '.join((
+ title,
+ '- [I]',
+ jsonParserShow['sous_titre'].encode('utf-8'),
+ '[/I]'))
+
+ if jsonParserShow['genre'] != '':
+ genre = \
+ jsonParserShow['genre'].encode('utf-8')
+
+ year = int(date[6:10])
+ day = date[:2]
+ month = date[3:5]
+ date = '.'.join((day, month, str(year)))
+ aired = '-'.join((str(year), month, day))
+ # date : string (%d.%m.%Y / 01.01.2009)
+ # aired : string (2008-12-07)
+ image = imgURL % (jsonParserShow['image'])
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': plot,
+ 'aired': aired,
+ 'date': date,
+ 'duration': duration,
+ 'year': year,
+ 'genre': genre,
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'thumb': image,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ id_diffusion=id_diffusion
+ ),
+ 'is_playable': True,
+ 'info': info
+ })
+
+ sort_methods = (
+ common.sp.xbmcplugin.SORT_METHOD_DATE,
+ common.sp.xbmcplugin.SORT_METHOD_DURATION,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE,
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED
+ )
+ return common.plugin.create_listing(
+ videos,
+ sort_methods=sort_methods,
+ content='tvshows')
+
+
+@common.plugin.cached(common.cache_time)
+def get_video_URL(params):
+ filPrgm = utils.get_webcontent(showInfo % (params.id_diffusion))
+ jsonParser = json.loads(filPrgm)
+ url_HD = ''
+ url_SD = ''
+ for video in jsonParser['videos']:
+ if video['format'] == 'hls_v5_os':
+ url_HD = video['url']
+ if video['format'] == 'm3u8-download':
+ url_SD = video['url']
+
+ desired_quality = common.plugin.get_setting(
+ params.channel_id + '.quality')
+
+ if desired_quality == 'HD' and url_HD is not None:
+ return url_HD
+ else:
+ return url_SD
diff --git a/plugin.video.catchuptvandmore/resources/lib/channels/fr/tf1.py b/plugin.video.catchuptvandmore/resources/lib/channels/fr/tf1.py
new file mode 100755
index 0000000..c10c181
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/channels/fr/tf1.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Original work (C) JUL1EN094, SPM, SylvainCecchetto
+ Copyright (C) 2016 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+from bs4 import BeautifulSoup as bs
+from resources.lib import utils
+from resources.lib import common
+
+url_root = "http://www.tf1.fr/"
+
+
+def channel_entry(params):
+ if 'list_shows' in params.next:
+ return list_shows(params)
+ elif 'list_videos' in params.next:
+ return list_videos(params)
+ elif 'play' in params.next:
+ return get_video_url(params)
+ else:
+ return None
+
+
+@common.plugin.cached(common.cache_time)
+def list_shows(params):
+ shows = []
+
+ url = ''.join((
+ url_root,
+ params.channel_name,
+ '/programmes-tv'))
+ file_path = utils.download_catalog(
+ url,
+ params.channel_name + '.html')
+ root_html = open(file_path).read()
+ root_soup = bs(root_html, 'html.parser')
+
+ if params.next == 'list_shows_1':
+ categories_soup = root_soup.find(
+ 'ul',
+ attrs={'class': 'filters_2 contentopen'})
+ for category in categories_soup.find_all('a'):
+ category_name = category.get_text().encode('utf-8')
+ category_url = category['data-target'].encode('utf-8')
+
+ shows.append({
+ 'label': category_name,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ category=category_url,
+ next='list_shows_2')})
+
+ sort_methods = (
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL)
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=sort_methods)
+
+ elif params.next == 'list_shows_2':
+ programs_soup = root_soup.find(
+ 'ul',
+ attrs={'id': 'js_filter_el_container'})
+ for program in programs_soup.find_all('li'):
+ category = program['data-type'].encode('utf-8')
+ if params.category == category or params.category == 'all':
+ program_url = program.find(
+ 'div',
+ class_='description')
+ program_url = program_url.find('a')['href'].encode('utf-8')
+ program_name = program.find(
+ 'p',
+ class_='program').get_text().encode('utf-8')
+ img = program.find('img')
+ try:
+ img = img['data-srcset'].encode('utf-8')
+ except:
+ img = img['srcset'].encode('utf-8')
+
+ img = 'http:' + img.split(',')[-1].split(' ')[0]
+
+ shows.append({
+ 'label': program_name,
+ 'thumb': img,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ program_url=program_url,
+ next='list_videos')})
+
+ sort_methods = (
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL)
+
+ shows = common.plugin.create_listing(
+ shows,
+ sort_methods=sort_methods)
+
+ return shows
+
+
+@common.plugin.cached(common.cache_time)
+def list_videos(params):
+ videos = []
+
+ url = ''.join((
+ url_root,
+ params.program_url,
+ '/videos'))
+ program_html = utils.get_webcontent(url)
+ program_soup = bs(program_html, 'html.parser')
+
+ grid = program_soup.find(
+ 'ul',
+ class_='grid')
+
+ for li in grid.find_all('li'):
+ video_type_string = li.find('strong').get_text().encode('utf-8')
+ video_type = video_type_string.lower()
+
+ if 'playlist' not in video_type:
+ if 'replay' in video_type or \
+ 'video' in video_type or \
+ common.plugin.get_setting(params.channel_id + '.bonus'):
+
+ title = li.find(
+ 'p',
+ class_='title').get_text().encode('utf-8')
+
+ try:
+ stitle = li.find(
+ 'p',
+ class_='stitle').get_text().encode('utf-8')
+ except:
+ stitle = ''
+
+ try:
+ duration = li.find(
+ 'span',
+ attrs={'data-format': 'duration'})
+ duration = int(duration.get_text().encode('utf-8'))
+ except:
+ duration = 0
+
+ img = li.find('img')
+ try:
+ img = img['data-srcset'].encode('utf-8')
+ except:
+ img = img['srcset'].encode('utf-8')
+
+ img = 'http:' + img.split(',')[-1].split(' ')[0]
+
+ aired = li.find(
+ 'span',
+ attrs={'data-format': None, 'class': 'momentDate'})
+ aired = aired.get_text().encode('utf-8')
+ aired = aired.split('T')[0]
+
+ day = aired.split('-')[2]
+ mounth = aired.split('-')[1]
+ year = aired.split('-')[0]
+ date = '.'.join((day, mounth, year))
+ # date : string (%d.%m.%Y / 01.01.2009)
+ # aired : string (2008-12-07)
+ program_id = li.find('a')['href'].encode('utf-8')
+
+ if 'replay' not in video_type and 'video' not in video_type:
+ title = title + ' - [I]' + video_type_string + '[/I]'
+
+ info = {
+ 'video': {
+ 'title': title,
+ 'plot': stitle,
+ 'aired': aired,
+ 'date': date,
+ 'duration': duration,
+ 'year': int(aired[:4]),
+ 'mediatype': 'tvshow'
+ }
+ }
+
+ videos.append({
+ 'label': title,
+ 'thumb': img,
+ 'url': common.plugin.get_url(
+ action='channel_entry',
+ next='play',
+ program_id=program_id,
+ ),
+ 'is_playable': True,
+ 'info': info
+ })
+
+ sort_methods = (
+ common.sp.xbmcplugin.SORT_METHOD_DATE,
+ common.sp.xbmcplugin.SORT_METHOD_DURATION,
+ common.sp.xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE,
+ common.sp.xbmcplugin.SORT_METHOD_UNSORTED
+ )
+ return common.plugin.create_listing(
+ videos,
+ sort_methods=sort_methods,
+ content='tvshows')
+
+
+@common.plugin.cached(common.cache_time)
+def get_video_url(params):
+ url = url_root + params.program_id
+ video_html = utils.get_webcontent(url)
+ video_html_soup = bs(video_html, 'html.parser')
+
+ iframe_player_soup = video_html_soup.find(
+ 'div',
+ class_='iframe_player')
+
+ data_src = iframe_player_soup['data-src'].encode('utf-8')
+
+ video_id = data_src[-8:]
+
+ return 'http://wat.tv/get/ipad/' + video_id + '.m3u8'
diff --git a/plugin.video.catchuptvandmore/resources/lib/common.py b/plugin.video.catchuptvandmore/resources/lib/common.py
new file mode 100755
index 0000000..5705b7f
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/common.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Copyright (C) 2016 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+from resources.lib import simpleplugin as sp
+import os
+
+plugin = sp.Plugin()
+addon = sp.Addon()
+
+cache_time = 10
+
+plugin_name = 'Catch-up TV & More'
diff --git a/plugin.video.catchuptvandmore/resources/lib/simpleplugin.py b/plugin.video.catchuptvandmore/resources/lib/simpleplugin.py
new file mode 100755
index 0000000..f3b6f9d
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/simpleplugin.py
@@ -0,0 +1,923 @@
+# -*- coding: utf-8 -*-
+# Created on: 03.06.2015
+"""
+SimplePlugin micro-framework for Kodi content plugins
+
+**Author**: Roman Miroshnychenko aka Roman V.M.
+
+**License**: `GPL v.3 <https://www.gnu.org/copyleft/gpl.html>`_
+"""
+
+import os
+import sys
+import re
+from datetime import datetime, timedelta
+import cPickle as pickle
+from urlparse import parse_qs
+from urllib import urlencode
+from functools import wraps
+from collections import MutableMapping, namedtuple
+from copy import deepcopy
+from types import GeneratorType
+from hashlib import md5
+from shutil import move
+import xbmcaddon
+import xbmc
+import xbmcplugin
+import xbmcgui
+
+__all__ = ['SimplePluginError', 'Storage', 'Addon', 'Plugin', 'Params']
+
+ListContext = namedtuple('ListContext', ['listing', 'succeeded', 'update_listing', 'cache_to_disk',
+ 'sort_methods', 'view_mode', 'content'])
+PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded'])
+
+
+class SimplePluginError(Exception):
+ """Custom exception"""
+ pass
+
+
+class Params(dict):
+ """
+ Params(**kwargs)
+
+ A class that stores parsed plugin call parameters
+
+ Parameters can be accessed both through :class:`dict` keys and
+ instance properties.
+
+ Example:
+
+ .. code-block:: python
+
+ @plugin.action('foo')
+ def action(params):
+ foo = params['foo'] # Access by key
+ bar = params.bar # Access through property. Both variants are equal
+ """
+ def __getattr__(self, item):
+ if item not in self:
+ raise AttributeError('Invalid parameter: "{0}"!'.format(item))
+ return self[item]
+
+ def __str__(self):
+ return '<Params {0}>'.format(super(Params, self).__repr__())
+
+ def __repr__(self):
+ return '<simpleplugin.Params object {0}>'.format(super(Params, self).__repr__())
+
+
+class Storage(MutableMapping):
+ """
+ Persistent storage for arbitrary data with a dictionary-like interface
+
+ It is designed as a context manager and better be used
+ with 'with' statement.
+
+ :param storage_dir: directory for storage
+ :type storage_dir: str
+ :param filename: the name of a storage file (optional)
+ :type filename: str
+
+ Usage::
+
+ with Storage('/foo/bar/storage/') as storage:
+ storage['key1'] = value1
+ value2 = storage['key2']
+
+ .. note:: After exiting :keyword:`with` block a :class:`Storage` instance is invalidated.
+ Storage contents are saved to disk only for a new storage or if the contents have been changed.
+ """
+ def __init__(self, storage_dir, filename='storage.pcl'):
+ """
+ Class constructor
+ """
+ self._storage = {}
+ self._hash = None
+ self._filename = os.path.join(storage_dir, filename)
+ try:
+ with open(self._filename, 'rb') as fo:
+ contents = fo.read()
+ self._storage = pickle.loads(contents)
+ self._hash = md5(contents).hexdigest()
+ except (IOError, pickle.PickleError, EOFError):
+ pass
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.flush()
+
+ def __getitem__(self, key):
+ return self._storage[key]
+
+ def __setitem__(self, key, value):
+ self._storage[key] = value
+
+ def __delitem__(self, key):
+ del self._storage[key]
+
+ def __iter__(self):
+ return self._storage.__iter__()
+
+ def __len__(self):
+ return len(self._storage)
+
+ def __str__(self):
+ return '<Storage {0}>'.format(self._storage)
+
+ def __repr__(self):
+ return '<simpleplugin.Storage object {0}>'.format(self._storage)
+
+ def flush(self):
+ """
+ Save storage contents to disk
+
+ This method saves new and changed :class:`Storage` contents to disk
+ and invalidates the Storage instance. Unchanged Storage is not saved
+ but simply invalidated.
+ """
+ contents = pickle.dumps(self._storage)
+ if self._hash is None or md5(contents).hexdigest() != self._hash:
+ tmp = self._filename + '.tmp'
+ try:
+ with open(tmp, 'wb') as fo:
+ fo.write(contents)
+ except:
+ os.remove(tmp)
+ raise
+ move(tmp, self._filename) # Atomic save
+ del self._storage
+
+ def copy(self):
+ """
+ Make a copy of storage contents
+
+ .. note:: this method performs a *deep* copy operation.
+
+ :return: a copy of storage contents
+ :rtype: dict
+ """
+ return deepcopy(self._storage)
+
+
+class Addon(object):
+ """
+ Base addon class
+
+ Provides access to basic addon parameters
+
+ :param id_: addon id, e.g. 'plugin.video.foo' (optional)
+ :type id_: str
+ """
+ def __init__(self, id_=''):
+ """
+ Class constructor
+ """
+ self._addon = xbmcaddon.Addon(id_)
+ self._configdir = xbmc.translatePath(self._addon.getAddonInfo('profile')).decode('utf-8')
+ self._ui_strings_map = None
+ if not os.path.exists(self._configdir):
+ os.mkdir(self._configdir)
+
+ def __getattr__(self, item):
+ """
+ Get addon setting as an Addon instance attribute
+
+ E.g. addon.my_setting is equal to addon.get_setting('my_setting')
+
+ :param item:
+ :type item: str
+ """
+ return self.get_setting(item)
+
+ def __str__(self):
+ return '<Addon [{0}]>'.format(self.id)
+
+ def __repr__(self):
+ return '<simpleplugin.Addon object [{0}]>'.format(self.id)
+
+ @property
+ def addon(self):
+ """
+ Kodi Addon instance that represents this Addon
+
+ :return: Addon instance
+ :rtype: xbmcaddon.Addon
+ """
+ return self._addon
+
+ @property
+ def id(self):
+ """
+ Addon ID
+
+ :return: Addon ID, e.g. 'plugin.video.foo'
+ :rtype: str
+ """
+ return self._addon.getAddonInfo('id')
+
+ @property
+ def path(self):
+ """
+ Addon path
+
+ :return: path to the addon folder
+ :rtype: str
+ """
+ return self._addon.getAddonInfo('path').decode('utf-8')
+
+ @property
+ def icon(self):
+ """
+ Addon icon
+
+ :return: path to the addon icon image
+ :rtype: str
+ """
+ icon = os.path.join(self.path, 'icon.png')
+ if os.path.exists(icon):
+ return icon
+ else:
+ return ''
+
+ @property
+ def fanart(self):
+ """
+ Addon fanart
+
+ :return: path to the addon fanart image
+ :rtype: str
+ """
+ fanart = os.path.join(self.path, 'fanart.jpg')
+ if os.path.exists(fanart):
+ return fanart
+ else:
+ return ''
+
+ @property
+ def config_dir(self):
+ """
+ Addon config dir
+
+ :return: path to the addon config dir
+ :rtype: str
+ """
+ return self._configdir
+
+ @property
+ def version(self):
+ """
+ Addon version
+
+ :return: addon version
+ :rtype: str
+ """
+ return self._addon.getAddonInfo('version')
+
+ def get_localized_string(self, id_):
+ """
+ Get localized UI string
+
+ :param id_: UI string ID
+ :type id_: int
+ :return: UI string in the current language
+ :rtype: str
+ """
+ return self._addon.getLocalizedString(id_).encode('utf-8')
+
+ def get_setting(self, id_, convert=True):
+ """
+ Get addon setting
+
+ If ``convert=True``, 'bool' settings are converted to Python :class:`bool` values,
+ and numeric strings to Python :class:`long` or :class:`float` depending on their format.
+
+ .. note:: Settings can also be read via :class:`Addon` instance poperties named as the respective settings.
+ I.e. ``addon.foo`` is equal to ``addon.get_setting('foo')``.
+
+ :param id_: setting ID
+ :type id_: str
+ :param convert: try to guess and convert the setting to an appropriate type
+ E.g. ``'1.0'`` will be converted to float ``1.0`` number, ``'true'`` to ``True`` and so on.
+ :type convert: bool
+ :return: setting value
+ """
+ setting = self._addon.getSetting(id_)
+ if convert:
+ if setting == 'true':
+ return True # Convert boolean strings to bool
+ elif setting == 'false':
+ return False
+ elif re.search(r'^-?\d+$', setting) is not None:
+ return long(setting) # Convert numeric strings to long
+ elif re.search(r'^-?\d+\.\d+$', setting) is not None:
+ return float(setting) # Convert numeric strings with a dot to float
+ return setting
+
+ def set_setting(self, id_, value):
+ """
+ Set addon setting
+
+ Python :class:`bool` type are converted to ``'true'`` or ``'false'``
+ Non-string/non-unicode values are converted to strings.
+
+ .. warning:: Setting values via :class:`Addon` instance properties is not supported!
+ Values can only be set using :meth:`Addon.set_setting` method.
+
+ :param id_: setting ID
+ :type id_: str
+ :param value: setting value
+ """
+ if isinstance(value, bool):
+ value = 'true' if value else 'false'
+ elif not isinstance(value, basestring):
+ value = str(value)
+ self._addon.setSetting(id_, value)
+
+ def log(self, message, level=xbmc.LOGDEBUG):
+ """
+ Add message to Kodi log starting with Addon ID
+
+ :param message: message to be written into the Kodi log
+ :type message: str
+ :param level: log level. :mod:`xbmc` module provides the necessary symbolic constants.
+ Default: ``xbmc.LOGDEBUG``
+ :type level: int
+ """
+ if isinstance(message, unicode):
+ message = message.encode('utf-8')
+ xbmc.log('{0} [v.{1}]: {2}'.format(self.id, self.version, message), level)
+
+ def log_notice(self, message):
+ """
+ Add NOTICE message to the Kodi log
+
+ :param message: message to write to the Kodi log
+ :type message: str
+ """
+ self.log(message, xbmc.LOGINFO)
+
+ def log_warning(self, message):
+ """
+ Add WARNING message to the Kodi log
+
+ :param message: message to write to the Kodi log
+ :type message: str
+ """
+ self.log(message, xbmc.LOGWARNING)
+
+ def log_error(self, message):
+ """
+ Add ERROR message to the Kodi log
+
+ :param message: message to write to the Kodi log
+ :type message: str
+ """
+ self.log(message, xbmc.LOGERROR)
+
+ def log_debug(self, message):
+ """
+ Add debug message to the Kodi log
+
+ :param message: message to write to the Kodi log
+ :type message: str
+ """
+ self.log(message, xbmc.LOGDEBUG)
+
+ def get_storage(self, filename='storage.pcl'):
+ """
+ Get a persistent :class:`Storage` instance for storing arbitrary values between addon calls.
+
+ A :class:`Storage` instance can be used as a context manager.
+
+ Example::
+
+ with plugin.get_storage() as storage:
+ storage['param1'] = value1
+ value2 = storage['param2']
+
+ .. note:: After exiting :keyword:`with` block a :class:`Storage` instance is invalidated.
+
+ :param filename: the name of a storage file (optional)
+ :type filename: str
+ :return: Storage object
+ :rtype: Storage
+ """
+ return Storage(self.config_dir, filename)
+
+ def cached(self, duration=10):
+ """
+ Cached decorator
+
+ Used to cache function return data
+
+ Usage::
+
+ @plugin.cached(30)
+ def my_func(*args, **kwargs):
+ # Do some stuff
+ return value
+
+ :param duration: caching duration in min (positive values only)
+ :type duration: int
+ :raises ValueError: if duration is zero or negative
+ """
+ def outer_wrapper(func):
+ @wraps(func)
+ def inner_wrapper(*args, **kwargs):
+ with self.get_storage('__cache__.pcl') as cache:
+ current_time = datetime.now()
+ key = func.__name__ + str(args) + str(kwargs)
+ try:
+ data, timestamp = cache[key]
+ if duration > 0 and current_time - timestamp > timedelta(minutes=duration):
+ raise KeyError
+ elif duration <= 0:
+ raise ValueError('Caching duration cannot be zero or negative!')
+ self.log_debug('Cache hit: {0}'.format(key))
+ except KeyError:
+ self.log_debug('Cache miss: {0}'.format(key))
+ data = func(*args, **kwargs)
+ cache[key] = (data, current_time)
+ return data
+ return inner_wrapper
+ return outer_wrapper
+
+ def gettext(self, ui_string):
+ """
+ Get a translated UI string from addon localization files.
+
+ This function emulates GNU Gettext for more convenient access
+ to localized addon UI strings. To reduce typing this method object
+ can be assigned to a ``_`` (single underscore) variable.
+
+ For using gettext emulation :meth:`Addon.initialize_gettext` method
+ needs to be called first. See documentation for that method for more info
+ about Gettext emulation.
+
+ :param ui_string: a UI string from English :file:`strings.po`.
+ :type ui_string: str
+ :return: a UI string from translated :file:`strings.po`.
+ :rtype: unicode
+ :raises simpleplugin.SimplePluginError: if :meth:`Addon.initialize_gettext` wasn't called first
+ or if a string is not found in English :file:`strings.po`.
+ """
+ if self._ui_strings_map is not None:
+ try:
+ return self.get_localized_string(self._ui_strings_map['strings'][ui_string])
+ except KeyError:
+ raise SimplePluginError('UI string "{0}" is not found in strings.po!'.format(ui_string))
+ else:
+ raise SimplePluginError('Addon localization is not initialized!')
+
+ def initialize_gettext(self):
+ """
+ Initialize GNU gettext emulation in addon
+
+ Kodi localization system for addons is not very convenient
+ because you need to operate with numeric string codes instead
+ of UI strings themselves which reduces code readability and
+ may lead to errors. The :class:`Addon` class provides facilities
+ for emulating GNU Gettext localization system.
+
+ This allows to use UI strings from addon's English :file:`strings.po`
+ file instead of numeric codes to return localized strings from
+ respective localized :file:`.po` files.
+
+ This method returns :meth:`Addon.gettext` method object that
+ can be assigned to a short alias to reduce typing. Traditionally,
+ ``_`` (a single underscore) is used for this purpose.
+
+ Example::
+
+ addon = simpleplugin.Addon()
+ _ = addon.initialize_gettext()
+
+ xbmcgui.Dialog().notification(_('Warning!'), _('Something happened'))
+
+ In the preceding example the notification strings will be replaced
+ with localized versions if these strings are translated.
+
+ :return: :meth:`Addon.gettext` method object
+ :raises simpleplugin.SimplePluginError: if the addon's English :file:`strings.po` file is missing
+ """
+ strings_po = os.path.join(self.path, 'resources', 'language', 'resource.language.en_gb', 'strings.po')
+ if os.path.exists(strings_po):
+ with open(strings_po, 'rb') as fo:
+ raw_strings = fo.read()
+ raw_strings_hash = md5(raw_strings).hexdigest()
+ gettext_pcl = '__gettext__.pcl'
+ with self.get_storage(gettext_pcl) as ui_strings_map:
+ if (not os.path.exists(os.path.join(self._configdir, gettext_pcl)) or
+ raw_strings_hash != ui_strings_map['hash']):
+ ui_strings = self._parse_po(raw_strings.split('\n'))
+ self._ui_strings_map = {
+ 'hash': raw_strings_hash,
+ 'strings': ui_strings
+ }
+ ui_strings_map['hash'] = raw_strings_hash
+ ui_strings_map['strings'] = ui_strings.copy()
+ else:
+ self._ui_strings_map = deepcopy(ui_strings_map)
+ else:
+ raise SimplePluginError('Unable to initialize localization because of missing English strings.po!')
+ return self.gettext
+
+ def _parse_po(self, strings):
+ """
+ Parses ``strings.po`` file into a dict of {'string': id} items.
+ """
+ ui_strings = {}
+ string_id = None
+ for string in strings:
+ if string_id is None and 'msgctxt' in string:
+ string_id = int(re.search(r'"#(\d+)"', string).group(1))
+ elif string_id is not None and 'msgid' in string:
+ ui_strings[re.search(r'"(.*?)"', string, re.U).group(1)] = string_id
+ string_id = None
+ return ui_strings
+
+
+class Plugin(Addon):
+ """
+ Plugin class
+
+ :param id_: plugin's id, e.g. 'plugin.video.foo' (optional)
+ :type id_: str
+
+ This class provides a simplified API to create virtual directories of playable items
+ for Kodi content plugins.
+ :class:`simpleplugin.Plugin` uses a concept of callable plugin actions (functions or methods)
+ that are defined using :meth:`Plugin.action` decorator.
+ A Plugin instance must have at least one action that is named ``'root'``.
+
+ Minimal example:
+
+ .. code-block:: python
+
+ from simpleplugin import Plugin
+
+ plugin = Plugin()
+
+ @plugin.action()
+ def root(params): # Mandatory item!
+ return [{'label': 'Foo',
+ 'url': plugin.get_url(action='some_action', param='Foo')},
+ {'label': 'Bar',
+ 'url': plugin.get_url(action='some_action', param='Bar')}]
+
+ @plugin.action()
+ def some_action(params):
+ return [{'label': params['param']}]
+
+ plugin.run()
+
+ An action callable receives 1 parameter -- params.
+ params is a dict-like object containing plugin call parameters (including action string)
+ The action callable can return
+ either a list/generator of dictionaries representing Kodi virtual directory items
+ or a resolved playable path (:class:`str` or :obj:`unicode`) for Kodi to play.
+
+ Example 1::
+
+ @plugin.action()
+ def list_action(params):
+ listing = get_listing(params) # Some external function to create listing
+ return listing
+
+ The ``listing`` variable is a Python list/generator of dict items.
+ Example 2::
+
+ @plugin.action()
+ def play_action(params):
+ path = get_path(params) # Some external function to get a playable path
+ return path
+
+ Each dict item can contain the following properties:
+
+ - label -- item's label (default: ``''``).
+ - label2 -- item's label2 (default: ``''``).
+ - thumb -- item's thumbnail (default: ``''``).
+ - icon -- item's icon (default: ``''``).
+ - path -- item's path (default: ``''``).
+ - fanart -- item's fanart (optional).
+ - art -- a dict containing all item's graphic (see :meth:`xbmcgui.ListItem.setArt` for more info) -- optional.
+ - stream_info -- a dictionary of ``{stream_type: {param: value}}`` items
+ (see :meth:`xbmcgui.ListItem.addStreamInfo`) -- optional.
+ - info -- a dictionary of ``{media: {param: value}}`` items
+ (see :meth:`xbmcgui.ListItem.setInfo`) -- optional
+ - context_menu - a list that contains 2-item tuples ``('Menu label', 'Action')``.
+ The items from the tuples are added to the item's context menu.
+ - url -- a callback URL for this list item.
+ - is_playable -- if ``True``, then this item is playable and must return a playable path or
+ be resolved via :meth:`Plugin.resolve_url` (default: ``False``).
+ - is_folder -- if ``True`` then the item will open a lower-level sub-listing. if ``False``,
+ the item either is a playable media or a general-purpose script
+ which neither creates a virtual folder nor points to a playable media (default: C{True}).
+ if ``'is_playable'`` is set to ``True``, then ``'is_folder'`` value automatically assumed to be ``False``.
+ - subtitles -- the list of paths to subtitle files (optional).
+ - mime -- item's mime type (optional).
+ - list_item -- an 'class:`xbmcgui.ListItem` instance (optional).
+ It is used when you want to set all list item properties by yourself.
+ If ``'list_item'`` property is present, all other properties,
+ except for ``'url'`` and ``'is_folder'``, are ignored.
+ - properties -- a dictionary of list item properties
+ (see :meth:`xbmcgui.ListItem.setProperty`) -- optional.
+
+ Example 3::
+
+ listing = [{ 'label': 'Label',
+ 'label2': 'Label 2',
+ 'thumb': 'thumb.png',
+ 'icon': 'icon.png',
+ 'fanart': 'fanart.jpg',
+ 'art': {'clearart': 'clearart.png'},
+ 'stream_info': {'video': {'codec': 'h264', 'duration': 1200},
+ 'audio': {'codec': 'ac3', 'language': 'en'}},
+ 'info': {'video': {'genre': 'Comedy', 'year': 2005}},
+ 'context_menu': [('Menu Item', 'Action')],
+ 'url': 'plugin:/plugin.video.test/?action=play',
+ 'is_playable': True,
+ 'is_folder': False,
+ 'subtitles': ['/path/to/subtitles.en.srt', '/path/to/subtitles.uk.srt'],
+ 'mime': 'video/mp4'
+ }]
+
+ Alternatively, an action callable can use :meth:`Plugin.create_listing` and :meth:`Plugin.resolve_url`
+ static methods to pass additional parameters to Kodi.
+
+ Example 4::
+
+ @plugin.action()
+ def list_action(params):
+ listing = get_listing(params) # Some external function to create listing
+ return Plugin.create_listing(listing, sort_methods=(2, 10, 17), view_mode=500)
+
+ Example 5::
+
+ @plugin.action()
+ def play_action(params):
+ path = get_path(params) # Some external function to get a playable path
+ return Plugin.resolve_url(path, succeeded=True)
+
+ If an action callable performs any actions other than creating a listing or
+ resolving a playable URL, it must return ``None``.
+ """
+ def __init__(self, id_=''):
+ """
+ Class constructor
+ """
+ super(Plugin, self).__init__(id_)
+ self._url = 'plugin://{0}/'.format(self.id)
+ self._handle = None
+ self.actions = {}
+
+ def __str__(self):
+ return '<Plugin {0}>'.format(sys.argv)
+
+ def __repr__(self):
+ return '<simpleplugin.Plugin object {0}>'.format(sys.argv)
+
+ @staticmethod
+ def get_params(paramstring):
+ """
+ Convert a URL-encoded paramstring to a Python dict
+
+ :param paramstring: URL-encoded paramstring
+ :type paramstring: str
+ :return: parsed paramstring
+ :rtype: Params
+ """
+ raw_params = parse_qs(paramstring)
+ params = Params()
+ for key, value in raw_params.iteritems():
+ params[key] = value[0] if len(value) == 1 else value
+ return params
+
+ def get_url(self, plugin_url='', **kwargs):
+ """
+ Construct a callable URL for a virtual directory item
+
+ If plugin_url is empty, a current plugin URL is used.
+ kwargs are converted to a URL-encoded string of plugin call parameters
+ To call a plugin action, 'action' parameter must be used,
+ if 'action' parameter is missing, then the plugin root action is called
+ If the action is not added to :class:`Plugin` actions, :class:`PluginError` will be raised.
+
+ :param plugin_url: plugin URL with trailing / (optional)
+ :type plugin_url: str
+ :param kwargs: pairs of key=value items
+ :return: a full plugin callback URL
+ :rtype: str
+ """
+ url = plugin_url or self._url
+ if kwargs:
+ return '{0}?{1}'.format(url, urlencode(kwargs, doseq=True))
+ return url
+
+ def action(self, name=None):
+ """
+ Action decorator
+
+ Defines plugin callback action. If action's name is not defined explicitly,
+ then the action is named after the decorated function.
+
+ .. warning:: Action's name must be unique.
+
+ A plugin must have at least one action named ``'root'`` implicitly or explicitly.
+
+ Example:
+
+ .. code-block:: python
+
+ @plugin.action() # The action is implicitly named 'root' after the decorated function
+ def root(params):
+ pass
+
+ @plugin.action('foo') # The action name is set explicitly
+ def foo_action(params):
+ pass
+
+ :param name: action's name (optional).
+ :type name: str
+ :raises simpleplugin.SimplePluginError: if the action with such name is already defined.
+ """
+ def wrap(func, name=name):
+ if name is None:
+ name = func.__name__
+ if name in self.actions:
+ raise SimplePluginError('Action "{0}" already defined!'.format(name))
+ self.actions[name] = func
+ return func
+ return wrap
+
+ def run(self, category=''):
+ """
+ Run plugin
+
+ :param category: str - plugin sub-category, e.g. 'Comedy'.
+ See :func:`xbmcplugin.setPluginCategory` for more info.
+ :type category: str
+ :raises simpleplugin.SimplePluginError: if unknown action string is provided.
+ """
+ self._handle = int(sys.argv[1])
+ if category:
+ xbmcplugin.setPluginCategory(self._handle, category)
+ params = self.get_params(sys.argv[2][1:])
+ action = params.get('action', 'root')
+ self.log_debug(str(self))
+ self.log_debug('Actions: {0}'.format(str(self.actions.keys())))
+ self.log_debug('Called action "{0}" with params "{1}"'.format(action, str(params)))
+ try:
+ action_callable = self.actions[action]
+ except KeyError:
+ raise SimplePluginError('Invalid action: "{0}"!'.format(action))
+ else:
+ result = action_callable(params)
+ self.log_debug('Action return value: {0}'.format(str(result)))
+ if isinstance(result, (list, GeneratorType)):
+ self._add_directory_items(self.create_listing(result))
+ elif isinstance(result, basestring):
+ self._set_resolved_url(self.resolve_url(result))
+ elif isinstance(result, tuple) and hasattr(result, 'listing'):
+ self._add_directory_items(result)
+ elif isinstance(result, tuple) and hasattr(result, 'path'):
+ self._set_resolved_url(result)
+ else:
+ self.log_debug('The action "{0}" has not returned any valid data to process.'.format(action))
+
+ @staticmethod
+ def create_listing(listing, succeeded=True, update_listing=False, cache_to_disk=False, sort_methods=None,
+ view_mode=None, content=None):
+ """
+ Create and return a context dict for a virtual folder listing
+
+ :param listing: the list of the plugin virtual folder items
+ :type listing: :class:`list` or :class:`types.GeneratorType`
+ :param succeeded: if ``False`` Kodi won't open a new listing and stays on the current level.
+ :type succeeded: bool
+ :param update_listing: if ``True``, Kodi won't open a sub-listing but refresh the current one.
+ :type update_listing: bool
+ :param cache_to_disk: cache this view to disk.
+ :type cache_to_disk: bool
+ :param sort_methods: the list of integer constants representing virtual folder sort methods.
+ :type sort_methods: tuple
+ :param view_mode: a numeric code for a skin view mode.
+ View mode codes are different in different skins except for ``50`` (basic listing).
+ :type view_mode: int
+ :param content: string - current plugin content, e.g. 'movies' or 'episodes'.
+ See :func:`xbmcplugin.setContent` for more info.
+ :type content: str
+ :return: context object containing necessary parameters
+ to create virtual folder listing in Kodi UI.
+ :rtype: ListContext
+ """
+ return ListContext(listing, succeeded, update_listing, cache_to_disk, sort_methods, view_mode, content)
+
+ @staticmethod
+ def resolve_url(path='', play_item=None, succeeded=True):
+ """
+ Create and return a context dict to resolve a playable URL
+
+ :param path: the path to a playable media.
+ :type path: str or unicode
+ :param play_item: a dict of item properties as described in the class docstring.
+ It allows to set additional properties for the item being played, like graphics, metadata etc.
+ if ``play_item`` parameter is present, then ``path`` value is ignored, and the path must be set via
+ ``'path'`` property of a ``play_item`` dict.
+ :type play_item: dict
+ :param succeeded: if ``False``, Kodi won't play anything
+ :type succeeded: bool
+ :return: context object containing necessary parameters
+ for Kodi to play the selected media.
+ :rtype: PlayContext
+ """
+ return PlayContext(path, play_item, succeeded)
+
+ @staticmethod
+ def create_list_item(item):
+ """
+ Create an :class:`xbmcgui.ListItem` instance from an item dict
+
+ :param item: a dict of ListItem properties
+ :type item: dict
+ :return: ListItem instance
+ :rtype: xbmcgui.ListItem
+ """
+ list_item = xbmcgui.ListItem(label=item.get('label', ''),
+ label2=item.get('label2', ''),
+ path=item.get('path', ''))
+ if int(xbmc.getInfoLabel('System.BuildVersion')[:2]) >= 16:
+ art = item.get('art', {})
+ art['thumb'] = item.get('thumb', '')
+ art['icon'] = item.get('icon', '')
+ art['fanart'] = item.get('fanart', '')
+ item['art'] = art
+ else:
+ list_item.setThumbnailImage(item.get('thumb', ''))
+ list_item.setIconImage(item.get('icon', ''))
+ list_item.setProperty('fanart_image', item.get('fanart', ''))
+ if item.get('art'):
+ list_item.setArt(item['art'])
+ if item.get('stream_info'):
+ for stream, stream_info in item['stream_info'].iteritems():
+ list_item.addStreamInfo(stream, stream_info)
+ if item.get('info'):
+ for media, info in item['info'].iteritems():
+ list_item.setInfo(media, info)
+ if item.get('context_menu') is not None:
+ list_item.addContextMenuItems(item['context_menu'])
+ if item.get('subtitles'):
+ list_item.setSubtitles(item['subtitles'])
+ if item.get('mime'):
+ list_item.setMimeType(item['mime'])
+ if item.get('properties'):
+ for key, value in item['properties'].iteritems():
+ list_item.setProperty(key, value)
+ return list_item
+
+ def _add_directory_items(self, context):
+ """
+ Create a virtual folder listing
+
+ :param context: context object
+ :type context: ListContext
+ """
+ self.log_debug('Creating listing from {0}'.format(str(context)))
+ if context.content is not None:
+ xbmcplugin.setContent(self._handle, context.content) # This must be at the beginning
+ for item in context.listing:
+ is_folder = item.get('is_folder', True)
+ if item.get('list_item') is not None:
+ list_item = item['list_item']
+ else:
+ list_item = self.create_list_item(item)
+ if item.get('is_playable'):
+ list_item.setProperty('IsPlayable', 'true')
+ is_folder = False
+ xbmcplugin.addDirectoryItem(self._handle, item['url'], list_item, is_folder)
+ if context.sort_methods is not None:
+ [xbmcplugin.addSortMethod(self._handle, method) for method in context.sort_methods]
+ xbmcplugin.endOfDirectory(self._handle,
+ context.succeeded,
+ context.update_listing,
+ context.cache_to_disk)
+ if context.view_mode is not None:
+ xbmc.executebuiltin('Container.SetViewMode({0})'.format(context.view_mode))
+
+ def _set_resolved_url(self, context):
+ """
+ Resolve a playable URL
+
+ :param context: context object
+ :type context: PlayContext
+ """
+ self.log_debug('Resolving URL from {0}'.format(str(context)))
+ if context.play_item is None:
+ list_item = xbmcgui.ListItem(path=context.path)
+ else:
+ list_item = self.create_list_item(context.play_item)
+ xbmcplugin.setResolvedUrl(self._handle, context.succeeded, list_item) \ No newline at end of file
diff --git a/plugin.video.catchuptvandmore/resources/lib/skeleton.py b/plugin.video.catchuptvandmore/resources/lib/skeleton.py
new file mode 100755
index 0000000..cf1c510
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/skeleton.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Copyright (C) 2016 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+
+categories = {
+ 'main_menu.fr': 'French channels'
+# 'main_menu.be': 'Belgian channels'
+}
+
+channels = {
+ 'main_menu.fr': {
+ 'channels.fr.tf1.tf1': 'TF1',
+ 'channels.fr.pluzz.france2': 'France 2',
+ 'channels.fr.pluzz.france3': 'France 3',
+ 'channels.fr.canalplus.canalplus': 'Canal +',
+ 'channels.fr.pluzz.france5': 'France 5',
+ 'channels.fr.6play.m6': 'M6',
+ 'channels.fr.arte.arte': 'Arte',
+ 'channels.fr.c.c8': 'C8',
+ 'channels.fr.6play.w9': 'W9',
+ 'channels.fr.tf1.tmc': 'TMC',
+ 'channels.fr.tf1.nt1': 'NT1',
+ 'channels.fr.6play.nrj12': 'NRJ 12',
+ 'channels.fr.pluzz.france4': 'France 4',
+ 'channels.fr.bfmtv.bfmtv': 'BFM TV',
+ 'channels.fr.itele.itele': 'i-Télé',
+ 'channels.fr.c.cstar': 'CStar',
+ 'channels.fr.gulli.gulli': 'Gulli'
+
+ }
+
+ # 'main_menu.be': {
+ # 'channels.be.rtbf.rtbf': 'RTBF'
+ # }
+}
diff --git a/plugin.video.catchuptvandmore/resources/lib/utils.py b/plugin.video.catchuptvandmore/resources/lib/utils.py
new file mode 100755
index 0000000..7545aa4
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/lib/utils.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+"""
+ Catch-up TV & More
+ Original work (C) JUL1EN094, SPM, SylvainCecchetto
+ Copyright (C) 2016 SylvainCecchetto
+
+ This file is part of Catch-up TV & More.
+
+ 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.
+"""
+
+import os
+import time
+import requests
+from random import randint
+from resources.lib import common
+
+
+user_data = common.sp.xbmc.translatePath(
+ os.path.join(
+ 'special://profile/addon_data',
+ common.addon.id))
+
+cache_path = common.sp.xbmc.translatePath(
+ os.path.join(
+ user_data,
+ 'cache'))
+
+default_ua = "Mozilla/5.0 (Windows NT 6.1; WOW64) " \
+ "AppleWebKit/537.36 (KHTML, like Gecko) " \
+ "Chrome/55.0.2883.87 Safari/537.36"
+
+user_agents = [
+ 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36'
+ ' (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',
+ 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14'
+ ' (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A',
+ 'Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.14'
+ ' (KHTML, like Gecko) Version/10.0.1 Safari/602.2.14',
+ 'Mozilla/5.0 (Windows NT 6.1; WOW64) '
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
+ 'Chrome/55.0.2883.87 Safari/537.36'
+]
+
+
+def get_random_ua_hdr():
+ ua = user_agents[randint(0, len(user_agents) - 1)]
+ return {
+ 'User-Agent': ua
+ }
+
+
+def download_catalog(
+ url,
+ file_name,
+ force_dl=False,
+ request_type='get',
+ post_dic={},
+ random_ua=False,
+ specific_headers={},
+ params={}):
+ file_name = format_filename(file_name)
+ common.addon.log('URL download_catalog : ' + url)
+
+ if not os.path.exists(cache_path):
+ os.makedirs(cache_path, mode=0777)
+ file_path = os.path.join(cache_path, file_name)
+
+ if os.path.exists(file_path):
+ mtime = os.stat(file_path).st_mtime
+ dl_file = (time.time() - mtime > 60)
+ else:
+ dl_file = True
+ if dl_file or force_dl:
+ if random_ua:
+ ua = user_agents[randint(0, len(user_agents) - 1)]
+ else:
+ ua = default_ua
+
+ if specific_headers:
+ headers = specific_headers
+ if 'User-Agent' not in headers:
+ headers['User-Agent'] = ua
+ else:
+ headers = {'User-Agent': ua}
+
+ if request_type == 'get':
+ r = requests.get(url, headers=headers, params=params)
+
+ elif request_type == 'post':
+ r = requests.get(
+ url, headers=headers, data=post_dic, params=params)
+
+ with open(file_path, 'wb') as f:
+ f.write(r.content)
+
+ return file_path
+
+
+def format_filename(filename):
+ keepcharacters = ('_', '.')
+ return "".join(
+ c for c in filename if c.isalnum() or c in keepcharacters).rstrip()
+
+
+def get_webcontent(
+ url,
+ request_type='get',
+ post_dic={},
+ random_ua=False,
+ specific_headers={},
+ params={}):
+ common.addon.log('URL get_webcontent : ' + url)
+ if random_ua:
+ ua = user_agents[randint(0, len(user_agents) - 1)]
+ else:
+ ua = default_ua
+
+ if specific_headers:
+ headers = specific_headers
+ if 'User-Agent' not in headers:
+ headers['User-Agent'] = ua
+ else:
+ headers = {'User-Agent': ua}
+
+ if request_type == 'get':
+ r = requests.get(url, headers=headers, params=params)
+
+ elif request_type == 'post':
+ r = requests.get(url, headers=headers, data=post_dic, params=params)
+
+ return r.content
+
+
+def get_redirected_url(
+ url,
+ random_ua=False,
+ specific_headers={}):
+ if random_ua:
+ ua = user_agents[randint(0, len(user_agents) - 1)]
+ else:
+ ua = default_ua
+
+ if specific_headers:
+ headers = specific_headers
+ if 'User-Agent' not in headers:
+ headers['User-Agent'] = ua
+ else:
+ headers = {'User-Agent': ua}
+
+ r = requests.head(url, allow_redirects=True, headers=headers)
+ return r.url
+
+
+def send_notification(
+ message, title=common.plugin_name, time=5000, sound=True):
+ common.sp.xbmcgui.Dialog().notification(
+ title, message, common.addon.icon, time)
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/arte.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/arte.png
new file mode 100755
index 0000000..3c003a2
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/arte.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/bfmtv.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/bfmtv.png
new file mode 100755
index 0000000..3ca5f2a
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/bfmtv.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/bfmtv_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/bfmtv_fanart.png
new file mode 100755
index 0000000..226487f
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/bfmtv_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/c8.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/c8.png
new file mode 100755
index 0000000..e18d86c
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/c8.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/c8_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/c8_fanart.png
new file mode 100755
index 0000000..05a9a6c
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/c8_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/canalplus.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/canalplus.png
new file mode 100755
index 0000000..6829f23
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/canalplus.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/canalplus_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/canalplus_fanart.png
new file mode 100755
index 0000000..2a2e3b4
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/canalplus_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/cstar.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/cstar.png
new file mode 100755
index 0000000..4d3a463
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/cstar.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/cstar_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/cstar_fanart.png
new file mode 100755
index 0000000..f7c45db
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/cstar_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/france2.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/france2.png
new file mode 100755
index 0000000..6f741cd
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/france2.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/france2_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/france2_fanart.png
new file mode 100755
index 0000000..8184c2a
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/france2_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/france3.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/france3.png
new file mode 100755
index 0000000..3309012
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/france3.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/france3_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/france3_fanart.png
new file mode 100755
index 0000000..dede4f8
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/france3_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/france4.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/france4.png
new file mode 100755
index 0000000..a8a1bba
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/france4.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/france5.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/france5.png
new file mode 100755
index 0000000..bc1f6ea
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/france5.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/france5_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/france5_fanart.png
new file mode 100755
index 0000000..3380814
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/france5_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/gulli.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/gulli.png
new file mode 100755
index 0000000..b2e1522
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/gulli.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/gulli_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/gulli_fanart.png
new file mode 100755
index 0000000..94739ab
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/gulli_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/itele.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/itele.png
new file mode 100755
index 0000000..2e1d937
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/itele.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/itele_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/itele_fanart.png
new file mode 100755
index 0000000..1bca166
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/itele_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/m6.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/m6.png
new file mode 100755
index 0000000..24852b7
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/m6.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/m6_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/m6_fanart.png
new file mode 100755
index 0000000..f5e8e8e
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/m6_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/nrj12.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/nrj12.png
new file mode 100755
index 0000000..0c4c9f4
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/nrj12.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/nrj12_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/nrj12_fanart.png
new file mode 100755
index 0000000..848d9c6
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/nrj12_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/nt1.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/nt1.png
new file mode 100755
index 0000000..59c8e33
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/nt1.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/nt1_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/nt1_fanart.png
new file mode 100755
index 0000000..bc00729
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/nt1_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/tf1.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/tf1.png
new file mode 100755
index 0000000..18b7490
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/tf1.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/tf1_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/tf1_fanart.png
new file mode 100755
index 0000000..e23d730
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/tf1_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/tmc.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/tmc.png
new file mode 100755
index 0000000..2ad9872
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/tmc.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/tmc_fanart.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/tmc_fanart.png
new file mode 100755
index 0000000..de8fef7
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/tmc_fanart.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/media/channels/fr/w9.png b/plugin.video.catchuptvandmore/resources/media/channels/fr/w9.png
new file mode 100755
index 0000000..a0c27c4
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/media/channels/fr/w9.png
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/screenshots/screenshot-01.jpg b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-01.jpg
new file mode 100755
index 0000000..11c6c33
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-01.jpg
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/screenshots/screenshot-02.jpg b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-02.jpg
new file mode 100755
index 0000000..da9e325
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-02.jpg
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/screenshots/screenshot-03.jpg b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-03.jpg
new file mode 100755
index 0000000..a5c6b9c
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-03.jpg
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/screenshots/screenshot-04.jpg b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-04.jpg
new file mode 100755
index 0000000..05ee326
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-04.jpg
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/screenshots/screenshot-05.jpg b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-05.jpg
new file mode 100755
index 0000000..f7d309f
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/screenshots/screenshot-05.jpg
Binary files differ
diff --git a/plugin.video.catchuptvandmore/resources/settings.xml b/plugin.video.catchuptvandmore/resources/settings.xml
new file mode 100755
index 0000000..31e8a65
--- /dev/null
+++ b/plugin.video.catchuptvandmore/resources/settings.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<settings>
+
+ <!-- Main menu -->
+ <category label="30000">
+ <!-- Hide main menu categories -->
+ <setting label="30010" type="lsep"/>
+ <setting label="30020" type="bool" id="main_menu.fr" default="true"/>
+<!-- <setting label="30021" type="bool" id="main_menu.be" default="true"/> -->
+ </category>
+
+
+ <!-- Channels -->
+ <category label="30001">
+ <!-- Hide channels from categories -->
+ <setting label="30011" type="lsep"/>
+ <setting label="30020" type="lsep" subsetting="true" visible="eq(1,true)"/>
+ <setting label="30020" type="bool" id="main_menu.fr" default="true" visible="false"/>
+
+ <setting label="TF1" type="bool" id="channels.fr.tf1.tf1" default="true" visible="eq(-1,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.tf1.tf1.order" default="1"/>
+
+ <setting label="France 2" type="bool" id="channels.fr.pluzz.france2" default="true" visible="eq(-3,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.pluzz.france2.order" default="2"/>
+
+ <setting label="France 3" type="bool" id="channels.fr.pluzz.france3" default="true" visible="eq(-5,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.pluzz.france3.order" default="3"/>
+
+ <setting label="Canal +" type="bool" id="channels.fr.canalplus.canalplus" default="true" visible="eq(-7,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.canalplus.canalplus.order" default="4"/>
+
+ <setting label="France 5" type="bool" id="channels.fr.pluzz.france5" default="true" visible="eq(-9,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.pluzz.france5.order" default="5"/>
+
+ <setting label="M6" type="bool" id="channels.fr.6play.m6" default="true" visible="eq(-11,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.6play.m6.order" default="6"/>
+
+ <setting label="Arte" type="bool" id="channels.fr.arte.arte" default="true" visible="eq(-13,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.arte.arte.order" default="7"/>
+
+ <setting label="C8" type="bool" id="channels.fr.c.c8" default="true" visible="eq(-15,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.c.c8.order" default="8"/>
+
+ <setting label="W9" type="bool" id="channels.fr.6play.w9" default="true" visible="eq(-17,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.6play.w9.order" default="9"/>
+
+ <setting label="TMC" type="bool" id="channels.fr.tf1.tmc" default="true" visible="eq(-19,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.tf1.tmc.order" default="10"/>
+
+ <setting label="NT1" type="bool" id="channels.fr.tf1.nt1" default="true" visible="eq(-21,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.tf1.nt1.order" default="11"/>
+
+ <setting label="NRJ 12" type="bool" id="channels.fr.6play.nrj12" default="true" visible="eq(-23,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.6play.nrj12.order" default="12"/>
+
+ <setting label="France 4" type="bool" id="channels.fr.pluzz.france4" default="true" visible="eq(-25,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.pluzz.france4.order" default="14"/>
+
+ <setting label="BFM TV" type="bool" id="channels.fr.bfmtv.bfmtv" default="true" visible="eq(-27,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.bfmtv.bfmtv.order" default="15"/>
+
+ <setting label="i-Télé" type="bool" id="channels.fr.itele.itele" default="true" visible="eq(-29,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.itele.itele.order" default="16"/>
+
+ <setting label="CStar" type="bool" id="channels.fr.c.cstar" default="true" visible="eq(-31,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.c.cstar.order" default="17"/>
+
+ <setting label="Gulli" type="bool" id="channels.fr.gulli.gulli" default="true" visible="eq(-33,true)"/>
+ <setting label="" type="number" visible="false" id="channels.fr.gulli.gulli.order" default="18"/>
+
+<!-- <setting label="30021" type="lsep" subsetting="true" visible="eq(1,true)"/>
+ <setting label="30021" type="bool" id="main_menu.be" default="true" visible="false"/>
+ <setting label="RTBF" type="bool" id="channels.be.rtbf.rtbf" default="true" visible="eq(-1,true)"/> -->
+
+ </category>
+
+
+ <!-- Quality and content (and also hidden settings) -->
+ <category label="30002">
+
+ <setting label="30020" type="lsep" subsetting="true" visible="eq(1,true)"/>
+ <setting label="30020" type="bool" id="main_menu.fr" default="true" visible="false"/>
+
+ <setting label="TF1" type="bool" id="channels.fr.tf1.tf1" default="true" visible="false"/>
+ <setting label="30070" type="bool" id="channels.fr.tf1.tf1.bonus" default="true" visible="eq(-1,true) + eq(-2,true)"/>
+
+ <setting label="France 2" type="bool" id="channels.fr.pluzz.france2" default="true" visible="false"/>
+ <setting label="30071" type="labelenum" id="channels.fr.pluzz.france2.quality" visible="eq(-1,true) + eq(-4,true)" values="HD|SD"/>
+
+ <setting label="France 3" type="bool" id="channels.fr.pluzz.france3" default="true" visible="false"/>
+ <setting label="30072" type="labelenum" id="channels.fr.pluzz.france3.quality" visible="eq(-1,true) + eq(-6,true)" values="HD|SD"/>
+
+ <setting label="France 4" type="bool" id="channels.fr.pluzz.france4" default="true" visible="false"/>
+ <setting label="30073" type="labelenum" id="channels.fr.pluzz.france4.quality" visible="eq(-1,true) + eq(-8,true)" values="HD|SD"/>
+
+ <setting label="BFM TV" type="bool" id="channels.fr.bfmtv.bfmtv" default="true" visible="false"/>
+ <setting label="30081" type="labelenum" id="channels.fr.bfmtv.bfmtv.quality" visible="eq(-1,true) + eq(-10,true)" values="HD+|HD|SD|SD-"/>
+
+ <setting label="France 5" type="bool" id="channels.fr.pluzz.france5" default="true" visible="false"/>
+ <setting label="30074" type="labelenum" id="channels.fr.pluzz.france5.quality" visible="eq(-1,true) + eq(-12,true)" values="HD|SD"/>
+
+ <setting label="France Ô" type="bool" id="channels.fr.pluzz.franceo" default="true" visible="false"/>
+ <setting label="30075" type="labelenum" id="channels.fr.pluzz.franceo.quality" visible="eq(-1,true) + eq(-14,true)" values="HD|SD"/>
+
+ <setting label="M6" type="bool" id="channels.fr.6play.m6" default="true" visible="false"/>
+ <setting label="30076" type="labelenum" id="channels.fr.6play.m6.quality" visible="eq(-1,true) + eq(-16,true)" values="Auto|Force HD|Force SD"/>
+
+ <setting label="Arte" type="bool" id="channels.fr.arte.arte" default="true" visible="false"/>
+ <setting label="30079" type="labelenum" id="channels.fr.arte.arte.quality" visible="eq(-1,true) + eq(-18,true)" values="Auto|HD+|HD|SD|SD-"/>
+ <setting label="30080" type="labelenum" id="channels.fr.arte.arte.language" visible="eq(-2,true) + eq(-19,true)" values="Auto|fr|de|en|es|pl"/>
+
+ <setting label="W9" type="bool" id="channels.fr.6play.w9" default="true" visible="false"/>
+ <setting label="30077" type="labelenum" id="channels.fr.6play.W9.quality" visible="eq(-1,true) + eq(-21,true)" values="Auto|Force HD|Force SD"/>
+
+ <setting label="6ter" type="bool" id="channels.fr.6play.6ter" default="true" visible="false"/>
+ <setting label="30078" type="labelenum" id="channels.fr.6play.6ter.quality" visible="eq(-1,true) + eq(-23,true)" values="Auto|Force HD|Force SD"/>
+
+ <setting label="" type="bool" id="show_hidden_items_information" default="true" visible="false"/>
+ </category>
+
+
+</settings> \ No newline at end of file