diff --git a/.gitignore b/.gitignore
index 5ff7483..b0e384c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,8 +73,6 @@ dist/
downloads/
eggs/
.eggs/
-lib/
-lib64/
parts/
sdist/
var/
diff --git a/addon.py b/addon.py
new file mode 100644
index 0000000..978a25d
--- /dev/null
+++ b/addon.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from codequick import Route, Resolver, Listitem, utils, run # @UnresolvedImport
+from codequick.storage import PersistentList # @UnresolvedImport
+import os.path
+import xbmcgui, xbmc
+from resources.lib.searchers import rutor
+from resources.lib.utils import localize, store_torrent_file, get_engine
+from resources.lib.overrrides.session import torrent_file_fetch
+from resources.lib.player import PlaybackWindow, VideoLoop
+
+video_extensions = ('.mp4', '.avi', '.3gp', '.ogv', '.mkv', '.ts', '.mpg', '.mpeg', '.webm', '.flv', '.vob')
+
+search_engines = [
+ rutor.SearchEngine
+ ]
+
+ROOT = os.path.abspath(os.path.dirname(__file__))
+ICON_DIR = os.path.join(ROOT, 'resources', 'icons')
+
+@Route.register
+def root(plugin, content_type='video'):
+ @Route.register_delayed
+ def set_view():
+ xbmc.executebuiltin('Container.SetViewMode("500")')
+ def build_item(a):
+ cb, name, image = a
+ i = Listitem.from_dict(cb, name)
+ i.art.local_thumb(image)
+ return i
+ return map(build_item, (
+ (search_history, localize(33052), 'history2.png'),
+ (new_search, localize(33053), 'search.png')
+ ))
+@Route.register
+def search_history(plugin):
+ @Route.register_delayed
+ def set_view():
+ xbmc.executebuiltin('Container.SetViewMode("51")')
+ h_list = PersistentList('search_history')
+ for i in h_list:
+ item = Listitem.from_dict(search, i, params={'search_query': i})
+ item.art.local_thumb('search.png')
+ yield item
+ else:
+ yield None
+@Route.register
+def new_search(plugin):
+ q = utils.keyboard('Поиск')
+ if not q:
+ return False
+ return search(plugin, q)
+@Route.register
+def search(plugin, search_query, thumb=None):
+ @Route.register_delayed
+ def set_view():
+ xbmc.executebuiltin('Container.SetViewMode("51")')
+ with PersistentList('search_history') as h_list:
+ if search_query in h_list:
+ del h_list[h_list.index(search_query)]
+ h_list.insert(0, search_query)
+ progress = xbmcgui.DialogProgress()
+ found_items = []
+ progress.create(localize(33054))
+ for p, se in zip(range(0, 100, 100 / len(search_engines)), search_engines):
+ progress.update(p, line1=se.name)
+ found_items.extend(se().search(search_query))
+ res_items = []
+ for i in sorted(found_items, key=lambda x: x.seeders, reverse=True):
+ if '2160p' in i.title: hd = '[2160p/{}] '.format(i.size)
+ elif '1080p' in i.title: hd = '[1080p/{}] '.format(i.size)
+ elif '720p' in i.title: hd = '[720p/{}] '.format(i.size)
+ else: hd = ''
+ item = Listitem.from_dict(
+ open_torrent,
+ '{}{} {} ({}/{})'.format(hd, i.title, i.size, i.seeders, i.leachers),
+ params={'url': i.url, 'cookies': i.cookies, 'referer': i.referer}
+ )
+ if thumb:
+ item.art['thumb'] = thumb
+ else:
+ item.art.local_thumb(i.icon)
+ res_items.append(item)
+ progress.close()
+ return res_items
+@Route.register
+def open_torrent(plugin, url='--back--', cookies={}, referer='', path=''):
+ if url == '--back--':
+ xbmc.executebuiltin('ActivateWindow("home")')
+ yield False
+ else:
+ @Route.register_delayed
+ def set_view():
+ xbmc.executebuiltin('Container.SetViewMode("51")')
+ tf = torrent_file_fetch(url, referer, cookies)
+ if not tf: yield False
+ t_full_path = store_torrent_file(tf)
+ e = get_engine(t_full_path)
+ files = sorted(list(filter(lambda x: x.name.decode('utf-8').startswith(path) and x.name.decode('utf-8').lower().endswith(video_extensions), e.list_from_info(media_types=['video']))),
+ key=lambda x: x.name)
+ dirs = list(set(list(map( lambda x: x.name.decode('utf-8')[len(path):].lstrip('/').split('/')[0], filter( lambda x: len(x.name.decode('utf-8')[len(path):].lstrip('/').split('/')) > 1, files ) ))))
+ for d in sorted(dirs):
+ item = Listitem.from_dict(open_torrent,
+ d,
+ params={'url': url, 'cookies': cookies, 'referer': referer, 'path': '{}/{}'.format(
+ path, d
+ ) if path != '' else d}
+ )
+ item.art.local_thumb('folder.png')
+ yield item
+ for i in range(len(files)):
+ f = files[i]
+ p = f.name.decode('utf-8')[len(path):].lstrip('/').split('/')
+ if len(p) == 1:
+ item = Listitem.from_dict(play_file,
+ '{} ({:.3f} GB)'.format(p[0], f.size / 1024.0 / 1024.0 /1024.0),
+ params={'t_full_path': t_full_path, 'f_index': i}
+ )
+ item.art.local_thumb('video.png')
+ yield item
+ else:
+ yield None
+@Route.register
+def play_file(plugin, t_full_path, f_index):
+ vl = VideoLoop(t_full_path)
+ vl.start(f_index)
+ return False
+if __name__ == '__main__':
+ run()
diff --git a/addon.xml b/addon.xml
new file mode 100644
index 0000000..2bbfaa3
--- /dev/null
+++ b/addon.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+ audio video
+
+
+ Plugin helps you to watch videos from p2p torrent-networks, without full predownload.
+ Plugin helps you to watch videos from p2p torrent-networks, without full predownload.
+ MIT
+
+ http://xbmc.ru/forum/showthread.php?t=14787
+ http://xbmc.ru/forum/showthread.php?t=14787
+ roman@ukamnya.ru
+ all
+ en_GB ru_RU
+
+ resources/icon.png
+ resources/fanart.jpg
+
+
+ Initial release
+ true
+
+
\ No newline at end of file
diff --git a/resources/__init__.py b/resources/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po
new file mode 100644
index 0000000..683a107
--- /dev/null
+++ b/resources/language/resource.language.en_gb/strings.po
@@ -0,0 +1,61 @@
+# Kodi Media Center language file
+# Addon Name: Torrenter3
+# Addon id: plugin.video.torrenter3
+# Addon Provider: inpos
+msgid ""
+msgstr ""
+"Project-Id-Version: Kodi Addons\n"
+"Report-Msgid-Bugs-To: roman@ukamnya.ru\n"
+"POT-Creation-Date: 2019-05-04 09:00+MSK\n"
+"PO-Revision-Date: 2019-05-04 12:00+MSK\n"
+"Last-Translator: Inpos\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en_GB\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "#33000"
+msgid "Torrenter3"
+msgstr "Torrenter3"
+
+msgctxt "#33001"
+msgid "General"
+msgstr "General"
+
+msgctxt "#33011"
+msgid "Storage folder"
+msgstr "Storage folder"
+
+msgctxt "#33012"
+msgid "Use SOCKS proxy"
+msgstr "Use SOCKS proxy"
+
+msgctxt "#33013"
+msgid "SOCKS IP address"
+msgstr "SOCKS IP addres"
+
+msgctxt "#33014"
+msgid "SOCKS port"
+msgstr "SOCKS port"
+
+msgctxt "#33051"
+msgid "Please specify storage folder in Settings!"
+msgstr "Please specify storage folder in Settings"
+
+msgctxt "#33052"
+msgid "Search history"
+msgstr "Search history"
+
+msgctxt "#33053"
+msgid "Search"
+msgstr "Search"
+
+msgctxt "#33054"
+msgid "Search on trackers"
+msgstr "Search on trackers"
+
+msgctxt "#33055"
+msgid "Waiting for download"
+msgstr "Waiting for download"
+
diff --git a/resources/language/resource.language.ru_ru/strings.po b/resources/language/resource.language.ru_ru/strings.po
new file mode 100644
index 0000000..d8d019f
--- /dev/null
+++ b/resources/language/resource.language.ru_ru/strings.po
@@ -0,0 +1,60 @@
+# Kodi Media Center language file
+# Addon Name: Torrenter3
+# Addon id: plugin.video.torrenter3
+# Addon Provider: inpos
+msgid ""
+msgstr ""
+"Project-Id-Version: Kodi Addons\n"
+"Report-Msgid-Bugs-To: roman@ukamnya.ru\n"
+"POT-Creation-Date: 2019-05-04 09:00+MSK\n"
+"PO-Revision-Date: 2019-05-04 12:00+MSK\n"
+"Last-Translator: Inpos\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru_RU\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+
+msgctxt "#33000"
+msgid "Torrenter3"
+msgstr "Torrenter3"
+
+msgctxt "#33001"
+msgid "General"
+msgstr "Основные"
+
+msgctxt "#33011"
+msgid "Storage folder"
+msgstr "Папка хранения"
+
+msgctxt "#33012"
+msgid "Use SOCKS proxy"
+msgstr "Использовать SOCKS-прокси"
+
+msgctxt "#33013"
+msgid "SOCKS IP address"
+msgstr "Адрес IP SOCKS"
+
+msgctxt "#33014"
+msgid "SOCKS port"
+msgstr "Порт SOCKS"
+
+msgctxt "#33051"
+msgid "Please specify storage folder in Settings!"
+msgstr "Укажите папку хранения"
+
+msgctxt "#33052"
+msgid "Search history"
+msgstr "История поиска"
+
+msgctxt "#33053"
+msgid "Search"
+msgstr "Поиск"
+
+msgctxt "#33054"
+msgid "Search on trackers"
+msgstr "Поиск на трекерах"
+
+msgctxt "#33055"
+msgid "Waiting for download"
+msgstr "Ожидаем начало загрузки"
diff --git a/resources/lib/__init__.py b/resources/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resources/lib/overrrides/__init__.py b/resources/lib/overrrides/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resources/lib/overrrides/session.py b/resources/lib/overrrides/session.py
new file mode 100644
index 0000000..6b8f574
--- /dev/null
+++ b/resources/lib/overrrides/session.py
@@ -0,0 +1,85 @@
+import urlquick # @UnresolvedImport
+from ..settings import option
+import socks
+
+class SocksiPyConnection(urlquick.HTTPConnection):
+ def __init__(self, proxytype, proxyaddr, proxyport=None, host='127.0.0.1', port=9050, rdns=True, username=None, password=None, **kwargs):
+ self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password)
+ urlquick.HTTPConnection.__init__(self, host, port=port, **kwargs)
+
+ def connect(self):
+ self.sock = socks.socksocket()
+ self.sock.setproxy(*self.proxyargs)
+ if type(self.timeout) in (int, float):
+ self.sock.settimeout(self.timeout)
+ self.sock.connect((self.host, self.port))
+
+class SocksiPyConnectionS(urlquick.HTTPSConnection):
+ def __init__(self, proxytype, proxyaddr, proxyport=None, host='127.0.0.1', port=9050, rdns=True, username=None, password=None, **kwargs):
+ self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password)
+ urlquick.HTTPSConnection.__init__(self, host, port=port, **kwargs)
+
+ def connect(self):
+ sock = socks.socksocket()
+ sock.setproxy(*self.proxyargs)
+ if type(self.timeout) in (int, float):
+ sock.settimeout(self.timeout)
+ sock.connect((self.host, self.port))
+ self.sock = urlquick.ssl.wrap_socket(sock, self.key_file, self.cert_file)
+
+class Session(urlquick.Session):
+ def connect(self, req, timeout, verify):
+ # Fetch connection from pool and attempt to reuse if available
+ pool = self.request_handler[req.type]
+ if req.host in pool:
+ try:
+ # noinspection PyTypeChecker
+ return self.send_request(pool[req.host], req)
+ except Exception as e:
+ # Remove the connection from the pool as it's unusable
+ pool[req.host].close()
+ del pool[req.host]
+
+ # Raise the exception if it's not a subclass of UrlError
+ if not isinstance(e, urlquick.UrlError):
+ raise
+
+
+ host_port = req.host.split(':')
+ host = host_port[0]
+ port = int(host_port[2]) if len(host_port) > 1 else 443 if req.type == 'https' else 80
+ # Create a new connection
+ if not option.get_boolean('use_socks'): # @UndefinedVariable
+ if req.type == "https":
+ context = urlquick.ssl._create_unverified_context() if verify is False else None
+ conn = urlquick.HTTPSConnection(host, port, timeout=timeout, context=context)
+ else:
+ conn = urlquick.HTTPConnection(host, port, timeout=timeout)
+ else:
+ if req.type == "https":
+ context = urlquick.ssl._create_unverified_context() if verify is False else None
+ conn = SocksiPyConnectionS(socks.PROXY_TYPE_SOCKS5, option['socks_ip'], proxyport=int(option['socks_port']),
+ host=host, port=port, timeout=timeout, context=context)
+ else:
+ conn = SocksiPyConnection(socks.PROXY_TYPE_SOCKS5, option['socks_ip'], proxyport=int(option['socks_port']),
+ host=host, port=port, timeout=timeout)
+
+ # Make first connection to server
+ response = self.send_request(conn, req)
+
+ # Add connection to the pool if the response is not set to close
+ if not response.will_close:
+ pool[req.host] = conn
+ return response
+def torrent_file_fetch(url, referer, cookies):
+ sess = Session()
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 YaBrowser/14.10.2062.12061 Safari/537.36',
+ 'Referer': referer
+ }
+ resp = sess.get(url, cookies=cookies, headers=headers)
+ if resp.ok:
+ return resp.content
+ else:
+ return None
+
diff --git a/resources/lib/player.py b/resources/lib/player.py
new file mode 100644
index 0000000..a7e8a11
--- /dev/null
+++ b/resources/lib/player.py
@@ -0,0 +1,62 @@
+import xbmc, xbmcgui
+from .utils import get_engine, localize
+
+EXPECTED_KBITS = 512
+
+class TorrentPlayer(xbmc.Player) :
+ pyrrent_engine = None
+ loop = None
+ def onPlayBackEnded(self):
+ self.pyrrent_engine.close()
+ self.loop.stopped = True
+ xbmc.Player().stop()
+ def onPlayBackPaused(self):
+ self.pyrrent_engine.pause()
+
+ def onPlayBackResumed(self):
+ self.pyrrent_engine.resume()
+
+ def onPlayBackStopped(self):
+ self.pyrrent_engine.close()
+ self.loop.stopped = True
+ xbmc.Player().stop()
+ def play(self, engine, f_index):
+ self.pyrrent_engine = engine
+ self.pyrrent_engine.start()
+ self.pyrrent_engine.activate_file(f_index)
+ monitor = xbmc.Monitor()
+ pw = xbmcgui.DialogProgress()
+ pw.create(localize(33055), line1='0 Kbit/s')
+ while True:
+ xbmc.sleep(500)
+ if monitor.abortRequested() or pw.iscanceled():
+ pw.close()
+ self.pyrrent_engine.close()
+ xbmc.Player().stop()
+ self.loop.stopped = True
+ return False
+ status = self.pyrrent_engine.status()
+ d_rate = status.download_rate
+ xbmc.log('*** DRATE: {}'.format(d_rate), level=xbmc.LOGNOTICE)
+ perc = d_rate / EXPECTED_KBITS * 100
+ if perc > 100: perc = 100
+ pw.update(perc, line1='{} kbit/s'.format(int(d_rate)))
+ if perc == 100:
+ pw.close()
+ break
+ fstat = self.pyrrent_engine.file_status(f_index)
+ listitem = xbmcgui.ListItem('.'.join(fstat.name.split('.')[:-1]), path=fstat.url)
+ xbmc.Player.play(self, fstat.url, listitem)
+
+class VideoLoop(object):
+ stopped = False
+ status = None
+ def __init__(self, torr_fp):
+ self.e = get_engine(torr_fp)
+ def start(self, f_index):
+ self.mediaPlayer = TorrentPlayer()
+ self.mediaPlayer.loop = self
+ self.mediaPlayer.play(self.e, f_index)
+ while not self.stopped:
+ self.status = self.e.status()
+ xbmc.sleep(1000)
diff --git a/resources/lib/searchers/__init__.py b/resources/lib/searchers/__init__.py
new file mode 100644
index 0000000..a5a2919
--- /dev/null
+++ b/resources/lib/searchers/__init__.py
@@ -0,0 +1,43 @@
+from codequick.utils import urljoin_partial # @UnresolvedImport
+import urlquick # @UnresolvedImport
+from ..settings import option
+from ..overrrides import session
+#import xbmc
+
+class ResultItem(object):
+ def __init__(self, url, title, size, seeders, leachers, icon='video.png', cookies={}, referer=''):
+ self.url = url
+ self.title = title
+ self.size = size
+ self.seeders = seeders
+ self.leachers = leachers
+ self.icon = icon
+ self.cookies = cookies
+ self.referer = referer
+
+class Searcher(object):
+ base_url = None
+ search_path = None
+ cookies = {}
+ headers = {}
+ name = 'BaseClass'
+ def __init__(self):
+ self.session = session.Session()
+ def prepare(self):
+ '''Login or something else if needed'''
+ def search(self, query):
+ self.prepare()
+ url_constructor = urljoin_partial(self.base_url)
+ s_url = url_constructor(self.search_path.format(urlquick.quote(query)))
+ self.set_headers()
+ resp = self.session.get(s_url, cookies=self.cookies, headers=self.headers)
+ self.cookies = resp.cookies
+ body = resp.parse('body')
+ return self.process(body)
+ def process(self, body):
+ '''Process element tree'''
+ def set_headers(self):
+ self.headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 YaBrowser/14.10.2062.12061 Safari/537.36',
+ 'Referer': self.base_url
+ }
diff --git a/resources/lib/searchers/rutor.py b/resources/lib/searchers/rutor.py
new file mode 100644
index 0000000..def1e07
--- /dev/null
+++ b/resources/lib/searchers/rutor.py
@@ -0,0 +1,22 @@
+from . import Searcher, urljoin_partial, ResultItem
+
+class SearchEngine(Searcher):
+ base_url = 'http://new-tor.top'
+ search_path = '/search/0/0/100/2/{}'
+ name = 'RuTor.org'
+ icon = 'searcher_rutor.png'
+ def process(self, body):
+ url_constructor = urljoin_partial(self.base_url)
+ rows = list(filter(lambda x: len(x.findall('.//a')) == 3, body.findall('.//tr')))
+ for r in rows:
+ links = r.findall('.//a')
+ url = url_constructor(links[0].get('href').strip())
+ title = links[2].text
+ seeders = int(list(r.findall('.//span[@class="green"]')[0].itertext())[0].strip())
+ leachers = int(list(r.findall('.//span[@class="red"]')[0].itertext())[0].strip())
+ size = list(
+ list(filter(lambda x: len(list(x.itertext())) == 1 and list(x.itertext())[0].strip().endswith(('GB', 'MB')),
+ r.findall('.//td'))
+ )[0].itertext()
+ )[0].strip()
+ yield ResultItem(url, title, size, seeders, leachers, self.icon, self.cookies, self.base_url)
diff --git a/resources/lib/settings.py b/resources/lib/settings.py
new file mode 100644
index 0000000..8b7c7cb
--- /dev/null
+++ b/resources/lib/settings.py
@@ -0,0 +1,9 @@
+from codequick.script import Settings # @UnresolvedImport
+import os
+
+option = Settings()
+storage_root = os.path.abspath(os.path.join(option['storage_dir'], 'Torrenter3'))
+storage_toorents_dir = os.path.join(storage_root, 'torrents')
+storage_download_dir = os.path.join(storage_root, 'download')
+if not os.path.exists(storage_toorents_dir): os.makedirs(storage_toorents_dir)
+if not os.path.exists(storage_download_dir): os.makedirs(storage_download_dir)
diff --git a/resources/lib/utils/__init__.py b/resources/lib/utils/__init__.py
new file mode 100644
index 0000000..c29f363
--- /dev/null
+++ b/resources/lib/utils/__init__.py
@@ -0,0 +1,45 @@
+from codequick.support import addon_data # @UnresolvedImport
+from urlquick import urljoin # @UnresolvedImport
+from ..settings import option, storage_toorents_dir, storage_download_dir
+import xbmcgui
+import os
+from hashlib import sha1
+from pyrrent2http import Engine # @UnresolvedImport
+import sys
+py3 = sys.version_info >= (3, 0)
+if py3:
+ from urllib.request import pathname2url # @UnresolvedImport
+else:
+ from urllib import pathname2url
+
+def localize(sid):
+ return addon_data.getLocalizedString(sid)
+def store_torrent_file(file_bytes):
+ h = sha1(file_bytes).hexdigest()
+ t_fname = '{}.torrent'.format(h)
+ full_path = os.path.join(storage_toorents_dir, t_fname)
+ if os.path.exists(full_path):
+ with open(full_path, 'rb') as f:
+ if sha1(f.read()).hexdigest() == h:
+ return full_path
+ with open(full_path, 'wb') as f:
+ f.write(file_bytes)
+ return full_path
+def torrent_full_path(t_fname):
+ return os.path.join(storage_toorents_dir, t_fname)
+
+def file_url(path):
+ if not path.startswith('file:'):
+ path = urljoin('file:', pathname2url(path))
+ return path
+
+def get_engine(torrent_uri):
+ return Engine(uri=file_url(torrent_uri), download_path=storage_download_dir,
+ encryption=1, keep_complete=False, keep_incomplete=False,
+ dht_routers=["router.bittorrent.com:6881", "router.utorrent.com:6881"], use_random_port=False, listen_port=6881,
+ user_agent='', enable_dht=True)
+
+while not option['storage_dir']:
+ dialog = xbmcgui.Dialog()
+ dialog.ok(localize(33000), localize(33051))
+ addon_data.openSettings()
diff --git a/resources/media/back_arrow.png b/resources/media/back_arrow.png
new file mode 100644
index 0000000..5fd236f
Binary files /dev/null and b/resources/media/back_arrow.png differ
diff --git a/resources/media/history2.png b/resources/media/history2.png
new file mode 100644
index 0000000..a26cefb
Binary files /dev/null and b/resources/media/history2.png differ
diff --git a/resources/media/search.png b/resources/media/search.png
new file mode 100644
index 0000000..d403947
Binary files /dev/null and b/resources/media/search.png differ
diff --git a/resources/media/searcher_rutor.png b/resources/media/searcher_rutor.png
new file mode 100644
index 0000000..e320917
Binary files /dev/null and b/resources/media/searcher_rutor.png differ
diff --git a/resources/media/video.png b/resources/media/video.png
new file mode 100644
index 0000000..b509ff4
Binary files /dev/null and b/resources/media/video.png differ
diff --git a/resources/settings.xml b/resources/settings.xml
new file mode 100644
index 0000000..5a7c4ce
--- /dev/null
+++ b/resources/settings.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+