├── README.md ├── accounts └── .gitkeep ├── configs └── .gitkeep ├── logs └── .gitkeep ├── main.py ├── requirements.txt └── settings.ini /README.md: -------------------------------------------------------------------------------- 1 | # qBittorent rclone 2 | 3 | This script helps you upload files downloaded by qBitorrent to Google Drive using Service accounts. 4 | 5 | ## How to use? 6 | * [Install python](https://www.python.org/) 7 | * [Download or install rclone](https://rclone.org/install/) (use latest version) 8 | * Download [source code folder](https://github.com/Abu3safeer/qbittorrent_rclone/archive/master.zip). 9 | * Rename folder to qbittorrent_rclone and copy it to your qBitorrent download folder. 10 | 11 | ![image](https://user-images.githubusercontent.com/12091003/91241948-c9f58e80-e74e-11ea-8b49-4bfd85018c19.png) 12 | 13 | * folder path will be like this according to the example. 14 | 15 | ``` 16 | D:\Downloads\Torrent\qbittorrent_rclone 17 | ``` 18 | 19 | * Now go to Options => download This check: 20 | - [x] Run external program on torrent completion 21 | * write this in the command field: 22 | ``` 23 | python "%D/qbittorrent_rclone/main.py" -t "%N" -c "%F" -r "%R" -s "%D" 24 | ``` 25 | ![image](https://user-images.githubusercontent.com/12091003/91515996-1a0d5600-e8f3-11ea-85c8-79f8d4837491.png) 26 | 27 | * Put your service accounts json files in accounts folder. 28 | * Put Shared Drive (Team Drive) id in settings.ini. 29 | * Put Google Drive folder id (The folder id you want to upload your files) in settings.ini file. 30 | * Select what command used by rclone from ``move`` or ``copy`` or ``sync`` (Default is ``move``). 31 | * Write rclone path (System PATH used by default) 32 | 33 | You are ready to go. 34 | 35 | 36 | ## About patterns 37 | This script can scan torrent name and then create a specific directory to save in Google drive, but it is still very limited so it is disabled by default. -------------------------------------------------------------------------------- /accounts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abu3safeer/qbittorrent_rclone/b6f3d48d29c3e5d8cdb7c8bd737793ae346832ba/accounts/.gitkeep -------------------------------------------------------------------------------- /configs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abu3safeer/qbittorrent_rclone/b6f3d48d29c3e5d8cdb7c8bd737793ae346832ba/configs/.gitkeep -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abu3safeer/qbittorrent_rclone/b6f3d48d29c3e5d8cdb7c8bd737793ae346832ba/logs/.gitkeep -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import argparse 3 | import subprocess 4 | from pathlib import Path 5 | import configparser 6 | import re 7 | 8 | # Get current script path 9 | script_path = Path(__file__).parent 10 | 11 | # Prepare logging 12 | log = logging.getLogger('qbittorrent_rclone') 13 | log.setLevel(1) 14 | log_format = logging.Formatter('%(asctime)s: %(message)s') 15 | stream_log = logging.StreamHandler() 16 | stream_log.setFormatter(log_format) 17 | file_log = logging.FileHandler(f'{script_path}/log.txt', encoding='utf-8') 18 | file_log.setFormatter(log_format) 19 | log.addHandler(stream_log) 20 | log.addHandler(file_log) 21 | 22 | # Prepare arguments receiver 23 | pars = argparse.ArgumentParser() 24 | pars.add_argument('--torrent_name', '-t', help='Torrent name') 25 | pars.add_argument('--content_path', '-c', help='Content path (same as root path for mutilfile torrent)') 26 | pars.add_argument('--root_path', '-r', help='Root path (first torrent subdirectory path)') 27 | pars.add_argument('--save_path', '-s', help='Save path') 28 | pars = pars.parse_args() 29 | 30 | if not (pars.torrent_name or pars.content_path or pars.root_path or pars.save_path): 31 | log.critical('One argument at least missing, please follow instructions here https://github.com/Abu3safeer/qbittorrent_rclone') 32 | exit() 33 | 34 | # Define variables 35 | torrent_name = pars.torrent_name # type: str 36 | content_path = pars.content_path # type: str 37 | root_path = pars.root_path # type: str 38 | save_path = pars.save_path # type: str 39 | 40 | # Prepare directories paths 41 | accounts_path = Path(f'{script_path}/accounts') 42 | configs_path = Path(f'{script_path}/configs') 43 | logs_path = Path(f'{script_path}/logs') 44 | settings_path = Path(f'{script_path}/settings.ini') 45 | logs_file_path = Path(f'{logs_path}/{torrent_name}.txt') 46 | 47 | # Check configs and logs directory 48 | if not configs_path.exists(): 49 | configs_path.mkdir() 50 | if not logs_path.exists(): 51 | logs_path.mkdir() 52 | 53 | # Change file handler name to Torrent name 54 | log.removeHandler(file_log) 55 | file_log = logging.FileHandler(logs_file_path, encoding='utf-8') 56 | file_log.setFormatter(log_format) 57 | log.addHandler(file_log) 58 | 59 | # prepare settings file 60 | if not (settings_path.is_file() and settings_path.exists()): 61 | log.critical('Cannot find file settings.ini, please download it from https://github.com/Abu3safeer/qbittorrent_rclone') 62 | exit() 63 | 64 | # Prepare settings 65 | settings = configparser.ConfigParser() 66 | settings.read(settings_path.resolve()) 67 | 68 | 69 | # Check for missing settings 70 | if not ( 71 | settings.has_option('main', 'rclone_path') and 72 | settings.has_option('main', 'google_drive_folder_id') and 73 | settings.has_option('main', 'team_drive') and 74 | settings.has_option('main', 'command') and 75 | settings.has_option('internal', 'sa_count') and 76 | settings.has_option('internal', 'current_sa') 77 | ): 78 | log.critical('Settings.ini file is currupted, please download it new new one from https://github.com/Abu3safeer/qbittorrent_rclone') 79 | exit() 80 | 81 | # Load settings to variables 82 | rclone_path = settings.get('main', 'rclone_path') 83 | 84 | # Check rclone if uses system PATH or full path 85 | if rclone_path == 'PATH': 86 | rclone_path = 'rclone' 87 | 88 | command = settings.get('main', 'command') 89 | allowed_commands = ['move', 'copy', 'sync'] 90 | if command not in allowed_commands: 91 | log.info(f'Cannot find command "{command}" in {allowed_commands}, set to default "move"') 92 | command = 'move' 93 | settings.set('main', 'command', command) 94 | 95 | google_drive_folder_id = settings.get('main', 'google_drive_folder_id') 96 | if len(google_drive_folder_id) < 1: 97 | log.critical('google_drive_folder_id is empty in setting.ini, cannot proceed') 98 | exit() 99 | 100 | team_drive = settings.get('main', 'team_drive') 101 | if len(team_drive) < 1: 102 | log.critical('team_drive is empty in setting.ini, cannot proceed') 103 | exit() 104 | 105 | sa_count = settings.get('internal', 'sa_count') 106 | current_sa = Path(settings.get('internal', 'current_sa')) 107 | 108 | # Check if accounts directory exists 109 | if not (accounts_path.is_dir() and accounts_path.exists()): 110 | accounts_path.mkdir() 111 | log.critical('Cannot find accounts folder,put service accounts inside it.') 112 | exit() 113 | 114 | # Check if accounts folder has service accounts as json 115 | if not accounts_path.rglob('*.json'): 116 | log.critical('accounts folder is empty.') 117 | exit() 118 | 119 | # Get all service account files 120 | service_accounts = [account for account in accounts_path.rglob('*.json')] 121 | 122 | # Count service accounts 123 | if len(service_accounts) < 1: 124 | log.critical('No service accounts found in accounts folder.') 125 | exit() 126 | 127 | sa_count = len(service_accounts) 128 | 129 | # Get next service account to avoid hitting 750GB limit 130 | for index, sa in enumerate(service_accounts, 1): 131 | if current_sa == sa: 132 | if sa_count == index: 133 | current_sa = service_accounts[0] 134 | else: 135 | current_sa = service_accounts[index] 136 | break 137 | 138 | # If no server account selected from above step, then select first service account found 139 | if not (current_sa.is_file() and current_sa.exists()): 140 | log.info('No service account selected before, setting first one') 141 | current_sa = service_accounts[0] 142 | 143 | # Update settings 144 | settings.set('internal', 'sa_count', str(sa_count)) 145 | settings.set('internal', 'current_sa', current_sa.__str__()) 146 | 147 | # Save settings 148 | settings_file = open(settings_path, 'w') 149 | settings.write(settings_file) 150 | settings_file.close() 151 | 152 | # Create config file for selected service account 153 | config_file_path = Path(f'{configs_path}/{current_sa.name}.conf') 154 | log.info(f'Config file created: {config_file_path}') 155 | rclone_config = configparser.ConfigParser() 156 | rclone_config['qbittorrent_rclone'] = { 157 | 'type': 'drive', 158 | 'scope': 'drive', 159 | 'team_drive': team_drive, 160 | 'root_folder_id': google_drive_folder_id, 161 | 'service_account_file': current_sa.__str__() 162 | } 163 | 164 | # Save config file for selected service account 165 | config_file = open(config_file_path, 'w') 166 | rclone_config.write(config_file) 167 | config_file.close() 168 | 169 | log.info(f"Processing file: {torrent_name}") 170 | 171 | # Here you can parse files and do what ever you want 172 | drive_save_path = '' 173 | 174 | # Start scanning patterns 175 | try: 176 | if settings.get('main', 'patterns') == 'yes': 177 | 178 | # Check if the torrent is an erai release 179 | if torrent_name.startswith('[Erai-raws]'): 180 | 181 | # Save to Erai folder 182 | drive_save_path += 'Erai-raws/' 183 | log.info('It is an Erai-raws release') 184 | 185 | # Erai pattern 186 | erai_pattern = re.compile(r'^\[Erai-raws\] (.+) - [\d ~-]+', re.MULTILINE) 187 | find = re.findall(erai_pattern, torrent_name) 188 | 189 | # If pattern found 190 | if find: 191 | # Create sub folder for the show 192 | drive_save_path += find[0] 193 | 194 | # if the pattern does not match 195 | else: 196 | if Path(content_path).is_dir(): 197 | drive_save_path += torrent_name 198 | 199 | # Check if the torrent is BDMV 200 | elif torrent_name.count('BDMV') or torrent_name.count('bdmv'): 201 | drive_save_path += '[BMDV]/' 202 | 203 | # Check if torrent has language code 204 | if torrent_name.count('AU'): 205 | drive_save_path += '[AU]' 206 | if torrent_name.count('FRA'): 207 | drive_save_path += '[FRA]' 208 | if torrent_name.count('GER'): 209 | drive_save_path += '[GER]' 210 | if torrent_name.count('ITA'): 211 | drive_save_path += '[ITA]' 212 | if torrent_name.count('JP'): 213 | drive_save_path += '[JP]' 214 | if torrent_name.count('UK'): 215 | drive_save_path += '[UK]' 216 | if torrent_name.count('US'): 217 | drive_save_path += '[US]' 218 | 219 | drive_save_path += '/' 220 | 221 | if Path(content_path).is_dir(): 222 | drive_save_path += torrent_name 223 | 224 | # Check if the torrent is REMUX 225 | elif torrent_name.count('REMUX'): 226 | drive_save_path += '[REMUX]/' 227 | 228 | # Check if torrent has language code 229 | if torrent_name.count('AU'): 230 | drive_save_path += '[AU]' 231 | if torrent_name.count('FRA'): 232 | drive_save_path += '[FRA]' 233 | if torrent_name.count('GER'): 234 | drive_save_path += '[GER]' 235 | if torrent_name.count('ITA'): 236 | drive_save_path += '[ITA]' 237 | if torrent_name.count('JP'): 238 | drive_save_path += '[JP]' 239 | if torrent_name.count('UK'): 240 | drive_save_path += '[UK]' 241 | if torrent_name.count('US'): 242 | drive_save_path += '[US]' 243 | 244 | drive_save_path += '/' 245 | 246 | if Path(content_path).is_dir(): 247 | drive_save_path += torrent_name 248 | 249 | # If no pattern found 250 | else: 251 | if Path(content_path).is_dir(): 252 | drive_save_path = torrent_name 253 | 254 | else: 255 | if Path(content_path).is_dir(): 256 | drive_save_path = torrent_name 257 | 258 | except Exception as excep: 259 | log.critical(excep) 260 | 261 | 262 | # Log path directory 263 | log.info('Current save drive path is: ' + drive_save_path) 264 | 265 | try: 266 | # Prepare the command 267 | popen_args = [ 268 | f'{rclone_path}', 269 | f'{command}', 270 | f'{content_path}', 271 | f'qbittorrent_rclone:{drive_save_path}', 272 | '--config', 273 | f'{config_file_path}', 274 | '--log-level', 275 | 'DEBUG', 276 | f'--log-file={logs_file_path}' 277 | ] 278 | log.critical(popen_args) 279 | subprocess.Popen(popen_args) 280 | except Exception as excep: 281 | log.critical(excep) 282 | 283 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | rclone_path = PATH 3 | team_drive = SHARED_TEAM_DRIVE_HERE 4 | google_drive_folder_id = ID_HERE 5 | command = move 6 | patterns = no 7 | 8 | [internal] 9 | sa_count = 10 | current_sa = 11 | 12 | --------------------------------------------------------------------------------