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