├── yts_scraper ├── __init__.py ├── main.py └── scraper.py ├── .gitignore ├── Gif.gif ├── setup.py ├── LICENSE └── README.md /yts_scraper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | test/ 4 | YTS_Scraper.egg-info/ -------------------------------------------------------------------------------- /Gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozencb/yts-scraper/HEAD/Gif.gif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( name='YTS Scraper', 4 | author='Ozencb', 5 | version='0.1.1', 6 | description='A command-line tool to for downloading .torrent files from YTS', 7 | packages=find_packages(), 8 | install_requires=['requests', 'argparse', 'tqdm', 'fake-useragent'], 9 | entry_points={'console_scripts': 'yts-scraper = yts_scraper.main:main'}, 10 | license=open('LICENSE').read(), 11 | keywords=['yts', 'yify', 'scraper', 'media', 'download', 'downloader', 'torrent'] 12 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Özenç Bilgili 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ Project Status: No Longer Maintained ⚠️ 2 | 3 | This project is no longer actively maintained. 4 | Issues and pull requests will not be reviewed, and no further updates are planned. 5 | 6 | You are welcome to fork the repository and continue development independently. 7 | 8 | Thank you to everyone who has used, contributed to, or supported this project. 9 | 10 | --- 11 | 12 | [![GitHub Issues](https://img.shields.io/github/issues/Ozencb/yts-scraper)](https://github.com/Ozencb/yts-scraper/issues) 13 | [![Stars](https://img.shields.io/github/stars/Ozencb/yts-scraper)](https://github.com/Ozencb/yts-scraper) 14 | [![Forks](https://img.shields.io/github/forks/Ozencb/yts-scraper)](https://github.com/Ozencb/yts-scraper) 15 | [![MIT](https://img.shields.io/github/license/Ozencb/yts-scraper)](../master/LICENSE) 16 | 17 | # YTS Scraper 18 | 19 | ![](Gif.gif) 20 | 21 | ## Description 22 | **yts-scraper** is a command-line tool for downloading .torrent files from YTS. 23 | It requires Python 3.0+. 24 | Note that this tool does not download the contents of a torrent file but downloads files with .torrent extension. 25 | You should use a Torrent client to open these files. 26 | 27 | ## Installation 28 | Make sure that setuptools is installed on your system before running setup. 29 | 30 | Linux: 31 | `sudo apt-get install python3-setuptools` 32 | 33 | Windows: 34 | `pip install setuptools` 35 | 36 | Then you can run `python setup.py install` to install YTS-Scraper on your system. 37 | 38 | ## Usage 39 | To start scraping run: 40 | 41 | `yts-scraper [OPTIONS]` 42 | 43 | 44 | For instance, this command downloads every 1080p sci-fi movie and their posters with an IMDb score of 8 or higher, and store them in rating>genre structured subdirectories. 45 | 46 | `yts-scraper -q 1080p -g sci-fi -r 8 -c rating-genre -b` 47 | 48 | ## Options 49 | 50 | | Commands | Description | 51 | |---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| 52 | |`-h` or `--help` |Prints help text. Also prints out all the available optional arguments. | 53 | |`-o` or `--output` |Output directory | 54 | |`-b` or `--background` |Append "-b" to download movie posters. This will pack .torrent file and the image together in a folder. | 55 | |`-m` or `--multiprocess` |Append -m to download using multiprocessor. This option makes the process significantly faster but is prone to raising flags and causing server to deny requests. | 56 | |`--csv-only` |Append --csv-only to log scraped data ONLY to a CSV file. With this argument torrent files will not be downloaded. | 57 | |`-i` or `--imdb-id` |Append -i to append IMDb ID to filename. | 58 | |`-q` or `--quality` |Video quality. Available options are: "all", "720p", "1080p", "3d" | 59 | |`-g` or `--genre` |Movie genre. Available options are: "all", "action", "adventure", "animation", "biography", "comedy", "crime", "documentary", "drama", "family", "fantasy", "film-noir", "game-show", "history", "horror", "music", "musical", "mystery", "news", "reality-tv", "romance", "sci-fi", "sport", "talk-show", "thriller", "war", "western".| 60 | |`-r` or `--rating` |Minimum rating score. Enter an integer between 0 and 9. | 61 | |`-s` or `--sort-by` |Download order. Available options are: "title", "year", "rating", "latest", "peers", "seeds", "download_count", "like_count", "date_added" | 62 | |`-c` or `--categorize-by` |Creates a folder structure. Available options are: "rating", "genre", "rating-genre", "genre-rating" | 63 | |`-y` or `--year-limit` |Filters out movies older than the given value. | 64 | |`-p` or `--page` |Can be used to skip ahead an amount of pages. | 65 | 66 | ## Disclaimer 67 | This is a proof of concept tool built mainly to practice programming. 68 | The tool downloads thousands of torrent files in bulk and some of these torrent files might be leading to copyrighted material. 69 | Although the downloaded files are not the contents themselves, accessing or storing these files might still be illegal in some parts of the world. So, take great care when using this tool and make sure that it is legal. 70 | -------------------------------------------------------------------------------- /yts_scraper/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import traceback 3 | from yts_scraper.scraper import Scraper 4 | 5 | 6 | def main(): 7 | desc = 'A command-line tool to for downloading .torrent files from YTS' 8 | 9 | 10 | parser = argparse.ArgumentParser(description=desc) 11 | parser.add_argument('-o', '--output', 12 | help='Output Directory', 13 | dest='output', 14 | type=str.lower, 15 | required=False) 16 | 17 | parser.add_argument('-q', '--quality', 18 | help='''Movie Quality. 19 | Valid arguments are: "all", "720p", "1080p", "3d" 20 | ''', 21 | dest='quality', 22 | type=str.lower, 23 | required=False, 24 | choices=['all', '720p', '1080p', '2160p', '3d'], 25 | default='1080p', 26 | const='1080p', 27 | nargs='?') 28 | 29 | parser.add_argument('-g', '--genre', 30 | help='''Movie Genre. 31 | Valid arguments are: "all", "action", "adventure", 32 | "animation", "biography", "comedy", 33 | "crime", "documentary", "drama", "family", 34 | "fantasy", "film-noir", "game-show", "history", 35 | "horror", "music", "musical", "mystery", "news", 36 | "reality-tv", "romance", "sci-fi", "sport", 37 | "talk-show", "thriller", "war", "western" 38 | ''', 39 | dest='genre', 40 | type=str.lower, 41 | required=False, 42 | choices=['all', 'action', 'adventure', 'animation', 'biography', 43 | 'comedy', 'crime', 'documentary', 'drama', 'family', 44 | 'fantasy', 'film-noir', 'game-show', 'history', 'horror', 45 | 'music', 'musical', 'mystery', 'news', 'reality-tv', 'romance', 46 | 'sci-fi', 'sport', 'talk-show', 'thriller', 'war', 'western'], 47 | default='all', 48 | const='all', 49 | nargs='?') 50 | 51 | parser.add_argument('-r', '--rating', 52 | help='Minimum rating score. Integer between 0-10', 53 | dest='rating', 54 | type=str.lower, 55 | required=False, 56 | choices=['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], 57 | default='0', 58 | const='0', 59 | nargs='?') 60 | 61 | parser.add_argument('-s', '--sort-by', 62 | help='''Sorting options. 63 | Valid arguments are: "title", "year", "rating", 64 | "latest", "peers", "seeds", "download_count", 65 | "like_count", "date_added" 66 | ''', 67 | dest='sort_by', 68 | type=str.lower, 69 | required=False, 70 | choices=['title', 'year', 'rating', 'latest', 'peers', 71 | 'seeds', 'download_count', 'like_count', 'date_added'], 72 | default='latest', 73 | const='latest', 74 | nargs='?') 75 | 76 | parser.add_argument('-c', '--categorize-by', 77 | help='''Creates a folder structure. 78 | Valid arguments are: "rating", "genre", 79 | "rating-genre","genre-rating 80 | ''', 81 | dest='categorize_by', 82 | type=str.lower, 83 | required=False, 84 | choices=['none', 'rating', 'genre', 'genre-rating', 'rating-genre'], 85 | default='rating', 86 | const='rating', 87 | nargs='?') 88 | 89 | parser.add_argument('-y', '--year-limit', 90 | help='Only downloads movies newer than given year.', 91 | dest='year_limit', 92 | type=int, 93 | required=False, 94 | default='0', 95 | const='0', 96 | nargs='?') 97 | 98 | parser.add_argument('-b', '--background', 99 | help='''Append -b to download movie posters. 100 | This will pack .torrent file and the image together in a folder. 101 | ''', 102 | dest='background', 103 | type=bool, 104 | required=False, 105 | default=False, 106 | const=True, 107 | nargs='?') 108 | 109 | parser.add_argument('-i', '--imdb-id', 110 | help='Append -i to append IMDb ID to filename.', 111 | dest='imdb_id', 112 | type=bool, 113 | required=False, 114 | default=False, 115 | const=True, 116 | nargs='?') 117 | 118 | parser.add_argument('-m', '--multiprocess', 119 | help='''Append -m to download using multiprocessor. 120 | This option makes the process significantly faster 121 | but is prone to raising flags and causing server to deny requests. 122 | ''', 123 | dest='multiprocess', 124 | type=bool, 125 | required=False, 126 | default=False, 127 | const=True, 128 | nargs='?') 129 | 130 | parser.add_argument('--csv-only', 131 | help='''Append --csv-only to log scraped data ONLY to a CSV file. 132 | With this argument torrent files will not be downloaded''', 133 | dest='csv_only', 134 | type=bool, 135 | required=False, 136 | default=False, 137 | const=True, 138 | nargs='?') 139 | 140 | parser.add_argument('-p', '--page', 141 | help='Enter an integer to skip ahead number of pages', 142 | dest='page', 143 | type=int, 144 | required=False, 145 | default=1, 146 | const=1, 147 | nargs='?') 148 | 149 | 150 | try: 151 | args = parser.parse_args() 152 | scraper = Scraper(args) 153 | scraper.download() 154 | 155 | except KeyboardInterrupt: 156 | print('\nKeypress Detected. Exiting...\n') 157 | except Exception: 158 | traceback.print_exc() 159 | exit(0) 160 | 161 | if __name__ == '__main__': 162 | main() 163 | -------------------------------------------------------------------------------- /yts_scraper/scraper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import math 4 | import json 5 | import csv 6 | from concurrent.futures.thread import ThreadPoolExecutor 7 | import requests 8 | from tqdm import tqdm 9 | from fake_useragent import UserAgent 10 | 11 | class Scraper: 12 | """ 13 | Scraper class. 14 | 15 | Must be initialized with args from argparser 16 | """ 17 | # Constructor 18 | def __init__(self, args): 19 | self.output = args.output 20 | self.genre = args.genre 21 | self.minimum_rating = args.rating 22 | self.quality = '3D' if (args.quality == '3d') else args.quality 23 | self.categorize = args.categorize_by 24 | self.sort_by = args.sort_by 25 | self.year_limit = args.year_limit 26 | self.page_arg = args.page 27 | self.poster = args.background 28 | self.imdb_id = args.imdb_id 29 | self.multiprocess = args.multiprocess 30 | self.csv_only = args.csv_only 31 | 32 | self.movie_count = None 33 | self.url = None 34 | self.existing_file_counter = None 35 | self.skip_exit_condition = None 36 | self.downloaded_movie_ids = None 37 | self.pbar = None 38 | 39 | 40 | # Set output directory 41 | if args.output: 42 | if not args.csv_only: 43 | os.makedirs(self.output, exist_ok=True) 44 | self.directory = os.path.join(os.path.curdir, self.output) 45 | else: 46 | if not args.csv_only: 47 | os.makedirs(self.categorize.title(), exist_ok=True) 48 | self.directory = os.path.join(os.path.curdir, self.categorize.title()) 49 | 50 | 51 | # Args for downloading in reverse chronological order 52 | if args.sort_by == 'latest': 53 | self.sort_by = 'date_added' 54 | self.order_by = 'desc' 55 | else: 56 | self.order_by = 'asc' 57 | 58 | 59 | # YTS API has a limit of 50 entries 60 | self.limit = 50 61 | 62 | 63 | # Connect to API and extract initial data 64 | def __get_api_data(self): 65 | # Formatted URL string 66 | url = '''https://yts.mx/api/v2/list_movies.json?quality={quality}&genre={genre}&minimum_rating={minimum_rating}&sort_by={sort_by}&order_by={order_by}&limit={limit}&page='''.format( 67 | quality=self.quality, 68 | genre=self.genre, 69 | minimum_rating=self.minimum_rating, 70 | sort_by=self.sort_by, 71 | order_by=self.order_by, 72 | limit=self.limit 73 | ) 74 | 75 | # Generate random user agent header 76 | try: 77 | user_agent = UserAgent() 78 | headers = {'User-Agent': user_agent.random} 79 | except: 80 | print('Error occurred during fake user agent generation.') 81 | 82 | # Exception handling for connection errors 83 | try: 84 | req = requests.get(url, timeout=5, verify=True, headers=headers) 85 | req.raise_for_status() 86 | except requests.exceptions.HTTPError as errh: 87 | print('HTTP Error:', errh) 88 | sys.exit(0) 89 | except requests.exceptions.ConnectionError as errc: 90 | print('Error Connecting:', errc) 91 | sys.exit(0) 92 | except requests.exceptions.Timeout as errt: 93 | print('Timeout Error:', errt) 94 | sys.exit(0) 95 | except requests.exceptions.RequestException as err: 96 | print('There was an error.', err) 97 | sys.exit(0) 98 | 99 | # Exception handling for JSON decoding errors 100 | try: 101 | data = req.json() 102 | except json.decoder.JSONDecodeError: 103 | print('Could not decode JSON') 104 | 105 | 106 | # Adjust movie count according to starting page 107 | if self.page_arg == 1: 108 | movie_count = data.get('data').get('movie_count') 109 | else: 110 | movie_count = (data.get('data').get('movie_count')) - ((self.page_arg - 1) * self.limit) 111 | 112 | self.movie_count = movie_count 113 | self.url = url 114 | 115 | def __initialize_download(self): 116 | # Used for exit/continue prompt that's triggered after 10 existing files 117 | self.existing_file_counter = 0 118 | self.skip_exit_condition = False 119 | 120 | # YTS API sometimes returns duplicate objects and 121 | # the script tries to download the movie more than once. 122 | # IDs of downloaded movie is stored in this array 123 | # to check if it's been downloaded before 124 | self.downloaded_movie_ids = [] 125 | 126 | # Calculate page count and make sure that it doesn't 127 | # get the value of 1 to prevent range(1, 1) 128 | if math.trunc(self.movie_count / self.limit) + 1 == 1: 129 | page_count = 2 130 | else: 131 | page_count = math.trunc(self.movie_count / self.limit) + 1 132 | 133 | range_ = range(int(self.page_arg), page_count) 134 | 135 | 136 | print('Initializing download with these parameters:\n') 137 | print('Directory:\t{}\nQuality:\t{}\nMovie Genre:\t{}\nMinimum Rating:\t{}\nCategorization:\t{}\nMinimum Year:\t{}\nStarting page:\t{}\nMovie posters:\t{}\nAppend IMDb ID:\t{}\nMultiprocess:\t{}\n' 138 | .format( 139 | self.directory, 140 | self.quality, 141 | self.genre, 142 | self.minimum_rating, 143 | self.categorize, 144 | self.year_limit, 145 | self.page_arg, 146 | str(self.poster), 147 | str(self.imdb_id), 148 | str(self.multiprocess) 149 | ) 150 | ) 151 | 152 | if self.movie_count <= 0: 153 | print('Could not find any movies with given parameters') 154 | sys.exit(0) 155 | else: 156 | print('Query was successful.') 157 | print('Found {} movies. Download starting...\n'.format(self.movie_count)) 158 | 159 | # Create progress bar 160 | self.pbar = tqdm( 161 | total=self.movie_count, 162 | position=0, 163 | leave=True, 164 | desc='Downloading', 165 | unit='Files' 166 | ) 167 | 168 | # Multiprocess executor 169 | # Setting max_workers to None makes executor utilize CPU number * 5 at most 170 | executor = ThreadPoolExecutor(max_workers=None) 171 | 172 | for page in range_: 173 | url = '{}{}'.format(self.url, str(page)) 174 | 175 | # Generate random user agent header 176 | try: 177 | user_agent = UserAgent() 178 | headers = {'User-Agent': user_agent.random} 179 | except: 180 | print('Error occurred during fake user agent generation.') 181 | 182 | # Send request to API 183 | page_response = requests.get(url, timeout=5, verify=True, headers=headers).json() 184 | 185 | movies = page_response.get('data').get('movies') 186 | 187 | # Movies found on current page 188 | if not movies: 189 | print('Could not find any movies on this page.\n') 190 | 191 | if self.multiprocess: 192 | # Wrap tqdm around executor to update pbar with every process 193 | tqdm( 194 | executor.map(self.__filter_torrents, movies), 195 | total=self.movie_count, 196 | position=0, 197 | leave=True 198 | ) 199 | 200 | else: 201 | for movie in movies: 202 | self.__filter_torrents(movie) 203 | 204 | self.pbar.close() 205 | print('Download finished.') 206 | 207 | 208 | # Determine which .torrent files to download 209 | def __filter_torrents(self, movie): 210 | movie_id = str(movie.get('id')) 211 | movie_rating = movie.get('rating') 212 | movie_genres = movie.get('genres') if movie.get('genres') else ['None'] 213 | movie_name_short = movie.get('title') 214 | imdb_id = movie.get('imdb_code') 215 | year = movie.get('year') 216 | language = movie.get('language') 217 | yts_url = movie.get('url') 218 | 219 | if year < self.year_limit: 220 | return 221 | 222 | 223 | 224 | # Every torrent option for current movie 225 | torrents = movie.get('torrents') 226 | # Remove illegal file/directory characters 227 | movie_name = movie.get('title_long').translate({ord(i):None for i in "'/\:*?<>|"}) 228 | 229 | # Used to multiple download messages for multi-folder categorization 230 | is_download_successful = False 231 | 232 | if movie_id in self.downloaded_movie_ids: 233 | return 234 | 235 | # In case movie has no available torrents 236 | if torrents is None: 237 | tqdm.write('Could not find any torrents for {}. Skipping...'.format(movie_name)) 238 | return 239 | 240 | bin_content_img = (requests.get(movie.get('large_cover_image'))).content if self.poster else None 241 | 242 | # Iterate through available torrent files 243 | for torrent in torrents: 244 | quality = torrent.get('quality') 245 | torrent_url = torrent.get('url') 246 | if self.categorize and self.categorize != 'rating': 247 | if self.quality == 'all' or self.quality == quality: 248 | bin_content_tor = (requests.get(torrent.get('url'))).content 249 | 250 | for genre in movie_genres: 251 | path = self.__build_path(movie_name, movie_rating, quality, genre, imdb_id) 252 | is_download_successful = self.__download_file(bin_content_tor, bin_content_img, path, movie_name, movie_id) 253 | else: 254 | if self.quality == 'all' or self.quality == quality: 255 | self.__log_csv(movie_id, imdb_id, movie_name_short, year, language, movie_rating, quality, yts_url, torrent_url) 256 | bin_content_tor = (requests.get(torrent_url)).content 257 | path = self.__build_path(movie_name, movie_rating, quality, None, imdb_id) 258 | is_download_successful = self.__download_file(bin_content_tor, bin_content_img, path, movie_name, movie_id) 259 | 260 | if is_download_successful and self.quality == 'all' or self.quality == quality: 261 | tqdm.write('Downloaded {} {}'.format(movie_name, quality.upper())) 262 | self.pbar.update() 263 | 264 | 265 | # Creates a file path for each download 266 | def __build_path(self, movie_name, rating, quality, movie_genre, imdb_id): 267 | if self.csv_only: 268 | return 269 | 270 | directory = self.directory 271 | 272 | if self.categorize == 'rating': 273 | directory += '/' + str(math.trunc(rating)) + '+' 274 | elif self.categorize == 'genre': 275 | directory += '/' + str(movie_genre) 276 | elif self.categorize == 'rating-genre': 277 | directory += '/' + str(math.trunc(rating)) + '+/' + movie_genre 278 | elif self.categorize == 'genre-rating': 279 | directory += '/' + str(movie_genre) + '/' + str(math.trunc(rating)) + '+' 280 | 281 | if self.poster: 282 | directory += '/' + movie_name 283 | 284 | os.makedirs(directory, exist_ok=True) 285 | 286 | if self.imdb_id: 287 | filename = '{} {} - {}'.format(movie_name, quality, imdb_id) 288 | else: 289 | filename = '{} {}'.format(movie_name, quality) 290 | 291 | path = os.path.join(directory, filename) 292 | return path 293 | 294 | # Write binary content to .torrent file 295 | def __download_file(self, bin_content_tor, bin_content_img, path, movie_name, movie_id): 296 | if self.csv_only: 297 | return 298 | 299 | if self.existing_file_counter > 10 and not self.skip_exit_condition: 300 | self.__prompt_existing_files() 301 | 302 | if os.path.isfile(path): 303 | tqdm.write('{}: File already exists. Skipping...'.format(movie_name)) 304 | self.existing_file_counter += 1 305 | return False 306 | 307 | with open(path + '.torrent', 'wb') as torrent: 308 | torrent.write(bin_content_tor) 309 | if self.poster: 310 | with open(path + '.jpg', 'wb') as torrent: 311 | torrent.write(bin_content_img) 312 | 313 | self.downloaded_movie_ids.append(movie_id) 314 | self.existing_file_counter = 0 315 | return True 316 | 317 | def __log_csv(self, id, imdb_id, name, year, language, rating, quality, yts_url, torrent_url): 318 | path = os.path.join(os.path.curdir, 'YTS-Scraper.csv') 319 | csv_exists = os.path.isfile(path) 320 | 321 | with open(path, mode='a') as csv_file: 322 | headers = ['YTS ID', 'IMDb ID', 'Movie Title', 'Year', 'Language', 'Rating', 'Quality', 'YTS URL', 'IMDb URL', 'Torrent URL'] 323 | writer = csv.DictWriter(csv_file, delimiter=',', lineterminator='\n', quotechar='"', quoting=csv.QUOTE_ALL, fieldnames=headers) 324 | 325 | if not csv_exists: 326 | writer.writeheader() 327 | 328 | writer.writerow({'YTS ID': id, 329 | 'IMDb ID': imdb_id, 330 | 'Movie Title': name, 331 | 'Year': year, 332 | 'Language': language, 333 | 'Rating': rating, 334 | 'Quality': quality, 335 | 'YTS URL': yts_url, 336 | 'IMDb URL': 'https://www.imdb.com/title/' + imdb_id, 337 | 'Torrent URL': torrent_url 338 | }) 339 | 340 | 341 | 342 | # Is triggered when the script hits 10 consecutive existing files 343 | def __prompt_existing_files(self): 344 | tqdm.write('Found 10 existing files in a row. Do you want to keep downloading? Y/N') 345 | exit_answer = input() 346 | 347 | if exit_answer.lower() == 'n': 348 | tqdm.write('Exiting...') 349 | sys.exit(0) 350 | elif exit_answer.lower() == 'y': 351 | tqdm.write('Continuing...') 352 | self.existing_file_counter = 0 353 | self.skip_exit_condition = True 354 | else: 355 | tqdm.write('Invalid input. Enter "Y" or "N".') 356 | 357 | def download(self): 358 | self.__get_api_data() 359 | self.__initialize_download() 360 | --------------------------------------------------------------------------------