├── 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 |
--------------------------------------------------------------------------------