# VERSION: 1.50 # AUTHOR: github.com/444995 # WILL GET UPDATED IF BROKEN # MIT License # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # USER SETTINGS PRIVATE_USERNAME = "REPLACE_ME" # A DANISHBYTES ACCOUNT HAS A PRIVATE USERNAME PUBLIC_USERNAME = "REPLACE_ME" # AND A PUBLIC USERNAME PASSWORD = "REPLACE_ME" # YOU ALSO NEED A PASSWORD CACHE_LOGIN_COOKIES = True # CACHE COOKIES (HIGHLY RECOMMENDED) COOKIES_FILE_NAME = "danishbytes.cookies" # THE PATH TO THE LOGIN USE_MAGNET_URLS = False # USES MAGNET LINKS OR TORRENT FILES DEPENDING ON TRUE/FALSE (FALSE IS RECOMMENDED) # IMPORTS import os import re import json import gzip import tempfile import urllib.parse import urllib.request import http.cookiejar as cookielib from urllib.error import HTTPError from novaprinter import prettyPrinter class HtmlExtractor: """ Extracts needed keys and tokens from a given HTML response. """ @staticmethod def extract_meta_content(html, name): """Extracts content from meta tag with the given name.""" pattern = r'<meta\s+[^>]*name=["\']{}["\'][^>]*>'.format(re.escape(name)) match = re.search(pattern, html, re.IGNORECASE) if match: tag = match.group() content_match = re.search(r'content=["\']([^"\']+)["\']', tag, re.IGNORECASE) if content_match: return content_match.group(1).strip() return None @staticmethod def extract_input_value(html, name): """Extracts value from input tag with the given name.""" pattern = r'<input\s+[^>]*name=["\']{}["\'][^>]*>'.format(re.escape(name)) match = re.search(pattern, html, re.IGNORECASE) if match: tag = match.group() value_match = re.search(r'value=["\']([^"\']*)["\']', tag, re.IGNORECASE) if value_match: return value_match.group(1).strip() return None @staticmethod def extract_attr(html, attr_name, num=-1): """Extracts the attribute value from the input tag with the given number.""" inputs = re.findall(r'<input\s+[^>]*>', html, re.IGNORECASE) if inputs: if num == -1: input_tag = inputs[-1] else: if num < len(inputs): input_tag = inputs[num] else: return None attr_match = re.search(r'{}=["\']([^"\']+)["\']'.format(re.escape(attr_name)), input_tag, re.IGNORECASE) if attr_match: return attr_match.group(1).strip() return None class danishbytes(object): """ DanishBytes engine for qBittorrent. """ name = 'DanishBytes' url = 'https://danishbytes.club' login_url = f"{url}/login" tracker_urls = [ "https://danishbytes2.org/announce", "https://dbytes.org/announce", "https://danishbytes.club/announce" ] categories_string = "&categories%5B%5D=" user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0' supported_categories = { 'all': '0', # 'all' is just every category combined 'movies': '1', 'tv': '2', 'music': '3', 'games': '4', 'software': '5', 'books': '8', } def __init__(self): """ Initializes the DanishBytes engine for qBittorrent. """ self.cookies_file_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), COOKIES_FILE_NAME ) self.html_extractor = HtmlExtractor() self.cookiejar = cookielib.LWPCookieJar(self.cookies_file_path) self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookiejar)) self.opener.addheaders = [ ('User-Agent', self.user_agent), ('Accept-Encoding', 'gzip, deflate'), ] if CACHE_LOGIN_COOKIES and os.path.exists(self.cookies_file_path): self.cookiejar.load( self.cookies_file_path, ignore_discard=True, ignore_expires=True ) else: self._login() self._set_csrf_token() def _login(self): """ Logs into DanishBytes; this automatically sets the cookies in the cookiejar when self.opener is used hence why cookies aren't returned """ # initial request to get cookies and keys response = self._make_request(self.login_url) html = response payload = { "_token": self.html_extractor.extract_meta_content(html, "csrf-token"), "private_username": PRIVATE_USERNAME, "username": PUBLIC_USERNAME, "password": PASSWORD, "remember": "on", "_captcha": self.html_extractor.extract_input_value(html, "_captcha"), "_username": self.html_extractor.extract_input_value(html, "_username"), self.html_extractor.extract_attr(html, "name"): self.html_extractor.extract_attr(html, "value") } try: self._make_request(self.login_url, data=payload) except HTTPError as e: raise Exception(f"Login failed with error code: {e.code}") from e if CACHE_LOGIN_COOKIES: self.cookiejar.save( self.cookies_file_path, ignore_discard=True, ignore_expires=True ) def download_torrent(self, info): """ Downloads the torrent file """ torrent_file = self._make_request(info) with tempfile.NamedTemporaryFile(suffix='.torrent', delete=False) as _file: _file.write(torrent_file.encode('utf-8')) print(f"{_file.name} {info}") def search(self, what, cat='all'): """ Searches for torrents on DanishBytes. """ category_param = self._get_category_param(cat) page_num = 1 while True: torrent_count = self._search_page(what, page_num, category_param) if torrent_count < 100: break page_num += 1 def _get_category_param(self, cat): """ Constructs the category parameter string for the search URL. """ if cat == 'all': return ''.join([self.categories_string + c for c in self.supported_categories.values()]) return self.categories_string + self.supported_categories[cat] def _search_page(self, what, page_num, category_param): """ Handles a single page of search results. """ search_url = f"{self.url}/torrents/filter?_token={self.csrf_token}&search={what}&page={page_num}&qty=100{category_param}" response = self._make_request(search_url) search_results = json.loads(response) for torrent in search_results['torrents']: self._print_torrent(torrent, search_results['rsskey'], search_results['passkey']) return len(search_results['torrents']) def _make_request(self, url, data=None): """ Makes a request to the given URL using urllib. """ encoded_data = urllib.parse.urlencode(data).encode() if data else None with self.opener.open(url, encoded_data or None) as response: content = gzip.GzipFile(fileobj=response).read() if response.info().get('Content-Encoding') == 'gzip' else response.read() return content.decode('utf-8', errors='replace') def _print_torrent(self, torrent, rss_key, pass_key): """ Prints a single torrent's details. """ _link = self._make_magnet_url(torrent, rss_key, pass_key) if USE_MAGNET_URLS else f"{self.url}/torrents/download/{torrent['id']}" prettyPrinter({ 'link': _link, 'name': torrent['name'], 'size': f"{str(torrent['size'])} B", 'seeds': torrent['seeders'], 'leech': torrent['leechers'], 'engine_url': self.url, 'desc_link': f"{self.url}/torrents/{torrent['id']}" }) def _make_magnet_url(self, torrent, rss_key, pass_key): """ Constructs the magnet URL for a torrent. """ magnet_url = "magnet:?" magnet_url += f"dn={urllib.parse.quote(torrent['name'])}&" magnet_url += f"xt=urn:btih:{torrent['info_hash']}&" magnet_url += f"as={self.url}/torrent/download/{torrent['id']}.{rss_key}&" magnet_url += f"xl={torrent['size']}&" for tracker in self.tracker_urls: magnet_url += f"tr={urllib.parse.quote(tracker)}/{pass_key}&" return magnet_url.rstrip('&') def _set_csrf_token(self): """ Sets the CSRF token for the current session, so it can be used for search requests. """ response = self._make_request(self.url) html = response self.csrf_token = self.html_extractor.extract_meta_content(html, "csrf-token") if __name__ == "__main__": # For testing purposes db = danishbytes() db.search("hello", "all") db.download_torrent("https://danishbytes.club/torrents/download/25600")