├── addon.xml
├── changelog.txt
├── credits.txt
├── fanart.jpg
├── icon.png
└── lib
├── __init__.py
└── resources
├── __init__.py
└── lib
├── __init__.py
├── indexers
├── __init__.py
├── channels.py
├── docu.py
├── episodes.py
├── furk.py
├── movies.py
├── navigator.py
└── tvshows.py
├── modules
├── __init__.py
├── anilist.py
├── cache.py
├── cfdecoder.py
├── cfscrape.py
├── changelog.py
├── checker.py
├── cleandate.py
├── cleangenre.py
├── cleantitle.py
├── client.py
├── control.py
├── debrid.py
├── directstream.py
├── dom_parser.py
├── dom_parser2.py
├── downloader.py
├── downloader_bennu.py
├── favourites.py
├── filmon.py
├── get_source_info.py
├── jsunfuck.py
├── jsunpack.py
├── keepalive.py
├── libtools.py
├── log_utils.py
├── metacache.py
├── playcount.py
├── player.py
├── proxy.py
├── pyaes
│ ├── __init__.py
│ ├── aes.py
│ ├── blockfeeder.py
│ └── util.py
├── regex.py
├── source_utils.py
├── sources.py
├── thexem.py
├── trailer.py
├── trakt.py
├── tvmaze.py
├── unjuice.py
├── utils.py
├── views.py
├── weblogin.py
├── workers.py
├── youtube.py
└── youtube_menu.py
└── sources
└── __init__.py
/addon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | all
10 | Exodus Redux
11 |
12 |
13 |
--------------------------------------------------------------------------------
/changelog.txt:
--------------------------------------------------------------------------------
1 | Exodus Redux Module Changelog:
2 | 2.0.3
3 | - Fix Trakt and IMDb lists
4 | 2.0.2
5 | - Fix Debrid Only playback
6 | 2.0.1
7 | - Replaced Lambdascraper with Openscrapers
8 | 2.0.0
9 | - Rebased
10 | 1.0.9
11 | - New Trakt.tv API key
12 | 1.0.8
13 | - Resume fix for Kodi 18
14 | - Removed Channels
15 | 1.0.7
16 | - Fix "In Theaters"
17 | 1.0.6
18 | - Cleanup module selection
19 | - Fix "In Theaters"
20 | - Fix Extended Info on TV Shows
21 | 1.0.5 -
22 | 1.0.4 -
23 | 1.0.3 -
24 | 1.0.2 -
25 | 1.0.1
26 | - Initial commit
27 |
--------------------------------------------------------------------------------
/credits.txt:
--------------------------------------------------------------------------------
1 | ExodusRedux is a fork of Exodus. It has the same basic look and feel as the original Exodus with functionality and themes incorporated from the various iterations of Exodus. The project builds on the efforts of the developers listed below.
2 |
3 | - Lambda
4 | - TKNorris
5 | - Mr.Blamo
6 | - Nixgates
7 | - I-A-C
8 | - WilsonMagic
9 | - 13Clowns
10 | - Eggman
11 | - Jor-El
12 | - Maud'Dib
13 | - Jewbmx
14 | - Team Supremacy
15 | - and any others i may have missed!
16 |
--------------------------------------------------------------------------------
/fanart.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-A-C/script.module.exodusredux/ec2ce6d8e28db15c23563a76dfa48d40251ca5c2/fanart.jpg
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-A-C/script.module.exodusredux/ec2ce6d8e28db15c23563a76dfa48d40251ca5c2/icon.png
--------------------------------------------------------------------------------
/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-A-C/script.module.exodusredux/ec2ce6d8e28db15c23563a76dfa48d40251ca5c2/lib/__init__.py
--------------------------------------------------------------------------------
/lib/resources/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-A-C/script.module.exodusredux/ec2ce6d8e28db15c23563a76dfa48d40251ca5c2/lib/resources/__init__.py
--------------------------------------------------------------------------------
/lib/resources/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-A-C/script.module.exodusredux/ec2ce6d8e28db15c23563a76dfa48d40251ca5c2/lib/resources/lib/__init__.py
--------------------------------------------------------------------------------
/lib/resources/lib/indexers/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/resources/lib/indexers/channels.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | '''
4 | Covenant Add-on
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 |
21 | from resources.lib.modules import cleangenre
22 | from resources.lib.modules import control
23 | from resources.lib.modules import client
24 | from resources.lib.modules import metacache
25 | from resources.lib.modules import workers
26 | from resources.lib.modules import trakt
27 |
28 | import sys,re,json,urllib,urlparse,datetime
29 |
30 | params = dict(urlparse.parse_qsl(sys.argv[2].replace('?',''))) if len(sys.argv) > 1 else dict()
31 |
32 | action = params.get('action')
33 |
34 | class channels:
35 | def __init__(self):
36 | self.list = [] ; self.items = []
37 |
38 | self.uk_datetime = self.uk_datetime()
39 | self.systime = (self.uk_datetime).strftime('%Y%m%d%H%M%S%f')
40 | self.tm_img_link = 'https://image.tmdb.org/t/p/w%s%s'
41 | self.lang = control.apiLanguage()['trakt']
42 |
43 | self.sky_now_link = 'http://epgservices.sky.com/5.1.1/api/2.0/channel/json/%s/now/nn/0'
44 | self.sky_programme_link = 'http://tv.sky.com/programme/channel/%s/%s/%s.json'
45 |
46 |
47 | def get(self):
48 | channels = [
49 | ('01', 'Sky Premiere', '4021'),
50 | ('02', 'Sky Premiere +1', '1823'),
51 | ('03', 'Sky Showcase', '4033'),
52 | ('04', 'Sky Greats', '1815'),
53 | ('05', 'Sky Disney', '4013'),
54 | ('06', 'Sky Family', '4018'),
55 | ('07', 'Sky Action', '4014'),
56 | ('08', 'Sky Comedy', '4019'),
57 | ('09', 'Sky Crime', '4062'),
58 | ('10', 'Sky Drama', '4016'),
59 | ('11', 'Sky Sci Fi', '4017'),
60 | ('12', 'Sky Select', '4020'),
61 | ('13', 'Film4', '4044'),
62 | ('14', 'Film4 +1', '1629'),
63 | ('15', 'TCM', '3811'),
64 | ('16', 'TCM +1', '5275')
65 | ]
66 |
67 | threads = []
68 | for i in channels: threads.append(workers.Thread(self.sky_list, i[0], i[1], i[2]))
69 | [i.start() for i in threads]
70 | [i.join() for i in threads]
71 |
72 | threads = []
73 | for i in range(0, len(self.items)): threads.append(workers.Thread(self.items_list, self.items[i]))
74 | [i.start() for i in threads]
75 | [i.join() for i in threads]
76 |
77 | self.list = metacache.local(self.list, self.tm_img_link, 'poster2', 'fanart')
78 |
79 | try: self.list = sorted(self.list, key=lambda k: k['num'])
80 | except: pass
81 |
82 | self.channelDirectory(self.list)
83 | return self.list
84 |
85 |
86 | def sky_list(self, num, channel, id):
87 | try:
88 | url = self.sky_now_link % id
89 | result = client.request(url, timeout='10')
90 | result = json.loads(result)
91 | match = result['listings'][id][0]['url']
92 |
93 | dt1 = (self.uk_datetime).strftime('%Y-%m-%d')
94 | dt2 = int((self.uk_datetime).strftime('%H'))
95 | if (dt2 < 6): dt2 = 0
96 | elif (dt2 >= 6 and dt2 < 12): dt2 = 1
97 | elif (dt2 >= 12 and dt2 < 18): dt2 = 2
98 | elif (dt2 >= 18): dt2 = 3
99 |
100 | url = self.sky_programme_link % (id, str(dt1), str(dt2))
101 | result = client.request(url, timeout='10')
102 | result = json.loads(result)
103 | result = result['listings'][id]
104 | result = [i for i in result if i['url'] == match][0]
105 |
106 | year = result['d']
107 | year = re.findall('[(](\d{4})[)]', year)[0].strip()
108 | year = year.encode('utf-8')
109 |
110 | title = result['t']
111 | title = title.replace('(%s)' % year, '').strip()
112 | title = client.replaceHTMLCodes(title)
113 | title = title.encode('utf-8')
114 |
115 | self.items.append((title, year, channel, num))
116 | except:
117 | pass
118 |
119 |
120 | def items_list(self, i):
121 | try:
122 | item = trakt.SearchAll(i[0], i[1], True)[0]
123 |
124 | content = item.get('movie')
125 | if not content: content = item.get('show')
126 | item = content
127 |
128 | title = item.get('title')
129 | title = client.replaceHTMLCodes(title)
130 |
131 | originaltitle = title
132 |
133 | year = item.get('year', 0)
134 | year = re.sub('[^0-9]', '', str(year))
135 |
136 | imdb = item.get('ids', {}).get('imdb', '0')
137 | imdb = 'tt' + re.sub('[^0-9]', '', str(imdb))
138 |
139 | tmdb = str(item.get('ids', {}).get('tmdb', 0))
140 |
141 | premiered = item.get('released', '0')
142 | try: premiered = re.compile('(\d{4}-\d{2}-\d{2})').findall(premiered)[0]
143 | except: premiered = '0'
144 |
145 | genre = item.get('genres', [])
146 | genre = [x.title() for x in genre]
147 | genre = ' / '.join(genre).strip()
148 | if not genre: genre = '0'
149 |
150 | duration = str(item.get('Runtime', 0))
151 |
152 | rating = item.get('rating', '0')
153 | if not rating or rating == '0.0': rating = '0'
154 |
155 | votes = item.get('votes', '0')
156 | try: votes = str(format(int(votes), ',d'))
157 | except: pass
158 |
159 | mpaa = item.get('certification', '0')
160 | if not mpaa: mpaa = '0'
161 |
162 | tagline = item.get('tagline', '0')
163 |
164 | plot = item.get('overview', '0')
165 |
166 | people = trakt.getPeople(imdb, 'movies')
167 |
168 | director = writer = ''
169 | if 'crew' in people and 'directing' in people['crew']:
170 | director = ', '.join([director['person']['name'] for director in people['crew']['directing'] if director['job'].lower() == 'director'])
171 | if 'crew' in people and 'writing' in people['crew']:
172 | writer = ', '.join([writer['person']['name'] for writer in people['crew']['writing'] if writer['job'].lower() in ['writer', 'screenplay', 'author']])
173 |
174 | cast = []
175 | for person in people.get('cast', []):
176 | cast.append({'name': person['person']['name'], 'role': person['character']})
177 | cast = [(person['name'], person['role']) for person in cast]
178 |
179 | try:
180 | if self.lang == 'en' or self.lang not in item.get('available_translations', [self.lang]): raise Exception()
181 |
182 | trans_item = trakt.getMovieTranslation(imdb, self.lang, full=True)
183 |
184 | title = trans_item.get('title') or title
185 | tagline = trans_item.get('tagline') or tagline
186 | plot = trans_item.get('overview') or plot
187 | except:
188 | pass
189 |
190 | self.list.append({'title': title, 'originaltitle': originaltitle, 'year': year, 'premiered': premiered, 'genre': genre, 'duration': duration, 'rating': rating, 'votes': votes, 'mpaa': mpaa, 'director': director, 'writer': writer, 'cast': cast, 'plot': plot, 'tagline': tagline, 'imdb': imdb, 'tmdb': tmdb, 'poster': '0', 'channel': i[2], 'num': i[3]})
191 | except:
192 | pass
193 |
194 |
195 | def uk_datetime(self):
196 | dt = datetime.datetime.utcnow() + datetime.timedelta(hours = 0)
197 | d = datetime.datetime(dt.year, 4, 1)
198 | dston = d - datetime.timedelta(days=d.weekday() + 1)
199 | d = datetime.datetime(dt.year, 11, 1)
200 | dstoff = d - datetime.timedelta(days=d.weekday() + 1)
201 | if dston <= dt < dstoff:
202 | return dt + datetime.timedelta(hours = 1)
203 | else:
204 | return dt
205 |
206 |
207 | def channelDirectory(self, items):
208 | if items == None or len(items) == 0: control.idle() ; sys.exit()
209 |
210 | sysaddon = sys.argv[0]
211 |
212 | syshandle = int(sys.argv[1])
213 |
214 | addonPoster, addonBanner = control.addonPoster(), control.addonBanner()
215 |
216 | addonFanart, settingFanart = control.addonFanart(), control.setting('fanart')
217 |
218 | try: isOld = False ; control.item().getArt('type')
219 | except: isOld = True
220 |
221 | isPlayable = 'true' if not 'plugin' in control.infoLabel('Container.PluginName') else 'false'
222 |
223 | playbackMenu = control.lang(32063).encode('utf-8') if control.setting('hosts.mode') == '2' else control.lang(32064).encode('utf-8')
224 |
225 | queueMenu = control.lang(32065).encode('utf-8')
226 |
227 | refreshMenu = control.lang(32072).encode('utf-8')
228 |
229 |
230 | for i in items:
231 | try:
232 | label = '[B]%s[/B] : %s (%s)' % (i['channel'].upper(), i['title'], i['year'])
233 | sysname = urllib.quote_plus('%s (%s)' % (i['title'], i['year']))
234 | systitle = urllib.quote_plus(i['title'])
235 | imdb, tmdb, year = i['imdb'], i['tmdb'], i['year']
236 |
237 | meta = dict((k,v) for k, v in i.iteritems() if not v == '0')
238 | meta.update({'code': imdb, 'imdbnumber': imdb, 'imdb_id': imdb})
239 | meta.update({'tmdb_id': tmdb})
240 | meta.update({'mediatype': 'movie'})
241 | meta.update({'trailer': '%s?action=trailer&name=%s' % (sysaddon, sysname)})
242 | #meta.update({'trailer': 'plugin://script.extendedinfo/?info=playtrailer&&id=%s' % imdb})
243 | meta.update({'playcount': 0, 'overlay': 6})
244 | try: meta.update({'genre': cleangenre.lang(meta['genre'], self.lang)})
245 | except: pass
246 |
247 | sysmeta = urllib.quote_plus(json.dumps(meta))
248 |
249 |
250 | url = '%s?action=play&title=%s&year=%s&imdb=%s&meta=%s&t=%s' % (sysaddon, systitle, year, imdb, sysmeta, self.systime)
251 | sysurl = urllib.quote_plus(url)
252 |
253 |
254 | cm = []
255 |
256 | cm.append((queueMenu, 'RunPlugin(%s?action=queueItem)' % sysaddon))
257 |
258 | cm.append((refreshMenu, 'RunPlugin(%s?action=refresh)' % sysaddon))
259 |
260 | cm.append((playbackMenu, 'RunPlugin(%s?action=alterSources&url=%s&meta=%s)' % (sysaddon, sysurl, sysmeta)))
261 |
262 | if isOld == True:
263 | cm.append((control.lang2(19033).encode('utf-8'), 'Action(Info)'))
264 |
265 |
266 | item = control.item(label=label)
267 |
268 | art = {}
269 |
270 | if 'poster2' in i and not i['poster2'] == '0':
271 | art.update({'icon': i['poster2'], 'thumb': i['poster2'], 'poster': i['poster2']})
272 | elif 'poster' in i and not i['poster'] == '0':
273 | art.update({'icon': i['poster'], 'thumb': i['poster'], 'poster': i['poster']})
274 | else:
275 | art.update({'icon': addonPoster, 'thumb': addonPoster, 'poster': addonPoster})
276 |
277 | art.update({'banner': addonBanner})
278 |
279 | if settingFanart == 'true' and 'fanart' in i and not i['fanart'] == '0':
280 | item.setProperty('Fanart_Image', i['fanart'])
281 | elif not addonFanart == None:
282 | item.setProperty('Fanart_Image', addonFanart)
283 |
284 | item.setArt(art)
285 | item.addContextMenuItems(cm)
286 | item.setProperty('IsPlayable', isPlayable)
287 | item.setInfo(type='Video', infoLabels = meta)
288 |
289 | video_streaminfo = {'codec': 'h264'}
290 | item.addStreamInfo('video', video_streaminfo)
291 |
292 | control.addItem(handle=syshandle, url=url, listitem=item, isFolder=False)
293 | except:
294 | pass
295 |
296 | control.content(syshandle, 'files')
297 | control.directory(syshandle, cacheToDisc=True)
298 |
299 |
300 |
--------------------------------------------------------------------------------
/lib/resources/lib/indexers/docu.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | '''
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, either version 3 of the License, or
8 | (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | along with this program. If not, see .
17 | '''
18 |
19 | import requests,os,sys,re,datetime,urlparse,json,xbmcgui,xbmcplugin
20 |
21 | from resources.lib.modules import log_utils
22 | from resources.lib.modules import cache
23 | from resources.lib.modules import client
24 | from resources.lib.modules import control
25 |
26 | sysaddon = sys.argv[0] ; syshandle = int(sys.argv[1])
27 | artPath = control.artPath() ; addonFanart = control.addonFanart()
28 |
29 | class documentary:
30 | def __init__(self):
31 | self.list = []
32 |
33 | self.docu_link = 'https://topdocumentaryfilms.com/'
34 | self.docu_cat_list = 'https://topdocumentaryfilms.com/watch-online/'
35 |
36 | def root(self):
37 | try:
38 | html = client.request(self.docu_cat_list)
39 |
40 | cat_list = client.parseDOM(html, 'div', attrs={'class':'sitemap-wraper clear'})
41 | for content in cat_list:
42 | cat_info = client.parseDOM(content, 'h2')[0]
43 | cat_url = client.parseDOM(cat_info, 'a', ret='href')[0]
44 | cat_title = client.parseDOM(cat_info, 'a')[0].encode('utf-8', 'ignore').decode('utf-8').replace("&","&").replace(''',"'").replace('"','"').replace(''',"'").replace('–',' - ').replace('’',"'").replace('‘',"'").replace('&','&').replace('â','')
45 | try:
46 | cat_icon = client.parseDOM(content, 'img', ret='data-src')[0]
47 | except:
48 | cat_icon = client.parseDOM(content, 'img', ret='src')[0]
49 | cat_action = 'docuHeaven&docuCat=%s' % cat_url
50 | self.list.append({'name': cat_title, 'url': cat_url, 'image': cat_icon, 'action': cat_action})
51 | except Exception as e:
52 | log_utils.log('documentary root : Exception - ' + str(e))
53 | pass
54 |
55 | self.list = self.list[::-1]
56 | self.addDirectory(self.list)
57 | return self.list
58 |
59 | def docu_list(self, url):
60 | try:
61 | html = client.request(url)
62 |
63 | cat_list = client.parseDOM(html, 'article', attrs={'class':'module'})
64 | for content in cat_list:
65 | docu_info = client.parseDOM(content, 'h2')[0]
66 | docu_url = client.parseDOM(docu_info, 'a', ret='href')[0]
67 | docu_title = client.parseDOM(docu_info, 'a')[0].replace("&","&").replace(''',"'").replace('"','"').replace(''',"'").replace('–',' - ').replace('’',"'").replace('‘',"'").replace('&','&').replace('â','')
68 | try:
69 | docu_icon = client.parseDOM(content, 'img', ret='data-src')[0]
70 | except:
71 | docu_icon = client.parseDOM(content, 'img', ret='src')[0]
72 | docu_action = 'docuHeaven&docuPlay=%s' % docu_url
73 | self.list.append({'name': docu_title, 'url': docu_url, 'image': docu_icon, 'action': docu_action})
74 |
75 | try:
76 | navi_content = client.parseDOM(html, 'div', attrs={'class':'pagination module'})[0]
77 | links = client.parseDOM(navi_content, 'a', ret='href')
78 | tmp_list = []
79 | link = links[(len(links)-1)]
80 | docu_action = 'docuHeaven&docuCat=%s' % link
81 | self.list.append({'name': control.lang(32053).encode('utf-8'), 'url': link, 'image': control.addonNext(), 'action': docu_action})
82 | except:
83 | pass
84 | except Exception as e:
85 | log_utils.log('documentary docu_list : Exception - ' + str(e))
86 | pass
87 |
88 | self.addDirectory(self.list)
89 | return self.list
90 |
91 | def docu_play(self, url):
92 | try:
93 | docu_page = client.request(url)
94 | docu_item = client.parseDOM(docu_page, 'meta', attrs={'itemprop':'embedUrl'}, ret='content')[0]
95 | if 'http:' not in docu_item and 'https:' not in docu_item:
96 | docu_item = 'https:' + docu_item
97 | url = docu_item
98 |
99 | docu_title = client.parseDOM(docu_page, 'meta', attrs={'property':'og:title'}, ret='content')[0].encode('utf-8', 'ignore').decode('utf-8').replace("&","&").replace(''',"'").replace('"','"').replace(''',"'").replace('–',' - ').replace('’',"'").replace('‘',"'").replace('&','&').replace('â','')
100 |
101 | if 'youtube' in url:
102 | if 'videoseries' not in url:
103 | video_id = client.parseDOM(docu_page, 'div', attrs={'class':'youtube-player'}, ret='data-id')[0]
104 | url = 'plugin://plugin.video.youtube/play/?video_id=%s' % video_id
105 | else:
106 | pass
107 | elif 'dailymotion' in url:
108 | video_id = client.parseDOM(docu_page, 'div', attrs={'class':'youtube-player'}, ret='data-id')[0]
109 | url = self.getDailyMotionStream(video_id)
110 | else:
111 | log_utils.log('Play Documentary: Unknown Host: ' + str(url))
112 | control.infoDialog('Unknown Host - Report To Developer: ' + str(url), sound=True, icon='INFO')
113 |
114 | control.execute('PlayMedia(%s)' % url)
115 |
116 | # item = xbmcgui.ListItem(str(docu_title), iconImage='DefaultVideo.png', thumbnailImage='DefaultVideo.png')
117 | # item.setInfo(type='Video', infoLabels={'Title': str(docu_title), 'Plot': str(docu_title)})
118 | # item.setProperty('IsPlayable','true')
119 | # item.setPath(url)
120 | # control.resolve(int(sys.argv[1]), True, item)
121 | except Exception as e:
122 | log_utils.log('docu_play: Exception - ' + str(e))
123 | pass
124 |
125 | def sort_key(self, elem):
126 | if elem[0] == "auto":
127 | return 1
128 | else:
129 | return int(elem[0].split("@")[0])
130 |
131 | # Code originally written by gujal, as part of the DailyMotion Addon in the official Kodi Repo. Modified to fit the needs here.
132 | def getDailyMotionStream(self, id):
133 | headers = {'User-Agent':'Android'}
134 | cookie = {'Cookie':"lang=en_US; ff=off"}
135 | r = requests.get("http://www.dailymotion.com/player/metadata/video/"+id,headers=headers,cookies=cookie)
136 | content = r.json()
137 | if content.get('error') is not None:
138 | Error = (content['error']['title'])
139 | xbmc.executebuiltin('XBMC.Notification(Info:,'+ Error +' ,5000)')
140 | return
141 | else:
142 | cc= content['qualities']
143 |
144 | cc = cc.items()
145 |
146 | cc = sorted(cc,key=self.sort_key,reverse=True)
147 | m_url = ''
148 | other_playable_url = []
149 |
150 | for source,json_source in cc:
151 | source = source.split("@")[0]
152 | for item in json_source:
153 |
154 | m_url = item.get('url',None)
155 |
156 | if m_url:
157 | if source == "auto" :
158 | continue
159 |
160 | elif int(source) <= 2 :
161 | if 'video' in item.get('type',None):
162 | return m_url
163 |
164 | elif '.mnft' in m_url:
165 | continue
166 | other_playable_url.append(m_url)
167 |
168 | if len(other_playable_url) >0: # probably not needed, only for last resort
169 | for m_url in other_playable_url:
170 |
171 | if '.m3u8?auth' in m_url:
172 | rr = requests.get(m_url,cookies=r.cookies.get_dict() ,headers=headers)
173 | if rr.headers.get('set-cookie'):
174 | print 'adding cookie to url'
175 | strurl = re.findall('(http.+)',rr.text)[0].split('#cell')[0]+'|Cookie='+rr.headers['set-cookie']
176 | else:
177 | strurl = re.findall('(http.+)',rr.text)[0].split('#cell')[0]
178 | return strurl
179 |
180 | def addDirectoryItem(self, name, query, thumb, icon, context=None, queue=False, isAction=True, isFolder=True):
181 | try: name = control.lang(name).encode('utf-8')
182 | except: pass
183 | url = '%s?action=%s' % (sysaddon, query) if isAction == True else query
184 | thumb = os.path.join(artPath, thumb) if not artPath == None else icon
185 | cm = []
186 | if queue == True: cm.append((queueMenu, 'RunPlugin(%s?action=queueItem)' % sysaddon))
187 | if not context == None: cm.append((control.lang(context[0]).encode('utf-8'), 'RunPlugin(%s?action=%s)' % (sysaddon, context[1])))
188 | item = control.item(label=name)
189 | item.addContextMenuItems(cm)
190 | item.setArt({'icon': thumb, 'thumb': thumb})
191 | if not addonFanart == None: item.setProperty('Fanart_Image', addonFanart)
192 | control.addItem(handle=syshandle, url=url, listitem=item, isFolder=isFolder)
193 |
194 | def endDirectory(self):
195 | control.content(syshandle, 'addons')
196 | control.directory(syshandle, cacheToDisc=True)
197 |
198 | def addDirectory(self, items, queue=False, isFolder=True):
199 | if items == None or len(items) == 0: control.idle() ; sys.exit()
200 |
201 | sysaddon = sys.argv[0]
202 |
203 | syshandle = int(sys.argv[1])
204 |
205 | addonFanart, addonThumb, artPath = control.addonFanart(), control.addonThumb(), control.artPath()
206 |
207 | queueMenu = control.lang(32065).encode('utf-8')
208 |
209 | playRandom = control.lang(32535).encode('utf-8')
210 |
211 | addToLibrary = control.lang(32551).encode('utf-8')
212 |
213 | for i in items:
214 | try:
215 | name = i['name']
216 |
217 | if i['image'].startswith('http'): thumb = i['image']
218 | elif not artPath == None: thumb = os.path.join(artPath, i['image'])
219 | else: thumb = addonThumb
220 |
221 | item = control.item(label=name)
222 |
223 | if isFolder:
224 | url = '%s?action=%s' % (sysaddon, i['action'])
225 | try: url += '&url=%s' % urllib.quote_plus(i['url'])
226 | except: pass
227 | item.setProperty('IsPlayable', 'false')
228 | else:
229 | url = '%s?action=%s' % (sysaddon, i['action'])
230 | try: url += '&url=%s' % i['url']
231 | except: pass
232 | item.setProperty('IsPlayable', 'true')
233 | item.setInfo("mediatype", "video")
234 | item.setInfo("audio", '')
235 |
236 | item.setArt({'icon': thumb, 'thumb': thumb})
237 | if not addonFanart == None: item.setProperty('Fanart_Image', addonFanart)
238 |
239 | control.addItem(handle=syshandle, url=url, listitem=item, isFolder=isFolder)
240 | except:
241 | pass
242 |
243 | control.content(syshandle, 'addons')
244 | control.directory(syshandle, cacheToDisc=True)
245 |
--------------------------------------------------------------------------------
/lib/resources/lib/indexers/furk.py:
--------------------------------------------------------------------------------
1 | '''
2 | Exodus Redux Add-on
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with this program. If not, see .
16 | '''
17 |
18 | from resources.lib.modules import control
19 | import sys, requests, json, urllib, urlparse, os
20 |
21 | sysaddon = sys.argv[0] ; syshandle = int(sys.argv[1])
22 | accepted_extensions = ['mkv','mp4','avi', 'm4v']
23 |
24 | class furk:
25 | def __init__(self):
26 | self.base_link = "https://www.furk.net"
27 | self.meta_search_link = "/api/plugins/metasearch?api_key=%s&q=%s"
28 | self.get_user_files_link = "/api/file/get?api_key=%s"
29 | self.file_info_link = "/api/file/info?api_key%s"
30 | self.file_link_link = "/api/file/link?"
31 | self.protect_file_link = "/api/file/protect?"
32 | self.user_feeds_link = "/api/feed/get?"
33 | self.add_download_link = "/api/dl/add?"
34 | self.api_key = control.setting('furk.api')
35 | self.list = []
36 |
37 | def user_files(self):
38 | if self.api_key == '':
39 | return ''
40 | try:
41 | s = requests.Session()
42 | url = self.base_link + self.get_user_files_link % self.api_key
43 | p = s.get(url)
44 | p = json.loads(p.text)
45 | files = p['files']
46 | for i in files:
47 | name = i['name']
48 | id = i['id']
49 | url_dl = ''
50 | for x in accepted_extensions:
51 | if i['url_dl'].endswith(x):
52 | url_dl = i['url_dl']
53 | else:
54 | continue
55 | if url_dl == '':
56 | continue
57 | if not int(i['files_num_video_player']) > 1:
58 | if int(i['ss_num']) > 0:
59 | thumb = i['ss_urls'][0]
60 | else:
61 | thumb = ''
62 |
63 | self.addDirectoryItem(name , url_dl, thumb, '', False)
64 |
65 | else:
66 | pass
67 | self.endDirectory()
68 | return ''
69 | except:
70 | pass
71 | def search(self):
72 | from resources.lib.indexers import navigator
73 |
74 | navigator.navigator().addDirectoryItem('New Search', 'furkSearchNew', 'search.png', 'search.png')
75 | try: from sqlite3 import dbapi2 as database
76 | except: from pysqlite2 import dbapi2 as database
77 |
78 | dbcon = database.connect(control.searchFile)
79 | dbcur = dbcon.cursor()
80 |
81 | try:
82 | dbcur.executescript("CREATE TABLE IF NOT EXISTS furk (ID Integer PRIMARY KEY AUTOINCREMENT, term);")
83 | except:
84 | pass
85 |
86 | dbcur.execute("SELECT * FROM furk ORDER BY ID DESC")
87 | lst = []
88 |
89 | delete_option = False
90 | for (id,term) in dbcur.fetchall():
91 | if term not in str(lst):
92 | delete_option = True
93 | navigator.navigator().addDirectoryItem(term, 'furkMetaSearch&url=%s' % term, 'search.png', 'search.png')
94 | lst += [(term)]
95 | dbcur.close()
96 |
97 | if delete_option:
98 | navigator.navigator().addDirectoryItem(32605, 'clearCacheSearch', 'tools.png', 'DefaultAddonProgram.png')
99 |
100 | navigator.navigator().endDirectory()
101 |
102 | def search_new(self):
103 | control.idle()
104 |
105 | t = control.lang(32010).encode('utf-8')
106 | k = control.keyboard('', t) ; k.doModal()
107 | q = k.getText() if k.isConfirmed() else None
108 |
109 | if (q == None or q == ''): return
110 |
111 | try: from sqlite3 import dbapi2 as database
112 | except: from pysqlite2 import dbapi2 as database
113 |
114 | dbcon = database.connect(control.searchFile)
115 | dbcur = dbcon.cursor()
116 | dbcur.execute("INSERT INTO furk VALUES (?,?)", (None,q))
117 | dbcon.commit()
118 | dbcur.close()
119 | url = urllib.quote_plus(q)
120 | url = '%s?action=furkMetaSearch&url=%s' % (sys.argv[0], urllib.quote_plus(url))
121 | control.execute('Container.Update(%s)' % url)
122 |
123 | def furk_meta_search(self, url):
124 | if self.api_key == '':
125 | return ''
126 | try:
127 | s = requests.Session()
128 | url = (self.base_link + self.meta_search_link % (self.api_key, url)).replace(' ', '+')
129 | p = s.get(url)
130 | p = json.loads(p.text)
131 | files = p['files']
132 | for i in files:
133 | name = i['name']
134 | id = i['id']
135 | url_dl = ''
136 | for x in accepted_extensions:
137 | if 'url_dl' in i:
138 | if i['url_dl'].endswith(x):
139 | url_dl = i['url_dl']
140 | else:
141 | continue
142 | else:
143 | continue
144 | if url_dl == '':
145 | continue
146 | if not int(i['files_num_video_player']) > 1:
147 | if int(i['ss_num']) > 0:
148 | thumb = i['ss_urls'][0]
149 | else:
150 | thumb = ''
151 |
152 | self.addDirectoryItem(name, url_dl, thumb, '', False)
153 |
154 | else:
155 | # print(i['name'])
156 | # self.addDirectoryItem(i['name'].encode('utf-8'), i['url_dl'], '', '')
157 | continue
158 | self.endDirectory()
159 | return ''
160 | except:
161 | pass
162 |
163 | def addDirectoryItem(self, name, query, thumb, icon, isAction=True):
164 | try:
165 | name = name.encode('utf-8')
166 | url = '%s?action=%s' % (sysaddon, query) if isAction == True else query
167 | item = control.item(label=name)
168 | item.setArt({'icon': thumb, 'thumb': thumb})
169 | control.addItem(handle=syshandle, url=url, listitem=item)
170 | except:
171 | pass
172 |
173 | def endDirectory(self):
174 | control.content(syshandle, 'addons')
175 | control.directory(syshandle, cacheToDisc=True)
176 |
--------------------------------------------------------------------------------
/lib/resources/lib/modules/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/resources/lib/modules/anilist.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | '''
4 | Covenant Add-on
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import urlparse, urllib
21 |
22 | from resources.lib.modules import cache
23 | from resources.lib.modules import client
24 | from resources.lib.modules import cleantitle
25 | from resources.lib.modules import utils
26 |
27 |
28 | def _getAniList(url):
29 | try:
30 | url = urlparse.urljoin('https://anilist.co', '/api%s' % url)
31 | return client.request(url, headers={'Authorization': '%s %s' % cache.get(_getToken, 1), 'Content-Type': 'application/x-www-form-urlencoded'})
32 | except:
33 | pass
34 |
35 |
36 | def _getToken():
37 | result = urllib.urlencode({'grant_type': 'client_credentials', 'client_id': 'kodiexodus-7erse', 'client_secret': 'XelwkDEccpHX2uO8NpqIjVf6zeg'})
38 | result = client.request('https://anilist.co/api/auth/access_token', post=result, headers={'Content-Type': 'application/x-www-form-urlencoded'}, error=True)
39 | result = utils.json_loads_as_str(result)
40 | return result['token_type'], result['access_token']
41 |
42 |
43 | def getAlternativTitle(title):
44 | try:
45 | t = cleantitle.get(title)
46 |
47 | r = _getAniList('/anime/search/%s' % title)
48 | r = [(i.get('title_romaji'), i.get('synonyms', [])) for i in utils.json_loads_as_str(r) if cleantitle.get(i.get('title_english', '')) == t]
49 | r = [i[1][0] if i[0] == title and len(i[1]) > 0 else i[0] for i in r]
50 | r = [i for i in r if i if i != title][0]
51 | return r
52 | except:
53 | pass
54 |
--------------------------------------------------------------------------------
/lib/resources/lib/modules/cache.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Covenant Add-on
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, either version 3 of the License, or
8 | (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | along with this program. If not, see .
17 | """
18 | import ast
19 | import hashlib
20 | import re
21 | import time
22 | from resources.lib.modules import control
23 |
24 | try:
25 | from sqlite3 import dbapi2 as db, OperationalError
26 | except ImportError:
27 | from pysqlite2 import dbapi2 as db, OperationalError
28 |
29 | """
30 | This module is used to get/set cache for every action done in the system
31 | """
32 |
33 | cache_table = 'cache'
34 |
35 |
36 | def get(function, duration, *args):
37 | # type: (function, int, object) -> object or None
38 | """
39 | Gets cached value for provided function with optional arguments, or executes and stores the result
40 | :param function: Function to be executed
41 | :param duration: Duration of validity of cache in hours
42 | :param args: Optional arguments for the provided function
43 | """
44 |
45 | try:
46 | key = _hash_function(function, args)
47 | cache_result = cache_get(key)
48 | if cache_result:
49 | if _is_cache_valid(cache_result['date'], duration):
50 | return ast.literal_eval(cache_result['value'].encode('utf-8'))
51 |
52 | fresh_result = repr(function(*args))
53 | if not fresh_result:
54 | # If the cache is old, but we didn't get fresh result, return the old cache
55 | if cache_result:
56 | return cache_result
57 | return None
58 |
59 | cache_insert(key, fresh_result)
60 | return ast.literal_eval(fresh_result.encode('utf-8'))
61 | except Exception:
62 | return None
63 |
64 |
65 | def timeout(function, *args):
66 | try:
67 | key = _hash_function(function, args)
68 | result = cache_get(key)
69 | return int(result['date'])
70 | except Exception:
71 | return None
72 |
73 |
74 | def cache_get(key):
75 | # type: (str, str) -> dict or None
76 | try:
77 | cursor = _get_connection_cursor()
78 | cursor.execute("SELECT * FROM %s WHERE key = ?" % cache_table, [key])
79 | return cursor.fetchone()
80 | except OperationalError:
81 | return None
82 |
83 |
84 | def cache_insert(key, value):
85 | # type: (str, str) -> None
86 | cursor = _get_connection_cursor()
87 | now = int(time.time())
88 | cursor.execute(
89 | "CREATE TABLE IF NOT EXISTS %s (key TEXT, value TEXT, date INTEGER, UNIQUE(key))"
90 | % cache_table
91 | )
92 | update_result = cursor.execute(
93 | "UPDATE %s SET value=?,date=? WHERE key=?"
94 | % cache_table, (value, now, key))
95 |
96 | if update_result.rowcount is 0:
97 | cursor.execute(
98 | "INSERT INTO %s Values (?, ?, ?)"
99 | % cache_table, (key, value, now)
100 | )
101 |
102 | cursor.connection.commit()
103 |
104 |
105 | def cache_clear():
106 | try:
107 | cursor = _get_connection_cursor()
108 |
109 | for t in [cache_table, 'rel_list', 'rel_lib']:
110 | try:
111 | cursor.execute("DROP TABLE IF EXISTS %s" % t)
112 | cursor.execute("VACUUM")
113 | cursor.commit()
114 | except:
115 | pass
116 | except:
117 | pass
118 |
119 |
120 | def cache_clear_meta():
121 | try:
122 | cursor = _get_connection_cursor_meta()
123 |
124 | for t in ['meta']:
125 | try:
126 | cursor.execute("DROP TABLE IF EXISTS %s" % t)
127 | cursor.execute("VACUUM")
128 | cursor.commit()
129 | except:
130 | pass
131 | except:
132 | pass
133 |
134 |
135 | def cache_clear_providers():
136 | try:
137 | cursor = _get_connection_cursor_providers()
138 |
139 | for t in ['rel_src', 'rel_url']:
140 | try:
141 | cursor.execute("DROP TABLE IF EXISTS %s" % t)
142 | cursor.execute("VACUUM")
143 | cursor.commit()
144 | except:
145 | pass
146 | except:
147 | pass
148 |
149 |
150 | def cache_clear_search():
151 | try:
152 | cursor = _get_connection_cursor_search()
153 |
154 | for t in ['tvshow', 'movies']:
155 | try:
156 | cursor.execute("DROP TABLE IF EXISTS %s" % t)
157 | cursor.execute("VACUUM")
158 | cursor.commit()
159 | except:
160 | pass
161 | except:
162 | pass
163 |
164 |
165 | def cache_clear_all():
166 | cache_clear()
167 | cache_clear_meta()
168 | cache_clear_providers()
169 |
170 |
171 | def _get_connection_cursor():
172 | conn = _get_connection()
173 | return conn.cursor()
174 |
175 |
176 | def _get_connection():
177 | control.makeFile(control.dataPath)
178 | conn = db.connect(control.cacheFile)
179 | conn.row_factory = _dict_factory
180 | return conn
181 |
182 |
183 | def _get_connection_cursor_meta():
184 | conn = _get_connection_meta()
185 | return conn.cursor()
186 |
187 |
188 | def _get_connection_meta():
189 | control.makeFile(control.dataPath)
190 | conn = db.connect(control.metacacheFile)
191 | conn.row_factory = _dict_factory
192 | return conn
193 |
194 |
195 | def _get_connection_cursor_providers():
196 | conn = _get_connection_providers()
197 | return conn.cursor()
198 |
199 |
200 | def _get_connection_providers():
201 | control.makeFile(control.dataPath)
202 | conn = db.connect(control.providercacheFile)
203 | conn.row_factory = _dict_factory
204 | return conn
205 |
206 |
207 | def _get_connection_cursor_search():
208 | conn = _get_connection_search()
209 | return conn.cursor()
210 |
211 |
212 | def _get_connection_search():
213 | control.makeFile(control.dataPath)
214 | conn = db.connect(control.searchFile)
215 | conn.row_factory = _dict_factory
216 | return conn
217 |
218 |
219 | def _dict_factory(cursor, row):
220 | d = {}
221 | for idx, col in enumerate(cursor.description):
222 | d[col[0]] = row[idx]
223 | return d
224 |
225 |
226 | def _hash_function(function_instance, *args):
227 | return _get_function_name(function_instance) + _generate_md5(args)
228 |
229 |
230 | def _get_function_name(function_instance):
231 | return re.sub('.+\smethod\s|.+function\s|\sat\s.+|\sof\s.+', '', repr(function_instance))
232 |
233 |
234 | def _generate_md5(*args):
235 | md5_hash = hashlib.md5()
236 | [md5_hash.update(str(arg)) for arg in args]
237 | return str(md5_hash.hexdigest())
238 |
239 |
240 | def _is_cache_valid(cached_time, cache_timeout):
241 | now = int(time.time())
242 | diff = now - cached_time
243 | return (cache_timeout * 3600) > diff
244 |
245 |
246 | def cache_version_check():
247 | if _find_cache_version():
248 | cache_clear();cache_clear_meta();cache_clear_providers()
249 | control.infoDialog(control.lang(32057).encode('utf-8'), sound=True, icon='INFO')
250 |
251 |
252 | def _find_cache_version():
253 | import os
254 |
255 | versionFile = os.path.join(control.dataPath, 'cache.v')
256 | try:
257 | with open(versionFile, 'rb') as fh: oldVersion = fh.read()
258 | except: oldVersion = '0'
259 | try:
260 | curVersion = control.addon('script.module.exodusredux').getAddonInfo('version')
261 | if oldVersion != curVersion:
262 | with open(versionFile, 'wb') as fh: fh.write(curVersion)
263 | return True
264 | else: return False
265 | except: return False
266 |
267 | def _find_cache_versionAlt(): #Added to keep track of plugin.video.exodusredux version
268 |
269 | import os
270 | versionFile = os.path.join(control.dataPath, 'cache.v2')
271 | try:
272 | with open(versionFile, 'rb') as fh: oldVersion = fh.read()
273 | except: oldVersion = '0'
274 | try:
275 | curVersion = control.addon('plugin.video.exodusredux').getAddonInfo('version')
276 | if oldVersion != curVersion:
277 | with open(versionFile, 'wb') as fh: fh.write(curVersion)
278 | return True
279 | else: return False
280 | except: return False
281 |
--------------------------------------------------------------------------------
/lib/resources/lib/modules/cfdecoder.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # --------------------------------------------------------------------------------
3 | # Cloudflare decoder
4 | # --------------------------------------------------------------------------------
5 |
6 | import re
7 | import time
8 | import urllib
9 | import urlparse
10 |
11 | from decimal import Decimal, ROUND_UP
12 |
13 |
14 | class Cloudflare:
15 | def __init__(self, response):
16 | self.timeout = 5
17 | self.domain = urlparse.urlparse(response["url"])[1]
18 | self.protocol = urlparse.urlparse(response["url"])[0]
19 | self.js_data = {}
20 | self.header_data = {}
21 |
22 | if not "var s,t,o,p,b,r,e,a,k,i,n,g,f" in response["data"] or "chk_jschl" in response["url"]:
23 | return
24 |
25 | try:
26 | self.js_data["auth_url"] = \
27 | re.compile('