@ -1,4 +1,4 @@
# VERSION: 1.5
# VERSION: 1.6
# AUTHORS: imDMG [imdmgg@gmail.com]
# AUTHORS: imDMG [imdmgg@gmail.com]
# rutracker.org search engine plugin for qBittorrent
# rutracker.org search engine plugin for qBittorrent
@ -7,14 +7,15 @@ import base64
import json
import json
import logging
import logging
import re
import re
import socket
import sys
import sys
import time
import time
from concurrent . futures import ThreadPoolExecutor
from concurrent . futures import ThreadPoolExecutor
from dataclasses import dataclass , field
from html import unescape
from html import unescape
from http . cookiejar import Cookie , MozillaCookieJar
from http . cookiejar import Cookie , MozillaCookieJar
from pathlib import Path
from pathlib import Path
from tempfile import NamedTemporaryFile
from tempfile import NamedTemporaryFile
from typing import Optional , Union
from urllib . error import URLError , HTTPError
from urllib . error import URLError , HTTPError
from urllib . parse import urlencode , unquote
from urllib . parse import urlencode , unquote
from urllib . request import build_opener , HTTPCookieProcessor , ProxyHandler
from urllib . request import build_opener , HTTPCookieProcessor , ProxyHandler
@ -25,29 +26,16 @@ except ImportError:
sys . path . insert ( 0 , str ( Path ( __file__ ) . parent . parent . absolute ( ) ) )
sys . path . insert ( 0 , str ( Path ( __file__ ) . parent . parent . absolute ( ) ) )
from novaprinter import prettyPrinter
from novaprinter import prettyPrinter
# default config
config = {
" torrentDate " : True ,
" username " : " USERNAME " ,
" password " : " PASSWORD " ,
" proxy " : False ,
" proxies " : {
" http " : " " ,
" https " : " "
} ,
" ua " : " Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0 "
}
FILE = Path ( __file__ )
FILE = Path ( __file__ )
BASEDIR = FILE . parent . absolute ( )
BASEDIR = FILE . parent . absolute ( )
FILENAME = FILE . name [ : - 3 ]
FILENAME = FILE . name [ : - 3 ]
FILE_J , FILE_C = [ BASEDIR / ( FILENAME + fl ) for fl in [ ' .json ' , ' .cookie ' ] ]
FILE_J , FILE_C = [ BASEDIR / ( FILENAME + fl ) for fl in [ " .json " , " .cookie " ] ]
PAGES = 50
PAGES = 50
def rng ( t ) :
def rng ( t : int ) - > range :
return range ( PAGES , - ( - t / / PAGES ) * PAGES , PAGES )
return range ( PAGES , - ( - t / / PAGES ) * PAGES , PAGES )
@ -56,8 +44,8 @@ RE_TORRENTS = re.compile(
r ' .+?data-ts_text= " ([-0-9]+?) " >.+?Личи " >( \ d+?)</.+?data-ts_text= " ( \ d+?) " > ' ,
r ' .+?data-ts_text= " ([-0-9]+?) " >.+?Личи " >( \ d+?)</.+?data-ts_text= " ( \ d+?) " > ' ,
re . S
re . S
)
)
RE_RESULTS = re . compile ( r ' Результатов \ sпоиска: \ s( \ d { 1,3}) \ s<span ' , re . S )
RE_RESULTS = re . compile ( r " Результатов \ sпоиска: \ s( \ d { 1,3}) \ s<span " , re . S )
PATTERNS = ( ' %s /tracker.php?nm= %s &c= %s ' , " %s &start= %s " )
PATTERNS = ( " %s tracker.php?nm= %s &c= %s " , " %s &start= %s " )
# base64 encoded image
# base64 encoded image
ICON = ( " AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAABMLAAATCw "
ICON = ( " AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAABMLAAATCw "
@ -92,60 +80,99 @@ logging.basicConfig(
logger = logging . getLogger ( __name__ )
logger = logging . getLogger ( __name__ )
@dataclass
class Config :
username : str = " USERNAME "
password : str = " PASSWORD "
torrent_date : bool = True
# magnet: bool = False
proxy : bool = False
# dynamic_proxy: bool = True
proxies : dict = field ( default_factory = lambda : { " http " : " " , " https " : " " } )
ua : str = ( " Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 "
" Firefox/38.0 " )
def __post_init__ ( self ) :
try :
try :
config = json . loads ( FILE_J . read_text ( ) )
if not self . _validate_json ( json . loads ( FILE_J . read_text ( ) ) ) :
logger . debug ( " Config is loaded. " )
raise ValueError ( " Incorrect json scheme ." )
except OSError as e :
except Exception as e :
logger . error ( e )
logger . error ( e )
# if file doesn't exist, we'll create it
FILE_J . write_text ( self . to_str ( ) )
FILE_J . write_text ( json . dumps ( config , indent = 4 , sort_keys = False ) )
( BASEDIR / f " { FILENAME } .ico " ) . write_bytes ( base64 . b64decode ( ICON ) )
# also write/rewrite ico file
( BASEDIR / ( FILENAME + ' .ico ' ) ) . write_bytes ( base64 . b64decode ( ICON ) )
def to_str ( self ) - > str :
logger . debug ( " Write files. " )
return json . dumps ( self . to_dict ( ) , indent = 4 , sort_keys = False )
def to_dict ( self ) - > dict :
return { self . _to_camel ( k ) : v for k , v in self . __dict__ . items ( ) }
def _validate_json ( self , obj : dict ) - > bool :
is_valid = True
for k , v in self . __dict__ . items ( ) :
_val = obj . get ( self . _to_camel ( k ) )
if type ( _val ) is not type ( v ) :
is_valid = False
continue
if type ( _val ) is dict :
for dk , dv in v . items ( ) :
if type ( _val . get ( dk ) ) is not type ( dv ) :
_val [ dk ] = dv
is_valid = False
setattr ( self , k , _val )
return is_valid
@staticmethod
def _to_camel ( s : str ) - > str :
return " " . join ( x . title ( ) if i else x
for i , x in enumerate ( s . split ( " _ " ) ) )
config = Config ( )
class Rutracker :
class Rutracker :
name = ' Rutracker '
name = " Rutracker "
url = ' https://rutracker.org/forum/ '
url = " https://rutracker.org/forum/ "
url_dl = url + ' dl.php?t= '
url_dl = url + " dl.php?t= "
url_login = url + ' login.php '
url_login = url + " login.php "
supported_categories = { ' all ' : ' -1 ' }
supported_categories = { " all " : " -1 " }
def __init__ ( self ) :
# error message
# error message
self . error = None
error : Optional [ str ] = None
# cookies
mcj = MozillaCookieJar ( )
# establish connection
# establish connection
self . session = build_opener ( )
session = build_opener ( HTTPCookieProcessor ( mcj ) )
def __init__ ( self ) :
# add proxy handler if needed
# add proxy handler if needed
if config [ ' proxy ' ] :
if config . proxy :
if any ( config [ ' proxies ' ] . values ( ) ) :
if any ( config . proxies . values ( ) ) :
self . session . add_handler ( ProxyHandler ( config [ ' proxies ' ] ) )
self . session . add_handler ( ProxyHandler ( config . proxies ) )
logger . debug ( " Proxy is set! " )
logger . debug ( " Proxy is set! " )
else :
else :
self . error = " Proxy enabled, but not set! "
self . error = " Proxy enabled, but not set! "
# change user-agent
# change user-agent
self . session . addheaders = [ ( ' User-Agent ' , config [ ' ua ' ] ) ]
self . session . addheaders = [ ( " User-Agent " , config . ua ) ]
# load local cookies
# load local cookies
mcj = MozillaCookieJar ( )
try :
try :
mcj . load ( FILE_C , ignore_discard = True )
self . mcj . load ( FILE_C , ignore_discard = True )
if ' bb_session ' in [ cookie . name for cookie in mcj ] :
if " bb_session " in [ cookie . name for cookie in self . mcj ] :
# if cookie.expires < int(time.time())
# if cookie.expires < int(time.time())
logger . info ( " Local cookies is loaded " )
logger . info ( " Local cookies is loaded " )
self . session . add_handler ( HTTPCookieProcessor ( mcj ) )
else :
else :
logger . info ( " Local cookies expired or bad " )
logger . info ( " Local cookies expired or bad " )
logger . debug ( f " That we have: { [ cookie for cookie in mcj ] } " )
logger . debug ( f " That we have: { [ cookie for cookie in self . mcj ] } " )
mcj . clear ( )
self . mcj . clear ( )
self . login ( mcj )
self . login ( )
except FileNotFoundError :
except FileNotFoundError :
self . login ( mcj )
self . login ( )
def search ( self , what , cat = ' all ' ) :
def search ( self , what : str , cat : str = " all " ) - > None :
if self . error :
if self . error :
self . pretty_error ( what )
self . pretty_error ( what )
return None
return None
@ -166,79 +193,81 @@ class Rutracker:
logger . debug ( f " --- { time . time ( ) - t0 } seconds --- " )
logger . debug ( f " --- { time . time ( ) - t0 } seconds --- " )
logger . info ( f " Found torrents: { total } " )
logger . info ( f " Found torrents: { total } " )
def download_torrent ( self , url : str ) :
def download_torrent ( self , url : str ) - > None :
# Download url
# Download url
response = self . _catch_error_ request ( url )
response = self . _request ( url )
if self . error :
if self . error :
self . pretty_error ( url )
self . pretty_error ( url )
return None
return None
# Create a torrent file
# Create a torrent file
with NamedTemporaryFile ( suffix = ' .torrent ' , delete = False ) as fd :
with NamedTemporaryFile ( suffix = " .torrent " , delete = False ) as fd :
fd . write ( response )
fd . write ( response )
# return file path
# return file path
logger . debug ( fd . name + " " + url )
logger . debug ( fd . name + " " + url )
print ( fd . name + " " + url )
print ( fd . name + " " + url )
def login ( self , mcj ) :
def login ( self ) - > None :
if self . error :
if self . error :
return None
return None
# if we wanna use https we mast add bb_ssl=1 to cookie
# if we wanna use https we mast add bb_ssl=1 to cookie
mcj . set_cookie ( Cookie ( 0 , " bb_ssl " , " 1 " , None , False , " .rutracker.org " ,
self . mcj . set_cookie ( Cookie ( 0 , " bb_ssl " , " 1 " , None , False ,
True , True , " /forum/ " , True , True ,
" .rutracker.org " , True , True , " /forum/ " ,
None , False , None , None , { } ) )
True , True , None , False , None , None , { } ) )
self . session . add_handler ( HTTPCookieProcessor ( mcj ) )
form_data = { " login_username " : config [ ' username ' ] ,
form_data = { " login_username " : config . username ,
" login_password " : config [ ' password ' ] ,
" login_password " : config . password ,
" login " : " Вход " }
" login " : " Вход " }
logger . debug ( f " Login. Data before: { form_data } " )
logger . debug ( f " Login. Data before: { form_data } " )
# so we first encode vals to cp1251 then do default decode whole string
# encoding to cp1251 then do default encode whole string
data_encoded = urlencode (
data_encoded = urlencode ( form_data , encoding = " cp1251 " ) . encode ( )
{ k : v . encode ( ' cp1251 ' ) for k , v in form_data . items ( ) }
) . encode ( )
logger . debug ( f " Login. Data after: { data_encoded } " )
logger . debug ( f " Login. Data after: { data_encoded } " )
self . _catch_error_ request ( self . url_login , data_encoded )
self . _request ( self . url_login , data_encoded )
if self . error :
if self . error :
return None
return None
logger . debug ( f " That we have: { [ cookie for cookie in mcj ] } " )
logger . debug ( f " That we have: { [ cookie for cookie in self . mcj ] } " )
if ' bb_session ' in [ cookie . name for cookie in mcj ] :
if " bb_session " in [ cookie . name for cookie in self . mcj ] :
mcj . save ( FILE_C , ignore_discard = True , ignore_expires = True )
self . mcj . save ( FILE_C , ignore_discard = True , ignore_expires = True )
logger . info ( " We successfully authorized " )
logger . info ( " We successfully authorized " )
else :
else :
self . error = " We not authorized, please check your credentials! "
self . error = " We not authorized, please check your credentials! "
logger . warning ( self . error )
logger . warning ( self . error )
def searching ( self , query , first = False ) :
def searching ( self , query : str , first : bool = False ) - > Union [ None , int ] :
response = self . _catch_error_ request ( query )
response = self . _request ( query )
if not response :
if self . error :
return None
return None
page , torrents_found = response . decode ( ' cp1251 ' ) , - 1
page , torrents_found = response . decode ( " cp1251 " ) , - 1
if first :
if first :
if " log-out-icon " not in page :
if " log-out-icon " not in page :
logger . debug ( " Looks like we lost session id, lets login " )
logger . debug ( " Looks like we lost session id, lets login " )
self . login ( MozillaCookieJar ( ) )
self . mcj . clear ( )
self . login ( )
if self . error :
if self . error :
return None
return None
# retry request because guests cant search
# retry request because guests cant search
response = self . _catch_error_ request ( query )
response = self . _request ( query )
if not response :
if self . error :
return None
return None
page = response . decode ( ' cp1251 ' )
page = response . decode ( " cp1251 " )
# firstly we check if there is a result
# firstly we check if there is a result
torrents_found = int ( RE_RESULTS . search ( page ) [ 1 ] )
result = RE_RESULTS . search ( page )
if not result :
self . error = " Unexpected page content "
return None
torrents_found = int ( result [ 1 ] )
if not torrents_found :
if not torrents_found :
return 0
return 0
self . draw ( page )
self . draw ( page )
return torrents_found
return torrents_found
def draw ( self , html : str ) :
def draw ( self , html : str ) - > None :
torrents = RE_TORRENTS . findall ( html )
for tor in RE_TORRENTS . findall ( html ) :
for tor in torrents :
local = time . strftime ( " % y. % m. %d " , time . localtime ( int ( tor [ 5 ] ) ) )
local = time . strftime ( " % y. % m. %d " , time . localtime ( int ( tor [ 5 ] ) ) )
torrent_date = f " [ { local } ] " if config [ ' torrentDate ' ] else " "
torrent_date = f " [ { local } ] " if config . torrent_date else " "
prettyPrinter ( {
prettyPrinter ( {
" engine_url " : self . url ,
" engine_url " : self . url ,
@ -249,33 +278,33 @@ class Rutracker:
" seeds " : max ( 0 , int ( tor [ 3 ] ) ) ,
" seeds " : max ( 0 , int ( tor [ 3 ] ) ) ,
" leech " : tor [ 4 ]
" leech " : tor [ 4 ]
} )
} )
del torrents
def _catch_error_request ( self , url = None , data = None , repeated = False ) :
url = url or self . url
def _request (
self , url : str , data : Optional [ bytes ] = None , repeated : bool = False
) - > Union [ bytes , None ] :
try :
try :
with self . session . open ( url , data , 5 ) as r :
with self . session . open ( url , data , 5 ) as r :
# checking that tracker isn't blocked
# checking that tracker isn't blocked
if r . url . startswith ( ( self . url , self . url_dl ) ) :
if r . get url( ) . startswith ( ( self . url , self . url_dl ) ) :
return r . read ( )
return r . read ( )
raise URLError ( f " { self . url } is blocked. Try another proxy. " )
self . error = f " { url } is blocked. Try another proxy. "
except ( socket . error , socket . timeout ) as err :
if not repeated :
return self . _catch_error_request ( url , data , True )
logger . error ( err )
self . error = f " { self . url } is not response! Maybe it is blocked. "
if " no host given " in err . args :
self . error = " Proxy is bad, try another! "
except ( URLError , HTTPError ) as err :
except ( URLError , HTTPError ) as err :
logger . error ( err . reason )
logger . error ( err . reason )
self . error = err . reason
error = str ( err . reason )
if hasattr ( err , ' code ' ) :
print ( err . info ( ) )
if " timed out " in error and not repeated :
logger . debug ( " Repeating request... " )
return self . _request ( url , data , True )
if " no host given " in error :
self . error = " Proxy is bad, try another! "
elif hasattr ( err , " code " ) :
self . error = f " Request to { url } failed with status: { err . code } "
self . error = f " Request to { url } failed with status: { err . code } "
else :
self . error = f " { url } is not response! Maybe it is blocked. "
return None
return None
def pretty_error ( self , what ) :
def pretty_error ( self , what : str ) - > None :
prettyPrinter ( { " engine_url " : self . url ,
prettyPrinter ( { " engine_url " : self . url ,
" desc_link " : " https://github.com/imDMG/qBt_SE " ,
" desc_link " : " https://github.com/imDMG/qBt_SE " ,
" name " : f " [ { unquote ( what ) } ][Error]: { self . error } " ,
" name " : f " [ { unquote ( what ) } ][Error]: { self . error } " ,
@ -291,9 +320,9 @@ class Rutracker:
rutracker = Rutracker
rutracker = Rutracker
if __name__ == " __main__ " :
if __name__ == " __main__ " :
if BASEDIR . parent . joinpath ( ' settings_gui.py ' ) . exists ( ) :
if BASEDIR . parent . joinpath ( " settings_gui.py " ) . exists ( ) :
from settings_gui import EngineSettingsGUI
from settings_gui import EngineSettingsGUI
EngineSettingsGUI ( FILENAME )
EngineSettingsGUI ( FILENAME )
engine = rutracker ( )
engine = rutracker ( )
engine . search ( ' doctor ' )
engine . search ( " doctor " )