├── src ├── __init__.py ├── addon.py ├── logger.py ├── main.py ├── torrent.py ├── debugger.py ├── utils.py ├── filter.py ├── jackett.py └── client.py ├── resources ├── images │ └── icon.png ├── language │ ├── English │ │ └── strings.po │ └── messages.pot └── settings.xml ├── LICENSE └── addon.xml /src/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /resources/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fugkco/script.elementum.jackett/HEAD/resources/images/icon.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (c) 2017 fugkco 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /src/addon.py: -------------------------------------------------------------------------------- 1 | from kodi_six import xbmcaddon, xbmcvfs 2 | 3 | ADDON = xbmcaddon.Addon() 4 | ID = ADDON.getAddonInfo("id") 5 | NAME = ADDON.getAddonInfo("name") 6 | PATH = ADDON.getAddonInfo("path") 7 | ICON = ADDON.getAddonInfo("icon") 8 | PROFILE = ADDON.getAddonInfo("profile") 9 | VERSION = ADDON.getAddonInfo("version") 10 | HOME = xbmcvfs.translatePath("special://home/addons/") 11 | TMP = xbmcvfs.translatePath("special://temp") 12 | if not HOME: 13 | HOME = '..' 14 | -------------------------------------------------------------------------------- /src/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from kodi_six import xbmc 4 | 5 | import addon 6 | 7 | 8 | class XBMCHandler(logging.StreamHandler): 9 | xbmc_levels = { 10 | 'DEBUG': 0, 11 | 'INFO': 2, 12 | 'WARNING': 3, 13 | 'ERROR': 4, 14 | 'CRITICAL': 5, 15 | } 16 | 17 | def emit(self, record): 18 | xbmc_level = self.xbmc_levels.get(record.levelname) 19 | xbmc.log(self.format(record), xbmc_level) 20 | 21 | 22 | log = logging.getLogger(addon.ID) 23 | 24 | handler = XBMCHandler() 25 | handler.setFormatter(logging.Formatter('[%(name)s] %(message)s')) 26 | log.addHandler(handler) 27 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from os import path 3 | 4 | import sys 5 | from elementum.provider import register 6 | from kodi_six import xbmcgui 7 | 8 | from logger import log 9 | 10 | sys.path.insert(0, path.realpath(path.join(path.dirname(__file__), '..', 'resources', 'libs'))) 11 | sys.path.insert(0, path.dirname(__file__)) 12 | 13 | if __name__ == '__main__': 14 | import debugger 15 | import utils 16 | import jackett 17 | 18 | if len(sys.argv) == 1: 19 | log.error("Elementum Jackett plugin must be run through Elementum") 20 | p_dialog = xbmcgui.Dialog() 21 | try: 22 | p_dialog.ok('Elementum [COLOR FFFF6B00]Jackett[/COLOR]', utils.translation(32800)) 23 | finally: 24 | del p_dialog 25 | 26 | sys.exit(1) 27 | 28 | if sys.argv[1] == "validate_settings": 29 | jackett.validate_client() 30 | else: 31 | debugger.load() 32 | register( 33 | lambda q: jackett.search(q), 34 | lambda q: jackett.search(q, 'movie'), 35 | lambda q: jackett.search(q, 'episode'), 36 | lambda q: jackett.search(q, 'season'), 37 | ) 38 | -------------------------------------------------------------------------------- /addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | executable 12 | 13 | 14 | Elementum Jackett provider 15 | Elementum Jackett is a provider that connects Elementum to Jacket. You need to run your own Jackett server. 16 | WTFPL, Version 2 17 | https://github.com/fugkco/script.elementum.jackett 18 | https://github.com/fugkco/script.elementum.jackett 19 | 20 | resources/images/icon.png 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/torrent.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | from http import client as httplib 4 | 5 | import requests 6 | from torf import Torrent, Magnet 7 | 8 | from logger import log 9 | 10 | session = requests.Session() 11 | session.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 ' \ 12 | '(KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36' 13 | 14 | 15 | def get_magnet(original_uri): 16 | magnet_prefix = 'magnet:' 17 | uri = original_uri 18 | 19 | while True: 20 | if len(uri) >= len(magnet_prefix) and uri[0:7] == magnet_prefix: 21 | return uri 22 | try: 23 | response = session.get(uri, allow_redirects=False, timeout=10) 24 | except requests.exceptions.Timeout as e: 25 | log.warning(f"Timeout while resolving torrent {uri}") 26 | break 27 | 28 | if response.is_redirect: 29 | uri = response.headers['Location'] 30 | elif response.status_code == httplib.OK and response.headers.get('Content-Type') == 'application/x-bittorrent': 31 | torrent = Torrent.read_stream(io.BytesIO(response.content)) 32 | return str(torrent.magnet()) 33 | else: 34 | log.warning(f"Could not get final redirect location for URI {original_uri}. " 35 | f"Response was: {response.status_code} {response.reason}") 36 | log.debug(f"Response for failed redirect {original_uri} is") 37 | log.debug("=" * 50) 38 | for (h, k) in list(response.headers.items()): 39 | log.debug(f"{h}: {k}") 40 | log.debug("") 41 | log.debug(base64.standard_b64encode(response.content)) 42 | log.debug("=" * 50) 43 | break 44 | 45 | return None 46 | 47 | 48 | def get_info_hash(magnet): 49 | return Magnet.from_string(magnet).infohash 50 | -------------------------------------------------------------------------------- /src/debugger.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | def load(): 4 | from utils import get_setting 5 | 6 | if get_setting("enable_debugger", bool): 7 | from logger import log 8 | import pkgutil 9 | import re 10 | from os import path 11 | import sys 12 | 13 | additional_libraries = get_setting("debugger_additional_libraries") 14 | if additional_libraries != "": 15 | if not path.exists(additional_libraries): 16 | log.error("Debugger has been enabled but additional libraries directory, skipping loading of debugger") 17 | return 18 | sys.path.append(additional_libraries) 19 | 20 | if pkgutil.find_loader("pydevd_pycharm") is None: 21 | log.error("Debugger currently only supports IntelliJ IDEA and derivatives. If you need additional ") 22 | return 23 | 24 | host = get_setting("debugger_host") 25 | valid_host_regex = re.compile(r''' 26 | ^ 27 | (?: 28 | (?:(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])) 29 | | 30 | (?:(?:(?:[a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+(?:[A-Za-z|[A-Za-z][A-Za-z0-9\‌-]*[A-Za-z0-9])) 31 | ) 32 | $ 33 | ''', re.VERBOSE) 34 | if not valid_host_regex.match(host): 35 | log.error("debugger: invalid host detected.. Skipping") 36 | return False 37 | 38 | try: 39 | port = get_setting("debugger_port", int) 40 | except ValueError: 41 | log.exception("debugger: invalid port detected") 42 | return 43 | 44 | if not (0 < int(port) <= 65535): 45 | log.exception("debugger: port must be between 0 and 65535") 46 | return 47 | 48 | import pydevd_pycharm 49 | pydevd_pycharm.settrace(host, port=port, stdoutToServer=True, stderrToServer=True) 50 | log.info("pycharm debugger successfully loaded") 51 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import hashlib 3 | import os 4 | import re 5 | from collections import OrderedDict 6 | 7 | from kodi_six import xbmcgui 8 | 9 | import addon 10 | from logger import log 11 | 12 | _plugin_setting_prefix = "elementum.jackett." 13 | 14 | PROVIDER_COLOR_MIN_BRIGHTNESS = 50 15 | 16 | UNKNOWN = 'unknown' 17 | 18 | resolutions = OrderedDict([ 19 | ('4k', [r'4k', r'2160[p]', r'uhd', r'4k', r'hd4k']), 20 | ('2k', [r'1440[p]', r'2k']), 21 | ('1080p', [r'1080[ip]', r'1920x1080', r'hd1080p?', r'fullhd', r'fhd', r'blu\W*ray', r'bd\W*remux']), 22 | ('720p', [r'720[p]', r'1280x720', r'hd720p?', r'hd\-?rip', r'b[rd]rip']), 23 | ('480p', [r'480[p]', r'xvid', r'dvd', r'dvdrip', r'hdtv', r'web\-(dl)?rip', r'iptv', r'sat\-?rip', 24 | r'tv\-?rip']), 25 | ('240p', [r'240[p]', r'vhs\-?rip']), 26 | (UNKNOWN, []), 27 | ]) 28 | 29 | _release_types = OrderedDict([ 30 | ('brrip', [r'brrip', r'bd\-?rip', r'blu\-?ray', r'bd\-?remux']), 31 | ('webdl', [r'web', r'web_?\-?dl', r'web\-?rip', r'dl\-?rip', r'yts']), 32 | ('hdrip', [r'hd\-?rip']), 33 | ('hdtv', [r'hd\-?tv']), 34 | ('dvd', [r'dvd', r'dvd\-?rip', r'vcd\-?rip', r'divx', r'xvid']), 35 | ('dvdscr', [r'dvd\-?scr(eener)?']), 36 | ('screener', [r'screener', r'scr']), 37 | ('3d', [r'3d']), 38 | ('telesync', [r'telesync', r'ts', r'tc']), 39 | ('cam', [r'cam(\-rip)?', r'hd\-?cam']), 40 | ('tvrip', [r'tv\-?rip', r'sat\-?rip', r'dvb']), 41 | ('vhsrip', [r'vhs\-?rip']), 42 | ('iptvrip', [r'iptv\-?rip']), 43 | ('trailer', [r'trailer']), 44 | ('workprint', [r'workprint']), 45 | ('line', [r'line']), 46 | ('h26x', [r'x26[45]']), 47 | (UNKNOWN, []) 48 | ]) 49 | 50 | 51 | def get_icon_path(icon='icon.png'): 52 | return os.path.join(addon.PATH, 'resources', 'images', icon) 53 | 54 | 55 | def translation(id_value): 56 | return addon.ADDON.getLocalizedString(id_value) 57 | 58 | 59 | def notify(message, image=None): 60 | dialog = xbmcgui.Dialog() 61 | dialog.notification(addon.NAME, message, icon=image, sound=False) 62 | del dialog 63 | 64 | 65 | def human_size(nbytes): 66 | suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] 67 | i = 0 68 | while nbytes >= 1024 and i < len(suffixes) - 1: 69 | nbytes /= 1024. 70 | i += 1 71 | f = f"{nbytes:.2f}".rstrip('0').rstrip('.') 72 | return f"{f} {suffixes[i]}" 73 | 74 | 75 | def get_resolution(name): 76 | return _search_re_keys(name, resolutions, "resolution", UNKNOWN) 77 | 78 | 79 | def get_release_type(name): 80 | return _search_re_keys(name, _release_types, "release type", UNKNOWN) 81 | 82 | 83 | def _search_re_keys(name, re_dict, log_msg, default=""): 84 | for result, search_keys in list(re_dict.items()): 85 | if bool(re.search(r'\W+(' + "|".join(search_keys) + r')\W*', name, re.IGNORECASE)): 86 | return result 87 | 88 | log.warning(f"Could not determine {log_msg} from filename '{name}'") 89 | return default 90 | 91 | 92 | def set_setting(key, value): 93 | addon.ADDON.setSetting(_plugin_setting_prefix + key, str(value)) 94 | 95 | 96 | def get_setting(key, converter=str, choices=None): 97 | from elementum.provider import get_setting as original_get_settings 98 | return original_get_settings(_plugin_setting_prefix + key, converter, choices) 99 | 100 | 101 | def get_provider_color(provider_name): 102 | hash = hashlib.sha256(provider_name.encode("utf")).hexdigest() 103 | colors = [] 104 | 105 | spec = 10 106 | for i in range(0, 3): 107 | offset = spec * i 108 | rounded = round(int(hash[offset:offset + spec], 16) / int("F" * spec, 16) * 255) 109 | colors.append(int(max(rounded, PROVIDER_COLOR_MIN_BRIGHTNESS))) 110 | 111 | while (sum(colors) / 3) < PROVIDER_COLOR_MIN_BRIGHTNESS: 112 | for i in range(0, 3): 113 | colors[i] += 10 114 | 115 | for i in range(0, 3): 116 | colors[i] = f'{colors[i]:02x}' 117 | 118 | return "FF" + "".join(colors).upper() 119 | -------------------------------------------------------------------------------- /src/filter.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from logger import log 3 | from utils import get_setting, UNKNOWN 4 | 5 | 6 | # 7 | # 8 | # def get_setting(setting, typ): 9 | # return typ( 10 | # { 11 | # 'sort_by': 0, 12 | # 13 | # 'filter_exclude_no_seed': True, 14 | # 'filter_keywords_enabled': False, 15 | # 'keywords_block': '', 16 | # 'keywords_require': '', 17 | # 18 | # 'filter_size_enabled': False, 19 | # 'size_include_unknown': True, 20 | # 'size_min': 0, 21 | # 'size_max': 100, 22 | # 'size_movies_min': 0.5, 23 | # 'size_movies_max': 30, 24 | # 'size_season_min': 0.5, 25 | # 'size_season_max': 10, 26 | # 'size_episode_min': 0, 27 | # 'size_episode_max': 1, 28 | # 29 | # 'filter_include_resolution_enabled': True, 30 | # 'include_resolution_4k': True, 31 | # 'include_resolution_2k': True, 32 | # 'include_resolution_1080p': True, 33 | # 'include_resolution_720p': True, 34 | # 'include_resolution_480p': True, 35 | # 'include_resolution_240p': False, 36 | # 'include_resolution_unknown': False, 37 | # 38 | # 'filter_include_release': True, 39 | # 'include_release_brrip': True, 40 | # 'include_release_webdl': True, 41 | # 'include_release_hdrip': True, 42 | # 'include_release_hdtv': True, 43 | # 'include_release_dvd': True, 44 | # 'include_release_dvdscr': True, 45 | # 'include_release_screener': True, 46 | # 'include_release_3d': False, 47 | # 'include_release_telesync': False, 48 | # 'include_release_cam': False, 49 | # 'include_release_tvrip': True, 50 | # 'include_release_iptvrip': True, 51 | # 'include_release_vhsrip': False, 52 | # 'include_release_trailer': False, 53 | # 'include_release_workprint': False, 54 | # 'include_release_line': False, 55 | # 'include_release_unknown': False, 56 | # }[setting] 57 | # ) 58 | 59 | def keywords(results): 60 | block_keywords = get_setting('keywords_block').split(",") 61 | require_keywords = get_setting('keywords_require').split(",") 62 | 63 | for word in block_keywords: 64 | results = [ 65 | result 66 | for result in results 67 | if word in result["name"] 68 | ] 69 | 70 | for word in require_keywords: 71 | results = [ 72 | result 73 | for result in results 74 | if word not in result["name"] 75 | ] 76 | 77 | return results 78 | 79 | 80 | def size(method, results): 81 | include_unknown = get_setting('size_include_' + UNKNOWN, bool) 82 | 83 | if method in ["movie", "season", "episode"]: 84 | min_size = get_setting('size_' + method + '_min', float) 85 | max_size = get_setting('size_' + method + '_max', float) 86 | else: 87 | min_size = get_setting('size_min', float) 88 | max_size = get_setting('size_max', float) 89 | 90 | # MB KB B 91 | min_size = min_size * (1024 * 1024 * 1024) 92 | max_size = max_size * (1024 * 1024 * 1024) 93 | 94 | return [ 95 | result 96 | for result in results 97 | if (size == -1 and include_unknown) or (size != -1 and min_size <= result["_size_bytes"] <= max_size) 98 | ] 99 | 100 | 101 | def resolution(results): 102 | filtered = [] 103 | for result in results: 104 | log.debug(f"res {result['name']}: name={result['_resolution']}; id={result['resolution']}") 105 | if get_setting('include_resolution_' + result["_resolution"], bool): 106 | filtered.append(result) 107 | 108 | return filtered 109 | 110 | 111 | def seed(results): 112 | return [ 113 | result 114 | for result in results 115 | if result["seeds"] > 0 116 | ] 117 | 118 | 119 | def unique(results): 120 | return list({v['info_hash'].lower(): v for v in results}.values()) 121 | 122 | 123 | def release_type(results): 124 | return [ 125 | result 126 | for result in results 127 | if get_setting('include_release_' + result["release_type"], bool) 128 | ] 129 | -------------------------------------------------------------------------------- /resources/language/English/strings.po: -------------------------------------------------------------------------------- 1 | # Kodi Media Center language file 2 | # Add-on Name: Elementum Burst 3 | # Add-on id: script.elementum.jackett 4 | # Add-on provider: fugkco 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Elementum Jackett\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2015-09-05 05:53-0400\n" 11 | "PO-Revision-Date: 2020-05-14 11:19-0400\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: en\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 1.8.4\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | msgctxt "#32000" 22 | msgid "Jackett" 23 | msgstr "" 24 | 25 | msgctxt "#32001" 26 | msgid "Host" 27 | msgstr "" 28 | 29 | msgctxt "#32002" 30 | msgid "API Key" 31 | msgstr "" 32 | 33 | msgctxt "#32003" 34 | msgid "Jackett settings valid" 35 | msgstr "" 36 | 37 | msgctxt "#32004" 38 | msgid "Validate settings" 39 | msgstr "" 40 | 41 | msgctxt "#32005" 42 | msgid "Validating..." 43 | msgstr "" 44 | 45 | msgctxt "#32006" 46 | msgid "Successfully connected to Jackett" 47 | msgstr "" 48 | 49 | msgctxt "#32007" 50 | msgid "Enable search with imdb_id" 51 | msgstr "" 52 | 53 | msgctxt "#32008" 54 | msgid "Limit torrent count (Faster)" 55 | msgstr "" 56 | 57 | msgctxt "#32050" 58 | msgid "Filtering" 59 | msgstr "" 60 | 61 | msgctxt "#32051" 62 | msgid "General" 63 | msgstr "" 64 | 65 | msgctxt "#32052" 66 | msgid "Sort returned results by" 67 | msgstr "" 68 | 69 | msgctxt "#32053" 70 | msgid "Resolution" 71 | msgstr "" 72 | 73 | msgctxt "#32054" 74 | msgid "Seeds" 75 | msgstr "" 76 | 77 | msgctxt "#32055" 78 | msgid "Size" 79 | msgstr "" 80 | 81 | msgctxt "#32056" 82 | msgid "Balanced" 83 | msgstr "" 84 | 85 | msgctxt "#32057" 86 | msgid "Hide torrents without seeds" 87 | msgstr "" 88 | 89 | msgctxt "#32058" 90 | msgid "When looking for an episode, do a secondary search for season" 91 | msgstr "" 92 | 93 | msgctxt "#32100" 94 | msgid "Filter Keywords" 95 | msgstr "" 96 | 97 | msgctxt "#32101" 98 | msgid "Enable" 99 | msgstr "" 100 | 101 | msgctxt "#32102" 102 | msgid "Block" 103 | msgstr "" 104 | 105 | msgctxt "#32103" 106 | msgid "Require" 107 | msgstr "" 108 | 109 | msgctxt "#32150" 110 | msgid "Filter Size" 111 | msgstr "" 112 | 113 | msgctxt "#32151" 114 | msgid "Enable" 115 | msgstr "" 116 | 117 | msgctxt "#32152" 118 | msgid "Include unknown file size" 119 | msgstr "" 120 | 121 | msgctxt "#32153" 122 | msgid "Min Size (GB)" 123 | msgstr "" 124 | 125 | msgctxt "#32154" 126 | msgid "Min Size (GB)" 127 | msgstr "" 128 | 129 | msgctxt "#32155" 130 | msgid "Min Movie Size (GB)" 131 | msgstr "" 132 | 133 | msgctxt "#32156" 134 | msgid "Max Movie Size (GB)" 135 | msgstr "" 136 | 137 | msgctxt "#32157" 138 | msgid "Min Season Size (GB)" 139 | msgstr "" 140 | 141 | msgctxt "#32158" 142 | msgid "Max Season Size (GB)" 143 | msgstr "" 144 | 145 | msgctxt "#32159" 146 | msgid "Min Episode Size (GB)" 147 | msgstr "" 148 | 149 | msgctxt "#32160" 150 | msgid "Max Episode Size (GB)" 151 | msgstr "" 152 | 153 | msgctxt "#32200" 154 | msgid "Filter Resolution" 155 | msgstr "" 156 | 157 | msgctxt "#32201" 158 | msgid "Enable" 159 | msgstr "" 160 | 161 | msgctxt "#32202" 162 | msgid "4K/2160p" 163 | msgstr "" 164 | 165 | msgctxt "#32203" 166 | msgid "2K/1440p" 167 | msgstr "" 168 | 169 | msgctxt "#32204" 170 | msgid "1080p" 171 | msgstr "" 172 | 173 | msgctxt "#32205" 174 | msgid "720p" 175 | msgstr "" 176 | 177 | msgctxt "#32206" 178 | msgid "480p" 179 | msgstr "" 180 | 181 | msgctxt "#32207" 182 | msgid "240p" 183 | msgstr "" 184 | 185 | msgctxt "#32208" 186 | msgid "Unknown" 187 | msgstr "" 188 | 189 | msgctxt "#32250" 190 | msgid "Filter Release Type" 191 | msgstr "" 192 | 193 | msgctxt "#32251" 194 | msgid "Enable" 195 | msgstr "" 196 | 197 | msgctxt "#32252" 198 | msgid "BRRip/BDRip/Bluray" 199 | msgstr "" 200 | 201 | msgctxt "#32253" 202 | msgid "WebDL/WebRip" 203 | msgstr "" 204 | 205 | msgctxt "#32254" 206 | msgid "HDRip" 207 | msgstr "" 208 | 209 | msgctxt "#32255" 210 | msgid "HDTV" 211 | msgstr "" 212 | 213 | msgctxt "#32256" 214 | msgid "DVDRip" 215 | msgstr "" 216 | 217 | msgctxt "#32257" 218 | msgid "h26x" 219 | msgstr "" 220 | 221 | msgctxt "#32258" 222 | msgid "DVDScr" 223 | msgstr "" 224 | 225 | msgctxt "#32259" 226 | msgid "Screener/SCR" 227 | msgstr "" 228 | 229 | msgctxt "#32260" 230 | msgid "3D" 231 | msgstr "" 232 | 233 | msgctxt "#32261" 234 | msgid "TeleSync/TS/TC" 235 | msgstr "" 236 | 237 | msgctxt "#32262" 238 | msgid "Cam/HDCam" 239 | msgstr "" 240 | 241 | msgctxt "#32263" 242 | msgid "TVRip/SATRip" 243 | msgstr "" 244 | 245 | msgctxt "#32264" 246 | msgid "IPTVRip" 247 | msgstr "" 248 | 249 | msgctxt "#32265" 250 | msgid "VHSRip" 251 | msgstr "" 252 | 253 | msgctxt "#32266" 254 | msgid "Trailer" 255 | msgstr "" 256 | 257 | msgctxt "#32267" 258 | msgid "Workprint" 259 | msgstr "" 260 | 261 | msgctxt "#32268" 262 | msgid "Line" 263 | msgstr "" 264 | 265 | msgctxt "#32269" 266 | msgid "Unknown" 267 | msgstr "" 268 | 269 | msgctxt "#32300" 270 | msgid "Advanced" 271 | msgstr "" 272 | 273 | msgctxt "#32301" 274 | msgid "Debugger" 275 | msgstr "" 276 | 277 | msgctxt "#32302" 278 | msgid "Enable" 279 | msgstr "" 280 | 281 | msgctxt "#32303" 282 | msgid "Additional libraries to include" 283 | msgstr "" 284 | 285 | msgctxt "#32304" 286 | msgid "Host" 287 | msgstr "" 288 | 289 | msgctxt "#32305" 290 | msgid "Port" 291 | msgstr "" 292 | 293 | msgctxt "#32600" 294 | msgid "Jackett host is invalid" 295 | msgstr "" 296 | 297 | msgctxt "#32601" 298 | msgid "Jackett API key is invalid" 299 | msgstr "" 300 | 301 | msgctxt "#32602" 302 | msgid "Searching..." 303 | msgstr "" 304 | 305 | msgctxt "#32603" 306 | msgid "Unable to connect to Jackett, are your settings correct?" 307 | msgstr "" 308 | 309 | msgctxt "#32604" 310 | msgid "Requesting results from Jackett server..." 311 | msgstr "" 312 | 313 | msgctxt "#32700" 314 | msgid "Jackett returned error: {}" 315 | msgstr "" 316 | 317 | msgctxt "#32701" 318 | msgid "Unable to determine Jackett capabilities" 319 | msgstr "" 320 | 321 | msgctxt "#32702" 322 | msgid "Jackett is unable to search by {}, falling back to query search" 323 | msgstr "" 324 | 325 | msgctxt "#32703" 326 | msgid "Critical error check log for more info" 327 | msgstr "" 328 | 329 | msgctxt "#32750" 330 | msgid "Filtering result..." 331 | msgstr "" 332 | 333 | msgctxt "#32751" 334 | msgid "Resolving {} magnet links..." 335 | msgstr "" 336 | 337 | msgctxt "#32752" 338 | msgid "Removing dublicates..." 339 | msgstr "" 340 | 341 | msgctxt "#32753" 342 | msgid "Sorting..." 343 | msgstr "" 344 | 345 | msgctxt "#32754" 346 | msgid "Returning to Elementum" 347 | msgstr "" 348 | 349 | msgctxt "#32800" 350 | msgid "This addon is a provider for Elementum. Nothing to do for me..." 351 | msgstr "" 352 | -------------------------------------------------------------------------------- /resources/language/messages.pot: -------------------------------------------------------------------------------- 1 | # Kodi Media Center language file 2 | # Add-on Name: Elementum Burst 3 | # Add-on id: script.elementum.jackett 4 | # Add-on provider: fugkco 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Elementum Jackett\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2015-09-05 05:53-0400\n" 11 | "PO-Revision-Date: 2015-09-05 06:39-0400\n" 12 | "Language: en\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Last-Translator: \n" 17 | "Language-Team: \n" 18 | "X-Generator: Poedit 1.8.4\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | 22 | msgctxt "#32000" 23 | msgid "Jackett" 24 | msgstr "" 25 | 26 | msgctxt "#32001" 27 | msgid "Host" 28 | msgstr "" 29 | 30 | msgctxt "#32002" 31 | msgid "API Key" 32 | msgstr "" 33 | 34 | msgctxt "#32003" 35 | msgid "Jackett settings valid" 36 | msgstr "" 37 | 38 | msgctxt "#32004" 39 | msgid "Validate settings" 40 | msgstr "" 41 | 42 | msgctxt "#32005" 43 | msgid "Validating..." 44 | msgstr "" 45 | 46 | msgctxt "#32006" 47 | msgid "Successfully connected to Jackett" 48 | msgstr "" 49 | 50 | msgctxt "#32007" 51 | msgid "Enable search with imdb_id" 52 | msgstr "" 53 | 54 | msgctxt "#32008" 55 | msgid "Limit torrent count (Faster)" 56 | msgstr "" 57 | 58 | msgctxt "#32050" 59 | msgid "Filtering" 60 | msgstr "" 61 | 62 | msgctxt "#32051" 63 | msgid "General" 64 | msgstr "" 65 | 66 | msgctxt "#32052" 67 | msgid "Sort returned results by" 68 | msgstr "" 69 | 70 | msgctxt "#32053" 71 | msgid "Resolution" 72 | msgstr "" 73 | 74 | msgctxt "#32054" 75 | msgid "Seeds" 76 | msgstr "" 77 | 78 | msgctxt "#32055" 79 | msgid "Size" 80 | msgstr "" 81 | 82 | msgctxt "#32056" 83 | msgid "Balanced" 84 | msgstr "" 85 | 86 | msgctxt "#32057" 87 | msgid "Hide torrents without seeds" 88 | msgstr "" 89 | 90 | msgctxt "#32058" 91 | msgid "When looking for an episode, do a secondary search for season" 92 | msgstr "" 93 | 94 | msgctxt "#32100" 95 | msgid "Filter Keywords" 96 | msgstr "" 97 | 98 | msgctxt "#32101" 99 | msgid "Enable" 100 | msgstr "" 101 | 102 | msgctxt "#32102" 103 | msgid "Block" 104 | msgstr "" 105 | 106 | msgctxt "#32103" 107 | msgid "Require" 108 | msgstr "" 109 | 110 | msgctxt "#32150" 111 | msgid "Filter Size" 112 | msgstr "" 113 | 114 | msgctxt "#32151" 115 | msgid "Enable" 116 | msgstr "" 117 | 118 | msgctxt "#32152" 119 | msgid "Include unknown file size" 120 | msgstr "" 121 | 122 | msgctxt "#32153" 123 | msgid "Min Size (GB)" 124 | msgstr "" 125 | 126 | msgctxt "#32154" 127 | msgid "Min Size (GB)" 128 | msgstr "" 129 | 130 | msgctxt "#32155" 131 | msgid "Min Movie Size (GB)" 132 | msgstr "" 133 | 134 | msgctxt "#32156" 135 | msgid "Max Movie Size (GB)" 136 | msgstr "" 137 | 138 | msgctxt "#32157" 139 | msgid "Min Season Size (GB)" 140 | msgstr "" 141 | 142 | msgctxt "#32158" 143 | msgid "Max Season Size (GB)" 144 | msgstr "" 145 | 146 | msgctxt "#32159" 147 | msgid "Min Episode Size (GB)" 148 | msgstr "" 149 | 150 | msgctxt "#32160" 151 | msgid "Max Episode Size (GB)" 152 | msgstr "" 153 | 154 | msgctxt "#32200" 155 | msgid "Filter Resolution" 156 | msgstr "" 157 | 158 | msgctxt "#32201" 159 | msgid "Enable" 160 | msgstr "" 161 | 162 | msgctxt "#32202" 163 | msgid "4K/2160p" 164 | msgstr "" 165 | 166 | msgctxt "#32203" 167 | msgid "2K/1440p" 168 | msgstr "" 169 | 170 | msgctxt "#32204" 171 | msgid "1080p" 172 | msgstr "" 173 | 174 | msgctxt "#32205" 175 | msgid "720p" 176 | msgstr "" 177 | 178 | msgctxt "#32206" 179 | msgid "480p" 180 | msgstr "" 181 | 182 | msgctxt "#32207" 183 | msgid "240p" 184 | msgstr "" 185 | 186 | msgctxt "#32208" 187 | msgid "Unknown" 188 | msgstr "" 189 | 190 | msgctxt "#32250" 191 | msgid "Filter Release Type" 192 | msgstr "" 193 | 194 | msgctxt "#32251" 195 | msgid "Enable" 196 | msgstr "" 197 | 198 | msgctxt "#32252" 199 | msgid "BRRip/BDRip/Bluray" 200 | msgstr "" 201 | 202 | msgctxt "#32253" 203 | msgid "WebDL/WebRip" 204 | msgstr "" 205 | 206 | msgctxt "#32254" 207 | msgid "HDRip" 208 | msgstr "" 209 | 210 | msgctxt "#32255" 211 | msgid "HDTV" 212 | msgstr "" 213 | 214 | msgctxt "#32256" 215 | msgid "DVDRip" 216 | msgstr "" 217 | 218 | msgctxt "#32257" 219 | msgid "h26x" 220 | msgstr "" 221 | 222 | msgctxt "#32258" 223 | msgid "DVDScr" 224 | msgstr "" 225 | 226 | msgctxt "#32259" 227 | msgid "Screener/SCR" 228 | msgstr "" 229 | 230 | msgctxt "#32260" 231 | msgid "3D" 232 | msgstr "" 233 | 234 | msgctxt "#32261" 235 | msgid "TeleSync/TS/TC" 236 | msgstr "" 237 | 238 | msgctxt "#32262" 239 | msgid "Cam/HDCam" 240 | msgstr "" 241 | 242 | msgctxt "#32263" 243 | msgid "TVRip/SATRip" 244 | msgstr "" 245 | 246 | msgctxt "#32264" 247 | msgid "IPTVRip" 248 | msgstr "" 249 | 250 | msgctxt "#32265" 251 | msgid "VHSRip" 252 | msgstr "" 253 | 254 | msgctxt "#32266" 255 | msgid "Trailer" 256 | msgstr "" 257 | 258 | msgctxt "#32267" 259 | msgid "Workprint" 260 | msgstr "" 261 | 262 | msgctxt "#32268" 263 | msgid "Line" 264 | msgstr "" 265 | 266 | msgctxt "#32269" 267 | msgid "Unknown" 268 | msgstr "" 269 | 270 | msgctxt "#32300" 271 | msgid "Advanced" 272 | msgstr "" 273 | 274 | msgctxt "#32301" 275 | msgid "Debugger" 276 | msgstr "" 277 | 278 | msgctxt "#32302" 279 | msgid "Enable" 280 | msgstr "" 281 | 282 | msgctxt "#32303" 283 | msgid "Additional libraries to include" 284 | msgstr "" 285 | 286 | msgctxt "#32304" 287 | msgid "Host" 288 | msgstr "" 289 | 290 | msgctxt "#32305" 291 | msgid "Port" 292 | msgstr "" 293 | 294 | msgctxt "#32600" 295 | msgid "Jackett host is invalid" 296 | msgstr "" 297 | 298 | msgctxt "#32601" 299 | msgid "Jackett API key is invalid" 300 | msgstr "" 301 | 302 | msgctxt "#32602" 303 | msgid "Searching..." 304 | msgstr "" 305 | 306 | msgctxt "#32603" 307 | msgid "Unable to connect to Jackett, are your settings correct?" 308 | msgstr "" 309 | 310 | msgctxt "#32604" 311 | msgid "Requesting results from Jackett server..." 312 | msgstr "" 313 | 314 | msgctxt "#32700" 315 | msgid "Jackett returned error: {}" 316 | msgstr "" 317 | 318 | msgctxt "#32701" 319 | msgid "Unable to determine Jackett capabilities" 320 | msgstr "" 321 | 322 | msgctxt "#32702" 323 | msgid "Jackett is unable to search by {}, falling back to query search" 324 | msgstr "" 325 | 326 | msgctxt "#32703" 327 | msgid "Critical error check log for more info" 328 | msgstr "" 329 | 330 | msgctxt "#32750" 331 | msgid "Filtering result..." 332 | msgstr "" 333 | 334 | msgctxt "#32751" 335 | msgid "Resolving {} magnet links..." 336 | msgstr "" 337 | 338 | msgctxt "#32752" 339 | msgid "Removing dublicates..." 340 | msgstr "" 341 | 342 | msgctxt "#32753" 343 | msgid "Sorting..." 344 | msgstr "" 345 | 346 | msgctxt "#32754" 347 | msgid "Returning to Elementum" 348 | msgstr "" 349 | 350 | msgctxt "#32800" 351 | msgid "This addon is a provider for Elementum. Nothing to do for me..." 352 | msgstr "" 353 | 354 | -------------------------------------------------------------------------------- /resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/jackett.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Burst processing thread 5 | """ 6 | 7 | import traceback 8 | from urllib.parse import urlparse 9 | 10 | import time 11 | from kodi_six import xbmc, xbmcgui 12 | 13 | import addon 14 | import filter 15 | import utils 16 | from client import Jackett 17 | from logger import log 18 | from utils import get_setting 19 | 20 | available_providers = 0 21 | special_chars = "()\"':.[]<>/\\?" 22 | 23 | 24 | def get_client(p_dialog: xbmcgui.DialogProgressBG = None): 25 | host = urlparse(get_setting('host')) 26 | if host.netloc == '' or host.scheme == '': 27 | log.warning(f"Host {get_setting('host')} is invalid. Can't return anything") 28 | utils.notify(utils.translation(32600), image=utils.get_icon_path()) 29 | return None 30 | 31 | api_key = get_setting('api_key') 32 | 33 | if len(api_key) != 32: 34 | utils.notify(utils.translation(32601), image=utils.get_icon_path()) 35 | return None 36 | else: 37 | log.debug(f"jackett host: {host}") 38 | log.debug(f"jackett api_key: {api_key[0:2]}{'*' * 26}{api_key[-4:]}") 39 | 40 | return Jackett(host=host.geturl(), api_key=api_key, p_dialog=p_dialog) 41 | 42 | 43 | def validate_client(): 44 | p_dialog = xbmcgui.DialogProgressBG() 45 | try: 46 | p_dialog.create('Elementum [COLOR FFFF6B00]Jackett[/COLOR]', utils.translation(32005)) 47 | get_client() 48 | if get_setting("settings_validated") == "Success": 49 | utils.notify(utils.translation(32006), image=utils.get_icon_path()) 50 | addon.ADDON.openSettings() 51 | finally: 52 | p_dialog.close() 53 | del p_dialog 54 | 55 | 56 | def search(payload, method="general"): 57 | payload = parse_payload(method, payload) 58 | 59 | log.debug(f"Searching with payload ({method}): f{payload}") 60 | 61 | p_dialog = xbmcgui.DialogProgressBG() 62 | p_dialog.create('Elementum [COLOR FFFF6B00]Jackett[/COLOR]', utils.translation(32602)) 63 | results = [] 64 | 65 | try: 66 | request_start_time = time.time() 67 | results = search_jackett(p_dialog, payload, method) 68 | request_end_time = time.time() 69 | request_time = round(request_end_time - request_start_time, 2) 70 | 71 | log.debug(f"All results: {results}") 72 | 73 | log.info(f"Jackett returned {len(results)} results in {request_time} seconds") 74 | except Exception as exc: 75 | utils.notify(utils.translation(32703)) 76 | log.error(f"Got exeption: {traceback.format_exc()}") 77 | finally: 78 | p_dialog.close() 79 | del p_dialog 80 | 81 | return results 82 | 83 | 84 | def parse_payload(method, payload): 85 | if method == 'general': 86 | if 'query' in payload: 87 | payload['title'] = payload['query'] 88 | payload['titles'] = { 89 | 'source': payload['query'] 90 | } 91 | else: 92 | payload = { 93 | 'title': payload, 94 | 'titles': { 95 | 'source': payload 96 | }, 97 | } 98 | 99 | payload['titles'] = dict((k.lower(), v) for k, v in list(payload['titles'].items())) 100 | 101 | if get_setting('kodi_language', bool): 102 | kodi_language = xbmc.getLanguage(xbmc.ISO_639_1) 103 | if not kodi_language: 104 | log.warning("Kodi returned empty language code...") 105 | elif kodi_language not in payload.get('titles', {}): 106 | log.info(f"No '{kodi_language}' translation available...") 107 | else: 108 | payload["search_title"] = payload["titles"][kodi_language] 109 | 110 | if "search_title" not in payload: 111 | log.info(f"Could not determine search title, falling back to normal title: {payload['title']}") 112 | payload["search_title"] = payload["title"] 113 | 114 | return payload 115 | 116 | 117 | def filter_results(method, results): 118 | log.debug(f"results before filtered: {results}") 119 | 120 | if get_setting('filter_keywords_enabled', bool): 121 | log.info(f"filtering keywords {len(results)}") 122 | results = filter.keywords(results) 123 | log.debug(f"filtering keywords results: {results}") 124 | 125 | if get_setting('filter_size_enabled', bool): 126 | log.info(f"filtering size {len(results)}") 127 | results = filter.size(method, results) 128 | log.debug(f"filtering size results: {results}") 129 | 130 | if get_setting('filter_include_resolution_enabled', bool): 131 | log.info(f"filtering resolution {len(results)}") 132 | results = filter.resolution(results) 133 | log.debug(f"filtering resolution results: {results}") 134 | 135 | if get_setting('filter_include_release', bool): 136 | log.info(f"filtering release type {len(results)}") 137 | results = filter.release_type(results) 138 | log.debug(f"filtering release type results: {results}") 139 | 140 | if get_setting('filter_exclude_no_seed', bool): 141 | log.info(f"filtering no seeds {len(results)}") 142 | results = filter.seed(results) 143 | log.debug(f"filtering no seeds results: {results}") 144 | 145 | # todo maybe rating and codec 146 | 147 | log.debug(f"Results resulted in {len(results)} results: {results}") 148 | 149 | return results 150 | 151 | 152 | def sort_results(results): 153 | sort_by = get_setting('sort_by', int) 154 | # 0 "Resolution" 155 | # 1 "Seeds" 156 | # 2 "Size" 157 | # 3 "Balanced" 158 | 159 | if sort_by == 0: 160 | sorted_results = sorted(results, key=lambda r: r["resolution"], reverse=True) 161 | elif sort_by == 1: 162 | sorted_results = sorted(results, key=lambda r: r['seeds'], reverse=True) 163 | elif sort_by == 2: 164 | sorted_results = sorted(results, key=lambda r: r['size'], reverse=True) 165 | else: 166 | # todo do something more advanced with the "balanced" option 167 | sorted_results = sorted(results, key=lambda r: r["seeds"] * 3 * r["resolution"], reverse=True) 168 | 169 | return sorted_results 170 | 171 | 172 | def search_jackett(p_dialog, payload, method): 173 | jackett = get_client(p_dialog) 174 | if jackett is None: 175 | utils.notify(utils.translation(32603), image=utils.get_icon_path()) 176 | return [] 177 | 178 | log.debug(f"Processing {method} with Jackett") 179 | p_dialog.update(message=utils.translation(32604)) 180 | if method == 'movie': 181 | res = jackett.search_movie(payload["search_title"], payload['year'], payload["imdb_id"]) 182 | elif method == 'season': 183 | res = jackett.search_season(payload["search_title"], payload["season"], payload["imdb_id"]) 184 | elif method == 'episode': 185 | res = jackett.search_episode(payload["search_title"], payload["season"], payload["episode"], payload["imdb_id"]) 186 | elif method == 'anime': 187 | log.warning("jackett provider does not yet support anime search") 188 | res = [] 189 | log.info(f"anime payload={payload}") 190 | # client.search_query(payload["search_title"], payload["season"], payload["episode"], payload["imdb_id"]) 191 | else: 192 | res = jackett.search_query(payload["search_title"]) 193 | 194 | log.debug(f"{method} search returned {len(res)} results") 195 | p_dialog.update(25, message=utils.translation(32750)) 196 | res = filter_results(method, res) 197 | 198 | res = jackett.async_magnet_resolve(res) 199 | 200 | p_dialog.update(90, message=utils.translation(32752)) 201 | res = filter.unique(res) 202 | log.info(f"filtering for unique items {len(res)}") 203 | log.debug(f"unique items results: {res}") 204 | 205 | p_dialog.update(95, message=utils.translation(32753)) 206 | res = sort_results(res) 207 | 208 | p_dialog.update(100, message=utils.translation(32754)) 209 | return res[:get_setting('max_results', int)] 210 | -------------------------------------------------------------------------------- /src/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.6 2 | # coding=utf-8 3 | import concurrent.futures 4 | import http.client as httplib 5 | import os 6 | import re 7 | import xml.etree.ElementTree as ET 8 | from urllib.parse import urljoin 9 | from xml.etree import ElementTree 10 | 11 | from kodi_six import xbmcgui 12 | from requests_toolbelt import sessions 13 | 14 | import torrent 15 | import utils 16 | from logger import log 17 | from utils import notify, translation, get_icon_path, human_size, get_resolution, get_release_type, get_setting, \ 18 | set_setting 19 | 20 | 21 | class Jackett(object): 22 | """docstring for Jackett""" 23 | 24 | _torznab_ns = "http://torznab.com/schemas/2015/feed" 25 | 26 | _torznab_elementum_mappings = { 27 | "tags": { 28 | "title": "name", 29 | "jackettindexer": "provider", 30 | "size": "size", 31 | }, 32 | "torznab_attrs": { 33 | "magneturl": "uri", 34 | "seeders": "seeds", 35 | "peers": "peers", 36 | "infohash": "info_hash", 37 | } 38 | } 39 | 40 | def __init__(self, host, api_key, p_dialog: xbmcgui.DialogProgressBG = None): 41 | super(Jackett, self).__init__() 42 | self.p_dialog = p_dialog 43 | self._api_key = api_key 44 | self._caps = {} 45 | 46 | self._session = sessions.BaseUrlSession(base_url=urljoin(host, "/api/v2.0/indexers/")) 47 | self.get_caps() 48 | 49 | def get_error(self, content): 50 | xml = ET.ElementTree(ET.fromstring(content)).getroot() 51 | if xml.tag == "error": 52 | return xml.attrib 53 | 54 | return None 55 | 56 | def get_caps(self): 57 | caps_resp = self._session.get("all/results/torznab", params={"t": "caps", "apikey": self._api_key}) 58 | 59 | if caps_resp.status_code != httplib.OK: 60 | notify(translation(32700).format(caps_resp.reason), image=get_icon_path()) 61 | log.error(f"Jackett return {caps_resp.reason}") 62 | set_setting('settings_validated', caps_resp.reason) 63 | return 64 | 65 | err = self.get_error(caps_resp.content) 66 | if err is not None: 67 | notify(translation(32700).format(err["description"]), image=get_icon_path()) 68 | log.error(f"got code {err['code']}: {err['description']}") 69 | set_setting('settings_validated', err["description"]) 70 | return 71 | 72 | set_setting('settings_validated', 'Success') 73 | 74 | xml = ET.ElementTree(ET.fromstring(caps_resp.content)).getroot() 75 | 76 | # todo handle gracefully, doesn't exist for individual trackers 77 | # self._caps["limits"] = xml.find("limits").attrib 78 | 79 | self._caps["search_tags"] = {} 80 | for type_tag in xml.findall('searching/*'): 81 | self._caps["search_tags"][type_tag.tag] = { 82 | "enabled": type_tag.attrib["available"] == "yes", 83 | "params": [p for p in type_tag.attrib['supportedParams'].split(",") if p], 84 | } 85 | 86 | log.info(f"Found capabilities: {self._caps}") 87 | # todo maybe categories are needed? 88 | 89 | def search_movie(self, title, year, imdb_id): 90 | if "search_tags" not in self._caps: 91 | notify(translation(32701), image=get_icon_path()) 92 | return [] 93 | 94 | movie_search_caps = self._caps["search_tags"]['movie-search'] 95 | if not movie_search_caps['enabled']: 96 | notify(translation(32702).format("movie"), image=get_icon_path()) 97 | log.warning("Jackett has no movie capabilities, please add a indexer that has movie capabilities. " 98 | "Falling back to query search...") 99 | return self.search_query(title + ' ' + str(year)) 100 | 101 | # todo what values are possible for imdb_id? 102 | movie_params = movie_search_caps["params"] 103 | request_params = { 104 | "t": "movie", 105 | } 106 | 107 | has_imdb_caps = 'imdbid' in movie_params 108 | log.debug(f"movie search; imdb_id={imdb_id}, has_imdb_caps={has_imdb_caps}") 109 | if imdb_id and has_imdb_caps and get_setting('search_by_imdb_key', bool): 110 | request_params["imdbid"] = imdb_id 111 | else: 112 | request_params["q"] = title + ' ' + str(year) 113 | log.debug(f"searching movie with query={request_params['q']}") 114 | 115 | return self._do_search_request(request_params) 116 | 117 | def search_shows(self, title, season=None, episode=None, imdb_id=None): 118 | if "search_tags" not in self._caps: 119 | notify(translation(32701), image=get_icon_path()) 120 | return [] 121 | 122 | tv_search_caps = self._caps["search_tags"]['tv-search'] 123 | if not tv_search_caps['enabled']: 124 | notify(translation(32702).format("show"), image=get_icon_path()) 125 | log.warning("Jackett has no tvsearch capabilities, please add a indexer that has tvsearch capabilities. " 126 | "Falling back to query search...") 127 | 128 | title_ep = title 129 | if bool(season): 130 | title_ep = "{} S{:0>2}".format(title_ep, season) 131 | if bool(episode): 132 | title_ep = "{}E{:0>2}".format(title_ep, episode) 133 | 134 | results = self.search_query(title_ep) 135 | if get_setting("search_season_on_episode", bool) and bool(season) and bool(episode): 136 | season_query = re.escape("{:0>2}".format(season)) 137 | results = results + self._filter_season(self.search_query("{} S{}".format(title, season_query)), season) 138 | 139 | return results 140 | 141 | # todo what values are possible for imdb_id? 142 | tv_params = tv_search_caps["params"] 143 | request_params = { 144 | "t": "tvsearch", 145 | } 146 | has_imdb_caps = 'imdbid' in tv_params 147 | log.debug(f"movie search; imdb_id={imdb_id}, has_imdb_caps={has_imdb_caps}") 148 | if imdb_id and has_imdb_caps and get_setting('search_by_imdb_key', bool): 149 | request_params["imdbid"] = imdb_id 150 | else: 151 | log.debug(f"searching tv show with query={title}, season={season}, episode={episode}") 152 | request_params["q"] = title 153 | if bool(season) and 'season' in tv_params: 154 | request_params["season"] = season 155 | if bool(episode) and 'ep' in tv_params: 156 | request_params["ep"] = episode 157 | 158 | results = self._do_search_request(request_params) 159 | if get_setting("search_season_on_episode", bool) and 'season' in request_params and 'ep' in request_params: 160 | del request_params['ep'] 161 | results = results + self._filter_season(self._do_search_request(request_params), season) 162 | 163 | return results 164 | 165 | def _filter_season(self, results, season): 166 | season_query = re.escape("{:0>2}".format(season)) 167 | s_re = re.compile(r'\bS(eason[\s.]?)?' + season_query + r'\b', re.IGNORECASE) 168 | ep_re = re.compile(r'\bE(p(isode)?[\s.]?)?\d+\b', re.IGNORECASE) 169 | 170 | return [ 171 | result for result in results 172 | if s_re.search(result['name']) and not ep_re.search(result['name']) 173 | ] 174 | 175 | def search_season(self, title, season, imdb_id): 176 | return self.search_shows(title, season=season, imdb_id=imdb_id) 177 | 178 | def search_episode(self, title, season, episode, imdb_id): 179 | return self.search_shows(title, season=season, episode=episode, imdb_id=imdb_id) 180 | 181 | def search_query(self, query): 182 | if not self._caps["search_tags"]['search']: 183 | notify(translation(32702).format("query"), image=get_icon_path()) 184 | log.warning("Jackett has no search capabilities, please add a indexer that has search capabilities.") 185 | return [] 186 | 187 | request_params = { 188 | "q": query 189 | } 190 | 191 | return self._do_search_request(request_params) 192 | 193 | def _get_with_progress(self, *args, **kwargs): 194 | if not self.p_dialog: 195 | r = self._session.get(*args, **kwargs) 196 | return r, r.content 197 | 198 | prog_from, prog_to = 0, 25 199 | self._update_progress(prog_from, prog_to, 0, 100) 200 | 201 | r = self._session.get(stream=True, *args, **kwargs) 202 | total_size = int(r.headers.get('content-length', 0)) 203 | search_resp = b"" 204 | for chunk in r.iter_content(64 * 1024): 205 | if chunk: 206 | search_resp += chunk 207 | self._update_progress(prog_from, prog_to, len(search_resp), total_size) 208 | 209 | return r, search_resp 210 | 211 | def _do_search_request(self, request_params): 212 | params = request_params.copy() 213 | if "apikey" not in params: 214 | params["apikey"] = self._api_key 215 | 216 | censored_params = params.copy() 217 | censored_key = censored_params['apikey'] 218 | censored_params['apikey'] = "{}{}{}".format(censored_key[0:2], "*" * 26, censored_key[-4:]) 219 | log.info(f"Making a request to Jackett using params {censored_params}") 220 | 221 | search_resp, content = self._get_with_progress("all/results/torznab", params=params) 222 | if search_resp.status_code != httplib.OK: 223 | notify(translation(32700).format(search_resp.reason), image=get_icon_path()) 224 | log.error(f"Jackett returned {search_resp.reason}") 225 | return [] 226 | 227 | err = self.get_error(content) 228 | if err is not None: 229 | notify(translation(32700).format(err["description"]), image=get_icon_path()) 230 | log.error(f"got code {err['code']}: {err['description']}") 231 | return [] 232 | 233 | log.info("Jackett returned response") 234 | log.debug("===============================") 235 | log.debug(content) 236 | log.debug("===============================") 237 | 238 | return self._parse_items(content) 239 | 240 | def _parse_items(self, resp_content): 241 | results = [] 242 | xml = ET.ElementTree(ET.fromstring(resp_content)) 243 | items = xml.getroot().findall("channel/item") 244 | log.info(f"Found {len(items)} items from response") 245 | for item in items: 246 | result = self._parse_item(item) 247 | if result is not None: 248 | results.append(result) 249 | 250 | return results 251 | 252 | # if we didn't get a magnet uri, attempt to resolve the magnet uri. 253 | # todo for some reason Elementum cannot resolve the link that gets proxied through Jackett. 254 | # So we will resolve it manually for Elementum for now. 255 | # In actuality, this should be fixed within Elementum 256 | def async_magnet_resolve(self, results): 257 | size = len(results) 258 | prog_from, prog_to = 25, 90 259 | self.p_dialog.update(prog_from, message=translation(32751).format(size)) 260 | 261 | failed, count = 0, 0 262 | with concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count() * 10) as executor: 263 | future_to_magnet = {executor.submit(torrent.get_magnet, res["uri"]): res for res in results} 264 | for future in concurrent.futures.as_completed(future_to_magnet): 265 | count += 1 266 | self._update_progress(prog_from, prog_to, count, size) 267 | res = future_to_magnet[future] 268 | try: 269 | magnet = future.result() 270 | except Exception as exc: 271 | log.warning('%r generated an exception: %s', res, exc) 272 | failed += 1 273 | else: 274 | if not magnet: 275 | continue 276 | log.debug(f"torrent: {res['name']} magnet uri {res['uri']} overridden by {magnet}") 277 | res["uri"] = magnet 278 | if not res["info_hash"]: 279 | res["info_hash"] = torrent.get_info_hash(res['uri']) 280 | 281 | log.warning(f"Failed to resolve {failed} magnet links") 282 | return results 283 | 284 | def _update_progress(self, pfrom, pto, current, total): 285 | if not self.p_dialog: 286 | return 287 | 288 | self.p_dialog.update(int((pfrom + (pto - pfrom) * (current / total)) // 1)) 289 | 290 | def _parse_item(self, item): 291 | result = { 292 | "name": None, 293 | "provider": "Unknown", 294 | "size": "Unknown", 295 | "uri": None, 296 | "seeds": "0", 297 | "peers": "0", 298 | "info_hash": "", 299 | "language": None, 300 | 301 | # todo would be nice to assign correct icons but that can be very time consuming due to the number 302 | # of indexers in Jackett 303 | "icon": get_icon_path(), 304 | 305 | "_size_bytes": -1 306 | } 307 | 308 | for ref in item: 309 | tag = ref.tag 310 | attrib = ref.attrib 311 | if tag == "{" + self._torznab_ns + "}attr": 312 | val = attrib["value"] 313 | if "name" in attrib and "value" in attrib and attrib["name"] and val and \ 314 | attrib["name"] in self._torznab_elementum_mappings["torznab_attrs"]: 315 | json = self._torznab_elementum_mappings["torznab_attrs"][attrib["name"]] 316 | result[json] = val 317 | continue 318 | 319 | if ref.tag in self._torznab_elementum_mappings["tags"] and ref.text is not None: 320 | json = self._torznab_elementum_mappings["tags"][ref.tag] 321 | val = ref.text.strip() 322 | 323 | result[json] = val 324 | 325 | if result["uri"] is None: 326 | link = item.find('link') 327 | jackett_uri = "" 328 | if link is not None: 329 | jackett_uri = link.text 330 | else: 331 | enclosure = item.find('enclosure') 332 | if enclosure is not None: 333 | jackett_uri = enclosure.attrib['url'] 334 | 335 | if jackett_uri != "": 336 | result["uri"] = jackett_uri 337 | 338 | # if not result["info_hash"]: 339 | # result["info_hash"] = torrent.get_info_hash(result['uri']) 340 | 341 | if result["name"] is None or result["uri"] is None: 342 | log.warning(f"Could not parse item; name = {result['name']}; uri = {result['uri']}") 343 | log.debug(f"Failed item is: {ElementTree.tostring(item, encoding='utf8')}") 344 | return None 345 | 346 | provider_color = utils.get_provider_color(result["provider"]) 347 | 348 | # result["name"] = result["name"].decode("utf-8") # might be needed for non-english items 349 | result["seeds"] = int(result["seeds"]) 350 | result["peers"] = int(result["peers"]) 351 | resolution = get_resolution(result["name"]) 352 | result["resolution"] = list(utils.resolutions.keys())[::-1].index(resolution) 353 | result["_resolution"] = resolution 354 | result["release_type"] = get_release_type(result["name"]) 355 | result["provider"] = f'[COLOR {provider_color}]{result["provider"]}[/COLOR]' 356 | 357 | if result["size"] != "Unknown": 358 | result["_size_bytes"] = int(result["size"]) 359 | result["size"] = human_size(result["_size_bytes"]) 360 | 361 | log.debug("final item: {}".format(result)) 362 | 363 | return result 364 | --------------------------------------------------------------------------------