├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── addon.xml ├── fanart.png ├── icon.png ├── jacktook.py ├── jacktook.select.json ├── jacktook.select.zip ├── lib ├── __init__.py ├── api │ ├── __init__.py │ ├── burst │ │ ├── provider.py │ │ └── utils.py │ ├── fanart │ │ ├── base.py │ │ ├── fanart.py │ │ └── utils.py │ ├── jacktorr │ │ └── jacktorr.py │ ├── plex │ │ ├── __init__.py │ │ ├── media_server.py │ │ ├── models │ │ │ └── plex_models.py │ │ ├── plex.py │ │ ├── settings.py │ │ └── utils.py │ ├── tmdbv3api │ │ ├── __init__.py │ │ ├── as_obj.py │ │ ├── exceptions.py │ │ ├── objs │ │ │ ├── __init__.py │ │ │ ├── account.py │ │ │ ├── anime.py │ │ │ ├── auth.py │ │ │ ├── certification.py │ │ │ ├── change.py │ │ │ ├── collection.py │ │ │ ├── company.py │ │ │ ├── configuration.py │ │ │ ├── credit.py │ │ │ ├── discover.py │ │ │ ├── episode.py │ │ │ ├── find.py │ │ │ ├── genre.py │ │ │ ├── group.py │ │ │ ├── keyword.py │ │ │ ├── list.py │ │ │ ├── movie.py │ │ │ ├── network.py │ │ │ ├── person.py │ │ │ ├── provider.py │ │ │ ├── review.py │ │ │ ├── search.py │ │ │ ├── season.py │ │ │ ├── trending.py │ │ │ └── tv.py │ │ ├── tmdb.py │ │ └── utils.py │ ├── trakt │ │ ├── base_cache.py │ │ ├── lists_cache.py │ │ ├── main_cache.py │ │ ├── trakt.py │ │ ├── trakt_cache.py │ │ └── trakt_utils.py │ └── tvdbapi │ │ ├── __init__.py │ │ └── tvdbapi.py ├── clients │ ├── anilist.py │ ├── anizip.py │ ├── base.py │ ├── burst │ │ ├── client.py │ │ └── providers.py │ ├── debrid │ │ ├── alldebrid.py │ │ ├── base.py │ │ ├── easydebrid.py │ │ ├── premiumize.py │ │ ├── realdebrid.py │ │ └── torbox.py │ ├── elfhosted.py │ ├── fma.py │ ├── jackett.py │ ├── jackgram │ │ ├── client.py │ │ └── utils.py │ ├── medifusion.py │ ├── peerflix.py │ ├── plex.py │ ├── prowlarr.py │ ├── search.py │ ├── simkl.py │ ├── stremio │ │ ├── addons_manager.py │ │ ├── catalogs.py │ │ ├── client.py │ │ ├── stream.py │ │ ├── stremio.py │ │ └── ui.py │ ├── tmdb │ │ ├── tmdb.py │ │ └── utils.py │ ├── torrentio.py │ ├── trakt │ │ ├── paginator.py │ │ └── trakt.py │ └── zilean.py ├── db │ ├── anime.py │ ├── bookmark.py │ ├── cached.py │ └── main.py ├── domain │ └── torrent.py ├── downloader.py ├── gui │ ├── base_window.py │ ├── custom_dialogs.py │ ├── custom_progress.py │ ├── filter_items_window.py │ ├── filter_type_window.py │ ├── next_window.py │ ├── play_window.py │ ├── resolver_window.py │ ├── resume_window.py │ ├── source_pack_select.py │ ├── source_pack_window.py │ └── source_select.py ├── navigation.py ├── player.py ├── router.py ├── updater.py ├── utils │ ├── anime │ │ ├── anilist.py │ │ ├── anizip.py │ │ └── simkl.py │ ├── clients │ │ └── utils.py │ ├── debrid │ │ ├── ad_utils.py │ │ ├── debrid_utils.py │ │ ├── ed_utils.py │ │ ├── pm_utils.py │ │ ├── rd_utils.py │ │ └── torbox_utils.py │ ├── general │ │ ├── items_menus.py │ │ ├── processors.py │ │ └── utils.py │ ├── kodi │ │ ├── kodi_formats.py │ │ ├── settings.py │ │ └── utils.py │ ├── localization │ │ ├── countries.py │ │ └── language_detection.py │ ├── parsers │ │ └── xmltodict.py │ ├── player │ │ └── utils.py │ ├── plex │ │ └── utils.py │ ├── stremio │ │ └── catalogs_utils.py │ ├── torrent │ │ ├── flatbencode.py │ │ ├── resolve_to_magnet.py │ │ └── torrserver_utils.py │ ├── torrentio │ │ └── utils.py │ └── views │ │ ├── last_files.py │ │ ├── last_titles.py │ │ └── shows.py └── vendor │ ├── bencodepy │ ├── bencode │ │ ├── BTL.py │ │ ├── __init__.py │ │ └── exceptions.py │ └── bencodepy │ │ ├── __init__.py │ │ ├── common.py │ │ ├── compat.py │ │ ├── decoder.py │ │ ├── encoder.py │ │ └── exceptions.py │ └── torf │ ├── __init__.py │ ├── _errors.py │ ├── _generate.py │ ├── _magnet.py │ ├── _reuse.py │ ├── _stream.py │ ├── _torrent.py │ └── _utils.py ├── resources ├── __init__.py ├── img │ ├── anime.png │ ├── clear.png │ ├── cloud.png │ ├── donate.png │ ├── download.png │ ├── genre.png │ ├── history.png │ ├── magnet.png │ ├── magnet2.png │ ├── movies.png │ ├── nextpage.png │ ├── search.png │ ├── settings.png │ ├── status.png │ ├── tmdb.png │ ├── trakt.png │ ├── trending.png │ └── tv.png ├── language │ ├── English │ │ └── strings.po │ ├── Portuguese (Brazil) │ │ └── strings.po │ ├── Portuguese │ │ └── strings.po │ └── Spanish │ │ └── strings.po ├── screenshots │ ├── home.png │ ├── settings.png │ └── tv.png ├── settings.xml └── skins │ └── Default │ ├── 1080i │ ├── custom_progress_dialog.xml │ ├── customdialog.xml │ ├── filter_items.xml │ ├── filter_type.xml │ ├── playing_next.xml │ ├── resolver.xml │ ├── resume_dialog.xml │ ├── source_select.xml │ └── source_select_direct.xml │ └── media │ ├── AddonWindow │ ├── black.png │ └── white.png │ ├── Button │ └── close.png │ ├── circle.png │ ├── jtk_clearlogo.png │ ├── jtk_fanart.png │ ├── left-circle.png │ ├── spinner.png │ ├── texture.png │ └── white.png ├── scripts └── manage_labels.py └── service.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.log 4 | tmp/ 5 | tests/ 6 | test2/ 7 | *.py[cod] 8 | *.egg 9 | build 10 | htmlcov 11 | venv/ 12 | __pycache__ 13 | 14 | 15 | -------------------------------------------------------------------------------- /addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | video 10 | 11 | 12 | 13 | jacktook 14 | Kodi addon for torrent streaming 15 | GPL-2.0-only 16 | https://github.com/Sam-Max/plugin.video.jacktook 17 | 18 | icon.png 19 | fanart.png 20 | resources/screenshots/home.png 21 | resources/screenshots/tv.png 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /fanart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/fanart.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/icon.png -------------------------------------------------------------------------------- /jacktook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from lib.router import addon_router 4 | 5 | addon_router() 6 | -------------------------------------------------------------------------------- /jacktook.select.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "[B][COLOR snow]Jacktook[/COLOR][/B]", 3 | "plugin": "plugin.video.jacktook", 4 | "priority": 200, 5 | "is_resolvable": "true", 6 | "play_movie": "plugin://plugin.video.jacktook/?action=search&mode=movies&rescrape=True&query={title}&ids=%7B%22tmdb_id%22%3A+%22{tmdb}%22%2C+%22tvdb_id%22%3A+%22{tvdb}%22%2C+%22imdb_id%22%3A+%22{imdb}%22%7D", 7 | "play_episode": "plugin://plugin.video.jacktook/?action=search&mode=tv&rescrape=True&query={showname}&ids=%7B%22tmdb_id%22%3A+%22{tmdb}%22%2C+%22tvdb_id%22%3A+%22{tvdb}%22%2C+%22imdb_id%22%3A+%22{imdb}%22%7D&tv_data=%7B%22name%22%3A+%22{title}%22%2C+%22episode%22%3A+%22{episode}%22%2C+%22season%22%3A+%22{season}%22%7D" 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /jacktook.select.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/jacktook.select.zip -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/lib/__init__.py -------------------------------------------------------------------------------- /lib/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/lib/api/__init__.py -------------------------------------------------------------------------------- /lib/api/burst/utils.py: -------------------------------------------------------------------------------- 1 | 2 | def assure_str(s): 3 | return s 4 | 5 | def str_to_bytes(s): 6 | return s.encode() 7 | 8 | 9 | def bytes_to_str(b): 10 | return b.decode() 11 | -------------------------------------------------------------------------------- /lib/api/fanart/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | 4 | 5 | def serialize_sets(obj): 6 | return sorted([str(i) for i in obj]) if isinstance(obj, set) else obj 7 | 8 | 9 | def valid_id_or_none(id_number): 10 | """ 11 | Helper function to check that an id number from an indexer is valid 12 | Checks if we have an id_number and it is not 0 or "0" 13 | :param id_number: The id number to check 14 | :return: The id number if valid, else None 15 | """ 16 | return id_number if id_number and id_number != "0" else None 17 | 18 | 19 | def md5_hash(value): 20 | """ 21 | Returns MD5 hash of given value 22 | :param value: object to hash 23 | :type value: object 24 | :return: Hexdigest of hash 25 | :rtype: str 26 | """ 27 | if isinstance(value, (tuple, dict, list, set)): 28 | value = json.dumps(value, sort_keys=True, default=serialize_sets) 29 | return hashlib.md5(str(value).encode("utf-8")).hexdigest() 30 | 31 | 32 | def extend_array(array1, array2): 33 | """ 34 | Safe combining of two lists 35 | :param array1: List to combine 36 | :type array1: list 37 | :param array2: List to combine 38 | :type array2: list 39 | :return: Combined lists 40 | :rtype: list 41 | """ 42 | result = [] 43 | if array1 and isinstance(array1, list): 44 | result.extend(array1) 45 | if array2 and isinstance(array2, list): 46 | result.extend(array2) 47 | return result 48 | -------------------------------------------------------------------------------- /lib/api/plex/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/lib/api/plex/__init__.py -------------------------------------------------------------------------------- /lib/api/plex/settings.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | class Settings(): 4 | identifier: str = str(uuid4()) 5 | product_name: str = 'Jacktook' 6 | plex_requests_timeout: int = 30 7 | plex_matching_token: str 8 | 9 | 10 | settings = Settings() 11 | -------------------------------------------------------------------------------- /lib/api/plex/utils.py: -------------------------------------------------------------------------------- 1 | 2 | class PlexUnauthorizedError(BaseException): 3 | pass 4 | 5 | 6 | class HTTPException(BaseException): 7 | def __init__( 8 | self, 9 | status_code: int, 10 | detail: str, 11 | ): 12 | self.status_code = status_code 13 | self.detail = detail 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/__init__.py: -------------------------------------------------------------------------------- 1 | from .objs.account import Account 2 | from .objs.auth import Authentication 3 | from .objs.certification import Certification 4 | from .objs.change import Change 5 | from .objs.collection import Collection 6 | from .objs.company import Company 7 | from .objs.configuration import Configuration 8 | from .objs.credit import Credit 9 | from .objs.discover import Discover 10 | from .objs.episode import Episode 11 | from .objs.find import Find 12 | from .objs.genre import Genre 13 | from .objs.group import Group 14 | from .objs.keyword import Keyword 15 | from .objs.list import List 16 | from .objs.movie import Movie 17 | from .objs.network import Network 18 | from .objs.person import Person 19 | from .objs.provider import Provider 20 | from .objs.review import Review 21 | from .objs.search import Search 22 | from .objs.season import Season 23 | from .objs.trending import Trending 24 | from .objs.tv import TV 25 | from .tmdb import TMDb 26 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/as_obj.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import sys 3 | from .exceptions import TMDbException 4 | 5 | 6 | class AsObj: 7 | def __init__(self, json=None, key=None, dict_key=False, dict_key_name=None): 8 | self._json = json if json else {} 9 | self._key = key 10 | self._dict_key = dict_key 11 | self._dict_key_name = dict_key_name 12 | self._obj_list = [] 13 | self._list_only = False 14 | if isinstance(self._json, list): 15 | self._obj_list = [AsObj(o) if isinstance(o, (dict, list)) else o for o in self._json] 16 | self._list_only = True 17 | elif dict_key: 18 | self._obj_list = [ 19 | AsObj({k: v}, key=k, dict_key_name=dict_key_name) if isinstance(v, (dict, list)) else v 20 | for k, v in self._json.items() 21 | ] 22 | self._list_only = True 23 | else: 24 | for key, value in self._json.items(): 25 | if isinstance(value, (dict, list)): 26 | if self._key and key == self._key: 27 | final = AsObj(value, dict_key=isinstance(value, dict), dict_key_name=key) 28 | self._obj_list = final 29 | else: 30 | final = AsObj(value) 31 | else: 32 | final = value 33 | if dict_key_name: 34 | setattr(self, dict_key_name, key) 35 | setattr(self, key, final) 36 | 37 | def _dict(self): 38 | return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} 39 | 40 | def __delitem__(self, key): 41 | return delattr(self, key) 42 | 43 | def __getitem__(self, key): 44 | if isinstance(key, int) and self._obj_list: 45 | return self._obj_list[key] 46 | else: 47 | return getattr(self, key) 48 | 49 | def __iter__(self): 50 | return (o for o in self._obj_list) if self._obj_list else iter(self._dict()) 51 | 52 | def __len__(self): 53 | return len(self._obj_list) if self._obj_list else len(self._dict()) 54 | 55 | def __repr__(self): 56 | return str(self._obj_list) if self._list_only else str(self._dict()) 57 | 58 | def __setitem__(self, key, value): 59 | return setattr(self, key, value) 60 | 61 | def __str__(self): 62 | return str(self._obj_list) if self._list_only else str(self._dict()) 63 | 64 | if sys.version_info >= (3, 8): 65 | def __reversed__(self): 66 | return reversed(self._dict()) 67 | 68 | if sys.version_info >= (3, 9): 69 | def __class_getitem__(self, key): 70 | return self.__dict__.__class_getitem__(key) 71 | 72 | def __ior__(self, value): 73 | return self._dict().__ior__(value) 74 | 75 | def __or__(self, value): 76 | return self._dict().__or__(value) 77 | 78 | def copy(self): 79 | return AsObj(self._json.copy(), key=self._key, dict_key=self._dict_key, dict_key_name=self._dict_key_name) 80 | 81 | def get(self, key, value=None): 82 | return self._dict().get(key, value) 83 | 84 | def items(self): 85 | return self._dict().items() 86 | 87 | def keys(self): 88 | return self._dict().keys() 89 | 90 | def pop(self, key, value=None): 91 | return self.__dict__.pop(key, value) 92 | 93 | def popitem(self): 94 | return self.__dict__.popitem() 95 | 96 | def setdefault(self, key, value=None): 97 | return self.__dict__.setdefault(key, value) 98 | 99 | def update(self, entries): 100 | return self.__dict__.update(entries) 101 | 102 | def values(self): 103 | return self._dict().values() 104 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/exceptions.py: -------------------------------------------------------------------------------- 1 | class TMDbException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/lib/api/tmdbv3api/objs/__init__.py -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/anime.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | from lib.api.tmdbv3api.utils import get_current_date, get_dates, years_tvshows 3 | 4 | 5 | class TmdbAnime(TMDb): 6 | _urls = { 7 | "discover_tv": "/discover/tv", 8 | "discover_movie": "/discover/movie", 9 | "search_tv": "/search/tv", 10 | "search_movie": "/search/movie", 11 | "tv_keywords": "/tv/%s/keywords", 12 | "movie_keywords": "/movie/%s/keywords", 13 | } 14 | 15 | def cartoons_popular(self, mode, page_no): 16 | return self._request_obj( 17 | ( 18 | self._urls["discover_tv"] 19 | if mode == "tv" 20 | else self._urls["discover_movie"] 21 | ), 22 | params="with_keywords=6513-cartoon&page=%s" % page_no, 23 | ) 24 | 25 | def animation_popular(self, mode, page_no): 26 | return self._request_obj( 27 | ( 28 | self._urls["discover_tv"] 29 | if mode == "tv" 30 | else self._urls["discover_movie"] 31 | ), 32 | params="with_keywords=297442&page=%s" % page_no, 33 | ) 34 | 35 | def anime_popular(self, mode, page_no): 36 | return self._request_obj( 37 | ( 38 | self._urls["discover_tv"] 39 | if mode == "tv" 40 | else self._urls["discover_movie"] 41 | ), 42 | params="with_keywords=210024&page=%s" % page_no, 43 | ) 44 | 45 | def anime_popular_recent(self, mode, page_no): 46 | return self._request_obj( 47 | ( 48 | self._urls["discover_tv"] 49 | if mode == "tv" 50 | else self._urls["discover_movie"] 51 | ), 52 | params=( 53 | "with_keywords=210024&sort_by=first_air_date.desc&include_null_first_air_dates=false&first_air_date_year=%s&page=%s" 54 | % (years_tvshows[0]["id"], page_no) 55 | ), 56 | ) 57 | 58 | def anime_year(self, params): 59 | return self._request_obj( 60 | ( 61 | self._urls["discover_tv"] 62 | if params["mode"] == "tv" 63 | else self._urls["discover_movie"] 64 | ), 65 | params="with_keywords=210024&include_null_first_air_dates=false&first_air_date_year=%s&page=%s" 66 | % (params["year"], params["page"]), 67 | ) 68 | 69 | def anime_genres(self, params): 70 | return self._request_obj( 71 | ( 72 | self._urls["discover_tv"] 73 | if params["mode"] == "tv" 74 | else self._urls["discover_movie"] 75 | ), 76 | params="&with_keywords=210024&with_genres=%s&include_null_first_air_dates=false&first_air_date.lte=%s&page=%s" 77 | % (params["genre_id"], get_current_date(), params["page"]), 78 | ) 79 | 80 | def anime_on_the_air(self, mode, page_no): 81 | current_date, future_date = get_dates(7, reverse=False) 82 | return self._request_obj( 83 | ( 84 | self._urls["discover_tv"] 85 | if mode == "tv" 86 | else self._urls["discover_movie"] 87 | ), 88 | params="with_keywords=210024&air_date.gte=%s&air_date.lte=%s&page=%s" 89 | % (current_date, future_date, page_no), 90 | ) 91 | 92 | def anime_search(self, query, mode, page_no, adult=False): 93 | params = "query=%s&page=%s" % (query, page_no) 94 | 95 | if adult is not None: 96 | params += "&include_adult=%s" % "true" if adult else "false" 97 | 98 | return self._request_obj( 99 | self._urls["search_tv"] if mode == "tv" else self._urls["search_movie"], 100 | params=params, 101 | key="results", 102 | ) 103 | 104 | def tmdb_keywords(self, mode, tmdb_id): 105 | return self._request_obj( 106 | ( 107 | self._urls["tv_keywords"] 108 | if mode == "tv" 109 | else self._urls["movie_keywords"] 110 | ) 111 | % tmdb_id, 112 | ) 113 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/auth.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | class Authentication(TMDb): 4 | _urls = { 5 | "create_request_token": "/authentication/token/new", 6 | "validate_with_login": "/authentication/token/validate_with_login", 7 | "create_session": "/authentication/session/new", 8 | "delete_session": "/authentication/session", 9 | } 10 | 11 | def __init__(self, username, password): 12 | super().__init__() 13 | self.username = username 14 | self.password = password 15 | self.expires_at = None 16 | self.request_token = self._create_request_token() 17 | self._authorise_request_token_with_login() 18 | self._create_session() 19 | 20 | def _create_request_token(self): 21 | """ 22 | Create a temporary request token that can be used to validate a TMDb user login. 23 | """ 24 | response = self._request_obj(self._urls["create_request_token"]) 25 | self.expires_at = response.expires_at 26 | return response.request_token 27 | 28 | def _create_session(self): 29 | """ 30 | You can use this method to create a fully valid session ID once a user has validated the request token. 31 | """ 32 | response = self._request_obj( 33 | self._urls["create_session"], 34 | method="POST", 35 | json={"request_token": self.request_token} 36 | ) 37 | self.session_id = response.session_id 38 | 39 | def _authorise_request_token_with_login(self): 40 | """ 41 | This method allows an application to validate a request token by entering a username and password. 42 | """ 43 | self._request_obj( 44 | self._urls["validate_with_login"], 45 | method="POST", 46 | json={ 47 | "username": self.username, 48 | "password": self.password, 49 | "request_token": self.request_token, 50 | } 51 | ) 52 | 53 | def delete_session(self): 54 | """ 55 | If you would like to delete (or "logout") from a session, call this method with a valid session ID. 56 | """ 57 | if self.has_session: 58 | self._request_obj( 59 | self._urls["delete_session"], 60 | method="DELETE", 61 | json={"session_id": self.session_id} 62 | ) 63 | self.session_id = "" 64 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/certification.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | class Certification(TMDb): 4 | _urls = { 5 | "movie_list": "/certification/movie/list", 6 | "tv_list": "/certification/tv/list", 7 | } 8 | 9 | def movie_list(self): 10 | """ 11 | Get an up to date list of the officially supported movie certifications on TMDB. 12 | :return: 13 | """ 14 | return self._request_obj(self._urls["movie_list"], key="certifications") 15 | 16 | def tv_list(self): 17 | """ 18 | Get an up to date list of the officially supported TV show certifications on TMDB. 19 | :return: 20 | """ 21 | return self._request_obj(self._urls["tv_list"], key="certifications") 22 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/change.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Change(TMDb): 5 | _urls = { 6 | "movie": "/movie/changes", 7 | "tv": "/tv/changes", 8 | "person": "/person/changes" 9 | } 10 | 11 | def _change_list(self, change_type, start_date="", end_date="", page=1): 12 | params = "page=%s" % page 13 | if start_date: 14 | params += "&start_date=%s" % start_date 15 | if end_date: 16 | params += "&end_date=%s" % end_date 17 | return self._request_obj( 18 | self._urls[change_type], 19 | params=params, 20 | key="results" 21 | ) 22 | 23 | def movie_change_list(self, start_date="", end_date="", page=1): 24 | """ 25 | Get the changes for a movie. By default only the last 24 hours are returned. 26 | You can query up to 14 days in a single query by using the start_date and end_date query parameters. 27 | :param start_date: str 28 | :param end_date: str 29 | :param page: int 30 | :return: 31 | """ 32 | return self._change_list("movie", start_date=start_date, end_date=end_date, page=page) 33 | 34 | def tv_change_list(self, start_date="", end_date="", page=1): 35 | """ 36 | Get a list of all of the TV show ids that have been changed in the past 24 hours. 37 | You can query up to 14 days in a single query by using the start_date and end_date query parameters. 38 | :param start_date: str 39 | :param end_date: str 40 | :param page: int 41 | :return: 42 | """ 43 | return self._change_list("tv", start_date=start_date, end_date=end_date, page=page) 44 | 45 | def person_change_list(self, start_date="", end_date="", page=1): 46 | """ 47 | Get a list of all of the person ids that have been changed in the past 24 hours. 48 | You can query up to 14 days in a single query by using the start_date and end_date query parameters. 49 | :param start_date: str 50 | :param end_date: str 51 | :param page: int 52 | :return: 53 | """ 54 | return self._change_list("person", start_date=start_date, end_date=end_date, page=page) 55 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/collection.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Collection(TMDb): 5 | _urls = { 6 | "details": "/collection/%s", 7 | "images": "/collection/%s/images", 8 | "translations": "/collection/%s/translations" 9 | } 10 | 11 | def details(self, collection_id): 12 | """ 13 | Get collection details by id. 14 | :param collection_id: int 15 | :return: 16 | """ 17 | return self._request_obj(self._urls["details"] % collection_id, key="parts") 18 | 19 | def images(self, collection_id): 20 | """ 21 | Get the images for a collection by id. 22 | :param collection_id: int 23 | :return: 24 | """ 25 | return self._request_obj(self._urls["images"] % collection_id) 26 | 27 | def translations(self, collection_id): 28 | """ 29 | Get the list translations for a collection by id. 30 | :param collection_id: int 31 | :return: 32 | """ 33 | return self._request_obj(self._urls["translations"] % collection_id, key="translations") 34 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/company.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Company(TMDb): 5 | _urls = { 6 | "details": "/company/%s", 7 | "alternative_names": "/company/%s/alternative_names", 8 | "images": "/company/%s/images", 9 | "movies": "/company/%s/movies" 10 | } 11 | 12 | def details(self, company_id): 13 | """ 14 | Get a companies details by id. 15 | :param company_id: int 16 | :return: 17 | """ 18 | return self._request_obj(self._urls["details"] % company_id) 19 | 20 | def alternative_names(self, company_id): 21 | """ 22 | Get the alternative names of a company. 23 | :param company_id: int 24 | :return: 25 | """ 26 | return self._request_obj(self._urls["alternative_names"] % company_id, key="results") 27 | 28 | def images(self, company_id): 29 | """ 30 | Get the alternative names of a company. 31 | :param company_id: int 32 | :return: 33 | """ 34 | return self._request_obj(self._urls["images"] % company_id, key="logos") 35 | 36 | def movies(self, company_id, page=1): 37 | """ 38 | Get the movies of a company by id. 39 | :param company_id: int 40 | :param page: int 41 | :return: 42 | """ 43 | return self._request_obj( 44 | self._urls["movies"] % company_id, 45 | params="page=%s" % page, 46 | key="results" 47 | ) 48 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/configuration.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from lib.api.tmdbv3api.tmdb import TMDb 3 | 4 | 5 | class Configuration(TMDb): 6 | _urls = { 7 | "api_configuration": "/configuration", 8 | "countries": "/configuration/countries", 9 | "jobs": "/configuration/jobs", 10 | "languages": "/configuration/languages", 11 | "primary_translations": "/configuration/primary_translations", 12 | "timezones": "/configuration/timezones" 13 | } 14 | 15 | def info(self): 16 | warnings.warn("info method is deprecated use tmdbv3api.Configuration().api_configuration()", 17 | DeprecationWarning) 18 | return self.api_configuration() 19 | 20 | def api_configuration(self): 21 | """ 22 | Get the system wide configuration info. 23 | """ 24 | return self._request_obj(self._urls["api_configuration"]) 25 | 26 | def countries(self): 27 | """ 28 | Get the list of countries (ISO 3166-1 tags) used throughout TMDb. 29 | """ 30 | return self._request_obj(self._urls["countries"]) 31 | 32 | def jobs(self): 33 | """ 34 | Get a list of the jobs and departments we use on TMDb. 35 | """ 36 | return self._request_obj(self._urls["jobs"]) 37 | 38 | def languages(self): 39 | """ 40 | Get the list of languages (ISO 639-1 tags) used throughout TMDb. 41 | """ 42 | return self._request_obj(self._urls["languages"]) 43 | 44 | def primary_translations(self): 45 | """ 46 | Get a list of the officially supported translations on TMDb. 47 | """ 48 | return self._request_obj(self._urls["primary_translations"]) 49 | 50 | def timezones(self): 51 | """ 52 | Get the list of timezones used throughout TMDb. 53 | """ 54 | return self._request_obj(self._urls["timezones"]) 55 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/credit.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | class Credit(TMDb): 4 | _urls = { 5 | "details": "/credit/%s" 6 | } 7 | 8 | def details(self, credit_id): 9 | """ 10 | Get a movie or TV credit details by id. 11 | :param credit_id: int 12 | :return: 13 | """ 14 | return self._request_obj(self._urls["details"] % credit_id) 15 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/discover.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | try: 4 | from urllib import urlencode 5 | except ImportError: 6 | from urllib.parse import urlencode 7 | 8 | 9 | class Discover(TMDb): 10 | _urls = { 11 | "movies": "/discover/movie", 12 | "tv": "/discover/tv" 13 | } 14 | 15 | def discover_movies(self, params): 16 | """ 17 | Discover movies by different types of data like average rating, number of votes, genres and certifications. 18 | :param params: dict 19 | :return: 20 | """ 21 | return self._request_obj(self._urls["movies"], urlencode(params), key="results") 22 | 23 | def discover_tv_shows(self, params): 24 | """ 25 | Discover TV shows by different types of data like average rating, number of votes, genres, 26 | the network they aired on and air dates. 27 | :param params: dict 28 | :return: 29 | """ 30 | return self._request_obj(self._urls["tv"], urlencode(params), key="results") 31 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/find.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Find(TMDb): 5 | _urls = { 6 | "find": "/find/%s" 7 | } 8 | 9 | def find(self, external_id, external_source): 10 | """ 11 | The find method makes it easy to search for objects in our database by an external id. For example, an IMDB ID. 12 | :param external_id: str 13 | :param external_source str 14 | :return: 15 | """ 16 | return self._request_obj( 17 | self._urls["find"] % external_id.replace("/", "%2F"), 18 | params="external_source=" + external_source 19 | ) 20 | 21 | def find_by_imdb_id(self, imdb_id): 22 | """ 23 | The find method makes it easy to search for objects in our database by an IMDB ID. 24 | :param imdb_id: str 25 | :return: 26 | """ 27 | return self.find(imdb_id, "imdb_id") 28 | 29 | def find_by_tvdb_id(self, tvdb_id): 30 | """ 31 | The find method makes it easy to search for objects in our database by a TVDB ID. 32 | :param tvdb_id: int 33 | :return: 34 | """ 35 | return self.find(tvdb_id, "tvdb_id") 36 | 37 | def find_by_freebase_mid(self, freebase_mid): 38 | """ 39 | The find method makes it easy to search for objects in our database by a Freebase MID. 40 | :param freebase_mid: str 41 | :return: 42 | """ 43 | return self.find(freebase_mid, "freebase_mid") 44 | 45 | def find_by_freebase_id(self, freebase_id): 46 | """ 47 | The find method makes it easy to search for objects in our database by a Freebase ID. 48 | :param freebase_id: str 49 | :return: 50 | """ 51 | return self.find(freebase_id, "freebase_id") 52 | 53 | def find_by_tvrage_id(self, tvrage_id): 54 | """ 55 | The find method makes it easy to search for objects in our database by a TVRage ID. 56 | :param tvrage_id: str 57 | :return: 58 | """ 59 | return self.find(tvrage_id, "tvrage_id") 60 | 61 | def find_by_facebook_id(self, facebook_id): 62 | """ 63 | The find method makes it easy to search for objects in our database by a Facebook ID. 64 | :param facebook_id: str 65 | :return: 66 | """ 67 | return self.find(facebook_id, "facebook_id") 68 | 69 | def find_by_instagram_id(self, instagram_id): 70 | """ 71 | The find method makes it easy to search for objects in our database by a Instagram ID. 72 | :param instagram_id: str 73 | :return: 74 | """ 75 | return self.find(instagram_id, "instagram_id") 76 | 77 | def find_by_twitter_id(self, twitter_id): 78 | """ 79 | The find method makes it easy to search for objects in our database by a Twitter ID. 80 | :param twitter_id: str 81 | :return: 82 | """ 83 | return self.find(twitter_id, "twitter_id") 84 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/genre.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Genre(TMDb): 5 | _urls = { 6 | "movie_list": "/genre/movie/list", 7 | "tv_list": "/genre/tv/list" 8 | } 9 | 10 | def movie_list(self): 11 | """ 12 | Get the list of official genres for movies. 13 | :return: 14 | """ 15 | return self._request_obj(self._urls["movie_list"], key="genres") 16 | 17 | def tv_list(self): 18 | """ 19 | Get the list of official genres for TV shows. 20 | :return: 21 | """ 22 | return self._request_obj(self._urls["tv_list"], key="genres") 23 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/group.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Group(TMDb): 5 | _urls = { 6 | "details": "/tv/episode_group/%s" 7 | } 8 | 9 | def details(self, group_id): 10 | """ 11 | Get the details of a TV episode group. 12 | :param group_id: int 13 | :return: 14 | """ 15 | return self._request_obj(self._urls["details"] % group_id, key="groups") 16 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/keyword.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | class Keyword(TMDb): 4 | _urls = { 5 | "details": "/keyword/%s", 6 | "movies": "/keyword/%s/movies" 7 | } 8 | 9 | def details(self, keyword_id): 10 | """ 11 | Get a keywords details by id. 12 | :param keyword_id: int 13 | :return: 14 | """ 15 | return self._request_obj(self._urls["details"] % keyword_id) 16 | 17 | def movies(self, keyword_id): 18 | """ 19 | Get the movies of a keyword by id. 20 | :param keyword_id: int 21 | :return: 22 | """ 23 | return self._request_obj(self._urls["movies"] % keyword_id, key="results") 24 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/list.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | class List(TMDb): 4 | _urls = { 5 | "details": "/list/%s", 6 | "check_status": "/list/%s/item_status", 7 | "create": "/list", 8 | "add_movie": "/list/%s/add_item", 9 | "remove_movie": "/list/%s/remove_item", 10 | "clear_list": "/list/%s/clear", 11 | "delete_list": "/list/%s", 12 | } 13 | 14 | def details(self, list_id): 15 | """ 16 | Get list details by id. 17 | :param list_id: int 18 | :return: 19 | """ 20 | return self._request_obj(self._urls["details"] % list_id, key="items") 21 | 22 | def check_item_status(self, list_id, movie_id): 23 | """ 24 | You can use this method to check if a movie has already been added to the list. 25 | :param list_id: int 26 | :param movie_id: int 27 | :return: 28 | """ 29 | return self._request_obj(self._urls["check_status"] % list_id, params="movie_id=%s" % movie_id)["item_present"] 30 | 31 | def create_list(self, name, description): 32 | """ 33 | You can use this method to check if a movie has already been added to the list. 34 | :param name: str 35 | :param description: str 36 | :return: 37 | """ 38 | return self._request_obj( 39 | self._urls["create"], 40 | params="session_id=%s" % self.session_id, 41 | method="POST", 42 | json={ 43 | "name": name, 44 | "description": description, 45 | "language": self.language 46 | } 47 | ).list_id 48 | 49 | def add_movie(self, list_id, movie_id): 50 | """ 51 | Add a movie to a list. 52 | :param list_id: int 53 | :param movie_id: int 54 | """ 55 | self._request_obj( 56 | self._urls["add_movie"] % list_id, 57 | params="session_id=%s" % self.session_id, 58 | method="POST", 59 | json={"media_id": movie_id} 60 | ) 61 | 62 | def remove_movie(self, list_id, movie_id): 63 | """ 64 | Remove a movie from a list. 65 | :param list_id: int 66 | :param movie_id: int 67 | """ 68 | self._request_obj( 69 | self._urls["remove_movie"] % list_id, 70 | params="session_id=%s" % self.session_id, 71 | method="POST", 72 | json={"media_id": movie_id} 73 | ) 74 | 75 | def clear_list(self, list_id): 76 | """ 77 | Clear all of the items from a list. 78 | :param list_id: int 79 | """ 80 | self._request_obj( 81 | self._urls["clear_list"] % list_id, 82 | params="session_id=%s&confirm=true" % self.session_id, 83 | method="POST" 84 | ) 85 | 86 | def delete_list(self, list_id): 87 | """ 88 | Delete a list. 89 | :param list_id: int 90 | """ 91 | self._request_obj( 92 | self._urls["delete_list"] % list_id, 93 | params="session_id=%s" % self.session_id, 94 | method="DELETE" 95 | ) 96 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/network.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Network(TMDb): 5 | _urls = { 6 | "details": "/network/%s", 7 | "alternative_names": "/network/%s/alternative_names", 8 | "images": "/network/%s/images" 9 | } 10 | 11 | def details(self, network_id): 12 | """ 13 | Get a networks details by id. 14 | :param network_id: int 15 | :return: 16 | """ 17 | return self._request_obj(self._urls["details"] % network_id) 18 | 19 | def alternative_names(self, network_id): 20 | """ 21 | Get the alternative names of a network. 22 | :param network_id: int 23 | :return: 24 | """ 25 | return self._request_obj( 26 | self._urls["alternative_names"] % network_id, 27 | key="results" 28 | ) 29 | 30 | def images(self, network_id): 31 | """ 32 | Get the TV network logos by id. 33 | :param network_id: int 34 | :return: 35 | """ 36 | return self._request_obj( 37 | self._urls["images"] % network_id, 38 | key="logos" 39 | ) 40 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/provider.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Provider(TMDb): 5 | _urls = { 6 | "regions": "/watch/providers/regions", # TODO: 7 | "movie": "/watch/providers/movie", # TODO: 8 | "tv": "/watch/providers/tv", # TODO: 9 | } 10 | 11 | def available_regions(self): 12 | """ 13 | Returns a list of all of the countries we have watch provider (OTT/streaming) data for. 14 | :return: 15 | """ 16 | return self._request_obj( 17 | self._urls["regions"], 18 | key="results" 19 | ) 20 | 21 | def movie_providers(self, region=None): 22 | """ 23 | Returns a list of the watch provider (OTT/streaming) data we have available for movies. 24 | :return: 25 | """ 26 | return self._request_obj( 27 | self._urls["movie"], 28 | params="watch_region=%s" % region if region else "", 29 | key="results" 30 | ) 31 | 32 | def tv_providers(self, region=None): 33 | """ 34 | Returns a list of the watch provider (OTT/streaming) data we have available for TV series. 35 | :return: 36 | """ 37 | return self._request_obj( 38 | self._urls["tv"], 39 | params="watch_region=%s" % region if region else "", 40 | key="results" 41 | ) 42 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/review.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Review(TMDb): 5 | _urls = { 6 | "details": "/review/%s", 7 | } 8 | 9 | def details(self, review_id): 10 | """ 11 | Get the primary person details by id. 12 | :param review_id: int 13 | :return: 14 | """ 15 | return self._request_obj(self._urls["details"] % review_id) 16 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/objs/trending.py: -------------------------------------------------------------------------------- 1 | from lib.api.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Trending(TMDb): 5 | _urls = {"trending": "/trending/%s/%s"} 6 | 7 | def _trending(self, media_type="all", time_window="day", page=1): 8 | return self._request_obj( 9 | self._urls["trending"] % (media_type, time_window), 10 | params="page=%s" % page 11 | ) 12 | 13 | def all_day(self, page=1): 14 | """ 15 | Get all daily trending 16 | :param page: int 17 | :return: 18 | """ 19 | return self._trending(media_type="all", time_window="day", page=page) 20 | 21 | def all_week(self, page=1): 22 | """ 23 | Get all weekly trending 24 | :param page: int 25 | :return: 26 | """ 27 | return self._trending(media_type="all", time_window="week", page=page) 28 | 29 | def movie_day(self, page=1): 30 | """ 31 | Get movie daily trending 32 | :param page: int 33 | :return: 34 | """ 35 | return self._trending(media_type="movie", time_window="day", page=page) 36 | 37 | def movie_week(self, page=1): 38 | """ 39 | Get movie weekly trending 40 | :param page: int 41 | :return: 42 | """ 43 | return self._trending(media_type="movie", time_window="week", page=page) 44 | 45 | def tv_day(self, page=1): 46 | """ 47 | Get tv daily trending 48 | :param page: int 49 | :return: 50 | """ 51 | return self._trending(media_type="tv", time_window="day", page=page) 52 | 53 | def tv_week(self, page=1): 54 | """ 55 | Get tv weekly trending 56 | :param page: int 57 | :return: 58 | """ 59 | return self._trending(media_type="tv", time_window="week", page=page) 60 | 61 | def person_day(self, page=1): 62 | """ 63 | Get person daily trending 64 | :param page: int 65 | :return: 66 | """ 67 | return self._trending(media_type="person", time_window="day", page=page) 68 | 69 | def person_week(self, page=1): 70 | """ 71 | Get person weekly trending 72 | :param page: int 73 | :return: 74 | """ 75 | return self._trending(media_type="person", time_window="week", page=page) 76 | -------------------------------------------------------------------------------- /lib/api/tmdbv3api/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | years_tvshows = [{'name': '2024', 'id': 2024}] 4 | 5 | def get_dates(days, reverse=True): 6 | current_date = get_current_date(return_str=False) 7 | if reverse: 8 | new_date = (current_date - datetime.timedelta(days=days)).strftime('%Y-%m-%d') 9 | else: 10 | new_date = (current_date + datetime.timedelta(days=days)).strftime('%Y-%m-%d') 11 | return str(current_date), new_date 12 | 13 | def get_current_date(return_str=True): 14 | if return_str: return str(datetime.date.today()) 15 | else: return datetime.date.today() -------------------------------------------------------------------------------- /lib/api/trakt/lists_cache.py: -------------------------------------------------------------------------------- 1 | from lib.api.trakt.base_cache import BaseCache, get_timestamp 2 | 3 | 4 | GET_ALL = "SELECT id FROM lists" 5 | DELETE_ALL = "DELETE FROM lists" 6 | CLEAN = "DELETE from lists WHERE CAST(expires AS INT) <= ?" 7 | 8 | 9 | class ListsCache(BaseCache): 10 | def __init__(self): 11 | BaseCache.__init__(self, "lists_db", "lists") 12 | 13 | def delete_all_lists(self): 14 | try: 15 | dbcon = self.manual_connect("lists_db") 16 | for i in dbcon.execute(GET_ALL): 17 | self.delete_memory_cache(str(i[0])) 18 | dbcon.execute(DELETE_ALL) 19 | dbcon.execute("VACUUM") 20 | return True 21 | except: 22 | return False 23 | 24 | def clean_database(self): 25 | try: 26 | dbcon = self.manual_connect("lists_db") 27 | dbcon.execute(CLEAN, (get_timestamp(),)) 28 | dbcon.execute("VACUUM") 29 | return True 30 | except: 31 | return False 32 | 33 | 34 | lists_cache = ListsCache() 35 | 36 | 37 | def lists_cache_object(function, string, args, json=False, expiration=48): 38 | cache = lists_cache.get(string) 39 | if cache is not None: 40 | return cache 41 | if isinstance(args, list): 42 | args = tuple(args) 43 | else: 44 | args = (args,) 45 | if json: 46 | result = function(*args).json() 47 | else: 48 | result = function(*args) 49 | lists_cache.set(string, result, expiration=expiration) 50 | return result 51 | -------------------------------------------------------------------------------- /lib/api/trakt/main_cache.py: -------------------------------------------------------------------------------- 1 | from lib.api.trakt.base_cache import BaseCache, get_timestamp 2 | 3 | 4 | GET_ALL = "SELECT id FROM maincache" 5 | DELETE_ALL = "DELETE FROM maincache" 6 | LIKE_SELECT = "SELECT id from maincache where id LIKE %s" 7 | LIKE_DELETE = "DELETE FROM maincache WHERE id LIKE %s" 8 | CLEAN = "DELETE from maincache WHERE CAST(expires AS INT) <= ?" 9 | 10 | 11 | class MainCache(BaseCache): 12 | def __init__(self): 13 | BaseCache.__init__(self, "maincache_db", "maincache") 14 | 15 | def delete_all(self): 16 | try: 17 | dbcon = self.manual_connect("maincache_db") 18 | for i in dbcon.execute(GET_ALL): 19 | self.delete_memory_cache(str(i[0])) 20 | dbcon.execute(DELETE_ALL) 21 | dbcon.execute("VACUUM") 22 | return True 23 | except: 24 | return False 25 | 26 | def delete_all_folderscrapers(self): 27 | dbcon = self.manual_connect("maincache_db") 28 | remove_list = [ 29 | str(i[0]) 30 | for i in dbcon.execute(LIKE_SELECT % "'FOLDERSCRAPER_%'").fetchall() 31 | ] 32 | if not remove_list: 33 | return True 34 | try: 35 | dbcon.execute(LIKE_DELETE % "'FOLDERSCRAPER_%'") 36 | dbcon.execute("VACUUM") 37 | for item in remove_list: 38 | self.delete_memory_cache(str(item)) 39 | return True 40 | except: 41 | return False 42 | 43 | def clean_database(self): 44 | try: 45 | dbcon = self.manual_connect("maincache_db") 46 | dbcon.execute(CLEAN, (get_timestamp(),)) 47 | dbcon.execute("VACUUM") 48 | return True 49 | except: 50 | return False 51 | 52 | 53 | main_cache = MainCache() 54 | 55 | 56 | def cache_object(function, string, args, json=True, expiration=24): 57 | cache = main_cache.get(string) 58 | if cache is not None: 59 | return cache 60 | if isinstance(args, list): 61 | args = tuple(args) 62 | else: 63 | args = (args,) 64 | if json: 65 | result = function(*args).json() 66 | else: 67 | result = function(*args) 68 | main_cache.set(string, result, expiration=expiration) 69 | return result 70 | -------------------------------------------------------------------------------- /lib/api/trakt/trakt_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import random 3 | import re 4 | import time 5 | import json 6 | from lib.utils.kodi.utils import action_url_run 7 | 8 | 9 | def add_trakt_watchlist_context_menu(media_type, ids): 10 | filtered_ids = clean_ids({ 11 | "tmdb": ids.get("tmdb_id") or ids.get("tmdb"), 12 | "tvdb": ids.get("tvdb_id") or ids.get("tvdb"), 13 | "imdb": ids.get("imdb_id") or ids.get("imdb"), 14 | }) 15 | return [ 16 | ( 17 | "Add to Trakt Watchlist", 18 | action_url_run( 19 | "trakt_add_to_watchlist", 20 | media_type=media_type, 21 | ids=json.dumps(filtered_ids), 22 | ), 23 | ), 24 | ( 25 | "Remove from Trakt Watchlist", 26 | action_url_run( 27 | "trakt_remove_from_watchlist", 28 | media_type=media_type, 29 | ids=json.dumps(filtered_ids), 30 | ), 31 | ), 32 | ] 33 | 34 | 35 | def clean_ids(ids_dict): 36 | return {k: v for k, v in ids_dict.items() if v not in (None, "", "null")} 37 | 38 | 39 | def jsondate_to_datetime(jsondate_object, resformat, remove_time=False): 40 | if not jsondate_object: 41 | return None 42 | if remove_time: 43 | datetime_object = datetime_workaround(jsondate_object, resformat).date() 44 | else: 45 | datetime_object = datetime_workaround(jsondate_object, resformat) 46 | return datetime_object 47 | 48 | 49 | def datetime_workaround(data, str_format): 50 | try: 51 | datetime_object = datetime.strptime(data, str_format) 52 | except: 53 | datetime_object = datetime(*(time.strptime(data, str_format)[0:6])) 54 | return datetime_object 55 | 56 | 57 | def sort_list(sort_key, sort_direction, list_data): 58 | try: 59 | reverse = sort_direction != "asc" 60 | if sort_key == "rank": 61 | return sorted(list_data, key=lambda x: x["rank"], reverse=reverse) 62 | if sort_key == "added": 63 | return sorted(list_data, key=lambda x: x["listed_at"], reverse=reverse) 64 | if sort_key == "title": 65 | return sorted( 66 | list_data, 67 | key=lambda x: title_key(x[x["type"]].get("title")), 68 | reverse=reverse, 69 | ) 70 | if sort_key == "released": 71 | return sorted( 72 | list_data, key=lambda x: released_key(x[x["type"]]), reverse=reverse 73 | ) 74 | if sort_key == "runtime": 75 | return sorted( 76 | list_data, key=lambda x: x[x["type"]].get("runtime", 0), reverse=reverse 77 | ) 78 | if sort_key == "popularity": 79 | return sorted( 80 | list_data, key=lambda x: x[x["type"]].get("votes", 0), reverse=reverse 81 | ) 82 | if sort_key == "percentage": 83 | return sorted( 84 | list_data, key=lambda x: x[x["type"]].get("rating", 0), reverse=reverse 85 | ) 86 | if sort_key == "votes": 87 | return sorted( 88 | list_data, key=lambda x: x[x["type"]].get("votes", 0), reverse=reverse 89 | ) 90 | if sort_key == "random": 91 | return sorted(list_data, key=lambda k: random.random()) 92 | return list_data 93 | except: 94 | return list_data 95 | 96 | 97 | def released_key(item): 98 | if "released" in item: 99 | return item["released"] or "2050-01-01" 100 | if "first_aired" in item: 101 | return item["first_aired"] or "2050-01-01" 102 | return "2050-01-01" 103 | 104 | 105 | def title_key(title): 106 | try: 107 | if title is None: 108 | title = "" 109 | articles = ["the", "a", "an"] 110 | match = re.match(r"^((\w+)\s+)", title.lower()) 111 | if match and match.group(2) in articles: 112 | offset = len(match.group(1)) 113 | else: 114 | offset = 0 115 | return title[offset:] 116 | except: 117 | return title 118 | 119 | 120 | def sort_for_article(_list, _key): 121 | _list.sort(key=lambda k: re.sub(r"(^the |^a |^an )", "", k[_key].lower())) 122 | return _list 123 | -------------------------------------------------------------------------------- /lib/api/tvdbapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/lib/api/tvdbapi/__init__.py -------------------------------------------------------------------------------- /lib/api/tvdbapi/tvdbapi.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from datetime import timedelta 3 | from lib.db.cached import cache 4 | from lib.utils.kodi.settings import is_cache_enabled, get_cache_expiration 5 | 6 | class TVDBAPI: 7 | def __init__(self): 8 | self.headers = {"User-Agent": "TheTVDB v.4 TV Scraper for Kodi"} 9 | self.apiKey = {"apikey": "edae60dc-1b44-4bac-8db7-65c0aaf5258b"} 10 | self.baseUrl = "https://api4.thetvdb.com/v4/" 11 | self.art = {} 12 | self.request_response = None 13 | self.threads = [] 14 | 15 | def get_token(self): 16 | identifier = "tvdb_token" 17 | token = cache.get(identifier, hashed_key=True) 18 | if token: 19 | return token 20 | else: 21 | res = requests.post(self.baseUrl + "login", json=self.apiKey, headers=self.headers) 22 | data = res.json() 23 | token = data["data"].get("token") 24 | cache.set( 25 | identifier, 26 | token, 27 | timedelta(hours=get_cache_expiration() if is_cache_enabled() else 0), 28 | hashed_key=True, 29 | ) 30 | return token 31 | 32 | def get_request(self, url): 33 | token = self.get_token() 34 | self.headers.update( 35 | { 36 | "Authorization": "Bearer {0}".format(token), 37 | "Accept": "application/json" 38 | } 39 | ) 40 | url = self.baseUrl + url 41 | response = requests.get(url, headers=self.headers) 42 | if response: 43 | response = response.json().get("data") 44 | self.request_response = response 45 | return response 46 | else: 47 | return None 48 | 49 | def get_imdb_id(self, tvdb_id): 50 | imdb_id = None 51 | url = "series/{}/extended".format(tvdb_id) 52 | data = self.get_request(url) 53 | if data: 54 | imdb_id = [x.get("id") for x in data["remoteIds"] if x.get("type") == 2] 55 | return imdb_id[0] if imdb_id else None 56 | 57 | def get_seasons(self, tvdb_id): 58 | url = "seasons/{}/extended".format(tvdb_id) 59 | data = self.get_request(url) 60 | return data 61 | -------------------------------------------------------------------------------- /lib/clients/anizip.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class AniZipApi: 5 | base_url = "https://api.ani.zip/" 6 | 7 | def mapping(self, anilist_id): 8 | params = {"anilist_id": anilist_id} 9 | res = self.make_request(url="mappings", params=params) 10 | if res: 11 | return res["mappings"] 12 | 13 | def episodes(self, anilist_id): 14 | params = {"anilist_id": anilist_id} 15 | res = self.make_request(url="mappings", params=params) 16 | if res: 17 | return res["episodes"] 18 | 19 | def make_request(self, url, params): 20 | res = requests.get( 21 | self.base_url + url, 22 | params=params, 23 | ) 24 | if res.status_code == 200: 25 | return res.json() 26 | else: 27 | kodilog(f"Error::{res.text}") 28 | -------------------------------------------------------------------------------- /lib/clients/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from requests import Session 3 | from typing import List, Optional 4 | from lib.domain.torrent import TorrentStream 5 | 6 | 7 | class BaseClient(ABC): 8 | def __init__(self, host: Optional[str], notification: Optional[callable]) -> None: 9 | self.host = host.rstrip("/") if host else "" 10 | self.notification = notification 11 | self.session = Session() 12 | 13 | @abstractmethod 14 | def search( 15 | self, 16 | tmdb_id: str, 17 | query: str, 18 | mode: str, 19 | media_type: str, 20 | season: Optional[int], 21 | episode: Optional[int], 22 | ) -> List[TorrentStream]: 23 | pass 24 | 25 | @abstractmethod 26 | def parse_response(self, res: any) -> List[TorrentStream]: 27 | pass 28 | 29 | def handle_exception(self, exception: Exception) -> None: 30 | exception_message = str(exception) 31 | if len(exception_message) > 70: 32 | exception_message = exception_message[:70] + "..." 33 | raise Exception(exception_message) 34 | -------------------------------------------------------------------------------- /lib/clients/burst/client.py: -------------------------------------------------------------------------------- 1 | from lib.clients.base import BaseClient, TorrentStream 2 | from lib.clients.burst.providers import ( 3 | burst_search, 4 | burst_search_episode, 5 | burst_search_movie, 6 | ) 7 | from lib.utils.kodi.utils import convert_size_to_bytes 8 | from typing import List, Optional, Dict, Any 9 | 10 | 11 | class Burst(BaseClient): 12 | def __init__(self, notification: callable) -> None: 13 | super().__init__("", notification) 14 | 15 | def search( 16 | self, 17 | tmdb_id: str, 18 | query: str, 19 | mode: str, 20 | media_type: str, 21 | season: Optional[int], 22 | episode: Optional[int], 23 | ) -> Optional[List[TorrentStream]]: 24 | try: 25 | if mode == "tv" or media_type == "tv": 26 | results = burst_search_episode(tmdb_id, query, season, episode) 27 | elif mode == "movies" or media_type == "movies": 28 | results = burst_search_movie(tmdb_id, query) 29 | else: 30 | results = burst_search(query) 31 | if results: 32 | results = self.parse_response(results) 33 | return results 34 | except Exception as e: 35 | self.handle_exception(f"Burst error: {str(e)}") 36 | 37 | def parse_response(self, res: List[Dict[str, Any]]) -> List[TorrentStream]: 38 | results = [] 39 | for _, r in res: 40 | results.append( 41 | TorrentStream( 42 | title=r.title, 43 | type="Torrent", 44 | indexer="Burst", 45 | guid=r.guid, 46 | infoHash="", 47 | size=convert_size_to_bytes(r.size), 48 | seeders=int(r.seeders), 49 | peers=int(r.peers), 50 | languages=[], 51 | fullLanguages="", 52 | provider=r.indexer, 53 | publishDate="", 54 | ) 55 | ) 56 | return results 57 | -------------------------------------------------------------------------------- /lib/clients/burst/providers.py: -------------------------------------------------------------------------------- 1 | from lib.api.burst.provider import ProviderListener, ProviderResult, get_providers, send_to_providers 2 | from lib.utils.kodi.utils import ADDON_NAME, kodilog 3 | 4 | from xbmcgui import DialogProgressBG 5 | 6 | 7 | class ResolveTimeoutError(Exception): 8 | pass 9 | 10 | 11 | class NoProvidersError(Exception): 12 | pass 13 | 14 | 15 | class ProviderListenerDialog(ProviderListener): 16 | def __init__(self, providers, method, timeout=10): 17 | super(ProviderListenerDialog, self).__init__(providers, method, timeout=timeout) 18 | self._total = len(providers) 19 | self._count = 0 20 | self._dialog = DialogProgressBG() 21 | 22 | def on_receive(self, sender): 23 | self._count += 1 24 | self._dialog.update(int(100 * self._count / self._total)) 25 | 26 | def __enter__(self): 27 | ret = super(ProviderListenerDialog, self).__enter__() 28 | self._dialog.create(ADDON_NAME, "Getting results from providers...") 29 | return ret 30 | 31 | def __exit__(self, exc_type, exc_val, exc_tb): 32 | try: 33 | return super(ProviderListenerDialog, self).__exit__( 34 | exc_type, exc_val, exc_tb 35 | ) 36 | finally: 37 | self._dialog.close() 38 | 39 | 40 | def run_providers_method(timeout, method, *args, **kwargs): 41 | providers = get_providers() 42 | if not providers: 43 | raise NoProvidersError("No available providers") 44 | with ProviderListenerDialog(providers, method, timeout=timeout) as listener: 45 | send_to_providers(providers, method, *args, **kwargs) 46 | return listener.data 47 | 48 | 49 | def run_provider_method(provider, timeout, method, *args, **kwargs): 50 | with ProviderListener((provider,), method, timeout=timeout) as listener: 51 | send_to_providers((provider,), method, *args, **kwargs) 52 | try: 53 | return listener.data[provider] 54 | except KeyError: 55 | raise ResolveTimeoutError("Timeout reached") 56 | 57 | 58 | def get_providers_results(method, *args, **kwargs): 59 | results = [] 60 | data = run_providers_method(30, method, *args, **kwargs) 61 | for provider, provider_results in data.items(): 62 | if not isinstance(provider_results, (tuple, list)): 63 | kodilog("Expecting list or tuple as results for %s:%s", provider, method) 64 | continue 65 | for provider_result in provider_results: 66 | try: 67 | _provider_result = ProviderResult(provider_result) 68 | except Exception as e: 69 | kodilog( 70 | "Invalid format on provider '%s' result (%s): %s", 71 | provider, 72 | provider_result, 73 | e, 74 | ) 75 | else: 76 | results.append((provider, _provider_result)) 77 | return results 78 | 79 | 80 | def search(method, *args, **kwargs): 81 | try: 82 | results = get_providers_results(method, *args, **kwargs) 83 | except NoProvidersError: 84 | results = None 85 | if results: 86 | return results 87 | elif results is None: 88 | raise Exception("No providers available") 89 | else: 90 | raise Exception("No results found") 91 | 92 | 93 | def burst_search(query): 94 | return search("search", query) 95 | 96 | 97 | def burst_search_movie(movie_id, query): 98 | return search( 99 | "search_movie", 100 | movie_id, 101 | query, 102 | ) 103 | 104 | 105 | def burst_search_show(show_id, query): 106 | return search( 107 | "search_show", 108 | show_id, 109 | query, 110 | ) 111 | 112 | 113 | def burst_search_season(show_id, show_title, season_number): 114 | return search( 115 | "search_season", 116 | show_id, 117 | show_title, 118 | int(season_number), 119 | ) 120 | 121 | 122 | def burst_search_episode(show_id, query, season_number, episode_number): 123 | return search( 124 | "search_episode", 125 | show_id, 126 | query, 127 | int(season_number), 128 | int(episode_number), 129 | ) 130 | -------------------------------------------------------------------------------- /lib/clients/debrid/alldebrid.py: -------------------------------------------------------------------------------- 1 | 2 | from lib.clients.debrid.base import DebridClient, ProviderException 3 | 4 | 5 | class AllDebrid(DebridClient): 6 | BASE_URL = "https://api.alldebrid.com/v4" 7 | 8 | def __init__(self, token: str): 9 | super().__init__(token) 10 | 11 | def initialize_headers(self): 12 | self.headers = {"Authorization": f"Bearer {self.token}"} 13 | 14 | def disable_access_token(self): 15 | pass 16 | 17 | def _handle_service_specific_errors(self, error_data: dict, status_code: int): 18 | pass 19 | 20 | def _make_request( 21 | self, 22 | method: str, 23 | url: str, 24 | data, 25 | json, 26 | params, 27 | is_return_none: bool = False, 28 | is_expected_to_fail: bool = False, 29 | ): 30 | params = params or {} 31 | url = self.BASE_URL + url 32 | return super()._make_request( 33 | method, url, data, json, params, is_return_none, is_expected_to_fail 34 | ) 35 | 36 | @staticmethod 37 | def _validate_error_response(response_data): 38 | if response_data.get("status") != "success": 39 | error_code = response_data.get("error", {}).get("code") 40 | if error_code == "AUTH_BAD_APIKEY": 41 | raise ProviderException("Invalid AllDebrid API key") 42 | elif error_code == "NO_SERVER": 43 | raise ProviderException( 44 | f"Failed to add magnet link to AllDebrid {response_data}" 45 | ) 46 | elif error_code == "AUTH_BLOCKED": 47 | raise ProviderException("API got blocked on AllDebrid") 48 | elif error_code == "MAGNET_MUST_BE_PREMIUM": 49 | raise ProviderException("Torrent must be premium on AllDebrid") 50 | elif error_code in {"MAGNET_TOO_MANY_ACTIVE", "MAGNET_TOO_MANY"}: 51 | raise ProviderException("Too many active torrents on AllDebrid") 52 | else: 53 | raise ProviderException( 54 | f"Failed to add magnet link to AllDebrid {response_data}" 55 | ) 56 | 57 | def add_magnet_link(self, magnet_link): 58 | response_data = self._make_request( 59 | "POST", "/magnet/upload", data={"magnets[]": magnet_link} 60 | ) 61 | self._validate_error_response(response_data) 62 | return response_data 63 | 64 | def get_user_torrent_list(self): 65 | return self._make_request("GET", "/magnet/status") 66 | 67 | def get_torrent_info(self, magnet_id): 68 | response = self._make_request("GET", "/magnet/status", params={"id": magnet_id}) 69 | return response.get("data", {}).get("magnets") 70 | 71 | def get_torrent_instant_availability(self, magnet_links): 72 | response = self._make_request( 73 | "POST", "/magnet/instant", data={"magnets[]": magnet_links} 74 | ) 75 | return response.get("data", {}).get("magnets", []) 76 | 77 | def get_available_torrent(self, info_hash): 78 | available_torrents = self.get_user_torrent_list() 79 | self._validate_error_response(available_torrents) 80 | if not available_torrents.get("data"): 81 | return None 82 | for torrent in available_torrents["data"]["magnets"]: 83 | if torrent["hash"] == info_hash: 84 | return torrent 85 | return None 86 | 87 | def create_download_link(self, link): 88 | response = self._make_request( 89 | "GET", 90 | "/link/unlock", 91 | params={"link": link}, 92 | is_expected_to_fail=True, 93 | ) 94 | if response.get("status") == "success": 95 | return response 96 | raise ProviderException( 97 | f"Failed to create download link from AllDebrid {response}", 98 | "transfer_error.mp4", 99 | ) 100 | 101 | def delete_torrent(self, magnet_id): 102 | return self._make_request("GET", "/magnet/delete", params={"id": magnet_id}) 103 | 104 | def get_user_info(self): 105 | return self._make_request("GET", "/user") 106 | -------------------------------------------------------------------------------- /lib/clients/debrid/base.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | import traceback 3 | import requests 4 | from abc import ABC, abstractmethod 5 | from lib.utils.kodi.utils import kodilog, notification 6 | 7 | 8 | class DebridClient(ABC): 9 | def __init__(self, token): 10 | self.headers = {} 11 | self.token = token 12 | self.initialize_headers() 13 | 14 | def _make_request( 15 | self, 16 | method, 17 | url, 18 | data=None, 19 | params=None, 20 | json=None, 21 | is_return_none=False, 22 | is_expected_to_fail=False, 23 | ): 24 | response = self._perform_request(method, url, data, params, json) 25 | self._handle_errors(response, is_expected_to_fail) 26 | return self._parse_response(response, is_return_none) 27 | 28 | def _perform_request(self, method, url, data, params, json): 29 | try: 30 | return requests.Session().request( 31 | method, 32 | url, 33 | params=params, 34 | data=data, 35 | json=json, 36 | headers=self.headers, 37 | timeout=15, 38 | ) 39 | except requests.exceptions.Timeout: 40 | raise ProviderException("Request timed out.") 41 | except requests.exceptions.ConnectionError: 42 | raise ProviderException("Failed to connect to Debrid service.") 43 | 44 | def _handle_errors(self, response, is_expected_to_fail): 45 | try: 46 | response.raise_for_status() 47 | except requests.RequestException as error: 48 | if is_expected_to_fail: 49 | return 50 | 51 | if response.headers.get("Content-Type") == "application/json": 52 | error_content = response.json() 53 | self._handle_service_specific_errors( 54 | error_content, error.response.status_code 55 | ) 56 | else: 57 | error_content = response.text() 58 | 59 | if error.response.status_code == 401: 60 | raise ProviderException("Invalid token") 61 | 62 | if error.response.status_code == 403: 63 | raise ProviderException("Forbidden") 64 | 65 | formatted_traceback = "".join(traceback.format_exception(error)) 66 | 67 | kodilog(formatted_traceback) 68 | kodilog(error_content) 69 | kodilog(error.response.status_code) 70 | 71 | raise ProviderException(f"API Error: {error_content}") 72 | 73 | @abstractmethod 74 | async def initialize_headers(self): 75 | raise NotImplementedError 76 | 77 | @abstractmethod 78 | async def disable_access_token(self): 79 | raise NotImplementedError 80 | 81 | @staticmethod 82 | def _parse_response(response, is_return_none): 83 | if is_return_none: 84 | return {} 85 | try: 86 | return response.json() 87 | except requests.JSONDecodeError as error: 88 | raise ProviderException( 89 | f"Failed to parse response error: {error}. \nresponse: {response.text}" 90 | ) 91 | 92 | @abstractmethod 93 | def _handle_service_specific_errors(self, error_data: dict, status_code: int): 94 | """ 95 | Service specific errors on api requests. 96 | """ 97 | raise NotImplementedError 98 | 99 | 100 | class ProviderException(Exception): 101 | def __init__(self, message): 102 | self.message = message 103 | super().__init__(self.message) 104 | notification(self.message) 105 | 106 | -------------------------------------------------------------------------------- /lib/clients/debrid/easydebrid.py: -------------------------------------------------------------------------------- 1 | from lib.clients.debrid.base import DebridClient, ProviderException 2 | 3 | 4 | class EasyDebrid(DebridClient): 5 | BASE_URL = "https://easydebrid.com/api/v1" 6 | 7 | def __init__(self, token, user_ip): 8 | self.user_ip = user_ip 9 | super().__init__(token) 10 | 11 | def initialize_headers(self): 12 | self.headers = {"Authorization": f"Bearer {self.token}"} 13 | if self.user_ip: 14 | self.headers["X-Forwarded-For"] = self.user_ip 15 | 16 | def disable_access_token(self): 17 | pass 18 | 19 | def _handle_service_specific_errors(self, error_data: dict, status_code: int): 20 | error_code = error_data.get("error") 21 | if error_code == "Unsupported link for direct download.": 22 | raise ProviderException( 23 | "Unsupported link for direct download.", 24 | ) 25 | 26 | def _make_request( 27 | self, 28 | method: str, 29 | url: str, 30 | data=None, 31 | params=None, 32 | json=None, 33 | is_return_none: bool = False, 34 | is_expected_to_fail: bool = False, 35 | ): 36 | params = params or {} 37 | url = self.BASE_URL + url 38 | return super()._make_request( 39 | method, 40 | url, 41 | data, 42 | params=params, 43 | json=json, 44 | is_return_none=is_return_none, 45 | is_expected_to_fail=is_expected_to_fail, 46 | ) 47 | 48 | def get_torrent_instant_availability(self, urls): 49 | return self._make_request( 50 | "POST", 51 | "/link/lookup", 52 | json={"urls": urls}, 53 | ) 54 | 55 | def create_download_link(self, magnet): 56 | return self._make_request( 57 | "POST", 58 | "/link/generate", 59 | json={"url": magnet}, 60 | ) 61 | 62 | def get_torrent_info(self, torrent_id: str): 63 | pass 64 | 65 | def get_user_info(self): 66 | return self._make_request("GET", "/user/details") 67 | -------------------------------------------------------------------------------- /lib/clients/debrid/torbox.py: -------------------------------------------------------------------------------- 1 | from lib.clients.debrid.base import DebridClient, ProviderException 2 | from lib.utils.kodi.utils import notification 3 | 4 | 5 | class Torbox(DebridClient): 6 | BASE_URL = "https://api.torbox.app/v1/api" 7 | 8 | def initialize_headers(self): 9 | if self.token: 10 | self.headers = {"Authorization": f"Bearer {self.token}"} 11 | 12 | def disable_access_token(self): 13 | pass 14 | 15 | def _handle_service_specific_errors(self, error_data: dict, status_code: int): 16 | error_code = error_data.get("error") 17 | if error_code in {"BAD_TOKEN", "AUTH_ERROR", "OAUTH_VERIFICATION_ERROR"}: 18 | raise ProviderException("Invalid Torbox token") 19 | elif error_code == "DOWNLOAD_TOO_LARGE": 20 | raise ProviderException("Download size too large for the user plan") 21 | elif error_code in {"ACTIVE_LIMIT", "MONTHLY_LIMIT"}: 22 | raise ProviderException("Download limit exceeded") 23 | elif error_code in {"DOWNLOAD_SERVER_ERROR", "DATABASE_ERROR"}: 24 | raise ProviderException("Torbox server error") 25 | 26 | def _make_request( 27 | self, 28 | method, 29 | url, 30 | data=None, 31 | params=None, 32 | json=None, 33 | is_return_none=False, 34 | is_expected_to_fail=False, 35 | ): 36 | params = params or {} 37 | url = self.BASE_URL + url 38 | return super()._make_request( 39 | method, 40 | url, 41 | data=data, 42 | params=params, 43 | json=json, 44 | is_return_none=is_return_none, 45 | is_expected_to_fail=is_expected_to_fail, 46 | ) 47 | 48 | def add_magnet_link(self, magnet_link): 49 | return self._make_request( 50 | "POST", 51 | "/torrents/createtorrent", 52 | data={"magnet": magnet_link}, 53 | is_expected_to_fail=False, 54 | ) 55 | 56 | def get_user_torrent_list(self): 57 | return self._make_request( 58 | "GET", 59 | "/torrents/mylist", 60 | params={"bypass_cache": "true"}, 61 | ) 62 | 63 | def get_torrent_info(self, magnet_id): 64 | response = self.get_user_torrent_list() 65 | torrent_list = response.get("data", {}) 66 | for torrent in torrent_list: 67 | if torrent.get("magnet", "") == magnet_id: 68 | return torrent 69 | 70 | def get_available_torrent(self, info_hash): 71 | response = self.get_user_torrent_list() 72 | torrent_list = response.get("data", {}) 73 | for torrent in torrent_list: 74 | if torrent.get("hash", "") == info_hash: 75 | return torrent 76 | 77 | def get_torrent_instant_availability(self, torrent_hashes): 78 | return self._make_request( 79 | "GET", 80 | "/torrents/checkcached", 81 | params={"hash": torrent_hashes, "format": "object"}, 82 | ) 83 | 84 | def create_download_link(self, torrent_id, filename, user_ip): 85 | params = { 86 | "token": self.token, 87 | "torrent_id": torrent_id, 88 | "file_id": filename, 89 | } 90 | if user_ip: 91 | params["user_ip"] = user_ip 92 | 93 | response = self._make_request( 94 | "GET", 95 | "/torrents/requestdl", 96 | params=params, 97 | is_expected_to_fail=True, 98 | ) 99 | if "successfully" in response.get("detail"): 100 | return response 101 | raise ProviderException( 102 | f"Failed to create download link from Torbox {response}", 103 | ) 104 | 105 | def download(self, magnet): 106 | response_data = self.add_magnet_link(magnet) 107 | if response_data.get("detail") is False: 108 | notification(f"Failed to add magnet link to Torbox {response_data}") 109 | else: 110 | notification(f"Magnet sent to cloud") 111 | 112 | def delete_torrent(self, torrent_id): 113 | self._make_request( 114 | "POST", 115 | "/torrents/controltorrent", 116 | data={"torrent_id": torrent_id, "operation": "Delete"}, 117 | is_expected_to_fail=False, 118 | ) 119 | -------------------------------------------------------------------------------- /lib/clients/elfhosted.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List, Dict, Any, Optional 3 | from lib.clients.base import BaseClient, TorrentStream 4 | from lib.utils.kodi.utils import convert_size_to_bytes, translation 5 | 6 | 7 | class Elfhosted(BaseClient): 8 | def __init__(self, host: str, notification: callable) -> None: 9 | super().__init__(host, notification) 10 | 11 | def search( 12 | self, 13 | imdb_id: str, 14 | mode: str, 15 | media_type: str, 16 | season: Optional[int], 17 | episode: Optional[int], 18 | ) -> Optional[List[TorrentStream]]: 19 | try: 20 | if mode == "tv" or media_type == "tv": 21 | url = f"{self.host}/stream/series/{imdb_id}:{season}:{episode}.json" 22 | elif mode == "movies" or media_type == "movies": 23 | url = f"{self.host}/stream/{mode}/{imdb_id}.json" 24 | res = self.session.get(url, timeout=10) 25 | if res.status_code != 200: 26 | return 27 | response = self.parse_response(res) 28 | return response 29 | except Exception as e: 30 | self.handle_exception(f"{translation(30231)}: {str(e)}") 31 | 32 | def parse_response(self, res: any) -> List[TorrentStream]: 33 | res = res.json() 34 | results = [] 35 | for item in res["streams"]: 36 | parsed_item = self.parse_stream_title(item["title"]) 37 | results.append( 38 | TorrentStream( 39 | title=parsed_item["title"], 40 | type="Torrent", 41 | indexer="Elfhosted", 42 | guid=item["infoHash"], 43 | infoHash=item["infoHash"], 44 | size=parsed_item["size"], 45 | publishDate="", 46 | seeders=0, 47 | peers=0, 48 | languages=[], 49 | fullLanguages="", 50 | provider="", 51 | ) 52 | ) 53 | return results 54 | 55 | def parse_stream_title(self, title: str) -> Dict[str, Any]: 56 | name = title.splitlines()[0] 57 | 58 | size_match = re.search(r"💾 (\d+(?:\.\d+)?\s*(GB|MB))", title, re.IGNORECASE) 59 | size = size_match.group(1) if size_match else "" 60 | size = convert_size_to_bytes(size) 61 | 62 | return { 63 | "title": name, 64 | "size": size, 65 | } 66 | -------------------------------------------------------------------------------- /lib/clients/fma.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | 4 | 5 | class FindMyAnime: 6 | base_url = "https://find-my-anime.dtimur.de/api" 7 | 8 | def get_anime_data(self, anime_id, anime_id_provider): 9 | params = {"id": anime_id, "provider": anime_id_provider, "includeAdult": "true"} 10 | return self.make_request(params=params) 11 | 12 | def make_request(self, params): 13 | res = requests.get( 14 | self.base_url, 15 | params=params, 16 | ) 17 | if res.status_code == 200: 18 | return res.json() 19 | else: 20 | raise Exception(f"FMA error::{res.text}") 21 | 22 | 23 | def extract_season(res): 24 | regexes = [ 25 | r"season\s(\d+)", 26 | r"\s(\d+)st\sseason(?:\s|$)", 27 | r"\s(\d+)nd\sseason(?:\s|$)", 28 | r"\s(\d+)rd\sseason(?:\s|$)", 29 | r"\s(\d+)th\sseason(?:\s|$)", 30 | ] 31 | s_ids = [] 32 | for regex in regexes: 33 | if isinstance(res.get("title"), dict): 34 | s_ids += [ 35 | re.findall(regex, name, re.IGNORECASE) 36 | for lang, name in res.get("title").items() 37 | if name is not None 38 | ] 39 | else: 40 | s_ids += [ 41 | re.findall(regex, name, re.IGNORECASE) for name in res.get("title") 42 | ] 43 | 44 | s_ids += [ 45 | re.findall(regex, name, re.IGNORECASE) for name in res.get("synonyms") 46 | ] 47 | 48 | s_ids = [s[0] for s in s_ids if s] 49 | 50 | if not s_ids: 51 | regex = r"\s(\d+)$" 52 | cour = False 53 | if isinstance(res.get("title"), dict): 54 | for lang, name in res.get("title").items(): 55 | if name is not None and ( 56 | " part " in name.lower() or " cour " in name.lower() 57 | ): 58 | cour = True 59 | break 60 | if not cour: 61 | s_ids += [ 62 | re.findall(regex, name, re.IGNORECASE) 63 | for lang, name in res.get("title").items() 64 | if name is not None 65 | ] 66 | s_ids += [ 67 | re.findall(regex, name, re.IGNORECASE) 68 | for name in res.get("synonyms") 69 | ] 70 | else: 71 | for name in res.get("title"): 72 | if " part " in name.lower() or " cour " in name.lower(): 73 | cour = True 74 | break 75 | if not cour: 76 | s_ids += [ 77 | re.findall(regex, name, re.IGNORECASE) for name in res.get("title") 78 | ] 79 | s_ids += [ 80 | re.findall(regex, name, re.IGNORECASE) 81 | for name in res.get("synonyms") 82 | ] 83 | s_ids = [s[0] for s in s_ids if s and int(s[0]) < 20] 84 | 85 | return s_ids 86 | -------------------------------------------------------------------------------- /lib/clients/jackett.py: -------------------------------------------------------------------------------- 1 | from lib.clients.base import BaseClient 2 | from lib.clients.base import TorrentStream 3 | 4 | from lib.utils.kodi.utils import translation 5 | from lib.utils.parsers import xmltodict 6 | from lib.utils.kodi.settings import get_jackett_timeout 7 | 8 | from typing import List, Optional, Callable, Any 9 | 10 | 11 | class Jackett(BaseClient): 12 | def __init__( 13 | self, host: str, apikey: str, notification: Callable[[str], None] 14 | ) -> None: 15 | super().__init__(host, notification) 16 | self.apikey = apikey 17 | self.base_url = f"{self.host}/api/v2.0/indexers/all/results/torznab/api?apikey={self.apikey}" 18 | 19 | def _build_url( 20 | self, 21 | query: str, 22 | mode: str, 23 | season: Optional[int] = None, 24 | episode: Optional[int] = None, 25 | categories: Optional[List[int]] = None, 26 | additional_params: Optional[dict] = None, 27 | ) -> str: 28 | url = f"{self.base_url}&q={query}" 29 | if mode == "tv": 30 | url += f"&t=tvsearch&season={season}&ep={episode}" 31 | elif mode == "movies": 32 | url += "&t=movie" 33 | else: 34 | url += "&t=search" 35 | 36 | if categories: 37 | url += f"&cat={','.join(map(str, categories))}" 38 | 39 | if additional_params: 40 | for key, value in additional_params.items(): 41 | url += f"&{key}={value}" 42 | 43 | return url 44 | 45 | def search( 46 | self, 47 | query: str, 48 | mode: str, 49 | season: Optional[int] = None, 50 | episode: Optional[int] = None, 51 | categories: Optional[List[int]] = None, 52 | additional_params: Optional[dict] = None, 53 | ) -> Optional[List[TorrentStream]]: 54 | try: 55 | url = self._build_url( 56 | query, mode, season, episode, categories, additional_params 57 | ) 58 | response = self.session.get(url, timeout=get_jackett_timeout()) 59 | if response.status_code != 200: 60 | self.notification(f"{translation(30229)} ({response.status_code})") 61 | return None 62 | return self.parse_response(response) 63 | except Exception as e: 64 | self.handle_exception(f"{translation(30229)}: {str(e)}") 65 | return None 66 | 67 | def parse_response(self, res: Any) -> Optional[List[TorrentStream]]: 68 | try: 69 | res_dict = xmltodict.parse(res.content) 70 | channel = res_dict.get("rss", {}).get("channel", {}) 71 | items = channel.get("item") 72 | if not items: 73 | return [] 74 | results: List[TorrentStream] = [] 75 | for item in items if isinstance(items, list) else [items]: 76 | self.extract_result(results, item) 77 | return results 78 | except Exception as e: 79 | self.handle_exception(f"Error parsing Jackett response: {str(e)}") 80 | return None 81 | 82 | 83 | def extract_result(self, results: List[TorrentStream], item: dict) -> None: 84 | attrs = item.get("torznab:attr", []) 85 | if isinstance(attrs, dict): 86 | attrs = [attrs] 87 | attributes = {attr.get("@name"): attr.get("@value") for attr in attrs} 88 | results.append( 89 | TorrentStream( 90 | title=item.get("title", ""), 91 | type="Torrent", 92 | indexer="Jackett", 93 | publishDate=item.get("pubDate", ""), 94 | provider=item.get("jackettindexer", {}).get("#text", ""), 95 | guid=item.get("guid", ""), 96 | url=item.get("link", ""), 97 | size=item.get("size", ""), 98 | seeders=int(attributes.get("seeders", 0) or 0), 99 | peers=int(attributes.get("peers", 0) or 0), 100 | infoHash=attributes.get("infohash", ""), 101 | languages=[], 102 | fullLanguages="", 103 | ) 104 | ) 105 | -------------------------------------------------------------------------------- /lib/clients/jackgram/client.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional, Any 2 | from lib.clients.base import BaseClient, TorrentStream 3 | from lib.utils.kodi.utils import kodilog, translation 4 | 5 | 6 | class Jackgram(BaseClient): 7 | def __init__(self, host: str, notification: callable) -> None: 8 | super().__init__(host, notification) 9 | 10 | def search( 11 | self, 12 | tmdb_id: str, 13 | query: str, 14 | mode: str, 15 | media_type: str, 16 | season: Optional[int], 17 | episode: Optional[int], 18 | ) -> Optional[List[TorrentStream]]: 19 | try: 20 | kodilog(f"Searching for {query} on Jackgram") 21 | 22 | if mode == "tv" or media_type == "tv": 23 | url = f"{self.host}/stream/series/{tmdb_id}:{season}:{episode}.json" 24 | elif mode == "movies" or media_type == "movies": 25 | url = f"{self.host}/stream/movie/{tmdb_id}.json" 26 | else: 27 | url = f"{self.host}/search?query={query}" 28 | 29 | kodilog(f"URL: {url}") 30 | 31 | res = self.session.get(url, timeout=10) 32 | if res.status_code != 200: 33 | return 34 | 35 | if mode in ["tv", "movies"]: 36 | return self.parse_response(res) 37 | else: 38 | return self.parse_response_search(res) 39 | except Exception as e: 40 | self.handle_exception(f"{translation(30232)}: {e}") 41 | 42 | def get_latest(self, page: int) -> Optional[Dict[str, Any]]: 43 | url = f"{self.host}/stream/latest?page={page}" 44 | res = self.session.get(url, timeout=10) 45 | if res.status_code != 200: 46 | return 47 | return res.json() 48 | 49 | def get_files(self, page: int) -> Optional[Dict[str, Any]]: 50 | url = f"{self.host}/stream/files?page={page}" 51 | res = self.session.get(url, timeout=10) 52 | if res.status_code != 200: 53 | return 54 | return res.json() 55 | 56 | def parse_response(self, res: Any) -> List[TorrentStream]: 57 | res = res.json() 58 | results = [] 59 | for item in res["streams"]: 60 | results.append( 61 | TorrentStream( 62 | title=item["title"], 63 | type="Direct", 64 | indexer=item["name"], 65 | size=item["size"], 66 | publishDate=item["date"], 67 | url=item["url"], 68 | ) 69 | ) 70 | return results 71 | 72 | def parse_response_search(self, res: Any) -> List[TorrentStream]: 73 | res = res.json() 74 | results = [] 75 | for item in res["results"]: 76 | if item.get("type") == "file": 77 | file_info = self._extract_file_info(item) 78 | results.append(TorrentStream(**file_info)) 79 | else: 80 | for file in item.get("files", []): 81 | file_info = self._extract_file_info(file) 82 | results.append(TorrentStream(**file_info)) 83 | return results 84 | 85 | def _extract_file_info(self, file): 86 | return { 87 | "title": file.get("title", ""), 88 | "type": "Direct", 89 | "indexer": file.get("name", ""), 90 | "size": file.get("size", ""), 91 | "publishDate": file.get("date", ""), 92 | "url": file.get("url", ""), 93 | } 94 | -------------------------------------------------------------------------------- /lib/clients/jackgram/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from lib.clients.jackgram.client import Jackgram 3 | from lib.clients.tmdb.utils import tmdb_get 4 | from lib.utils.clients.utils import validate_host 5 | from lib.utils.general.utils import ( 6 | Indexer, 7 | add_next_button, 8 | execute_thread_pool, 9 | list_item, 10 | set_content_type, 11 | set_media_infoTag, 12 | set_watched_title 13 | ) 14 | 15 | from lib.utils.kodi.utils import ( 16 | ADDON_HANDLE, 17 | build_url, 18 | get_setting, 19 | notification, 20 | set_view, 21 | ) 22 | 23 | from xbmcplugin import addDirectoryItem, endOfDirectory 24 | from xbmcgui import ListItem 25 | 26 | 27 | def check_jackgram_active(): 28 | jackgram_enabled = get_setting("jackgram_enabled") 29 | if not jackgram_enabled: 30 | notification("You need to activate Jackgram indexer") 31 | return False 32 | return True 33 | 34 | 35 | def check_and_get_jackgram_client(): 36 | if not check_jackgram_active(): 37 | return None 38 | host = get_setting("jackgram_host") 39 | if not validate_host(host, Indexer.TELEGRAM): 40 | return None 41 | return Jackgram(host, notification) 42 | 43 | 44 | def process_results(results, callback, next_button_action, page): 45 | execute_thread_pool(results, callback) 46 | add_next_button(next_button_action, page=page) 47 | endOfDirectory(ADDON_HANDLE) 48 | set_view("widelist") 49 | 50 | 51 | def get_telegram_files(params): 52 | page = int(params.get("page")) 53 | jackgram_client = check_and_get_jackgram_client() 54 | if not jackgram_client: 55 | return 56 | results = jackgram_client.get_files(page=page) 57 | process_results(results, telegram_files, "get_telegram_files", page) 58 | 59 | 60 | def telegram_files(res): 61 | item = list_item(res["file_name"], icon="trending.png") 62 | item.setProperty("IsPlayable", "true") 63 | addDirectoryItem( 64 | ADDON_HANDLE, 65 | build_url("play_torrent", data=res), 66 | item, 67 | isFolder=False, 68 | ) 69 | 70 | 71 | def get_telegram_latest(params): 72 | page = int(params.get("page")) 73 | jackgram_client = check_and_get_jackgram_client() 74 | if not jackgram_client: 75 | return 76 | results = jackgram_client.get_latest(page=page) 77 | process_results(results, telegram_latest_items, "get_telegram_latest", page) 78 | 79 | 80 | def telegram_latest_items(res): 81 | mode = res["type"] 82 | title = res["title"] 83 | details = tmdb_get(f"{mode}_details", res["tmdb_id"]) 84 | 85 | tmdb_id = res["tmdb_id"] 86 | imdb_id = details.external_ids.get("imdb_id") 87 | tvdb_id = details.external_ids.get("tvdb_id") 88 | res["ids"] = {"tmdb_id": tmdb_id, "tvdb_id": tvdb_id, "imdb_id": imdb_id} 89 | 90 | list_item = ListItem(label=title) 91 | set_media_infoTag(list_item, metadata=details, mode=mode) 92 | 93 | addDirectoryItem( 94 | ADDON_HANDLE, 95 | build_url("get_telegram_latest_files", data=json.dumps(res)), 96 | list_item, 97 | isFolder=True, 98 | ) 99 | 100 | 101 | def get_telegram_latest_files(params): 102 | res = json.loads(params["data"]) 103 | set_watched_title(title=res["title"], ids=res["ids"], tg_data=res, mode="tg_latest") 104 | set_content_type(res["type"]) 105 | execute_thread_pool(res["files"], telegram_latest_files, res) 106 | endOfDirectory(ADDON_HANDLE) 107 | 108 | 109 | def telegram_latest_files(file, data): 110 | mode = file["mode"] 111 | title = file["title"] 112 | 113 | list_item = ListItem(label=title) 114 | if mode == "tv": 115 | details = tmdb_get( 116 | "episode_details", 117 | params={ 118 | "id": data["tmdb_id"], 119 | "season": file["season"], 120 | "episode": file["episode"], 121 | }, 122 | ) 123 | else: 124 | details = tmdb_get("movie_details", data["tmdb_id"]) 125 | 126 | list_item.setProperty("IsPlayable", "true") 127 | set_media_infoTag(list_item, metadata=details, mode=mode) 128 | 129 | addDirectoryItem( 130 | ADDON_HANDLE, 131 | build_url("play_torrent", data=file), 132 | list_item, 133 | isFolder=False, 134 | ) 135 | -------------------------------------------------------------------------------- /lib/clients/peerflix.py: -------------------------------------------------------------------------------- 1 | import re 2 | from lib.clients.base import BaseClient 3 | from lib.utils.general.utils import USER_AGENT_HEADER 4 | from lib.utils.kodi.utils import translation 5 | 6 | 7 | class Peerflix(BaseClient): 8 | def __init__(self, host, notification, language): 9 | super().__init__(host, notification) 10 | self.language = language.lower() 11 | 12 | def search(self, imdb_id, mode, media_type, season, episode): 13 | try: 14 | if mode == "tv" or media_type == "tv": 15 | url = f"{self.host}/language={self.language}/stream/series/{imdb_id}:{season}:{episode}.json" 16 | elif mode == "movies" or media_type == "movies": 17 | url = f"{self.host}/language={self.language}/stream/movie/{imdb_id}.json" 18 | res = self.session.get(url, headers=USER_AGENT_HEADER, timeout=10) 19 | if res.status_code != 200: 20 | return 21 | return self.parse_response(res) 22 | except Exception as e: 23 | self.handle_exception(f"{translation(30234)}: {str(e)}") 24 | 25 | def parse_response(self, res): 26 | res = res.json() 27 | results = [] 28 | for item in res["streams"]: 29 | results.append( 30 | { 31 | "title": item["title"].splitlines()[0], 32 | "type": "Torrent", 33 | "indexer": "Peerflix", 34 | "guid": item["infoHash"], 35 | "infoHash": item["infoHash"], 36 | "size":item["sizebytes"] or 0, 37 | "seeders": item.get("seed", 0) or 0, 38 | "languages": [item["language"]], 39 | "fullLanguages": [item["language"]], 40 | "provider": self.extract_provider(item["title"]), 41 | "publishDate": "", 42 | "peers": 0, 43 | } 44 | ) 45 | return results 46 | 47 | def extract_provider(self, title): 48 | match = re.search(r"🌐.* ([^ \n]+)", title) 49 | return match.group(1) if match else "" 50 | -------------------------------------------------------------------------------- /lib/clients/plex.py: -------------------------------------------------------------------------------- 1 | from lib.api.plex.media_server import convert_to_plex_id, get_media 2 | 3 | 4 | class Plex: 5 | def __init__(self, discovery_url, auth_token, access_token, notification): 6 | self.discovery_url = discovery_url 7 | self.auth_token = auth_token 8 | self.access_token = access_token 9 | self._notification = notification 10 | 11 | def search(self, imdb_id, mode, media_type, season, episode): 12 | plex_id = convert_to_plex_id( 13 | url=self.discovery_url, 14 | access_token=self.access_token, 15 | auth_token=self.auth_token, 16 | id=imdb_id, 17 | mode=mode, 18 | media_type=media_type, 19 | season=season, 20 | episode=episode, 21 | ) 22 | 23 | if not plex_id: 24 | return 25 | 26 | media = get_media( 27 | url=self.discovery_url, 28 | token=self.access_token, 29 | guid=plex_id, 30 | ) 31 | 32 | streams = [meta.get_streams() for meta in media] 33 | return self.parse_response(streams) 34 | 35 | def parse_response(self, streams): 36 | results = [] 37 | for stream in streams: 38 | for item in stream: 39 | results.append( 40 | { 41 | "title": item["title"], 42 | "indexer": item["indexer"], 43 | "downloadUrl": item["url"], 44 | "publishDate": "", 45 | } 46 | ) 47 | return results 48 | -------------------------------------------------------------------------------- /lib/clients/prowlarr.py: -------------------------------------------------------------------------------- 1 | from lib.clients.base import BaseClient, TorrentStream 2 | from lib.utils.kodi.utils import translation 3 | from lib.utils.kodi.settings import get_prowlarr_timeout 4 | from typing import List, Optional, Any 5 | 6 | 7 | class Prowlarr(BaseClient): 8 | def __init__(self, host: str, apikey: str, notification: callable) -> None: 9 | super().__init__(host, notification) 10 | self.base_url = f"{self.host}/api/v1/search" 11 | self.apikey = apikey 12 | 13 | def search( 14 | self, 15 | query: str, 16 | mode: str, 17 | season: Optional[int] = None, 18 | episode: Optional[int] = None, 19 | indexers: Optional[str] = None, 20 | ) -> Optional[List[TorrentStream]]: 21 | headers = { 22 | "Accept": "application/json", 23 | "Content-Type": "application/json", 24 | "X-Api-Key": self.apikey, 25 | } 26 | try: 27 | params = {"query": query, "type": "search"} 28 | if mode == "tv": 29 | params["categories"] = [5000, 8000] 30 | if season is not None and episode is not None: 31 | params["query"] = f"{query} S{int(season):02d}E{int(episode):02d}" 32 | elif mode == "movies": 33 | params["categories"] = [2000, 8000] 34 | elif mode == "anime": 35 | params["categories"] = [2000, 5070, 5000, 127720, 140679] 36 | if indexers: 37 | # Accept comma or space separated 38 | indexer_ids = [ 39 | i.strip() for i in indexers.replace(",", " ").split() if i.strip() 40 | ] 41 | if indexer_ids: 42 | params["indexerIds"] = indexer_ids 43 | response = self.session.get( 44 | self.base_url, 45 | params=params, 46 | timeout=get_prowlarr_timeout(), 47 | headers=headers, 48 | ) 49 | if response.status_code != 200: 50 | self.notification(f"{translation(30230)} {response.status_code}") 51 | return None 52 | return self.parse_response(response) 53 | except Exception as e: 54 | self.handle_exception(f"{translation(30230)}: {str(e)}") 55 | return None 56 | 57 | def parse_response(self, res: Any) -> List[TorrentStream]: 58 | response = res.json() 59 | results = [] 60 | for res in response: 61 | results.append( 62 | TorrentStream( 63 | title=res.get("title", ""), 64 | type="Torrent", 65 | indexer="Prowlarr", 66 | provider=res.get("indexer"), 67 | peers=int(res.get("peers", 0)), 68 | seeders=int(res.get("seeders", 0)), 69 | guid=res.get("guid", ""), 70 | infoHash=res.get("infoHash", ""), 71 | size=int(res.get("size", 0)), 72 | languages=res.get("languages", []), 73 | fullLanguages=res.get("fullLanguages", ""), 74 | publishDate=res.get("publishDate", ""), 75 | ) 76 | ) 77 | return results 78 | -------------------------------------------------------------------------------- /lib/clients/simkl.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | class SIMKL: 5 | def __init__(self): 6 | self.ClientID = ( 7 | "59dfdc579d244e1edf6f89874d521d37a69a95a1abd349910cb056a1872ba2c8" 8 | ) 9 | self.base_url = "https://api.simkl.com/" 10 | self.imagePath = "https://wsrv.nl/?url=https://simkl.in/episodes/%s_w.webp" 11 | 12 | def _to_url(self, url=""): 13 | if url.startswith("/"): 14 | url = url[1:] 15 | return "%s/%s" % (self.baseUrl[:-1], url) 16 | 17 | def make_request(self, endpoint, params): 18 | res = requests.get( 19 | f"{self.base_url}{endpoint}", 20 | params=params, 21 | ) 22 | if res.status_code == 200: 23 | return json.loads(res.text) 24 | else: 25 | raise Exception(f"Simkl Error::{res.text}") 26 | 27 | def get_anime_info(self, anilist_id): 28 | _, simkl_id = self.get_simkl_id("anilist", anilist_id) 29 | params = {"extended": "full", "client_id": self.ClientID} 30 | return self.make_request("anime/" + str(simkl_id), params=params) 31 | 32 | def get_anilist_episodes(self, mal_id): 33 | _, simkl_id = self.get_simkl_id("mal", mal_id) 34 | params = { 35 | "extended": "full", 36 | } 37 | return self.make_request("anime/episodes/" + str(simkl_id), params=params) 38 | 39 | def get_simkl_id(self, send_id, anime_id): 40 | params = { 41 | send_id: anime_id, 42 | "client_id": self.ClientID, 43 | } 44 | res = self.make_request("search/id", params=params) 45 | simkl = res[0]["ids"]["simkl"] 46 | return simkl 47 | 48 | def get_mapping_ids(self, send_id, anime_id): 49 | simkl_id = self.get_simkl_id(send_id, anime_id) 50 | params = {"extended": "full", "client_id": self.ClientID} 51 | res = self.make_request("anime/" + str(simkl_id), params=params) 52 | return res["ids"] 53 | -------------------------------------------------------------------------------- /lib/clients/stremio/client.py: -------------------------------------------------------------------------------- 1 | from json import JSONDecodeError 2 | from requests.exceptions import RequestException, Timeout, TooManyRedirects 3 | from requests import Session 4 | from lib.utils.general.utils import USER_AGENT_HEADER 5 | from lib.utils.kodi.utils import kodilog 6 | 7 | 8 | class Stremio: 9 | def __init__(self, authKey=None): 10 | self.authKey = authKey 11 | self.session = Session() 12 | self.session.headers.update(USER_AGENT_HEADER) 13 | 14 | def _request(self, method, url, data=None): 15 | try: 16 | if method == 'GET': 17 | resp = self.session.get(url, timeout=10) 18 | elif method == 'POST': 19 | resp = self.session.post(url, json=data, timeout=10) 20 | else: 21 | raise ValueError(f"Unsupported HTTP method: {method}") 22 | 23 | if resp.status_code != 200: 24 | kodilog(f"Status code {resp.status_code} received for URL: {url}. Response: {resp.text}") 25 | resp.raise_for_status() 26 | 27 | try: 28 | return resp.json() 29 | except JSONDecodeError: 30 | kodilog(f"Failed to decode JSON response for URL: {url}. Response: {resp.text}") 31 | raise 32 | except Timeout: 33 | kodilog(f"Request timed out for URL: {url}") 34 | raise 35 | except TooManyRedirects: 36 | kodilog(f"Too many redirects encountered for URL: {url}") 37 | raise 38 | except RequestException as e: 39 | kodilog(f"Failed to fetch data from {url}: {e}") 40 | raise 41 | 42 | def _get(self, url): 43 | return self._request('GET', url) 44 | 45 | def _post(self, url, data): 46 | return self._request('POST', url, data) 47 | 48 | def login(self, email, password): 49 | """Login to Stremio account.""" 50 | 51 | data = { 52 | "authKey": self.authKey, 53 | "email": email, 54 | "password": password, 55 | } 56 | 57 | res = self._post("https://api.strem.io/api/login", data) 58 | self.authKey = res.get("result", {}).get("authKey", None) 59 | 60 | def dataExport(self): 61 | """Export user data.""" 62 | assert self.authKey, "Login first" 63 | data = {"authKey": self.authKey} 64 | res = self._post("https://api.strem.io/api/dataExport", data) 65 | exportId = res.get("result", {}).get("exportId", None) 66 | 67 | dataExport = self._get( 68 | f"https://api.strem.io/data-export/{exportId}/export.json" 69 | ) 70 | return dataExport 71 | 72 | def get_community_addons(self): 73 | """Get community addons.""" 74 | response = self._get("https://stremio-addons.com/catalog.json") 75 | return response 76 | 77 | def get_my_addons(self): 78 | """Get user addons.""" 79 | response = self.dataExport() 80 | return response.get("addons", {}).get("addons", []) 81 | -------------------------------------------------------------------------------- /lib/clients/stremio/stream.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | class Stream: 4 | def __init__(self, json_string): 5 | if isinstance(json_string, str): 6 | try: 7 | data = json.loads(json_string) 8 | except json.JSONDecodeError as e: 9 | raise ValueError(f"Invalid JSON string: {e}") 10 | elif isinstance(json_string, dict): 11 | data = json_string 12 | else: 13 | raise ValueError("Input must be a JSON string or a dictionary.") 14 | 15 | # Initialize required attributes 16 | self.url = data.get("url") 17 | self.ytId = data.get("ytId") 18 | self.infoHash = data.get("infoHash") 19 | self.fileIdx = data.get("fileIdx") 20 | self.externalUrl = data.get("externalUrl") 21 | 22 | # Initialize optional attributes 23 | self.name = data.get("name") 24 | self.title = data.get("title") # deprecated 25 | self.description = data.get( 26 | "description", self.title 27 | ) # Use `title` as fallback 28 | self.subtitles = data.get("subtitles", []) 29 | self.sources = data.get("sources", []) 30 | 31 | # Initialize behavior hints 32 | behavior_hints = data.get("behaviorHints", {}) 33 | self.countryWhitelist = behavior_hints.get("countryWhitelist", []) 34 | self.notWebReady = behavior_hints.get("notWebReady", False) 35 | self.bingeGroup = behavior_hints.get("bingeGroup") 36 | self.proxyHeaders = behavior_hints.get("proxyHeaders", {}) 37 | self.videoHash = behavior_hints.get("videoHash") 38 | self.videoSize = behavior_hints.get("videoSize") 39 | self.filename = behavior_hints.get("filename") 40 | 41 | # Validation for at least one stream identifier 42 | if not (self.url or self.ytId or self.infoHash or self.externalUrl): 43 | raise ValueError( 44 | "At least one of 'url', 'ytId', 'infoHash', or 'externalUrl' must be specified." 45 | ) 46 | 47 | def get_parsed_title(self) -> str: 48 | title = self.filename or self.description or self.title 49 | return title.splitlines()[0] if title else "" 50 | 51 | def get_parsed_size(self) -> int: 52 | return self.videoSize or 0 53 | 54 | def __repr__(self): 55 | return f"Stream(name={self.name}, url={self.url}, ytId={self.ytId}, infoHash={self.infoHash}, externalUrl={self.externalUrl})" 56 | -------------------------------------------------------------------------------- /lib/clients/torrentio.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List, Dict, Tuple, Optional, Any 3 | from lib.clients.base import BaseClient, TorrentStream 4 | from lib.utils.localization.countries import find_language_by_unicode 5 | from lib.utils.kodi.utils import convert_size_to_bytes, kodilog, translation 6 | from lib.utils.general.utils import USER_AGENT_HEADER, unicode_flag_to_country_code 7 | 8 | 9 | class Torrentio(BaseClient): 10 | def __init__(self, host: str, notification: callable) -> None: 11 | super().__init__(host, notification) 12 | 13 | def search( 14 | self, 15 | imdb_id: str, 16 | mode: str, 17 | media_type: str, 18 | season: Optional[int], 19 | episode: Optional[int], 20 | ) -> Optional[List[TorrentStream]]: 21 | try: 22 | kodilog(f"Searching for {imdb_id} on Torrentio") 23 | 24 | if mode == "tv" or media_type == "tv": 25 | url = f"{self.host}/stream/series/{imdb_id}:{season}:{episode}.json" 26 | elif mode == "movies" or media_type == "movies": 27 | url = f"{self.host}/stream/{mode}/{imdb_id}.json" 28 | 29 | kodilog(f"URL: {url}") 30 | 31 | res = self.session.get(url, headers=USER_AGENT_HEADER, timeout=10) 32 | if res.status_code != 200: 33 | return 34 | return self.parse_response(res) 35 | except Exception as e: 36 | self.handle_exception(f"{translation(30228)}: {str(e)}") 37 | 38 | def parse_response(self, res: Any) -> List[TorrentStream]: 39 | res = res.json() 40 | results = [] 41 | for item in res["streams"]: 42 | parsed_item = self.parse_stream_title(item["title"]) 43 | results.append( 44 | TorrentStream( 45 | title=parsed_item["title"], 46 | type="Torrent", 47 | indexer="Torrentio", 48 | guid=item["infoHash"], 49 | infoHash=item["infoHash"], 50 | size=parsed_item["size"], 51 | seeders=parsed_item["seeders"], 52 | languages=parsed_item["languages"], 53 | fullLanguages=parsed_item["full_languages"], 54 | provider=parsed_item["provider"], 55 | publishDate="", 56 | peers=0, 57 | ) 58 | ) 59 | return results 60 | 61 | def parse_stream_title(self, title: str) -> Dict[str, Any]: 62 | name = title.splitlines()[0] 63 | 64 | size_match = re.search(r"💾 (\d+(?:\.\d+)?\s*(GB|MB))", title, re.IGNORECASE) 65 | size = size_match.group(1) if size_match else "" 66 | size = convert_size_to_bytes(size) 67 | 68 | seeders_match = re.search(r"👤 (\d+)", title) 69 | seeders = int(seeders_match.group(1)) if seeders_match else None 70 | 71 | languages, full_languages = self.extract_languages(title) 72 | 73 | provider = self.extract_provider(title) 74 | 75 | return { 76 | "title": name, 77 | "size": size, 78 | "seeders": seeders, 79 | "languages": languages, 80 | "full_languages": full_languages, 81 | "provider": provider, 82 | } 83 | 84 | def extract_languages(self, title: str) -> Tuple[List[str], List[str]]: 85 | languages = [] 86 | full_languages = [] 87 | # Regex to match unicode country flag emojis 88 | flag_emojis = re.findall(r"[\U0001F1E6-\U0001F1FF]{2}", title) 89 | if flag_emojis: 90 | for flag in flag_emojis: 91 | languages.append(unicode_flag_to_country_code(flag).upper()) 92 | full_lang = find_language_by_unicode(flag) 93 | if (full_lang != None) and (full_lang not in full_languages): 94 | full_languages.append(full_lang) 95 | return languages, full_languages 96 | 97 | def extract_provider(self, title: str) -> str: 98 | match = re.search(r"⚙.* ([^ \n]+)", title) 99 | return match.group(1) if match else "" 100 | -------------------------------------------------------------------------------- /lib/clients/trakt/paginator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from lib.api.trakt.base_cache import connect_database 3 | 4 | 5 | class Paginator: 6 | def __init__(self, page_size=10): 7 | self.page_size = page_size 8 | self.total_pages = 0 9 | self.current_page = 0 10 | 11 | def _connect(self): 12 | return connect_database(database_name="paginator_db") 13 | 14 | def _store_page(self, page_number, total_pages, data): 15 | conn = self._connect() 16 | cursor = conn.cursor() 17 | cursor.execute(''' 18 | INSERT OR REPLACE INTO paginated_data (id, page_number, data, total_pages) 19 | VALUES (?, ?, ?, ?) 20 | ''', (f'page_{page_number}', page_number, json.dumps(data), total_pages)) 21 | conn.commit() 22 | conn.close() 23 | 24 | def _retrieve_page(self, page_number): 25 | conn = self._connect() 26 | cursor = conn.cursor() 27 | cursor.execute(''' 28 | SELECT data FROM paginated_data WHERE page_number = ? 29 | ''', (page_number,)) 30 | result = cursor.fetchone() 31 | conn.close() 32 | if result and result[0] is not None: 33 | return json.loads(result[0]) 34 | return None 35 | 36 | def _clear_table(self): 37 | conn = self._connect() 38 | cursor = conn.cursor() 39 | cursor.execute('DELETE FROM paginated_data') 40 | conn.commit() 41 | conn.close() 42 | 43 | def initialize(self, data): 44 | self._clear_table() 45 | self.total_pages = (len(data) + self.page_size - 1) // self.page_size 46 | for i in range(self.total_pages): 47 | start = i * self.page_size 48 | end = start + self.page_size 49 | page_data = data[start:end] 50 | self._store_page(i + 1, self.total_pages, page_data) 51 | 52 | def get_page(self, page_number): 53 | self.current_page = page_number - 1 54 | data = self._retrieve_page(page_number) 55 | if data is None: 56 | raise IndexError("Page number not found in database: Requested page {}".format(page_number)) 57 | return data 58 | 59 | def next_page(self): 60 | """Move to the next page.""" 61 | if self.current_page < self.total_pages - 1: 62 | self.current_page += 1 63 | return self.get_page(self.current_page + 1) 64 | 65 | def previous_page(self): 66 | """Move to the previous page.""" 67 | if self.current_page > 0: 68 | self.current_page -= 1 69 | return self.get_page(self.current_page + 1) 70 | 71 | def current_page_data(self): 72 | """Get the data for the current page.""" 73 | return self.get_page(self.current_page + 1) 74 | 75 | 76 | 77 | paginator_db = Paginator() 78 | -------------------------------------------------------------------------------- /lib/db/anime.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | from sqlite3 import dbapi2 as db 4 | from lib.utils.kodi.utils import notification 5 | import xbmcaddon 6 | from xbmcvfs import translatePath 7 | 8 | 9 | try: 10 | OTAKU_ADDON = xbmcaddon.Addon("script.otaku.mappings") 11 | TRANSLATEPATH = translatePath 12 | mappingPath = TRANSLATEPATH(OTAKU_ADDON.getAddonInfo("path")) 13 | mappingDB = os.path.join(mappingPath, "resources", "data", "anime_mappings.db") 14 | except: 15 | OTAKU_ADDON = None 16 | 17 | mappingDB_lock = threading.Lock() 18 | 19 | 20 | def get_all_ids(anilist_id): 21 | if OTAKU_ADDON is None: 22 | notification("Otaku (script.otaku.mappings) not found") 23 | return 24 | mappingDB_lock.acquire() 25 | conn = db.connect(mappingDB, timeout=60.0) 26 | conn.row_factory = _dict_factory 27 | conn.execute("PRAGMA FOREIGN_KEYS = 1") 28 | cursor = conn.cursor() 29 | cursor.execute("SELECT * FROM anime WHERE anilist_id IN ({0})".format(anilist_id)) 30 | mapping = cursor.fetchone() 31 | cursor.close() 32 | try_release_lock(mappingDB_lock) 33 | all_ids = {} 34 | if mapping: 35 | if mapping["thetvdb_id"] is not None: 36 | all_ids.update({"tvdb": str(mapping["thetvdb_id"])}) 37 | if mapping["themoviedb_id"] is not None: 38 | all_ids.update({"tmdb": str(mapping["themoviedb_id"])}) 39 | if mapping["anidb_id"] is not None: 40 | all_ids.update({"anidb": str(mapping["anidb_id"])}) 41 | if mapping["imdb_id"] is not None: 42 | all_ids.update({"imdb": str(mapping["imdb_id"])}) 43 | if mapping["trakt_id"] is not None: 44 | all_ids.update({"trakt": str(mapping["trakt_id"])}) 45 | return all_ids 46 | 47 | 48 | def try_release_lock(lock): 49 | if lock.locked(): 50 | lock.release() 51 | 52 | 53 | def _dict_factory(cursor, row): 54 | d = {} 55 | for idx, col in enumerate(cursor.description): 56 | d[col[0]] = row[idx] 57 | return d 58 | -------------------------------------------------------------------------------- /lib/db/bookmark.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import xbmc 3 | from xbmcvfs import translatePath 4 | import xbmcaddon 5 | import os 6 | 7 | 8 | class BookmarkDb: 9 | def __init__(self): 10 | self.create_paths() 11 | self.create_bookmarks_table() 12 | 13 | def create_paths(self): 14 | userdata_path = translatePath(xbmcaddon.Addon().getAddonInfo("profile")) 15 | database_path_raw = translatePath(os.path.join(userdata_path, "databases")) 16 | if not os.path.exists(database_path_raw): 17 | os.makedirs(database_path_raw) 18 | self.db_path = translatePath(os.path.join(database_path_raw, ".bookmarks.db")) 19 | 20 | def create_bookmarks_table(self): 21 | try: 22 | conn = sqlite3.connect( 23 | self.db_path, 24 | isolation_level=None, 25 | check_same_thread=False, 26 | ) 27 | cursor = conn.cursor() 28 | cursor.execute( 29 | """ 30 | CREATE TABLE IF NOT EXISTS bookmarks ( 31 | url TEXT PRIMARY KEY, 32 | bookmark REAL 33 | ) 34 | """ 35 | ) 36 | conn.commit() 37 | conn.close() 38 | except sqlite3.Error as e: 39 | xbmc.log( 40 | f"Error in BookmarkDb.create_bookmarks_table: {str(e)}", 41 | level=xbmc.LOGERROR, 42 | ) 43 | 44 | def set_bookmark(self, url, time): 45 | try: 46 | conn = sqlite3.connect(self.db_path) 47 | cursor = conn.cursor() 48 | cursor.execute( 49 | "INSERT OR REPLACE INTO bookmarks (url, bookmark) VALUES (?, ?)", 50 | (url, time), 51 | ) 52 | conn.commit() 53 | conn.close() 54 | except sqlite3.Error as e: 55 | xbmc.log(f"Error in BookmarkDb.set_bookmark: {str(e)}", level=xbmc.LOGERROR) 56 | 57 | def get_bookmark(self, url): 58 | try: 59 | conn = sqlite3.connect(self.db_path) 60 | cursor = conn.cursor() 61 | cursor.execute("SELECT bookmark FROM bookmarks WHERE url=?", (url,)) 62 | row = cursor.fetchone() 63 | conn.close() 64 | return row[0] if row else 0.0 65 | except sqlite3.Error as e: 66 | xbmc.log(f"Error in BookmarkDb.get_bookmark: {str(e)}", level=xbmc.LOGERROR) 67 | return 0.0 68 | 69 | def clear_bookmarks(self): 70 | try: 71 | conn = sqlite3.connect(self.db_path) 72 | cursor = conn.cursor() 73 | cursor.execute("DELETE FROM bookmarks") 74 | conn.commit() 75 | conn.close() 76 | except sqlite3.Error as e: 77 | xbmc.log(f"Error in BookmarkDb.clear_bookmarks: {str(e)}", level=xbmc.LOGERROR) 78 | 79 | -------------------------------------------------------------------------------- /lib/db/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | from lib.utils.kodi.utils import ADDON_ID 4 | import xbmcvfs 5 | 6 | 7 | class MainDatabase: 8 | def __init__(self): 9 | BASE_DATABASE = { 10 | "jt:watch": {}, 11 | "jt:lth": {}, 12 | "jt:lfh": {}, 13 | } 14 | 15 | data_dir = xbmcvfs.translatePath( 16 | os.path.join("special://profile/addon_data/", ADDON_ID) 17 | ) 18 | database_path = os.path.join(data_dir, "database.pickle") 19 | xbmcvfs.mkdirs(data_dir) 20 | 21 | if os.path.exists(database_path): 22 | with open(database_path, "rb") as f: 23 | database = pickle.load(f) 24 | else: 25 | database = {} 26 | 27 | database = {**BASE_DATABASE, **database} 28 | 29 | self.database = database 30 | self.database_path = database_path 31 | self.addon_xml_path = xbmcvfs.translatePath( 32 | os.path.join("special://home/addons/", ADDON_ID, "addon.xml") 33 | ) 34 | 35 | def set_data(self, key, subkey, value): 36 | if subkey in self.database[key]: 37 | del self.database[key][subkey] 38 | self.database[key][subkey]= value 39 | self.commit() 40 | 41 | def set_query(self, key, value): 42 | self.database[key]= value 43 | self.commit() 44 | 45 | def get_query(self, key): 46 | return self.database[key] 47 | 48 | def commit(self): 49 | with open(self.database_path, "wb") as f: 50 | pickle.dump(self.database, f) 51 | 52 | 53 | main_db = MainDatabase() 54 | -------------------------------------------------------------------------------- /lib/domain/torrent.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class TorrentStream: 6 | title: str 7 | type: str 8 | indexer: str 9 | guid: str 10 | infoHash: str 11 | size: int 12 | seeders: int 13 | languages: list 14 | fullLanguages: str 15 | provider: str 16 | publishDate: str 17 | peers: int 18 | quality: str = "N/A" 19 | url: str = "" 20 | isPack: bool = False 21 | isCached: bool = False 22 | -------------------------------------------------------------------------------- /lib/gui/custom_progress.py: -------------------------------------------------------------------------------- 1 | from lib.utils.kodi.utils import kodilog 2 | import xbmcgui 3 | 4 | 5 | class CustomProgressDialog(xbmcgui.WindowXMLDialog): 6 | def __init__(self, xml_file: str, location: str): 7 | super().__init__(xml_file, location) 8 | self.title = "Downloading" 9 | self.message = "Please wait..." 10 | self.progress = 0 11 | self.cancelled = False 12 | 13 | def show_dialog(self): 14 | self.show() 15 | self.onInit() 16 | 17 | def close_dialog(self): 18 | self.close() 19 | 20 | def onInit(self): 21 | self.getControl(12001).setLabel(self.title) 22 | self.getControl(12002).setLabel(self.message) 23 | self.getControl(12004).setPercent(self.progress) 24 | self.setFocusId(12003) 25 | 26 | def update_progress(self, percent, message=None): 27 | self.progress = percent 28 | self.getControl(12004).setPercent(percent) 29 | if message: 30 | self.getControl(12002).setLabel(message) 31 | 32 | def onClick(self, controlId): 33 | kodilog(f"Control clicked: {controlId}") 34 | if controlId == 12003: # Cancel button 35 | self.cancelled = True 36 | self.close_dialog() 37 | elif controlId == 12005: # Close button 38 | self.close_dialog() 39 | 40 | def onAction(self, action): 41 | kodilog(f"Action received: {action.getId()}") 42 | if action.getId() in (10, 92): 43 | self.close_dialog() 44 | -------------------------------------------------------------------------------- /lib/gui/filter_items_window.py: -------------------------------------------------------------------------------- 1 | import xbmcgui 2 | from lib.gui.base_window import BaseWindow 3 | 4 | class FilterWindow(BaseWindow): 5 | def __init__(self, xml_file, location, filter): 6 | super().__init__(xml_file, location, None) 7 | self.filter = filter 8 | self.selected_quality = None 9 | 10 | def onInit(self): 11 | self.list_control = self.getControl(2000) 12 | self.list_control.reset() 13 | 14 | for q in self.filter: 15 | item = xbmcgui.ListItem(label=q) 16 | self.list_control.addItem(item) 17 | reset_item = xbmcgui.ListItem(label="Reset Filter") 18 | 19 | self.list_control.addItem(reset_item) 20 | 21 | self.set_default_focus(self.list_control, 2000, control_list_reset=True) 22 | 23 | 24 | def handle_action(self, action_id, control_id=None): 25 | if action_id == 7: # Select 26 | pos = self.list_control.getSelectedPosition() 27 | if pos < len(self.filter): 28 | self.selected_filter = self.filter[pos] 29 | else: 30 | self.selected_filter = None # Reset 31 | self.close() 32 | elif action_id in (2, 9, 10, 13, 92): # Back/Escape 33 | self.close() -------------------------------------------------------------------------------- /lib/gui/filter_type_window.py: -------------------------------------------------------------------------------- 1 | import xbmcgui 2 | from lib.gui.base_window import BaseWindow 3 | 4 | 5 | class FilterTypeWindow(BaseWindow): 6 | def __init__(self, xml_file, location): 7 | super().__init__(xml_file, location, None) 8 | self.selected_type = None 9 | 10 | def onInit(self): 11 | self.list_control = self.getControl(2000) 12 | self.list_control.reset() 13 | 14 | quality_item = xbmcgui.ListItem(label="Filter by Quality") 15 | quality_item.setProperty("type", "quality") 16 | self.list_control.addItem(quality_item) 17 | 18 | provider_item = xbmcgui.ListItem(label="Filter by Provider") 19 | provider_item.setProperty("type", "provider") 20 | self.list_control.addItem(provider_item) 21 | 22 | reset_item = xbmcgui.ListItem(label="Reset Filter") 23 | reset_item.setProperty("type", "reset") 24 | 25 | self.list_control.addItem(reset_item) 26 | 27 | self.set_default_focus(self.list_control, 2000, control_list_reset=True) 28 | 29 | def handle_action(self, action_id, control_id=None): 30 | if action_id == 7: # Select 31 | pos = self.list_control.getSelectedPosition() 32 | if pos == 0: 33 | self.selected_type = "quality" 34 | elif pos == 1: 35 | self.selected_type = "provider" 36 | else: 37 | self.selected_type = None # Reset 38 | self.close() 39 | elif action_id in (2, 9, 10, 13, 92): # Back/Escape 40 | self.close() -------------------------------------------------------------------------------- /lib/gui/next_window.py: -------------------------------------------------------------------------------- 1 | from lib.gui.play_window import PlayWindow 2 | 3 | 4 | class PlayNext(PlayWindow): 5 | def __init__(self, xml_file, xml_location, item_information=None): 6 | super().__init__(xml_file, xml_location, item_information=item_information) 7 | self.default_action = 2 8 | 9 | def smart_play_action(self): 10 | if self.default_action == 1 and self.playing_file == self.getPlayingFile() and not self.closed: 11 | self.pause() 12 | -------------------------------------------------------------------------------- /lib/gui/play_window.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from lib.gui.base_window import BaseWindow 3 | from lib.utils.kodi.utils import kodilog 4 | import xbmc 5 | 6 | 7 | class PlayWindow(BaseWindow): 8 | def __init__(self, xml_file, xml_location, item_information=None): 9 | try: 10 | super().__init__(xml_file, xml_location, item_information=item_information) 11 | self.player = xbmc.Player() 12 | self.playing_file = self.getPlayingFile() 13 | self.duration = self.getTotalTime() - self.getTime() 14 | self.closed = False 15 | except Exception as e: 16 | kodilog(f"Error PlayWindow: {e}") 17 | 18 | def __del__(self): 19 | self.player = None 20 | del self.player 21 | 22 | def getTotalTime(self): 23 | return self.player.getTotalTime() if self.isPlaying() else 0 24 | 25 | def getTime(self): 26 | return self.player.getTime() if self.isPlaying() else 0 27 | 28 | def isPlaying(self): 29 | return self.player.isPlaying() 30 | 31 | def getPlayingFile(self): 32 | return self.player.getPlayingFile() 33 | 34 | def seekTime(self, seekTime): 35 | self.player.seekTime(seekTime) 36 | 37 | def pause(self): 38 | self.player.pause() 39 | 40 | def onInit(self): 41 | self.background_tasks() 42 | super().onInit() 43 | 44 | def calculate_percent(self): 45 | return ((int(self.getTotalTime()) - int(self.getTime())) / float(self.duration)) * 100 46 | 47 | def background_tasks(self): 48 | try: 49 | try: 50 | progress_bar = self.getControlProgress(3014) 51 | except RuntimeError: 52 | progress_bar = None 53 | 54 | while ( 55 | int(self.getTotalTime()) - int(self.getTime()) > 2 56 | and not self.closed 57 | and self.playing_file == self.getPlayingFile() 58 | ): 59 | xbmc.sleep(500) 60 | if progress_bar is not None: 61 | progress_bar.setPercent(self.calculate_percent()) 62 | 63 | self.smart_play_action() 64 | except Exception as e: 65 | kodilog(f"Error: {e}") 66 | 67 | self.close() 68 | 69 | @abc.abstractmethod 70 | def smart_play_action(self): 71 | """ 72 | Perform the default smartplay action at window timeout 73 | :return: 74 | """ 75 | 76 | def close(self): 77 | self.closed = True 78 | super().close() 79 | 80 | def handle_action(self, action, control_id=None): 81 | if action == 7: 82 | if control_id == 3001: 83 | xbmc.executebuiltin('PlayerControl(BigSkipForward)') 84 | self.close() 85 | if control_id == 3002: 86 | self.close() 87 | -------------------------------------------------------------------------------- /lib/gui/resolver_window.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | from lib.gui.base_window import BaseWindow 3 | from lib.gui.source_pack_select import SourcePackSelect 4 | from lib.utils.debrid.debrid_utils import get_pack_info 5 | from lib.utils.kodi.utils import ADDON_PATH 6 | from lib.domain.torrent import TorrentStream 7 | from lib.utils.player.utils import resolve_playback_source 8 | 9 | 10 | class ResolverWindow(BaseWindow): 11 | def __init__( 12 | self, 13 | xml_file: str, 14 | location: Optional[str] = None, 15 | source: Optional[TorrentStream] = None, 16 | item_information: Optional[Dict[str, Any]] = None, 17 | previous_window: Optional[BaseWindow] = None, 18 | close_callback: Optional[Any] = None, 19 | ) -> None: 20 | super().__init__(xml_file, location, item_information=item_information) 21 | self.stream_data: Optional[Any] = None 22 | self.progress: int = 1 23 | self.resolver: Optional[Any] = None 24 | self.source: Optional[TorrentStream] = source 25 | self.pack_select: bool = False 26 | self.item_information: Optional[Dict[str, Any]] = item_information 27 | self.close_callback: Optional[Any] = close_callback 28 | self.playback_info: Optional[Dict[str, Any]] = None 29 | self.pack_data: Optional[Any] = None 30 | self.previous_window: Optional[BaseWindow] = previous_window 31 | self.setProperty("enable_busy_spinner", "false") 32 | 33 | def doModal( 34 | self, 35 | pack_select: bool = False, 36 | ) -> Optional[Dict[str, Any]]: 37 | self.pack_select = pack_select 38 | 39 | if not self.source: 40 | return 41 | 42 | self._update_window_properties(self.source) 43 | super().doModal() 44 | 45 | def onInit(self) -> None: 46 | super().onInit() 47 | self.resolve_source() 48 | 49 | def resolve_source(self) -> Optional[Dict[str, Any]]: 50 | if self.source.isPack or self.pack_select: 51 | self.resolve_pack() 52 | else: 53 | self.resolve_single_source() 54 | 55 | self.close() 56 | return self.playback_info 57 | 58 | def resolve_single_source(self) -> None: 59 | url, magnet, is_torrent = self.get_source_details(source=self.source) 60 | source_data = self.prepare_source_data(self.source, url, magnet, is_torrent) 61 | self.playback_info = resolve_playback_source(source_data) 62 | 63 | if self.playback_info and self.playback_info.get("is_pack"): 64 | self.resolve_pack() 65 | 66 | def resolve_pack(self) -> None: 67 | self.pack_data = get_pack_info( 68 | type=self.source.type, 69 | info_hash=self.source.infoHash, 70 | ) 71 | 72 | self.window = SourcePackSelect( 73 | "source_select.xml", 74 | ADDON_PATH, 75 | source=self.source, 76 | pack_info=self.pack_data, 77 | item_information=self.item_information, 78 | ) 79 | 80 | self.playback_info = self.window.doModal() 81 | del self.window 82 | 83 | def _update_window_properties(self, source: TorrentStream) -> None: 84 | self.setProperty("enable_busy_spinner", "true") 85 | -------------------------------------------------------------------------------- /lib/gui/resume_window.py: -------------------------------------------------------------------------------- 1 | from lib.gui.base_window import BaseWindow 2 | 3 | class ResumeDialog(BaseWindow): 4 | def __init__(self, xml_file, xml_location, **kwargs): 5 | super().__init__(xml_file, xml_location) 6 | self.resume = None 7 | self.resume_percent = kwargs.get("resume_percent", 0.0) 8 | 9 | def doModal(self): 10 | super().doModal() 11 | 12 | def onInit(self): 13 | super().onInit() 14 | self.getControl(1002).setLabel( 15 | f"Resume from {self.resume_percent:.1f}%" 16 | ) # Resume Button 17 | self.getControl(1003).setLabel("Start from Beginning") # Start Button 18 | 19 | def onClick(self, control_id): 20 | if control_id == 1002: # Resume button ID 21 | self.resume = True 22 | elif control_id == 1003: # Start button ID 23 | self.resume = False 24 | self.close() 25 | 26 | def handle_action(self, action_id, control_id=None): 27 | if action_id in (10, 92): # Back or Escape 28 | self.resume = None 29 | self.close() 30 | -------------------------------------------------------------------------------- /lib/gui/source_pack_select.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | from lib.gui.source_pack_window import SourcePackWindow 3 | from lib.utils.general.utils import Debrids 4 | from lib.domain.torrent import TorrentStream 5 | from lib.utils.player.utils import resolve_playback_source 6 | 7 | 8 | class SourcePackSelect(SourcePackWindow): 9 | def __init__( 10 | self, 11 | xml_file: str, 12 | location: str, 13 | source: Optional[TorrentStream] = None, 14 | pack_info: Optional[Dict[str, Any]] = None, 15 | item_information: Optional[Dict[str, Any]] = None, 16 | ) -> None: 17 | super().__init__( 18 | xml_file, location, pack_info=pack_info, item_information=item_information 19 | ) 20 | self.source: Optional[TorrentStream] = source 21 | self.pack_info: Optional[Dict[str, Any]] = pack_info 22 | self.position: int = -1 23 | self.playback_info: Optional[Dict[str, Any]] = None 24 | self.setProperty("instant_close", "false") 25 | self.setProperty("resolving", "false") 26 | 27 | def doModal(self) -> Optional[Dict[str, Any]]: 28 | super().doModal() 29 | return self.playback_info 30 | 31 | def handle_action(self, action_id: int, control_id: Optional[int] = None) -> None: 32 | self.position = self.display_list.getSelectedPosition() 33 | if action_id == 7: 34 | if control_id == 1000: 35 | self._resolve_item() 36 | 37 | def _resolve_item(self) -> None: 38 | self.setProperty("resolving", "true") 39 | 40 | if self.source and self.source.type in [Debrids.RD, Debrids.TB]: 41 | file_id, name = self.pack_info["files"][self.position] 42 | self.playback_info = resolve_playback_source( 43 | data={ 44 | "title": name, 45 | "type": self.source.type, 46 | "is_torrent": False, 47 | "is_pack": True, 48 | "pack_info": { 49 | "file_id": file_id, 50 | "torrent_id": self.pack_info["id"], 51 | }, 52 | "mode": self.item_information["mode"], 53 | "ids": self.item_information["ids"], 54 | "tv_data": self.item_information["tv_data"], 55 | } 56 | ) 57 | else: 58 | url, title = self.pack_info["files"][self.position] 59 | self.playback_info = { 60 | "title": title, 61 | "type": self.source.type, 62 | "is_torrent": False, 63 | "is_pack": True, 64 | "mode": self.item_information.get("mode"), 65 | "ids": self.item_information.get("ids"), 66 | "tv_data": self.item_information.get("tv_data"), 67 | "url": url, 68 | } 69 | 70 | if not self.playback_info: 71 | self.setProperty("resolving", "false") 72 | self.close() 73 | 74 | self.setProperty("instant_close", "true") 75 | self.close() 76 | -------------------------------------------------------------------------------- /lib/gui/source_pack_window.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import xbmcgui 3 | from lib.gui.base_window import BaseWindow 4 | 5 | 6 | class SourcePackWindow(BaseWindow): 7 | def __init__(self, xml_file, location, source=None, pack_info=None, item_information=None): 8 | super().__init__(xml_file, location, item_information=item_information) 9 | self.pack_info = pack_info 10 | self.source = source 11 | self.display_list = None 12 | 13 | def onInit(self): 14 | self.display_list = self.getControlList(1000) 15 | self.populate_sources_list() 16 | 17 | self.set_default_focus(self.display_list, 1000, control_list_reset=True) 18 | super().onInit() 19 | 20 | def populate_sources_list(self): 21 | self.display_list.reset() 22 | 23 | for file_tuple in self.pack_info["files"]: 24 | _, title= file_tuple 25 | menu_item = xbmcgui.ListItem(label=title) 26 | menu_item.setProperty("title", title) 27 | menu_item.setProperty("type", self.source.type) 28 | menu_item.setProperty("quality", self.source.quality) 29 | self.display_list.addItem(menu_item) 30 | 31 | @abc.abstractmethod 32 | def handle_action(self, action_id, control_id=None): 33 | pass 34 | -------------------------------------------------------------------------------- /lib/utils/anime/anizip.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lib.clients.anizip import AniZipApi 3 | from lib.db.anime import get_all_ids 4 | from lib.utils.kodi.utils import ADDON_HANDLE, ADDON_PATH, play_media 5 | from lib.utils.general.utils import get_cached, set_cached, set_media_infotag 6 | 7 | from xbmcgui import ListItem 8 | from xbmcplugin import addDirectoryItem, endOfDirectory 9 | 10 | 11 | def search_anizip_episodes(title, anilist_id, plugin): 12 | res = search_anizip_api(anilist_id) 13 | anizip_parse_show_results(res, title, anilist_id, plugin) 14 | 15 | 16 | def search_anizip_api(anilist_id): 17 | cached_results = get_cached(type, params=(anilist_id)) 18 | if cached_results: 19 | return cached_results 20 | 21 | anizip = AniZipApi() 22 | res = anizip.episodes(anilist_id) 23 | 24 | set_cached(res, type, params=(anilist_id)) 25 | return res 26 | 27 | 28 | def anizip_parse_show_results(response, title, anilist_id, plugin): 29 | for res in response.values(): 30 | season = res.get("seasonNumber", 0) 31 | if season == 0: 32 | continue 33 | episode = res["episodeNumber"] 34 | 35 | ep_name = res["title"]["en"] 36 | if ep_name: 37 | ep_name = f"{season}x{episode} {ep_name}" 38 | else: 39 | ep_name = f"Episode {episode}" 40 | 41 | tv_data = f"{ep_name}(^){episode}(^){season}" 42 | 43 | description = res.get("overview", "") 44 | date = res["airdate"] 45 | poster = res.get("image", "") 46 | 47 | ids = get_all_ids(anilist_id) 48 | if ids is None: 49 | return 50 | imdb_id = ids.get("imdb", -1) 51 | tvdb_id = ids.get("tvdb", -1) 52 | tmdb_id = ids.get("tmdb", -1) 53 | 54 | ids=f"{tmdb_id}, {tvdb_id}, {imdb_id}" 55 | 56 | list_item = ListItem(label=ep_name) 57 | list_item.setArt( 58 | { 59 | "poster": poster, 60 | "icon": os.path.join(ADDON_PATH, "resources", "img", "trending.png"), 61 | "fanart": poster, 62 | } 63 | ) 64 | list_item.setProperty("IsPlayable", "false") 65 | 66 | set_media_infotag( 67 | list_item, 68 | mode="tv", 69 | name=ep_name, 70 | overview=description, 71 | ep_name=ep_name, 72 | air_date=date, 73 | ) 74 | 75 | list_item.addContextMenuItems( 76 | [ 77 | ( 78 | "Rescrape item", 79 | play_media( 80 | name="search", 81 | mode="anime", 82 | query=title, 83 | ids=ids, 84 | tv_data=tv_data, 85 | rescrape=True, 86 | ), 87 | ) 88 | ] 89 | ) 90 | 91 | addDirectoryItem( 92 | ADDON_HANDLE, 93 | url_for( 94 | name="search", 95 | mode="anime", 96 | query=title, 97 | ids=ids, 98 | tv_data=tv_data, 99 | ), 100 | list_item, 101 | isFolder=True, 102 | ) 103 | 104 | endOfDirectory(ADDON_HANDLE) 105 | -------------------------------------------------------------------------------- /lib/utils/anime/simkl.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from lib.clients.fma import FindMyAnime, extract_season 4 | from lib.clients.simkl import SIMKL 5 | from lib.utils.kodi.utils import ADDON_HANDLE, ADDON_PATH 6 | from lib.utils.general.utils import ( 7 | get_cached, 8 | set_cached, 9 | set_media_infotag, 10 | ) 11 | 12 | from xbmcgui import ListItem 13 | from xbmcplugin import addDirectoryItem, endOfDirectory 14 | 15 | 16 | IMAGE_PATH = "https://wsrv.nl/?url=https://simkl.in/episodes/%s_w.webp" 17 | 18 | 19 | def search_simkl_episodes(title, anilist_id, mal_id, plugin): 20 | fma = FindMyAnime() 21 | data = fma.get_anime_data(anilist_id, "Anilist") 22 | s_id = extract_season(data[0]) if data else "" 23 | season = s_id[0] if s_id else 1 24 | try: 25 | res = search_simkl_api(mal_id) 26 | simkl_parse_show_results(res, title, season, plugin) 27 | except Exception as e: 28 | kodilog(e) 29 | 30 | 31 | def search_simkl_api(mal_id): 32 | cached_results = get_cached(type, params=(mal_id)) 33 | if cached_results: 34 | return cached_results 35 | 36 | simkl = SIMKL() 37 | res = simkl.get_anilist_episodes(mal_id) 38 | 39 | set_cached(res, type, params=(mal_id)) 40 | return res 41 | 42 | 43 | def simkl_parse_show_results(response, title, season, plugin): 44 | for res in response: 45 | if res["type"] == "episode": 46 | episode = res["episode"] 47 | ep_name = res.get("title") 48 | if ep_name: 49 | ep_name = f"{season}x{episode} {ep_name}" 50 | else: 51 | ep_name = f"Episode {episode}" 52 | 53 | description = res.get("description", "") 54 | 55 | date = res.get("date", "") 56 | match = re.search(r"\d{4}-\d{2}-\d{2}", date) 57 | if match: 58 | date = match.group() 59 | 60 | poster = IMAGE_PATH % res.get("img", "") 61 | 62 | list_item = ListItem(label=ep_name) 63 | list_item.setArt( 64 | { 65 | "poster": poster, 66 | "icon": os.path.join( 67 | ADDON_PATH, "resources", "img", "trending.png" 68 | ), 69 | "fanart": poster, 70 | } 71 | ) 72 | list_item.setProperty("IsPlayable", "false") 73 | 74 | set_media_infotag( 75 | list_item, 76 | mode="tv", 77 | name=ep_name, 78 | overview=description, 79 | ep_name=ep_name, 80 | air_date=date, 81 | ) 82 | 83 | addDirectoryItem( 84 | ADDON_HANDLE, 85 | url_for( 86 | name="search", 87 | mode="anime", 88 | query=title, 89 | ids=f"{-1}, {-1}, {-1}", 90 | tv_data=f"{ep_name}(^){episode}(^){season}", 91 | ), 92 | list_item, 93 | isFolder=True, 94 | ) 95 | 96 | endOfDirectory(ADDON_HANDLE) 97 | -------------------------------------------------------------------------------- /lib/utils/clients/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from lib.clients.burst.client import Burst 3 | from lib.clients.jackett import Jackett 4 | from lib.clients.jackgram.client import Jackgram 5 | from lib.clients.prowlarr import Prowlarr 6 | from lib.clients.zilean import Zilean 7 | from lib.utils.kodi.utils import get_setting, notification, translation 8 | from lib.utils.general.utils import Indexer 9 | from lib.utils.kodi.settings import get_int_setting 10 | 11 | 12 | def validate_host(host: Optional[str], indexer: Indexer) -> Optional[bool]: 13 | if not host: 14 | notification(f"{translation(30223)}: {indexer}") 15 | return None 16 | return True 17 | 18 | 19 | def validate_key(api_key: Optional[str], indexer: Indexer) -> Optional[bool]: 20 | if not api_key: 21 | notification(f"{translation(30221)}: {indexer}") 22 | return None 23 | return True 24 | 25 | 26 | def update_dialog(title: str, message: str, dialog: object) -> None: 27 | dialog.update(0, f"Jacktook [COLOR FFFF6B00]{title}[/COLOR]", message) 28 | 29 | 30 | def validate_credentials( 31 | indexer: Indexer, host: Optional[str], api_key: Optional[str] = None 32 | ) -> bool: 33 | """ 34 | Validates the host and API key for a given indexer. 35 | """ 36 | if not validate_host(host, indexer): 37 | return False 38 | if api_key is not None and not validate_key(api_key, indexer): 39 | return False 40 | return True 41 | 42 | 43 | def get_client(indexer: Indexer) -> Optional[object]: 44 | if indexer == Indexer.JACKETT: 45 | host = get_setting("jackett_host") 46 | api_key = get_setting("jackett_apikey") 47 | if not validate_credentials(indexer, host, api_key): 48 | return 49 | return Jackett(host, api_key, notification) 50 | 51 | elif indexer == Indexer.PROWLARR: 52 | host = get_setting("prowlarr_host") 53 | api_key = get_setting("prowlarr_apikey") 54 | if not validate_credentials(indexer, host, api_key): 55 | return 56 | return Prowlarr(host, api_key, notification) 57 | 58 | elif indexer == Indexer.JACKGRAM: 59 | host = get_setting("jackgram_host") 60 | if not validate_credentials(indexer, host): 61 | return 62 | return Jackgram(host, notification) 63 | 64 | elif indexer == Indexer.ZILEAN: 65 | timeout = get_int_setting("zilean_timeout") 66 | host = get_setting("zilean_host") 67 | if not validate_credentials(indexer, host): 68 | return 69 | return Zilean(host, timeout, notification) 70 | 71 | elif indexer == Indexer.BURST: 72 | return Burst(notification) 73 | -------------------------------------------------------------------------------- /lib/utils/debrid/ad_utils.py: -------------------------------------------------------------------------------- 1 | from lib.clients.debrid.alldebrid import AllDebrid 2 | from lib.clients.debrid.base import ProviderException 3 | from lib.utils.kodi.utils import get_setting, kodilog, notification 4 | from lib.utils.general.utils import ( 5 | get_cached, 6 | get_random_color, 7 | info_hash_to_magnet, 8 | set_cached, 9 | supported_video_extensions, 10 | ) 11 | 12 | EXTENSIONS = supported_video_extensions()[:-1] 13 | 14 | client = AllDebrid(token=get_setting("alldebrid_token")) 15 | 16 | 17 | def add_ad_torrent(info_hash): 18 | kodilog("ad_utils::add_ad_torrent") 19 | torrent_info = client.get_available_torrent(info_hash) 20 | if torrent_info: 21 | if torrent_info["status"] == "Ready": 22 | return torrent_info.get("id") 23 | elif torrent_info["statusCode"] == 7: 24 | client.delete_torrent(torrent_info.get("id")) 25 | raise ProviderException("Not enough seeders available to parse magnet link") 26 | else: 27 | magnet = info_hash_to_magnet(info_hash) 28 | response = client.add_magnet_link(magnet) 29 | if not response.get("success", False): 30 | raise Exception( 31 | f"Failed to add magnet: {response.get('error', 'Unknown error')}" 32 | ) 33 | return response["data"]["magnets"][0]["id"] 34 | 35 | 36 | def get_ad_link(info_hash): 37 | torrent_id = add_ad_torrent(info_hash) 38 | if torrent_id: 39 | torrent_info = client.get_torrent_info(torrent_id) 40 | file = max(torrent_info["files"], key=lambda x: x.get("size", 0)) 41 | response_data = client.create_download_link( 42 | torrent_info.get("id"), file.get("id") 43 | ) 44 | return response_data.get("data") 45 | 46 | 47 | def get_ad_pack_link(file_id, torrent_id): 48 | response = client.create_download_link(torrent_id, file_id) 49 | return response.get("data") 50 | 51 | 52 | def get_ad_pack_info(info_hash): 53 | info = get_cached(info_hash) 54 | if info: 55 | return info 56 | torrent_info = add_ad_torrent(info_hash) 57 | info = {} 58 | if torrent_info: 59 | info["id"] = torrent_info["id"] 60 | if len(torrent_info["files"]) > 0: 61 | files_names = [ 62 | item["name"] 63 | for item in torrent_info["files"] 64 | for x in EXTENSIONS 65 | if item["short_name"].lower().endswith(x) 66 | ] 67 | files = [] 68 | for id, name in enumerate(files_names): 69 | tracker_color = get_random_color("AD") 70 | title = f"[B][COLOR {tracker_color}][AD-Cached][/COLOR][/B]-{name}" 71 | files.append((id, title)) 72 | info["files"] = files 73 | set_cached(info, info_hash) 74 | return info 75 | else: 76 | notification("Not a torrent pack") 77 | 78 | 79 | -------------------------------------------------------------------------------- /lib/utils/debrid/ed_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, List, Any 3 | from lib.clients.debrid.easydebrid import EasyDebrid 4 | from lib.utils.kodi.utils import dialog_text, get_setting, kodilog, notification 5 | from lib.utils.general.utils import ( 6 | Debrids, 7 | debrid_dialog_update, 8 | filter_debrid_episode, 9 | get_cached, 10 | get_public_ip, 11 | info_hash_to_magnet, 12 | set_cached, 13 | supported_video_extensions, 14 | ) 15 | from lib.domain.torrent import TorrentStream 16 | 17 | 18 | class EasyDebridHelper: 19 | def __init__(self): 20 | self.client = EasyDebrid( 21 | token=get_setting("easydebrid_token"), user_ip=get_public_ip() 22 | ) 23 | 24 | def check_ed_cached( 25 | self, 26 | results: List[TorrentStream], 27 | cached_results: List[Dict], 28 | uncached_results: List[Dict], 29 | total: int, 30 | dialog: Any, 31 | lock: Any, 32 | ) -> None: 33 | filtered_results = [res for res in results if res.infoHash] 34 | if filtered_results: 35 | magnets = [info_hash_to_magnet(res.infoHash) for res in filtered_results] 36 | torrents_info = self.client.get_torrent_instant_availability(magnets) 37 | cached_response = torrents_info.get("cached", []) 38 | 39 | for res in results: 40 | debrid_dialog_update("ED", total, dialog, lock) 41 | res.type = Debrids.ED 42 | 43 | if res in filtered_results: 44 | index = filtered_results.index(res) 45 | if cached_response[index] is True: 46 | res.isCached = True 47 | cached_results.append(res) 48 | else: 49 | res.isCached = False 50 | uncached_results.append(res) 51 | else: 52 | res.isCached = False 53 | uncached_results.append(res) 54 | 55 | def get_ed_link(self, info_hash, data): 56 | magnet = info_hash_to_magnet(info_hash) 57 | response_data = self.client.create_download_link(magnet) 58 | files = response_data.get("files", []) 59 | if not files: 60 | return 61 | 62 | if len(files) > 1: 63 | if data["tv_data"]: 64 | season = data["tv_data"].get("season", "") 65 | episode = data["tv_data"].get("episode", "") 66 | files = filter_debrid_episode(files, episode_num=episode, season_num=season) 67 | if not files: 68 | return 69 | else: 70 | data["is_pack"] = True 71 | return 72 | 73 | return files[0].get("url") 74 | 75 | def get_ed_pack_info(self, info_hash): 76 | info = get_cached(info_hash) 77 | if info: 78 | return info 79 | 80 | extensions = supported_video_extensions()[:-1] 81 | magnet = info_hash_to_magnet(info_hash) 82 | response_data = self.client.create_download_link(magnet) 83 | torrent_files = response_data.get("files", []) 84 | 85 | if len(torrent_files) <= 1: 86 | notification("Not a torrent pack") 87 | return 88 | 89 | files = [ 90 | (item["url"], item["filename"]) 91 | for item in torrent_files 92 | if any(item["filename"].lower().endswith(x) for x in extensions) 93 | ] 94 | 95 | info = {"files": files} 96 | set_cached(info, info_hash) 97 | return info 98 | 99 | def get_ed_info(self): 100 | user = self.client.get_user_info() 101 | expiration_timestamp = user["paid_until"] 102 | 103 | expires = datetime.fromtimestamp(expiration_timestamp) 104 | days_remaining = (expires - datetime.today()).days 105 | 106 | body = [ 107 | f"[B]Expires:[/B] {expires}", 108 | f"[B]Days Remaining:[/B] {days_remaining}", 109 | ] 110 | dialog_text("Easy-Debrid", "\n".join(body)) 111 | -------------------------------------------------------------------------------- /lib/utils/debrid/pm_utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import Dict, List, Any 3 | from lib.clients.debrid.premiumize import Premiumize 4 | from lib.utils.kodi.utils import get_setting, kodilog, notification 5 | from lib.utils.general.utils import ( 6 | Debrids, 7 | debrid_dialog_update, 8 | filter_debrid_episode, 9 | get_cached, 10 | info_hash_to_magnet, 11 | set_cached, 12 | supported_video_extensions, 13 | ) 14 | from lib.domain.torrent import TorrentStream 15 | 16 | 17 | class PremiumizeHelper: 18 | def __init__(self): 19 | self.client = Premiumize(token=get_setting("premiumize_token")) 20 | 21 | def check_pm_cached( 22 | self, 23 | results: List[TorrentStream], 24 | cached_results: List[Dict], 25 | uncached_results: List[Dict], 26 | total: int, 27 | dialog: Any, 28 | lock: Any, 29 | ) -> None: 30 | """Checks if torrents are cached in Premiumize.""" 31 | hashes = [res.infoHash for res in results] 32 | torrents_info = self.client.get_torrent_instant_availability(hashes) 33 | cached_response = torrents_info.get("response", []) 34 | 35 | for index, res in enumerate(copy.deepcopy(results)): 36 | debrid_dialog_update("PM", total, dialog, lock) 37 | res.type = Debrids.PM 38 | 39 | if index < len(cached_response) and cached_response[index] is True: 40 | res.isCached = True 41 | cached_results.append(res) 42 | else: 43 | res.isCached = False 44 | uncached_results.append(res) 45 | 46 | def get_pm_link(self, info_hash, data): 47 | """Gets a direct download link for a Premiumize torrent.""" 48 | magnet = info_hash_to_magnet(info_hash) 49 | response_data = self.client.create_download_link(magnet) 50 | 51 | if response_data.get("status") == "error": 52 | kodilog( 53 | f"Failed to get link from Premiumize: {response_data.get('message')}" 54 | ) 55 | return None 56 | 57 | content = response_data.get("content", []) 58 | 59 | if len(content) > 1: 60 | if data["tv_data"]: 61 | season = data["tv_data"].get("season", "") 62 | episode = data["tv_data"].get("episode", "") 63 | content = filter_debrid_episode(content, episode_num=episode, season_num=season) 64 | if not content: 65 | return 66 | else: 67 | data["is_pack"] = True 68 | return 69 | 70 | return content[0].get("stream_link") 71 | 72 | 73 | def get_pm_pack_info(self, info_hash): 74 | """Retrieves information about a torrent pack, including file names.""" 75 | info = get_cached(info_hash) 76 | if info: 77 | return info 78 | 79 | extensions = supported_video_extensions()[:-1] 80 | magnet = info_hash_to_magnet(info_hash) 81 | response_data = self.client.create_download_link(magnet) 82 | 83 | if response_data.get("status") == "error": 84 | notification( 85 | f"Failed to get link from Premiumize: {response_data.get('message')}" 86 | ) 87 | return None 88 | 89 | torrent_content = response_data.get("content", []) 90 | if len(torrent_content) <= 1: 91 | notification("Not a torrent pack") 92 | return None 93 | 94 | files = [ 95 | (item.get("link"), item.get("path").rsplit("/", 1)[-1]) 96 | for item in torrent_content 97 | if any(item.get("path", "").lower().endswith(ext) for ext in extensions) 98 | and item.get("link") 99 | ] 100 | 101 | info = {"files": files} 102 | set_cached(info, info_hash) 103 | return info 104 | -------------------------------------------------------------------------------- /lib/utils/debrid/torbox_utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import Dict, List, Any 3 | from lib.clients.debrid.torbox import Torbox 4 | from lib.utils.kodi.utils import get_setting, notification 5 | from lib.utils.general.utils import ( 6 | Debrids, 7 | debrid_dialog_update, 8 | get_cached, 9 | get_public_ip, 10 | info_hash_to_magnet, 11 | set_cached, 12 | supported_video_extensions, 13 | ) 14 | from lib.domain.torrent import TorrentStream 15 | 16 | EXTENSIONS = supported_video_extensions()[:-1] 17 | 18 | 19 | class TorboxException(Exception): 20 | def __init__(self, message): 21 | self.message = message 22 | super().__init__(self.message) 23 | 24 | 25 | class TorboxHelper: 26 | def __init__(self): 27 | self.client = Torbox(token=get_setting("torbox_token")) 28 | 29 | def check_torbox_cached( 30 | self, 31 | results: List[TorrentStream], 32 | cached_results: List[Dict], 33 | uncached_results: List[Dict], 34 | total: int, 35 | dialog: Any, 36 | lock: Any, 37 | ) -> None: 38 | hashes = [res.infoHash for res in results] 39 | response = self.client.get_torrent_instant_availability(hashes) 40 | cached_response = response.get("data", []) 41 | 42 | for res in copy.deepcopy(results): 43 | debrid_dialog_update("TB", total, dialog, lock) 44 | res.type = Debrids.TB 45 | 46 | with lock: 47 | if res.infoHash in cached_response: 48 | res.isCached = True 49 | cached_results.append(res) 50 | else: 51 | res.isCached = False 52 | uncached_results.append(res) 53 | 54 | def add_torbox_torrent(self, info_hash): 55 | torrent_info = self.client.get_available_torrent(info_hash) 56 | if ( 57 | torrent_info 58 | and torrent_info.get("download_finished") 59 | and torrent_info.get("download_present") 60 | ): 61 | return torrent_info 62 | 63 | magnet = info_hash_to_magnet(info_hash) 64 | response = self.client.add_magnet_link(magnet) 65 | 66 | if not response.get("success"): 67 | raise TorboxException(f"Failed to add magnet link to Torbox: {response}") 68 | 69 | if "Found Cached" in response.get("detail", ""): 70 | return self.client.get_available_torrent(info_hash) 71 | 72 | def get_torbox_link(self, info_hash): 73 | torrent_info = self.add_torbox_torrent(info_hash) 74 | if torrent_info: 75 | file = max(torrent_info["files"], key=lambda x: x.get("size", 0)) 76 | response_data = self.client.create_download_link( 77 | torrent_info.get("id"), file.get("id"), get_public_ip() 78 | ) 79 | return response_data.get("data") 80 | 81 | def get_torbox_pack_link(self, file_id, torrent_id): 82 | response = self.client.create_download_link(torrent_id, file_id) 83 | return response.get("data") 84 | 85 | def get_torbox_pack_info(self, info_hash): 86 | info = get_cached(info_hash) 87 | if info: 88 | return info 89 | 90 | torrent_info = self.add_torbox_torrent(info_hash) 91 | if not torrent_info: 92 | return None 93 | 94 | info = {"id": torrent_info["id"], "files": []} 95 | torrent_files = torrent_info.get("files", []) 96 | 97 | if not torrent_files: 98 | notification("Not a torrent pack") 99 | return None 100 | 101 | files = [ 102 | (id, item["name"]) 103 | for id, item in enumerate(torrent_files) 104 | if any(item["short_name"].lower().endswith(ext) for ext in EXTENSIONS) 105 | ] 106 | 107 | info["files"] = files 108 | set_cached(info, info_hash) 109 | return info 110 | -------------------------------------------------------------------------------- /lib/utils/kodi/settings.py: -------------------------------------------------------------------------------- 1 | from .utils import get_setting, get_property, ADDON_ID 2 | import xbmc 3 | 4 | 5 | def addon_settings(): 6 | return xbmc.executebuiltin(f"Addon.OpenSettings({ADDON_ID})") 7 | 8 | 9 | def is_auto_play(): 10 | return get_setting("auto_play") 11 | 12 | 13 | def get_int_setting(setting): 14 | return int(get_setting(setting)) 15 | 16 | 17 | def update_delay(fallback=45): 18 | return get_property("update.delay") or fallback 19 | 20 | 21 | def update_action(fallback=2): 22 | return get_property("update.action") or fallback 23 | 24 | 25 | def is_cache_enabled(): 26 | return get_setting("cache_enabled") 27 | 28 | 29 | def cache_clear_update(): 30 | return get_setting("clear_cache_update") 31 | 32 | 33 | def get_cache_expiration(): 34 | return get_int_setting("cache_expiration") 35 | 36 | 37 | def get_jackett_timeout(): 38 | return get_int_setting("jackett_timeout") 39 | 40 | 41 | def get_prowlarr_timeout(): 42 | return get_int_setting("prowlarr_timeout") 43 | 44 | 45 | def trakt_client(): 46 | return get_setting("trakt_client", "") 47 | 48 | 49 | def trakt_secret(): 50 | return get_setting("trakt_secret", "") 51 | 52 | 53 | def trakt_lists_sort_order(setting): 54 | return int(get_setting('trakt_sort_%s' % setting, '0')) -------------------------------------------------------------------------------- /lib/utils/player/utils.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | from lib.utils.debrid.debrid_utils import ( 3 | get_debrid_direct_url, 4 | get_debrid_pack_direct_url, 5 | ) 6 | from lib.utils.kodi.utils import ( 7 | get_setting, 8 | is_elementum_addon, 9 | is_jacktorr_addon, 10 | is_torrest_addon, 11 | notification, 12 | translation, 13 | ) 14 | from lib.utils.general.utils import ( 15 | Debrids, 16 | IndexerType, 17 | Players, 18 | torrent_clients, 19 | ) 20 | from xbmcgui import Dialog 21 | from typing import Any, Dict, Optional 22 | 23 | 24 | def resolve_playback_source(data: Dict[str, Any]) -> Optional[Dict[str, Any]]: 25 | indexer_type: str = data.get("type", "") 26 | is_pack: bool = data.get("is_pack", False) 27 | 28 | torrent_enable = get_setting("torrent_enable") 29 | torrent_client = get_setting("torrent_client") 30 | 31 | if indexer_type in [IndexerType.DIRECT, IndexerType.STREMIO_DEBRID]: 32 | return data 33 | 34 | addon_url = get_addon_url(data, torrent_enable, torrent_client) 35 | if addon_url: 36 | data["url"] = addon_url 37 | else: 38 | data["url"] = get_debrid_url(data, indexer_type, is_pack) 39 | 40 | return data 41 | 42 | 43 | def get_addon_url( 44 | data: Dict[str, Any], torrent_enable: bool, torrent_client: str 45 | ) -> Optional[str]: 46 | magnet: str = data.get("magnet", "") 47 | url: str = data.get("url", "") 48 | mode: str = data.get("mode", "") 49 | ids: Any = data.get("ids", "") 50 | 51 | if torrent_enable: 52 | return get_torrent_addon_url_for_client(torrent_client, magnet, url, mode, ids) 53 | elif data.get("is_torrent", False): 54 | return get_torrent_addon_url_select(magnet, url, mode, ids) 55 | return None 56 | 57 | 58 | def get_torrent_addon_url_for_client( 59 | client: str, magnet: str, url: str, mode: str, ids: Any 60 | ) -> Optional[str]: 61 | if client in [Players.TORREST]: 62 | return get_torrest_url(magnet, url) 63 | elif client in [Players.ELEMENTUM]: 64 | return get_elementum_url(magnet, url, mode, ids) 65 | elif client in [Players.JACKTORR]: 66 | return get_jacktorr_url(magnet, url) 67 | return None 68 | 69 | 70 | def get_torrent_addon_url_select( 71 | magnet: str, url: str, mode: str, ids: Any 72 | ) -> Optional[str]: 73 | chosen_client = Dialog().select(translation(30800), torrent_clients) 74 | if chosen_client < 0: 75 | return None 76 | selected_client = torrent_clients[chosen_client] 77 | return get_torrent_addon_url_for_client(selected_client, magnet, url, mode, ids) 78 | 79 | 80 | def get_debrid_url( 81 | data: Dict[str, Any], indexer_type: str, is_pack: bool 82 | ) -> Optional[str]: 83 | if is_pack and indexer_type in [Debrids.RD, Debrids.TB]: 84 | pack_info = data.get("pack_info", {}) 85 | file_id = pack_info.get("file_id", "") 86 | torrent_id = pack_info.get("torrent_id", "") 87 | return get_debrid_pack_direct_url(file_id, torrent_id, indexer_type) 88 | return get_debrid_direct_url(indexer_type, data) 89 | 90 | 91 | def get_elementum_url(magnet: str, url: str, mode: str, ids: Any) -> Optional[str]: 92 | if not is_elementum_addon(): 93 | notification(translation(30252)) 94 | return None 95 | 96 | if ids: 97 | tmdb_id = ids["tmdb_id"] 98 | else: 99 | tmdb_id = "" 100 | 101 | uri: str = magnet or url or "" 102 | return f"plugin://plugin.video.elementum/play?uri={quote(uri)}&type={mode}&tmdb={tmdb_id}" 103 | 104 | 105 | def get_jacktorr_url(magnet: str, url: str) -> Optional[str]: 106 | if not is_jacktorr_addon(): 107 | notification(translation(30253)) 108 | return None 109 | if magnet: 110 | _url = f"plugin://plugin.video.jacktorr/play_magnet?magnet={quote(magnet)}" 111 | else: 112 | _url = f"plugin://plugin.video.jacktorr/play_url?url={quote(url)}" 113 | return _url 114 | 115 | 116 | def get_torrest_url(magnet: str, url: str) -> Optional[str]: 117 | if not is_torrest_addon(): 118 | notification(translation(30250)) 119 | return None 120 | if magnet: 121 | _url = f"plugin://plugin.video.torrest/play_magnet?magnet={quote(magnet)}" 122 | else: 123 | _url = f"plugin://plugin.video.torrest/play_url?url={quote(url)}" 124 | return _url 125 | -------------------------------------------------------------------------------- /lib/utils/plex/utils.py: -------------------------------------------------------------------------------- 1 | from lib.api.plex.media_server import check_server_connection, get_servers 2 | from lib.api.plex.plex import PlexApi 3 | from lib.utils.kodi.utils import get_setting, kodilog, set_setting 4 | from xbmcgui import Dialog 5 | 6 | 7 | plex = PlexApi() 8 | 9 | server_config = {"discoveryUrl": [], "streamingUrl": []} 10 | 11 | def plex_login(): 12 | success = plex.login() 13 | if success: 14 | user = plex.get_plex_user() 15 | if user: 16 | set_setting("plex_user", user.username) 17 | 18 | 19 | def validate_server(): 20 | servers = get_servers("https://plex.tv/api/v2", get_setting("plex_token")) 21 | if servers: 22 | server_names = [s.name for s in servers] 23 | chosen_server = Dialog().select("Select server", server_names) 24 | if chosen_server < 0: 25 | return 26 | for s in servers: 27 | if s.name == server_names[chosen_server]: 28 | set_setting("plex_server_name", s.name) 29 | set_setting("plex_server_token", s.access_token) 30 | for c in s.connections: 31 | server_config["streamingUrl"].append(c["uri"]) 32 | if c["local"] is not True: 33 | server_config["discoveryUrl"].append(c["uri"]) 34 | break 35 | discovery_test(server_config["discoveryUrl"], get_setting("plex_server_token")) 36 | streaming_test(server_config["streamingUrl"], get_setting("plex_server_token")) 37 | 38 | 39 | def discovery_test(urls, access_token): 40 | kodilog("Making discovery test...") 41 | kodilog(urls) 42 | for url in urls: 43 | success = check_server_connection(url, access_token) 44 | if success: 45 | set_setting("plex_discovery_url", url) 46 | break 47 | 48 | 49 | def streaming_test(urls, access_token): 50 | kodilog("Making streaming test...") 51 | kodilog(urls) 52 | for url in urls: 53 | success = check_server_connection(url, access_token) 54 | if success: 55 | set_setting("plex_streaming_url", url) 56 | break 57 | 58 | def plex_logout(): 59 | plex.logout() -------------------------------------------------------------------------------- /lib/utils/stremio/catalogs_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from lib.clients.stremio.stremio import StremioAddonCatalogsClient 3 | from lib.db.cached import cache 4 | from lib.utils.kodi.utils import kodilog 5 | from lib.utils.kodi.settings import get_cache_expiration, is_cache_enabled 6 | 7 | 8 | def catalogs_get_cache(path, params, *args, **kwargs): 9 | identifier = f"{path}{params}{args}" 10 | data = cache.get(identifier, hashed_key=True) 11 | if data: 12 | return data 13 | 14 | handlers = { 15 | "search_catalog": lambda p: StremioAddonCatalogsClient(params).search(p), 16 | "list_stremio_catalog": lambda p: StremioAddonCatalogsClient(params).get_catalog_info(p), 17 | "list_stremio_seasons": lambda: StremioAddonCatalogsClient(params).get_meta_info(), 18 | "list_stremio_episodes": lambda: StremioAddonCatalogsClient(params).get_meta_info(), 19 | "list_stremio_tv": lambda: StremioAddonCatalogsClient(params).get_stream_info(), 20 | } 21 | 22 | try: 23 | handler = handlers.get(path, lambda: None) 24 | if args or kwargs: 25 | data = handler(*args, **kwargs) 26 | else: 27 | data = handler() 28 | except Exception as e: 29 | kodilog(f"Error: {e}") 30 | return {} 31 | 32 | if data is not None: 33 | cache.set( 34 | identifier, 35 | data, 36 | timedelta(hours=get_cache_expiration() if is_cache_enabled() else 0), 37 | hashed_key=True, 38 | ) 39 | 40 | return data -------------------------------------------------------------------------------- /lib/utils/torrent/resolve_to_magnet.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from lib.utils.general.utils import USER_AGENT_HEADER 3 | 4 | 5 | 6 | def resolve_to_magnet(url): 7 | """ 8 | Perform a GET request to the URL and check if it redirects to a magnet 9 | 10 | Args: 11 | url (str): The URL to check. 12 | 13 | Returns: 14 | str: The magnet URL if the URL points or redirects to a magnet 15 | 16 | """ 17 | if url.startswith('magnet:?'): 18 | return url 19 | 20 | try: 21 | response = requests.get(url, headers=USER_AGENT_HEADER, timeout=10, allow_redirects=False, stream=True) 22 | is_redirect = response.is_redirect or response.is_permanent_redirect 23 | redirect_location = response.headers.get('Location') if is_redirect else None 24 | 25 | if is_redirect and redirect_location.startswith('magnet:?'): 26 | return redirect_location 27 | except requests.RequestException as e: 28 | return None 29 | finally: 30 | try: 31 | response.close() 32 | except NameError: 33 | pass # Response not defined if an exception occurred before the request 34 | 35 | return None -------------------------------------------------------------------------------- /lib/utils/torrentio/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from lib.db.cached import cache 3 | from lib.utils.kodi.settings import get_cache_expiration, is_cache_enabled 4 | import xbmcgui 5 | 6 | 7 | items = [ 8 | "YTS", 9 | "EZTV", 10 | "RARBG", 11 | "1337x", 12 | "ThePirateBay", 13 | "KickassTorrents", 14 | "TorrentGalaxy", 15 | "MagnetDL", 16 | "HorribleSubs", 17 | "NyaaSi", 18 | "TokyoTosho", 19 | "AniDex", 20 | "Rutor", 21 | "Rutracker", 22 | "Comando", 23 | "BluDV", 24 | "Torrent9", 25 | "MejorTorrent", 26 | "Wolfmax4k", 27 | "Cinecalidad", 28 | ] 29 | 30 | 31 | def open_providers_selection(identifier="torrentio_providers"): 32 | cached_providers = cache.get(identifier, hashed_key=True) 33 | if cached_providers: 34 | choice = xbmcgui.Dialog().yesno( 35 | "Providers Selection Dialog", 36 | f"Your current Providers are: \n{','.join(cached_providers)}\n\nDo you want to change?", 37 | yeslabel="Ok", 38 | nolabel="No", 39 | ) 40 | if not choice: 41 | return 42 | providers_selection() 43 | else: 44 | providers_selection() 45 | 46 | 47 | def providers_selection(identifier="torrentio_providers"): 48 | selected = xbmcgui.Dialog().multiselect("Select Providers", items) 49 | if selected: 50 | providers = [items[i] for i in selected] 51 | cache.set( 52 | identifier, 53 | providers, 54 | timedelta(hours=get_cache_expiration() if is_cache_enabled() else 0), 55 | hashed_key=True, 56 | ) 57 | xbmcgui.Dialog().ok( 58 | "Selection Dialog", f"Successfully selected: {',' .join(providers)}" 59 | ) 60 | else: 61 | xbmcgui.Dialog().notification( 62 | "Selection", "No providers selected", xbmcgui.NOTIFICATION_INFO 63 | ) 64 | 65 | 66 | def filter_torrentio_provider(results, identifier="torrentio_providers"): 67 | selected_providers = cache.get(identifier, hashed_key=True) 68 | if not selected_providers: 69 | return results 70 | 71 | filtered_results = [ 72 | res 73 | for res in results 74 | if res["indexer"] != "Torrentio" 75 | or (res["indexer"] == "Torrentio" and res["provider"] in selected_providers) 76 | ] 77 | return filtered_results 78 | -------------------------------------------------------------------------------- /lib/utils/views/last_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lib.db.main import main_db 3 | from lib.utils.kodi.utils import ADDON_HANDLE, ADDON_PATH, build_url 4 | from xbmcgui import ListItem 5 | from xbmcplugin import ( 6 | addDirectoryItem, 7 | endOfDirectory, 8 | setPluginCategory, 9 | ) 10 | 11 | 12 | def show_last_files(): 13 | setPluginCategory(ADDON_HANDLE, f"Last Files - History") 14 | 15 | list_item = ListItem(label="Clear Files") 16 | list_item.setArt( 17 | {"icon": os.path.join(ADDON_PATH, "resources", "img", "clear.png")} 18 | ) 19 | addDirectoryItem( 20 | ADDON_HANDLE, 21 | build_url("clear_history", type="lfh"), 22 | list_item, 23 | ) 24 | 25 | for title, data in reversed(main_db.database["jt:lfh"].items()): 26 | formatted_time = data["timestamp"] 27 | label = f"{title}—{formatted_time}" 28 | list_item = ListItem(label=label) 29 | list_item.setArt( 30 | {"icon": os.path.join(ADDON_PATH, "resources", "img", "magnet.png")} 31 | ) 32 | list_item.setProperty("IsPlayable", "true") 33 | 34 | addDirectoryItem( 35 | ADDON_HANDLE, 36 | build_url( 37 | "play_torrent", 38 | data=data, 39 | ), 40 | list_item, 41 | False, 42 | ) 43 | endOfDirectory(ADDON_HANDLE) -------------------------------------------------------------------------------- /lib/utils/views/last_titles.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from lib.db.main import main_db 4 | from lib.utils.kodi.utils import ADDON_HANDLE, ADDON_PATH, build_url 5 | from xbmcgui import ListItem 6 | from xbmcplugin import ( 7 | addDirectoryItem, 8 | endOfDirectory, 9 | setPluginCategory, 10 | ) 11 | 12 | 13 | def show_last_titles(): 14 | setPluginCategory(ADDON_HANDLE, f"Last Titles - History") 15 | 16 | list_item = ListItem(label="Clear Titles") 17 | list_item.setArt( 18 | {"icon": os.path.join(ADDON_PATH, "resources", "img", "clear.png")} 19 | ) 20 | 21 | addDirectoryItem(ADDON_HANDLE, build_url("clear_history", type="lth"), list_item) 22 | 23 | for title, data in reversed(main_db.database["jt:lth"].items()): 24 | formatted_time = data["timestamp"] 25 | 26 | list_item = ListItem(label=f"{title}— {formatted_time}") 27 | list_item.setArt( 28 | {"icon": os.path.join(ADDON_PATH, "resources", "img", "trending.png")} 29 | ) 30 | 31 | mode = data["mode"] 32 | ids = data.get("ids") 33 | 34 | if mode == "tv": 35 | addDirectoryItem( 36 | ADDON_HANDLE, 37 | build_url( 38 | "tv_seasons_details", 39 | ids=ids, 40 | mode=mode, 41 | ), 42 | list_item, 43 | isFolder=True, 44 | ) 45 | elif mode == "movies": 46 | list_item.setProperty("IsPlayable", "true") 47 | addDirectoryItem( 48 | ADDON_HANDLE, 49 | build_url( 50 | "search", 51 | mode=mode, 52 | query=title, 53 | ids=ids, 54 | ), 55 | list_item, 56 | isFolder=False, 57 | ) 58 | elif mode == "tg_latest": 59 | addDirectoryItem( 60 | ADDON_HANDLE, 61 | build_url( 62 | "get_telegram_latest_files", 63 | data=json.dumps(data.get("tg_data")), 64 | ), 65 | list_item, 66 | isFolder=True, 67 | ) 68 | 69 | endOfDirectory(ADDON_HANDLE) -------------------------------------------------------------------------------- /lib/utils/views/shows.py: -------------------------------------------------------------------------------- 1 | from lib.clients.tmdb.utils import tmdb_get 2 | from lib.utils.kodi.utils import ADDON_HANDLE, build_url, play_media 3 | from lib.utils.general.utils import ( 4 | get_fanart_details, 5 | set_media_infoTag, 6 | ) 7 | 8 | from xbmcgui import ListItem 9 | from xbmcplugin import addDirectoryItem 10 | 11 | 12 | def show_season_info(ids, mode, media_type): 13 | tmdb_id, tvdb_id, imdb_id = ids.values() 14 | 15 | if imdb_id: 16 | res = tmdb_get("find_by_imdb_id", imdb_id) 17 | tmdb_id = res["tv_results"][0]["id"] 18 | ids = {"tmdb_id": tmdb_id, "tvdb_id": tvdb_id, "imdb_id": imdb_id} 19 | 20 | details = tmdb_get("tv_details", tmdb_id) 21 | name = details.name 22 | seasons = details.seasons 23 | fanart_details = get_fanart_details(tvdb_id=tvdb_id, mode=mode) 24 | 25 | for season in seasons: 26 | season_name = season.name 27 | if "Specials" in season_name: 28 | continue 29 | 30 | if "Miniseries" in season_name: 31 | season_name = "Season 1" 32 | 33 | season_number = season.season_number 34 | if season_number == 0: 35 | continue 36 | 37 | list_item = ListItem(label=season_name) 38 | 39 | set_media_infoTag( 40 | list_item, metadata=details, fanart_details=fanart_details, mode=mode 41 | ) 42 | 43 | list_item.setProperty("IsPlayable", "false") 44 | 45 | addDirectoryItem( 46 | ADDON_HANDLE, 47 | build_url( 48 | "tv_episodes_details", 49 | tv_name=name, 50 | ids=ids, 51 | mode=mode, 52 | media_type=media_type, 53 | season=season_number, 54 | ), 55 | list_item, 56 | isFolder=True, 57 | ) 58 | 59 | 60 | def show_episode_info(tv_name, season, ids, mode, media_type): 61 | season_details = tmdb_get( 62 | "season_details", {"id": ids.get("tmdb_id"), "season": season} 63 | ) 64 | 65 | fanart_data = get_fanart_details(tvdb_id=ids.get("tvdb_id"), mode=mode) 66 | 67 | for episode in season_details.episodes: 68 | ep_name = episode.name 69 | episode_number = episode.episode_number 70 | 71 | tv_data = {"name": ep_name, "episode": episode_number, "season": season} 72 | 73 | list_item = ListItem(label=f"{season}x{episode_number}. {ep_name}") 74 | 75 | set_media_infoTag( 76 | list_item, metadata=episode, fanart_details=fanart_data, mode="episode" 77 | ) 78 | 79 | list_item.setProperty("IsPlayable", "true") 80 | list_item.addContextMenuItems( 81 | [ 82 | ( 83 | "Rescrape item", 84 | play_media( 85 | name="search", 86 | mode=mode, 87 | query=tv_name, 88 | ids=ids, 89 | tv_data=tv_data, 90 | rescrape=True, 91 | ), 92 | ) 93 | ] 94 | ) 95 | 96 | addDirectoryItem( 97 | ADDON_HANDLE, 98 | build_url( 99 | "search", 100 | mode=mode, 101 | media_type=media_type, 102 | query=tv_name, 103 | ids=ids, 104 | tv_data=tv_data, 105 | ), 106 | list_item, 107 | isFolder=False, 108 | ) 109 | -------------------------------------------------------------------------------- /lib/vendor/bencodepy/bencode/BTL.py: -------------------------------------------------------------------------------- 1 | """bencode.py - `BTL` backwards compatibility support.""" 2 | 3 | # Compatibility with previous versions: 4 | from bencode.exceptions import BencodeDecodeError as BTFailure # noqa: F401 5 | 6 | 7 | __all__ = ( 8 | 'BTFailure' 9 | ) 10 | -------------------------------------------------------------------------------- /lib/vendor/bencodepy/bencode/__init__.py: -------------------------------------------------------------------------------- 1 | # The contents of this file are subject to the BitTorrent Open Source License 2 | # Version 1.1 (the License). You may not copy or use this file, in either 3 | # source code or executable form, except in compliance with the License. You 4 | # may obtain a copy of the License at http://www.bittorrent.com/license/. 5 | # 6 | # Software distributed under the License is distributed on an AS IS basis, 7 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 8 | # for the specific language governing rights and limitations under the 9 | # License. 10 | 11 | # Written by Petru Paler 12 | 13 | """bencode.py - bencode encoder + decoder.""" 14 | 15 | from bencode.BTL import BTFailure 16 | from bencode.exceptions import BencodeDecodeError 17 | 18 | from bencodepy import Bencached, Bencode 19 | 20 | __all__ = ( 21 | 'BTFailure', 22 | 'Bencached', 23 | 'BencodeDecodeError', 24 | 'bencode', 25 | 'bdecode', 26 | 'bread', 27 | 'bwrite', 28 | 'encode', 29 | 'decode' 30 | ) 31 | 32 | 33 | DEFAULT = Bencode( 34 | encoding='utf-8', 35 | encoding_fallback='value', 36 | dict_ordered=True, 37 | dict_ordered_sort=True 38 | ) 39 | 40 | bdecode = DEFAULT.decode 41 | bencode = DEFAULT.encode 42 | bread = DEFAULT.read 43 | bwrite = DEFAULT.write 44 | 45 | decode = bdecode 46 | encode = bencode 47 | -------------------------------------------------------------------------------- /lib/vendor/bencodepy/bencode/exceptions.py: -------------------------------------------------------------------------------- 1 | """bencode.py - encoder + decode exceptions.""" 2 | from bencodepy.exceptions import BencodeDecodeError 3 | 4 | __all__ = ( 5 | 'BencodeDecodeError', 6 | ) 7 | -------------------------------------------------------------------------------- /lib/vendor/bencodepy/bencodepy/common.py: -------------------------------------------------------------------------------- 1 | """bencode.py - common.""" 2 | 3 | 4 | class Bencached(object): 5 | __slots__ = ['bencoded'] 6 | 7 | def __init__(self, s): 8 | self.bencoded = s 9 | -------------------------------------------------------------------------------- /lib/vendor/bencodepy/bencodepy/compat.py: -------------------------------------------------------------------------------- 1 | """bencode.py - compatibility helpers.""" 2 | 3 | import sys 4 | 5 | PY2 = sys.version_info[0] == 2 6 | PY3 = sys.version_info[0] == 3 7 | 8 | 9 | def is_binary(s): 10 | if PY3: 11 | return isinstance(s, bytes) 12 | 13 | return isinstance(s, str) 14 | 15 | 16 | def is_text(s): 17 | if PY3: 18 | return isinstance(s, str) 19 | 20 | return isinstance(s, unicode) # noqa: F821 21 | 22 | 23 | def to_binary(s): 24 | if is_binary(s): 25 | return s 26 | 27 | if is_text(s): 28 | return s.encode('utf-8', 'strict') 29 | 30 | raise TypeError("expected binary or text (found %s)" % type(s)) 31 | -------------------------------------------------------------------------------- /lib/vendor/bencodepy/bencodepy/exceptions.py: -------------------------------------------------------------------------------- 1 | """bencode.py - exceptions.""" 2 | 3 | 4 | class BencodeDecodeError(Exception): 5 | """Bencode decode error.""" 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /lib/vendor/torf/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of torf. 2 | # 3 | # torf is free software: you can redistribute it and/or modify it under the 4 | # terms of the GNU General Public License as published by the Free Software 5 | # Foundation, either version 3 of the License, or (at your option) any later 6 | # version. 7 | # 8 | # torf is distributed in the hope that it will be useful, but WITHOUT ANY 9 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 10 | # A PARTICULAR PURPOSE. See the GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with torf. If not, see . 14 | 15 | # flake8: noqa 16 | 17 | """ 18 | Create and parse torrent files and magnet URIs 19 | """ 20 | 21 | __version__ = '4.2.0' 22 | 23 | from ._errors import * 24 | from ._magnet import Magnet 25 | from ._stream import TorrentFileStream 26 | from ._torrent import Torrent 27 | from ._utils import File, Filepath 28 | -------------------------------------------------------------------------------- /resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/__init__.py -------------------------------------------------------------------------------- /resources/img/anime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/anime.png -------------------------------------------------------------------------------- /resources/img/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/clear.png -------------------------------------------------------------------------------- /resources/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/cloud.png -------------------------------------------------------------------------------- /resources/img/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/donate.png -------------------------------------------------------------------------------- /resources/img/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/download.png -------------------------------------------------------------------------------- /resources/img/genre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/genre.png -------------------------------------------------------------------------------- /resources/img/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/history.png -------------------------------------------------------------------------------- /resources/img/magnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/magnet.png -------------------------------------------------------------------------------- /resources/img/magnet2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/magnet2.png -------------------------------------------------------------------------------- /resources/img/movies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/movies.png -------------------------------------------------------------------------------- /resources/img/nextpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/nextpage.png -------------------------------------------------------------------------------- /resources/img/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/search.png -------------------------------------------------------------------------------- /resources/img/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/settings.png -------------------------------------------------------------------------------- /resources/img/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/status.png -------------------------------------------------------------------------------- /resources/img/tmdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/tmdb.png -------------------------------------------------------------------------------- /resources/img/trakt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/trakt.png -------------------------------------------------------------------------------- /resources/img/trending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/trending.png -------------------------------------------------------------------------------- /resources/img/tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/img/tv.png -------------------------------------------------------------------------------- /resources/screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/screenshots/home.png -------------------------------------------------------------------------------- /resources/screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/screenshots/settings.png -------------------------------------------------------------------------------- /resources/screenshots/tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/screenshots/tv.png -------------------------------------------------------------------------------- /resources/skins/Default/1080i/custom_progress_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 600 4 | 400 5 | 800 6 | 300 7 | 8 | 12005 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 0.40 18 | 19 | 20 | 800 21 | 300 22 | 23 | 800 24 | 300 25 | circle.png 26 | CC000000 27 | 28 | 29 | 30 30 | 780 31 | 40 32 | center 33 | font14 34 | white 35 | AA000000 36 | 37 | 38 | 80 39 | 780 40 | 60 41 | center 42 | font12 43 | white 44 | AA000000 45 | true 46 | 47 | 48 | 180 49 | 40 50 | 720 51 | 20 52 | percentage 53 | 54 | 55 | horizontal 56 | 220 57 | 200 58 | 100 59 | 60 | 61 | auto 62 | 56 63 | 64 | center 65 | 20 66 | font14 67 | DDFFFFFF 68 | EEFFFFFF 69 | DDFFFFFF 70 | circle.png 71 | circle.png 72 | no 73 | 74 | 75 | 76 | auto 77 | 56 78 | 79 | center 80 | 20 81 | font14 82 | DDFFFFFF 83 | EEFFFFFF 84 | DDFFFFFF 85 | circle.png 86 | circle.png 87 | no 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /resources/skins/Default/1080i/customdialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 600 4 | 300 5 | 800 6 | 400 7 | 8 | 9 | 32503 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0.40 19 | 20 | 21 | 22 | 800 23 | 400 24 | 25 | 26 | 27 | 800 28 | 400 29 | circle.png 30 | CC000000 31 | 32 | 33 | 34 | 35 | font14 36 | center 37 | white 38 | AA000000 39 | 40 | 41 | 42 | 43 | font14 44 | 90 45 | 780 46 | 150 47 | center 48 | white 49 | AA000000 50 | true 51 | 52 | 53 | 54 | 55 | 745 56 | 50 57 | 50 58 | Button/close.png 59 | Button/close.png 60 | 61 | 62 | 63 | 64 | 600 65 | 100 66 | 280 67 | 90 68 | 69 | 70 | 71 | -6 72 | -2 73 | circle.png 74 | CCFF0000 75 | 76 | 77 | 78 | 79 | font14 80 | center 81 | center 82 | 100 83 | white 84 | FF000000 85 | true 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /resources/skins/Default/1080i/filter_items.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 635 4 | 215 5 | 650 6 | 650 7 | 8 | 2000 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 0.40 18 | 19 | 20 | 21 | 22 | 0 23 | 0 24 | 650 25 | 650 26 | circle.png 27 | CC000000 28 | 29 | 30 | 31 | 32 | 0 33 | 30 34 | 650 35 | 60 36 | font16_title 37 | white 38 | center 39 | 40 | 41 | 42 | 43 | 44 | 25 45 | 110 46 | 600 47 | 510 48 | 2000 49 | 2000 50 | 2000 51 | 2000 52 | 20 53 | 54 | 55 | 56 | 57 | 600 58 | 70 59 | 60 | 0 61 | 10 62 | 600 63 | 50 64 | circle.png 65 | CC222222 66 | 67 | 68 | 0 69 | 10 70 | 600 71 | 50 72 | font14 73 | white 74 | center 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 600 84 | 70 85 | 86 | 0 87 | 10 88 | 600 89 | 50 90 | circle.png 91 | FF12A0C7 92 | 93 | 94 | 0 95 | 10 96 | 600 97 | 50 98 | font14 99 | white 100 | center 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /resources/skins/Default/1080i/filter_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 710 4 | 290 5 | 500 6 | 400 7 | 8 | 2000 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 0.40 18 | 19 | 20 | 21 | 22 | 0 23 | 0 24 | 500 25 | 400 26 | circle.png 27 | CC000000 28 | 29 | 30 | 31 | 32 | 0 33 | 50 34 | 500 35 | 60 36 | font16_title 37 | white 38 | center 39 | 40 | 41 | 42 | 43 | 44 | 50 45 | 150 46 | 400 47 | 200 48 | 2000 49 | 2000 50 | 2000 51 | 2000 52 | 30 53 | 54 | 55 | 56 | 57 | 400 58 | 60 59 | 60 | 0 61 | 10 62 | 60 63 | 40 64 | circle.png 65 | CC222222 66 | 67 | 68 | 80 69 | 10 70 | 320 71 | 40 72 | font14 73 | white 74 | left 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 400 84 | 60 85 | 86 | 0 87 | 10 88 | 60 89 | 40 90 | circle.png 91 | FF12A0C7 92 | 93 | 94 | 80 95 | 10 96 | 320 97 | 40 98 | font14 99 | white 100 | left 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /resources/skins/Default/1080i/playing_next.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog.Close(fullscreeninfo,true) 4 | Dialog.Close(videoosd,true) 5 | 6 | 3001 7 | 8 | 9 | 10 | 20 11 | 150 12 | 520 13 | 220 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -6 30 | -6 31 | 540 32 | 240 33 | circle.png 34 | 00000000 35 | 36 | 37 | 38 | 39 | 40 | 41 | 120 42 | 100 43 | true 44 | 45 | 46 | 47 | circle.png 48 | circle.png 49 | 50 | 51 | 52 | 53 | 100 54 | circle.png 55 | 56 | 57 | 58 | 59 | 10 60 | 20 61 | 62 | 63 | 64 | 20 65 | 120 66 | 67 | 68 | 20 69 | font12 70 | ffffffff 71 | 72 | 73 | 74 | 75 | 76 | 77 | horizontal 78 | 56 79 | 20 80 | 20 81 | 20 82 | right 83 | 84 | 85 | 86 | 87 | 56 88 | auto 89 | font12 90 | 20 91 | ddffffff 92 | eeffffff 93 | ddffffff 94 | center 95 | circle.png 96 | circle.png 97 | no 98 | 99 | 100 | 101 | 102 | 103 | 56 104 | auto 105 | font12 106 | 20 107 | DDFFFFFF 108 | EEFFFFFF 109 | DDFFFFFF 110 | center 111 | circle.png 112 | circle.png 113 | no 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /resources/skins/Default/1080i/resume_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1002 4 | 5 | 6 | 400 7 | 500 8 | 520 9 | 220 10 | 11 | 12 | 13 | vertical 14 | 150 15 | 15 16 | right 17 | 18 | 19 | 56 20 | auto 21 | font12 22 | 20 23 | ddffffff 24 | eeffffff 25 | ddffffff 26 | circle.png 27 | circle.png 28 | no 29 | 30 | 31 | 32 | 56 33 | auto 34 | font12 35 | 20 36 | DDFFFFFF 37 | EEFFFFFF 38 | DDFFFFFF 39 | circle.png 40 | circle.png 41 | no 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /resources/skins/Default/media/AddonWindow/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/AddonWindow/black.png -------------------------------------------------------------------------------- /resources/skins/Default/media/AddonWindow/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/AddonWindow/white.png -------------------------------------------------------------------------------- /resources/skins/Default/media/Button/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/Button/close.png -------------------------------------------------------------------------------- /resources/skins/Default/media/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/circle.png -------------------------------------------------------------------------------- /resources/skins/Default/media/jtk_clearlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/jtk_clearlogo.png -------------------------------------------------------------------------------- /resources/skins/Default/media/jtk_fanart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/jtk_fanart.png -------------------------------------------------------------------------------- /resources/skins/Default/media/left-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/left-circle.png -------------------------------------------------------------------------------- /resources/skins/Default/media/spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/spinner.png -------------------------------------------------------------------------------- /resources/skins/Default/media/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/texture.png -------------------------------------------------------------------------------- /resources/skins/Default/media/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sam-Max/plugin.video.jacktook/1e71e88bfefa86846c407612a0c6db963dec78bd/resources/skins/Default/media/white.png -------------------------------------------------------------------------------- /service.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from lib.api.trakt.base_cache import setup_databases 3 | from lib.utils.kodi.utils import ( 4 | get_kodi_version, 5 | get_property, 6 | get_setting, 7 | kodilog, 8 | set_property, 9 | clear_property, 10 | dialog_ok, 11 | translatePath, 12 | ) 13 | from time import time 14 | from lib.utils.kodi.settings import update_action, update_delay 15 | from lib.updater import updates_check_addon 16 | 17 | import xbmcaddon 18 | import xbmcvfs 19 | import xbmc 20 | 21 | first_run_update_prop = "jacktook.first_run_update" 22 | pause_services_prop = "jacktook.pause_services" 23 | 24 | 25 | class OnNotificationActions: 26 | def run(self, sender, method, data): 27 | if sender == "xbmc": 28 | if method in ("GUI.OnScreensaverActivated", "System.OnSleep"): 29 | set_property(pause_services_prop, "true") 30 | elif method in ("GUI.OnScreensaverDeactivated", "System.OnWake"): 31 | clear_property(pause_services_prop) 32 | 33 | 34 | class CheckKodiVersion: 35 | def run(self): 36 | kodilog("Checking Kodi version") 37 | if get_kodi_version() < 20: 38 | dialog_ok( 39 | "Jacktook", 40 | "Kodi 20 or above required[CR]Please update Kodi to use addon", 41 | ) 42 | 43 | 44 | class DatabaseSetup: 45 | def run(self): 46 | setup_databases() 47 | 48 | 49 | class UpdateCheck: 50 | def run(self): 51 | kodilog("Update Check Service Started") 52 | if get_property(first_run_update_prop) == "true": 53 | return 54 | end_pause = time() + update_delay() 55 | monitor, player = xbmc.Monitor(), xbmc.Player() 56 | wait_for_abort, is_playing = monitor.waitForAbort, player.isPlayingVideo 57 | while not monitor.abortRequested(): 58 | while time() < end_pause: 59 | wait_for_abort(1) 60 | while get_property(pause_services_prop) == "true" or is_playing(): 61 | wait_for_abort(1) 62 | updates_check_addon(update_action()) 63 | break 64 | set_property(first_run_update_prop, "true") 65 | try: 66 | del monitor 67 | except: 68 | pass 69 | try: 70 | del player 71 | except: 72 | pass 73 | kodilog("Update Check Service Finished") 74 | 75 | 76 | def TMDBHelperAutoInstall(): 77 | try: 78 | _ = xbmcaddon.Addon("plugin.video.themoviedb.helper") 79 | except RuntimeError: 80 | return 81 | 82 | tmdb_helper_path = "special://home/addons/plugin.video.themoviedb.helper/resources/players/jacktook.select.json" 83 | if xbmcvfs.exists(tmdb_helper_path): 84 | return 85 | jacktook_select_path = ( 86 | "special://home/addons/plugin.video.jacktook/jacktook.select.json" 87 | ) 88 | if not xbmcvfs.exists(jacktook_select_path): 89 | kodilog("jacktook.select.json file not found!") 90 | return 91 | 92 | ok = xbmcvfs.copy(jacktook_select_path, tmdb_helper_path) 93 | if not ok: 94 | kodilog("Error installing jacktook.select.json file!") 95 | return 96 | 97 | class DownloaderSetup(): 98 | def run(self): 99 | download_dir = get_setting("download_dir") 100 | translated_path = translatePath(download_dir) 101 | if not xbmcvfs.exists(translated_path): 102 | xbmcvfs.mkdir(translated_path) 103 | 104 | class JacktookMOnitor(xbmc.Monitor): 105 | def __init__(self): 106 | xbmc.Monitor.__init__(self) 107 | self.start() 108 | 109 | def start(self): 110 | CheckKodiVersion().run() 111 | DatabaseSetup().run() 112 | Thread(target=UpdateCheck().run).start() 113 | DownloaderSetup().run() 114 | TMDBHelperAutoInstall() 115 | 116 | def onNotification(self, sender, method, data): 117 | OnNotificationActions().run(sender, method, data) 118 | 119 | 120 | JacktookMOnitor().waitForAbort() 121 | --------------------------------------------------------------------------------