From 19e47425dca56290ac64ba157d234ea4847f4479 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 00:49:28 +1000 Subject: [PATCH 01/33] Auto search trackers for ID and description Don't rely only on PTP or require torrent ID, auto search instead. No idea if I broke the manual arguments, will check that later. HDB with is_disc working for ID. BLU searching is working, but needs to be updated to search file list in the API instead. Also waiting for a PR to be approved so that we can search folder names. --- data/example-config.py | 11 +- src/prep.py | 242 ++++++++++++++++++++--------------------- src/trackers/BLU.py | 2 +- src/trackers/COMMON.py | 11 +- src/trackers/HDB.py | 56 +++++++--- upload.py | 4 +- 6 files changed, 174 insertions(+), 152 deletions(-) diff --git a/data/example-config.py b/data/example-config.py index 5013ab63d..703614322 100644 --- a/data/example-config.py +++ b/data/example-config.py @@ -36,9 +36,9 @@ "TRACKERS" : { # Which trackers do you want to upload to? - # Available tracker: BLU, BHD, AITHER, STC, STT, SN, THR, R4E, HP, ACM, PTP, LCD, LST, PTER, NBL, ANT, MTV, CBR, RTF, HUNO, BHDTV, LT, PTER, TL, TDC, HDT, OE, RF, OTW, FNP, UTP, AL + # Available tracker: BLU, BHD, AITHER, STC, STT, SN, THR, R4E, HP, ACM, PTP, LCD, LST, PTER, NBL, ANT, MTV, CBR, RTF, HUNO, BHDTV, LT, PTER, TL, TDC, HDT, OE, RF, OTW, FNP, UTP, AL, HDB # Remove the ones not used to save being asked everytime - "default_trackers" : "BLU, BHD, AITHER, STC, STT, SN, THR, R4E, HP, ACM, PTP, LCD, LST, PTER, NBL, ANT, MTV, CBR, RTF, HUNO, BHDTV, LT, PTER, TL, TDC, HDT, OE, RF, OTW, FNP, UTP, AL", + "default_trackers" : "BLU, BHD, AITHER, STC, STT, SN, THR, R4E, HP, ACM, PTP, LCD, LST, PTER, NBL, ANT, MTV, CBR, RTF, HUNO, BHDTV, LT, PTER, TL, TDC, HDT, OE, RF, OTW, FNP, UTP, AL, HDB", "BLU" : { "useAPI" : False, # Set to True if using BLU @@ -219,6 +219,13 @@ "announce_url" : "https://animelovers.club/announce/customannounceurl", # "anon" : False }, + "HDB" : { + "useAPI" : True, + "username" : "HDB username", + "passkey" : "HDB passkey", + "announce_url" : "https://hdbits.org/announce/Custom_Announce_URL", + "anon" : False, + }, "MANUAL" : { # Uncomment and replace link with filebrowser (https://github.com/filebrowser/filebrowser) link to the Upload-Assistant directory, this will link to your filebrowser instead of uploading to uguu.se # "filebrowser" : "https://domain.tld/filebrowser/files/Upload-Assistant/" diff --git a/src/prep.py b/src/prep.py index 26566927f..c06a8b4b5 100644 --- a/src/prep.py +++ b/src/prep.py @@ -73,6 +73,67 @@ def __init__(self, screens, img_host, config): self.img_host = img_host.lower() tmdb.API_KEY = config['DEFAULT']['tmdb_api'] + async def update_metadata_from_tracker(self, tracker_name, tracker_instance, meta, search_term, search_file_folder): + tracker_key = tracker_name.lower() + manual_key = f"{tracker_key}_manual" + found_match = False + + console.print(f"[cyan]Attempting to search {tracker_name} with search_term: {search_term}[/cyan]") + + if meta.get(tracker_key) is not None: + meta[manual_key] = meta[tracker_key] + console.print(f"[cyan]{tracker_name} ID found in meta, reusing existing ID: {meta[tracker_key]}[/cyan]") + if tracker_name == "BLU": + blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, meta[tracker_key]) + # Check if we got valid data + if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: + console.print(f"[green]Valid data found on {tracker_name}, setting meta values[/green]") + if blu_tmdb not in [None, '0']: + meta['tmdb_manual'] = blu_tmdb + if blu_imdb not in [None, '0']: + meta['imdb'] = str(blu_imdb) + if blu_tvdb not in [None, '0']: + meta['tvdb_id'] = blu_tvdb + if blu_mal not in [None, '0']: + meta['mal'] = blu_mal + if blu_desc not in [None, '0', '']: + meta['blu_desc'] = blu_desc + if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: + meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() + if meta.get('image_list', []) == []: + meta['image_list'] = blu_imagelist + found_match = True # Set flag if any relevant data is found + else: + console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") + else: + meta['imdb'], meta['ext_torrenthash'] = await tracker_instance.get_imdb_from_torrent_id(meta[tracker_key]) + if meta['imdb']: + console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}[/green]") + found_match = True + else: + console.print(f"[yellow]No IMDb ID found on {tracker_name}[/yellow]") + else: + console.print(f"[cyan]Searching {tracker_name} using search_term: {search_term}[/cyan]") + if tracker_name == "PTP": + imdb, tracker_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) + elif tracker_name == "HDB": + console.print(f"[cyan]HDB search using folder/file: {search_term}[/cyan]") + # Pass search_term as a string, not a list + imdb, tvdb_id, hdb_name, meta['ext_torrenthash'], tracker_id = await tracker_instance.search_filename(search_term, search_file_folder) + + meta['tvdb_id'] = str(tvdb_id) if tvdb_id else meta.get('tvdb_id') + meta['hdb_name'] = hdb_name + else: + imdb = tracker_id = None + + if imdb: + console.print(f"[green]{tracker_name} IMDb ID found: {imdb}[/green]") + meta['imdb'] = str(imdb) + found_match = True + if tracker_id: + meta[tracker_key] = tracker_id + + return meta, found_match async def gather_prep(self, meta, mode): meta['mode'] = mode @@ -80,7 +141,7 @@ async def gather_prep(self, meta, mode): meta['isdir'] = os.path.isdir(meta['path']) base_dir = meta['base_dir'] - if meta.get('uuid', None) == None: + if meta.get('uuid', None) is None: folder_id = os.path.basename(meta['path']) meta['uuid'] = folder_id if not os.path.exists(f"{base_dir}/tmp/{meta['uuid']}"): @@ -89,181 +150,138 @@ async def gather_prep(self, meta, mode): if meta['debug']: console.print(f"[cyan]ID: {meta['uuid']}") - meta['is_disc'], videoloc, bdinfo, meta['discs'] = await self.get_disc(meta) - - # If BD: + + # Debugging information + console.print(f"Debug: meta['filelist'] before population: {meta.get('filelist', 'Not Set')}") + if meta['is_disc'] == "BDMV": video, meta['scene'], meta['imdb'] = self.is_scene(meta['path'], meta.get('imdb', None)) - meta['filelist'] = [] + meta['filelist'] = [] # No filelist for discs, use path + search_term = os.path.basename(meta['path']) + search_file_folder = 'folder' try: - guess_name = bdinfo['title'].replace('-',' ') - filename = guessit(re.sub(r"[^0-9a-zA-Z\[\\]]+", " ", guess_name), {"excludes" : ["country", "language"]})['title'] + guess_name = bdinfo['title'].replace('-', ' ') + filename = guessit(re.sub(r"[^0-9a-zA-Z\[\\]]+", " ", guess_name), {"excludes": ["country", "language"]})['title'] untouched_filename = bdinfo['title'] try: meta['search_year'] = guessit(bdinfo['title'])['year'] except Exception: meta['search_year'] = "" except Exception: - guess_name = bdinfo['label'].replace('-',' ') - filename = guessit(re.sub(r"[^0-9a-zA-Z\[\\]]+", " ", guess_name), {"excludes" : ["country", "language"]})['title'] + guess_name = bdinfo['label'].replace('-', ' ') + filename = guessit(re.sub(r"[^0-9a-zA-Z\[\\]]+", " ", guess_name), {"excludes": ["country", "language"]})['title'] untouched_filename = bdinfo['label'] try: meta['search_year'] = guessit(bdinfo['label'])['year'] except Exception: meta['search_year'] = "" - if meta.get('resolution', None) == None: + if meta.get('resolution', None) is None: meta['resolution'] = self.mi_resolution(bdinfo['video'][0]['res'], guessit(video), width="OTHER", scan="p", height="OTHER", actual_height=0) - # if meta.get('sd', None) == None: meta['sd'] = self.is_sd(meta['resolution']) mi = None mi_dump = None - #IF DVD + elif meta['is_disc'] == "DVD": video, meta['scene'], meta['imdb'] = self.is_scene(meta['path'], meta.get('imdb', None)) meta['filelist'] = [] - guess_name = meta['discs'][0]['path'].replace('-',' ') - # filename = guessit(re.sub("[^0-9a-zA-Z]+", " ", guess_name))['title'] - filename = guessit(guess_name, {"excludes" : ["country", "language"]})['title'] + search_term = os.path.basename(meta['path']) + search_file_folder = 'folder' + guess_name = meta['discs'][0]['path'].replace('-', ' ') + filename = guessit(guess_name, {"excludes": ["country", "language"]})['title'] untouched_filename = os.path.basename(os.path.dirname(meta['discs'][0]['path'])) try: meta['search_year'] = guessit(meta['discs'][0]['path'])['year'] except Exception: meta['search_year'] = "" - if meta.get('edit', False) == False: + if not meta.get('edit', False): mi = self.exportInfo(f"{meta['discs'][0]['path']}/VTS_{meta['discs'][0]['main_set'][0][:2]}_1.VOB", False, meta['uuid'], meta['base_dir'], export_text=False) meta['mediainfo'] = mi else: mi = meta['mediainfo'] - #NTSC/PAL meta['dvd_size'] = await self.get_dvd_size(meta['discs']) meta['resolution'] = self.get_resolution(guessit(video), meta['uuid'], base_dir) meta['sd'] = self.is_sd(meta['resolution']) + elif meta['is_disc'] == "HDDVD": video, meta['scene'], meta['imdb'] = self.is_scene(meta['path'], meta.get('imdb', None)) meta['filelist'] = [] - guess_name = meta['discs'][0]['path'].replace('-','') - filename = guessit(guess_name, {"excludes" : ["country", "language"]})['title'] + search_term = os.path.basename(meta['path']) + search_file_folder = 'folder' + guess_name = meta['discs'][0]['path'].replace('-', '') + filename = guessit(guess_name, {"excludes": ["country", "language"]})['title'] untouched_filename = os.path.basename(meta['discs'][0]['path']) videopath = meta['discs'][0]['largest_evo'] try: meta['search_year'] = guessit(meta['discs'][0]['path'])['year'] except Exception: meta['search_year'] = "" - if meta.get('edit', False) == False: + if not meta.get('edit', False): mi = self.exportInfo(meta['discs'][0]['largest_evo'], False, meta['uuid'], meta['base_dir'], export_text=False) meta['mediainfo'] = mi else: mi = meta['mediainfo'] meta['resolution'] = self.get_resolution(guessit(video), meta['uuid'], base_dir) meta['sd'] = self.is_sd(meta['resolution']) - #If NOT BD/DVD/HDDVD + else: - videopath, meta['filelist'] = self.get_video(videoloc, meta.get('mode', 'discord')) + videopath, meta['filelist'] = self.get_video(videoloc, meta.get('mode', 'discord')) + search_term = os.path.basename(meta['filelist'][0]) if meta['filelist'] else None + search_file_folder = 'file' video, meta['scene'], meta['imdb'] = self.is_scene(videopath, meta.get('imdb', None)) - guess_name = ntpath.basename(video).replace('-',' ') - filename = guessit(re.sub(r"[^0-9a-zA-Z\[\\]]+", " ", guess_name), {"excludes" : ["country", "language"]}).get("title", guessit(re.sub("[^0-9a-zA-Z]+", " ", guess_name), {"excludes" : ["country", "language"]})["title"]) + guess_name = ntpath.basename(video).replace('-', ' ') + filename = guessit(re.sub(r"[^0-9a-zA-Z\[\\]]+", " ", guess_name), {"excludes": ["country", "language"]}).get("title", guessit(re.sub("[^0-9a-zA-Z]+", " ", guess_name), {"excludes": ["country", "language"]})["title"]) untouched_filename = os.path.basename(video) try: meta['search_year'] = guessit(video)['year'] except Exception: meta['search_year'] = "" - if meta.get('edit', False) == False: + if not meta.get('edit', False): mi = self.exportInfo(videopath, meta['isdir'], meta['uuid'], base_dir, export_text=True) meta['mediainfo'] = mi else: mi = meta['mediainfo'] - if meta.get('resolution', None) == None: + if meta.get('resolution', None) is None: meta['resolution'] = self.get_resolution(guessit(video), meta['uuid'], base_dir) - # if meta.get('sd', None) == None: meta['sd'] = self.is_sd(meta['resolution']) - - - - if " AKA " in filename.replace('.',' '): + + if " AKA " in filename.replace('.', ' '): filename = filename.split('AKA')[0] meta['filename'] = filename meta['bdinfo'] = bdinfo + # Debugging information after population + console.print(f"Debug: meta['filelist'] after population: {meta.get('filelist', 'Not Set')}") - - - - # Reuse information from other trackers - if str(self.config['TRACKERS'].get('PTP', {}).get('useAPI')).lower() == "true": - ptp = PTP(config=self.config) - if meta.get('ptp', None) != None: - meta['ptp_manual'] = meta['ptp'] - meta['imdb'], meta['ext_torrenthash'] = await ptp.get_imdb_from_torrent_id(meta['ptp']) - else: - if meta['is_disc'] in [None, ""]: - ptp_search_term = os.path.basename(meta['filelist'][0]) - search_file_folder = 'file' - else: - search_file_folder = 'folder' - ptp_search_term = os.path.basename(meta['path']) - ptp_imdb, ptp_id, meta['ext_torrenthash'] = await ptp.get_ptp_id_imdb(ptp_search_term, search_file_folder) - if ptp_imdb != None: - meta['imdb'] = ptp_imdb - if ptp_id != None: - meta['ptp'] = ptp_id - - if str(self.config['TRACKERS'].get('HDB', {}).get('useAPI')).lower() == "true": - hdb = HDB(config=self.config) - if meta.get('ptp', None) == None or meta.get('hdb', None) != None: - hdb_imdb = hdb_tvdb = hdb_id = None - hdb_id = meta.get('hdb') - if hdb_id != None: - meta['hdb_manual'] = hdb_id - hdb_imdb, hdb_tvdb, meta['hdb_name'], meta['ext_torrenthash'] = await hdb.get_info_from_torrent_id(hdb_id) - else: - if meta['is_disc'] in [None, ""]: - hdb_imdb, hdb_tvdb, meta['hdb_name'], meta['ext_torrenthash'], hdb_id = await hdb.search_filename(meta['filelist']) - else: - # Somehow search for disc - pass - if hdb_imdb != None: - meta['imdb'] = str(hdb_imdb) - if hdb_tvdb != None: - meta['tvdb_id'] = str(hdb_tvdb) - if hdb_id != None: - meta['hdb'] = hdb_id - - if str(self.config['TRACKERS'].get('BLU', {}).get('useAPI')).lower() == "true": - blu = BLU(config=self.config) - if meta.get('blu', None) != None: - meta['blu_manual'] = meta['blu'] - blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist = await COMMON(self.config).unit3d_torrent_info("BLU", blu.torrent_url, meta['blu']) - if blu_tmdb not in [None, '0']: - meta['tmdb_manual'] = blu_tmdb - if blu_imdb not in [None, '0']: - meta['imdb'] = str(blu_imdb) - if blu_tvdb not in [None, '0']: - meta['tvdb_id'] = blu_tvdb - if blu_mal not in [None, '0']: - meta['mal'] = blu_mal - if blu_desc not in [None, '0', '']: - meta['blu_desc'] = blu_desc - if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: - if blu_category.upper() == 'TV SHOW': - meta['category'] = 'TV' - else: - meta['category'] = blu_category.upper() - if meta.get('image_list', []) == []: - meta['image_list'] = blu_imagelist - else: - # Seach automatically - pass - - - - + # Reuse information from trackers with fallback + if search_term: # Ensure there's a valid search term + found_match = False + + if str(self.config['TRACKERS'].get('PTP', {}).get('useAPI')).lower() == "true": + ptp = PTP(config=self.config) + console.print(f"[cyan]Attempting to search PTP with search_term: {search_term}[/cyan]") + meta, found_match = await self.update_metadata_from_tracker('PTP', ptp, meta, search_term, search_file_folder) + + if not found_match and str(self.config['TRACKERS'].get('HDB', {}).get('useAPI')).lower() == "true": + console.print(f"[cyan]Attempting to search HDB with search_term: {search_term}[/cyan]") + hdb = HDB(config=self.config) + meta, found_match = await self.update_metadata_from_tracker('HDB', hdb, meta, search_term, search_file_folder) + + if not found_match and str(self.config['TRACKERS'].get('BLU', {}).get('useAPI')).lower() == "true": + console.print(f"[cyan]Attempting to search BLU with search_term: {search_term}[/cyan]") + blu = BLU(config=self.config) + meta, found_match = await self.update_metadata_from_tracker('BLU', blu, meta, search_term, search_file_folder) + + if not found_match: + console.print("[yellow]No matches found on any trackers.[/yellow]") + else: + console.print("[yellow]Warning: No valid search term available, skipping tracker updates.[/yellow]") # Take Screenshots if meta['is_disc'] == "BDMV": @@ -298,9 +316,6 @@ async def gather_prep(self, meta, mode): except KeyboardInterrupt: s.terminate() - - - meta['tmdb'] = meta.get('tmdb_manual', None) if meta.get('type', None) == None: meta['type'] = self.get_type(video, meta['scene'], meta['is_disc']) @@ -363,22 +378,14 @@ async def gather_prep(self, meta, mode): meta['repack'] = re.search(r"REPACK[\d]?", meta['edition'])[0] meta['edition'] = re.sub(r"REPACK[\d]?", "", meta['edition']).strip().replace(' ', ' ') - - #WORK ON THIS meta.get('stream', False) meta['stream'] = self.stream_optimized(meta['stream']) meta.get('anon', False) meta['anon'] = self.is_anon(meta['anon']) - - - meta = await self.gen_desc(meta) return meta - - - """ Determine if disc and if so, get bdinfo """ @@ -784,13 +791,6 @@ def is_scene(self, video, imdb=None): console.print("[yellow]SRRDB: No match found, or request has timed out") return video, scene, imdb - - - - - - - """ Generate Screenshots """ diff --git a/src/trackers/BLU.py b/src/trackers/BLU.py index 9f2757327..cf9d4a14d 100644 --- a/src/trackers/BLU.py +++ b/src/trackers/BLU.py @@ -214,4 +214,4 @@ async def search_existing(self, meta): console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) - return dupes + return dupes \ No newline at end of file diff --git a/src/trackers/COMMON.py b/src/trackers/COMMON.py index 87fc0ccfb..75b5689f1 100644 --- a/src/trackers/COMMON.py +++ b/src/trackers/COMMON.py @@ -31,8 +31,7 @@ async def add_tracker_torrent(self, meta, tracker, source_flag, new_tracker, com new_torrent.metainfo['comment'] = comment new_torrent.metainfo['info']['source'] = source_flag Torrent.copy(new_torrent).write(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}]{meta['clean_name']}.torrent", overwrite=True) - - + async def unit3d_edit_desc(self, meta, tracker, signature, comparison=False, desc_header=""): base = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'r', encoding='utf8').read() with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}]DESCRIPTION.txt", 'w', encoding='utf8') as descfile: @@ -79,10 +78,7 @@ async def unit3d_edit_desc(self, meta, tracker, signature, comparison=False, des descfile.write(signature) descfile.close() return - - - async def unit3d_region_ids(self, region): region_id = { 'AFG': 1, 'AIA': 2, 'ALA': 3, 'ALG': 4, 'AND': 5, 'ANG': 6, 'ARG': 7, 'ARM': 8, 'ARU': 9, @@ -151,6 +147,7 @@ async def unit3d_torrent_info(self, tracker, torrent_url, id): params = {'api_token' : self.config['TRACKERS'][tracker].get('api_key', '')} url = f"{torrent_url}{id}" response = requests.get(url=url, params=params) + console.print(f"[green]Searching {tracker} for: [bold yellow]{filename}[/bold yellow]") try: response = response.json() attributes = response['attributes'] @@ -185,8 +182,6 @@ async def parseCookieFile(self, cookiefile): cookies[lineFields[5]] = lineFields[6] return cookies - - async def ptgen(self, meta, ptgen_site="", ptgen_retry=3): ptgen = "" url = 'https://ptgen.zhenzhen.workers.dev' @@ -338,4 +333,4 @@ async def filter_dupes(self, dupes, meta): allow = False if allow and each not in new_dupes: new_dupes.append(each) - return new_dupes + return new_dupes \ No newline at end of file diff --git a/src/trackers/HDB.py b/src/trackers/HDB.py index c8530261f..893ae9a25 100644 --- a/src/trackers/HDB.py +++ b/src/trackers/HDB.py @@ -26,7 +26,6 @@ def __init__(self, config): self.signature = None self.banned_groups = [""] - async def get_type_category_id(self, meta): cat_id = "EXIT" # 6 = Audio Track @@ -512,33 +511,54 @@ async def get_info_from_torrent_id(self, hdb_id): console.print("Failed to get info from HDB ID. Either the site is down or your credentials are invalid") return hdb_imdb, hdb_tvdb, hdb_name, hdb_torrenthash - async def search_filename(self, filelist): + async def search_filename(self, search_term, search_file_folder): hdb_imdb = hdb_tvdb = hdb_name = hdb_torrenthash = hdb_id = None url = "https://hdbits.org/api/torrents" - data = { - "username" : self.username, - "passkey" : self.passkey, - "limit" : 100, - "file_in_torrent" : os.path.basename(filelist[0]) - } + + if search_file_folder == 'folder': # Handling disc case + data = { + "username": self.username, + "passkey": self.passkey, + "limit": 100, + "folder_in_torrent": os.path.basename(search_term) # Using folder name for search + } + console.print(f"[green]Searching HDB for folder: [bold yellow]{os.path.basename(search_term)}[/bold yellow]") + else: # Handling non-disc case + data = { + "username": self.username, + "passkey": self.passkey, + "limit": 100, + "file_in_torrent": os.path.basename(search_term) # Using filename for search + } + console.print(f"[green]Searching HDB for file: [bold yellow]{os.path.basename(search_term)}[/bold yellow]") + response = requests.get(url, json=data) - console.print(f"[green]Searching HDB for: [bold yellow]{os.path.basename(filelist[0])}[/bold yellow]") + if response.ok: try: - response = response.json() - if response['data'] != []: - for each in response['data']: - if each['numfiles'] == len(filelist): - hdb_imdb = each.get('imdb', {'id' : None}).get('id') - hdb_tvdb = each.get('tvdb', {'id' : None}).get('id') + response_json = response.json() + # console.print(f"[green]HDB API response: {response_json}[/green]") # Log the entire response for debugging + + # Check if 'data' key is present + if 'data' not in response_json: + console.print(f"[red]Error: 'data' key not found in HDB API response. Full response: {response_json}[/red]") + return hdb_imdb, hdb_tvdb, hdb_name, hdb_torrenthash, hdb_id + + if response_json['data'] != []: + for each in response_json['data']: + if search_file_folder == 'folder' or each['numfiles'] == len(search_term): # Handle folder or filelist match + hdb_imdb = each.get('imdb', {'id': None}).get('id') + hdb_tvdb = each.get('tvdb', {'id': None}).get('id') hdb_name = each['name'] hdb_torrenthash = each['hash'] hdb_id = each['id'] console.print(f'[bold green]Matched release with HDB ID: [yellow]{hdb_id}[/yellow][/bold green]') return hdb_imdb, hdb_tvdb, hdb_name, hdb_torrenthash, hdb_id - except: + except Exception as e: console.print_exception() + console.print(f"[red]Failed to parse HDB API response. Error: {str(e)}[/red]") else: - console.print("Failed to get info from HDB ID. Either the site is down or your credentials are invalid") - console.print(f'[yellow]Could not find a matching release on HDB') + console.print(f"[red]Failed to get info from HDB. Status code: {response.status_code}, Reason: {response.reason}[/red]") + + console.print(f'[yellow]Could not find a matching release on HDB[/yellow]') return hdb_imdb, hdb_tvdb, hdb_name, hdb_torrenthash, hdb_id \ No newline at end of file diff --git a/upload.py b/upload.py index fc33d083b..848f7f811 100644 --- a/upload.py +++ b/upload.py @@ -248,8 +248,8 @@ async def do_the_thing(base_dir): ####### Upload to Trackers ####### #################################### common = COMMON(config=config) - api_trackers = ['BLU', 'AITHER', 'STC', 'R4E', 'STT', 'RF', 'ACM','LCD','LST','HUNO', 'SN', 'LT', 'NBL', 'ANT', 'JPTV', 'TDC', 'OE', 'BHDTV', 'RTF', 'OTW', 'FNP', 'CBR', 'UTP', 'AL'] - http_trackers = ['HDB', 'TTG', 'FL', 'PTER', 'HDT', 'MTV'] + api_trackers = ['BLU', 'AITHER', 'STC', 'R4E', 'STT', 'RF', 'ACM','LCD','LST','HUNO', 'SN', 'LT', 'NBL', 'ANT', 'JPTV', 'TDC', 'OE', 'BHDTV', 'RTF', 'OTW', 'FNP', 'CBR', 'UTP', 'AL', 'HDB'] + http_trackers = ['TTG', 'FL', 'PTER', 'HDT', 'MTV'] tracker_class_map = { 'BLU' : BLU, 'BHD': BHD, 'AITHER' : AITHER, 'STC' : STC, 'R4E' : R4E, 'THR' : THR, 'STT' : STT, 'HP' : HP, 'PTP' : PTP, 'RF' : RF, 'SN' : SN, 'ACM' : ACM, 'HDB' : HDB, 'LCD': LCD, 'TTG' : TTG, 'LST' : LST, 'HUNO': HUNO, 'FL' : FL, 'LT' : LT, 'NBL' : NBL, 'ANT' : ANT, 'PTER': PTER, 'JPTV' : JPTV, From 6e21868bad72e1b48da8d996d0ae1896d0a43b7e Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 00:51:34 +1000 Subject: [PATCH 02/33] Update docker build for this branch --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index d96e23a6c..557a516bb 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -5,7 +5,7 @@ on: branches: - master - develop - - anime-integers + - ID-and-Description env: REGISTRY: ghcr.io From ff1d09d574109b8fee321f2d1093ccfb5ba6cf6a Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 00:59:09 +1000 Subject: [PATCH 03/33] Add linter --- .github/workflows/lint.yaml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/lint.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 000000000..f0f37dbd9 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,31 @@ +name: Lint + +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + + - name: Run linter + run: flake8 . \ No newline at end of file From 7ab744a0d108e824555039651eed690c085de816 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 01:03:23 +1000 Subject: [PATCH 04/33] Stop linter complaining 1 first check --- cogs/commands.py | 91 +----------------------------------------------- 1 file changed, 1 insertion(+), 90 deletions(-) diff --git a/cogs/commands.py b/cogs/commands.py index 7a92faa64..dc1138aa2 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -23,13 +23,10 @@ from glob import glob import argparse - - class Commands(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.Cog.listener() async def on_guild_join(self, guild): """ @@ -87,7 +84,6 @@ async def upload(self, ctx, path, *args, message_id=0, search_args=tuple()): else: await ctx.send("Invalid Path") - @commands.command() async def args(self, ctx): f""" @@ -103,56 +99,6 @@ async def args(self, ctx): await ctx.send(f"```{help[1991:]}```") else: await ctx.send(help.format_help()) - # await ctx.send(""" - # ```Optional arguments: - - # -s, --screens [SCREENS] - # Number of screenshots - # -c, --category [{movie,tv,fanres}] - # Category - # -t, --type [{disc,remux,encode,webdl,web-dl,webrip,hdtv}] - # Type - # -res, --resolution - # [{2160p,1080p,1080i,720p,576p,576i,480p,480i,8640p,4320p,other}] - # Resolution - # -tmdb, --tmdb [TMDB] - # TMDb ID - # -g, --tag [TAG] - # Group Tag - # -serv, --service [SERVICE] - # Streaming Service - # -edition, --edition [EDITION] - # Edition - # -d, --desc [DESC] - # Custom Description (string) - # -nfo, --nfo - # Use .nfo in directory for description - # -k, --keywords [KEYWORDS] - # Add comma seperated keywords e.g. 'keyword, keyword2, etc' - # -reg, --region [REGION] - # Region for discs - # -a, --anon Upload anonymously - # -st, --stream Stream Optimized Upload - # -debug, --debug Debug Mode```""") - - - # @commands.group(invoke_without_command=True) - # async def foo(self, ctx): - # """ - # check out my subcommands! - # """ - # await ctx.send('check out my subcommands!') - - # @foo.command(aliases=['an_alias']) - # async def bar(self, ctx): - # """ - # I have an alias!, I also belong to command 'foo' - # """ - # await ctx.send('foo bar!') - - - - @commands.command() async def edit(self, ctx, uuid=None, *args): @@ -187,11 +133,6 @@ async def edit(self, ctx, uuid=None, *args): meta['name_notag'], meta['name'], meta['clean_name'], meta['potential_missing'] = await prep.get_name(meta) await self.send_embed_and_upload(ctx, meta) - - - - - @commands.group(invoke_without_command=True) async def search(self, ctx, *, args=None): """ @@ -250,8 +191,6 @@ def check(reaction, user): else: await self.upload(ctx, files_total[0], search_args=tuple(args.split(" ")), message_id=message.id) - - @search.command() async def dir(self, ctx, *, args=None): """ @@ -311,14 +250,7 @@ def check(reaction, user): await self.upload(ctx, path=folders_total[0], search_args=tuple(args.split(" ")), message_id=message.id) # await ctx.send(folders_total) return - - - - - - - - + async def send_embed_and_upload(self,ctx,meta): prep = Prep(screens=meta['screens'], img_host=meta['imghost'], config=config) meta['name_notag'], meta['name'], meta['clean_name'], meta['potential_missing'] = await prep.get_name(meta) @@ -368,7 +300,6 @@ async def send_embed_and_upload(self,ctx,meta): else: meta['client'] = 'none' - #Format for embed if meta['tag'] == "": tag = "" @@ -464,17 +395,6 @@ def check(reaction, user): await msg.clear_reactions() await msg.edit(embed=cancel_embed) return - # except ManualException: - # msg = await ctx.fetch_message(meta['embed_msg_id']) - # await msg.clear_reactions() - # archive_url = await prep.package(meta) - # if archive_url == False: - # archive_fail_embed = discord.Embed(title="Unable to upload prep files", description=f"The files can be found at `tmp/{meta['title']}.tar`", color=0xff0000) - # await msg.edit(embed=archive_fail_embed) - # else: - # archive_embed = discord.Embed(title="Files can be found at:",description=f"{archive_url} or `tmp/{meta['title']}.tar`", color=0x00ff40) - # await msg.edit(embed=archive_embed) - # return else: #Check which are selected and upload to them @@ -494,9 +414,6 @@ def check(reaction, user): await msg.edit(embed=upload_embed) await msg.clear_reactions() - - - client = Clients(config=config) if "MANUAL" in tracker_list: for manual_tracker in tracker_list: @@ -591,8 +508,6 @@ def check(reaction, user): await msg.edit(embed=upload_embed) return None - - async def dupe_embed(self, dupes, meta, emojis, channel): if not dupes: print("No dupes found") @@ -651,10 +566,6 @@ async def get_missing(self, meta): def setup(bot): bot.add_cog(Commands(bot)) - - - - class CancelException(Exception): pass From d46bbdbbf7db4c5a1381230bacc8a211c0d9a4a3 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 01:09:11 +1000 Subject: [PATCH 05/33] Stop linter complaining 2 --- .github/workflows/.flake8 | 2 ++ cogs/commands.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/.flake8 diff --git a/.github/workflows/.flake8 b/.github/workflows/.flake8 new file mode 100644 index 000000000..905c8bbe4 --- /dev/null +++ b/.github/workflows/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 160 \ No newline at end of file diff --git a/cogs/commands.py b/cogs/commands.py index dc1138aa2..fb3cbe917 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -17,12 +17,12 @@ from datetime import datetime import asyncio import json -import shutil import multiprocessing from pathlib import Path from glob import glob import argparse + class Commands(commands.Cog): def __init__(self, bot): self.bot = bot From 54919c6c236582993697b2ffd6afa9bf68841f66 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 01:11:05 +1000 Subject: [PATCH 06/33] Move the flake8 config file --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..905c8bbe4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 160 \ No newline at end of file From 73946c4b39d1774cd27dff2137322d701fb6bf49 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 01:32:38 +1000 Subject: [PATCH 07/33] Stop linter complaining 3 --- cogs/commands.py | 76 +++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/cogs/commands.py b/cogs/commands.py index fb3cbe917..ffc7af74f 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -43,7 +43,7 @@ async def upload(self, ctx, path, *args, message_id=0, search_args=tuple()): return parser = Args(config) - if path == None: + if path is None: await ctx.send("Missing Path") return elif path.lower() == "-h": @@ -58,17 +58,17 @@ async def upload(self, ctx, path, *args, message_id=0, search_args=tuple()): try: args = (meta['path'],) + args + search_args meta, help, before_args = parser.parse(args, meta) - except SystemExit as error: + except SystemExit: await ctx.send(f"Invalid argument detected, use `{config['DISCORD']['command_prefix']}args` for list of valid args") return - if meta['imghost'] == None: + if meta['imghost'] is None: meta['imghost'] = config['DEFAULT']['img_host_1'] # if not meta['unattended']: # ua = config['DEFAULT'].get('auto_mode', False) # if str(ua).lower() == "true": # meta['unattended'] = True prep = Prep(screens=meta['screens'], img_host=meta['imghost'], config=config) - preparing_embed = discord.Embed(title=f"Preparing to upload:", description=f"```{path}```", color=0xffff00) + preparing_embed = discord.Embed(title="Preparing to upload:", description=f"```{path}```", color=0xffff00) if message_id == 0: message = await ctx.send(embed=preparing_embed) meta['embed_msg_id'] = message.id @@ -99,7 +99,7 @@ async def args(self, ctx): await ctx.send(f"```{help[1991:]}```") else: await ctx.send(help.format_help()) - + @commands.command() async def edit(self, ctx, uuid=None, *args): """ @@ -107,7 +107,7 @@ async def edit(self, ctx, uuid=None, *args): """ if ctx.channel.id != int(config['DISCORD']['discord_channel_id']): return - if uuid == None: + if uuid is None: await ctx.send("Missing ID, please try again using the ID in the footer") parser = Args(config) base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) @@ -118,7 +118,7 @@ async def edit(self, ctx, uuid=None, *args): except FileNotFoundError: await ctx.send("ID not found, please try again using the ID in the footer") return - prep = Prep(screens=meta['screens'], img_host=meta['imghost'], config=config) + prep = Prep(screens=meta['screens'], img_host=meta['imghost'], config=config) try: args = (meta['path'],) + args meta, help, before_args = parser.parse(args, meta) @@ -129,7 +129,7 @@ async def edit(self, ctx, uuid=None, *args): new_msg = await msg.channel.send(f"Editing {meta['uuid']}") meta['embed_msg_id'] = new_msg.id meta['edit'] = True - meta = await prep.gather_prep(meta=meta, mode="discord") + meta = await prep.gather_prep(meta=meta, mode="discord") meta['name_notag'], meta['name'], meta['clean_name'], meta['potential_missing'] = await prep.get_name(meta) await self.send_embed_and_upload(ctx, meta) @@ -147,14 +147,14 @@ async def search(self, ctx, *, args=None): args = args.replace(search_terms, '') while args.startswith(" "): args = args[1:] - except SystemExit as error: + except SystemExit: await ctx.send(f"Invalid argument detected, use `{config['DISCORD']['command_prefix']}args` for list of valid args") return if ctx.channel.id != int(config['DISCORD']['discord_channel_id']): return search = Search(config=config) - if search_terms == None: + if search_terms is None: await ctx.send("Missing search term(s)") return files_total = await search.searchFile(search_terms) @@ -175,14 +175,12 @@ async def search(self, ctx, *, args=None): message = await ctx.send(embed=embed) await message.add_reaction(config['DISCORD']['discord_emojis']['UPLOAD']) channel = message.channel - def check(reaction, user): if reaction.message.id == message.id: - if str(user.id) == config['DISCORD']['admin_id']: + if str(user.id) == config['DISCORD']['admin_id']: if str(reaction.emoji) == config['DISCORD']['discord_emojis']['UPLOAD']: return reaction - try: await self.bot.wait_for("reaction_add", timeout=120, check=check) @@ -205,14 +203,14 @@ async def dir(self, ctx, *, args=None): args = args.replace(search_terms, '') while args.startswith(" "): args = args[1:] - except SystemExit as error: + except SystemExit: await ctx.send(f"Invalid argument detected, use `{config['DISCORD']['command_prefix']}args` for list of valid args") return if ctx.channel.id != int(config['DISCORD']['discord_channel_id']): return search = Search(config=config) - if search_terms == None: + if search_terms is None: await ctx.send("Missing search term(s)") return folders_total = await search.searchFolder(search_terms) @@ -234,13 +232,11 @@ async def dir(self, ctx, *, args=None): await message.add_reaction(config['DISCORD']['discord_emojis']['UPLOAD']) channel = message.channel - def check(reaction, user): if reaction.message.id == message.id: - if str(user.id) == config['DISCORD']['admin_id']: + if str(user.id) == config['DISCORD']['admin_id']: if str(reaction.emoji) == config['DISCORD']['discord_emojis']['UPLOAD']: return reaction - try: await self.bot.wait_for("reaction_add", timeout=120, check=check) @@ -254,7 +250,7 @@ def check(reaction, user): async def send_embed_and_upload(self,ctx,meta): prep = Prep(screens=meta['screens'], img_host=meta['imghost'], config=config) meta['name_notag'], meta['name'], meta['clean_name'], meta['potential_missing'] = await prep.get_name(meta) - + if meta.get('uploaded_screens', False) == False: if meta.get('embed_msg_id', '0') != '0': message = await ctx.fetch_message(meta['embed_msg_id']) @@ -262,7 +258,7 @@ async def send_embed_and_upload(self,ctx,meta): else: message = await ctx.send(embed=discord.Embed(title="Uploading Screenshots", color=0xffff00)) meta['embed_msg_id'] = message.id - + channel = message.channel.id return_dict = multiprocessing.Manager().dict() u = multiprocessing.Process(target = prep.upload_screens, args=(meta, meta['screens'], 1, 0, meta['screens'], [], return_dict)) @@ -275,7 +271,7 @@ async def send_embed_and_upload(self,ctx,meta): meta['uploaded_screens'] = True #Create base .torrent - + if len(glob(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent")) == 0: if meta.get('embed_msg_id', '0') != '0': message = await ctx.fetch_message(int(meta['embed_msg_id'])) @@ -362,13 +358,13 @@ async def send_embed_and_upload(self,ctx,meta): await message.add_reaction(config['DISCORD']['discord_emojis']['UPLOAD']) #Save meta to json - with open (f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as f: + with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as f: json.dump(meta, f, indent=4) f.close() - + def check(reaction, user): if reaction.message.id == meta['embed_msg_id']: - if str(user.id) == config['DISCORD']['admin_id']: + if str(user.id) == config['DISCORD']['admin_id']: if str(reaction.emoji) == config['DISCORD']['discord_emojis']['UPLOAD']: return reaction if str(reaction.emoji) == config['DISCORD']['discord_emojis']['CANCEL']: @@ -396,7 +392,7 @@ def check(reaction, user): await msg.edit(embed=cancel_embed) return else: - + #Check which are selected and upload to them msg = await ctx.fetch_message(message.id) tracker_list = list() @@ -408,7 +404,7 @@ def check(reaction, user): tracker = list(config['DISCORD']['discord_emojis'].keys())[list(config['DISCORD']['discord_emojis'].values()).index(str(each))] if tracker not in ("UPLOAD"): tracker_list.append(tracker) - + upload_embed_description = ' / '.join(tracker_list) upload_embed = discord.Embed(title=f"Uploading `{meta['name']}` to:", description=upload_embed_description, color=0x00ff40) await msg.edit(embed=upload_embed) @@ -419,23 +415,23 @@ def check(reaction, user): for manual_tracker in tracker_list: manual_tracker = manual_tracker.replace(" ", "") if manual_tracker.upper() == "BLU": - blu = BLU(config=config) + blu = BLU(config=config) await blu.edit_desc(meta) if manual_tracker.upper() == "BHD": bhd = BHD(config=config) - await bhd.edit_desc(meta) + await bhd.edit_desc(meta) if manual_tracker.upper() == "AITHER": aither = AITHER(config=config) - await aither.edit_desc(meta) + await aither.edit_desc(meta) if manual_tracker.upper() == "STC": stc = STC(config=config) - await stc.edit_desc(meta) + await stc.edit_desc(meta) if manual_tracker.upper() == "LCD": lcd = LCD(config=config) await lcd.edit_desc(meta) if manual_tracker.upper() == "CBR": cbr = CBR(config=config) - await cbr.edit_desc(meta) + await cbr.edit_desc(meta) archive_url = await prep.package(meta) upload_embed_description = upload_embed_description.replace('MANUAL', '~~MANUAL~~') if archive_url == False: @@ -475,7 +471,7 @@ def check(reaction, user): await client.add_to_client(meta, "AITHER") upload_embed_description = upload_embed_description.replace('AITHER', '~~AITHER~~') upload_embed = discord.Embed(title=f"Uploaded `{meta['name']}` to:", description=upload_embed_description, color=0x00ff40) - await msg.edit(embed=upload_embed) + await msg.edit(embed=upload_embed) if "STC" in tracker_list: stc = STC(config=config) dupes = await stc.search_existing(meta) @@ -485,7 +481,7 @@ def check(reaction, user): await client.add_to_client(meta, "STC") upload_embed_description = upload_embed_description.replace('STC', '~~STC~~') upload_embed = discord.Embed(title=f"Uploaded `{meta['name']}` to:", description=upload_embed_description, color=0x00ff40) - await msg.edit(embed=upload_embed) + await msg.edit(embed=upload_embed) if "LCD" in tracker_list: lcd = LCD(config=config) dupes = await lcd.search_existing(meta) @@ -505,19 +501,19 @@ def check(reaction, user): await client.add_to_client(meta, "CBR") upload_embed_description = upload_embed_description.replace('CBR', '~~CBR~~') upload_embed = discord.Embed(title=f"Uploaded `{meta['name']}` to:", description=upload_embed_description, color=0x00ff40) - await msg.edit(embed=upload_embed) + await msg.edit(embed=upload_embed) return None - + async def dupe_embed(self, dupes, meta, emojis, channel): if not dupes: print("No dupes found") - meta['upload'] = True + meta['upload'] = True return meta else: dupe_text = "\n\n•".join(dupes) dupe_text = f"```•{dupe_text}```" embed = discord.Embed(title="Check if these are actually dupes!", description=dupe_text, color=0xff0000) - embed.set_footer(text=f"{emojis['CANCEL']} to abort upload | {emojis['UPLOAD']} to upload anyways") + embed.set_footer(text=f"{emojis['CANCEL']} to abort upload | {emojis['UPLOAD']} to upload anyways") message = await channel.send(embed=embed) await message.add_reaction(emojis['CANCEL']) await asyncio.sleep(0.3) @@ -525,7 +521,7 @@ async def dupe_embed(self, dupes, meta, emojis, channel): def check(reaction, user): if reaction.message.id == message.id: - if str(user.id) == config['DISCORD']['admin_id']: + if str(user.id) == config['DISCORD']['admin_id']: if str(reaction.emoji) == emojis['UPLOAD']: return reaction if str(reaction.emoji) == emojis['CANCEL']: @@ -559,7 +555,7 @@ async def get_missing(self, meta): missing.append('--imdb') if isinstance(meta['potential_missing'], list) and len(meta['potential_missing']) > 0: for each in meta['potential_missing']: - if meta.get(each, '').replace(' ', '') == "": + if meta.get(each, '').replace(' ', '') == "": missing.append(f"--{each}") return missing @@ -570,4 +566,4 @@ class CancelException(Exception): pass class ManualException(Exception): - pass + pass \ No newline at end of file From 1bab9ebafcf6c32d28a8191aff7e9ef10f433297 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 10:51:13 +1000 Subject: [PATCH 08/33] More code cleanup --- data/example-config.py | 337 ++++++++++++++++++++--------------------- src/prep.py | 31 +--- src/trackers/BHD.py | 42 ++--- src/trackers/HDB.py | 2 +- 4 files changed, 189 insertions(+), 223 deletions(-) diff --git a/data/example-config.py b/data/example-config.py index 703614322..1e2a23f5d 100644 --- a/data/example-config.py +++ b/data/example-config.py @@ -1,16 +1,16 @@ config = { - "DEFAULT" : { - + "DEFAULT": { + # ------ READ THIS ------ # Any lines starting with the # symbol are commented and will not be used. # If you change any of these options, remove the # # ----------------------- - "tmdb_api" : "tmdb_api key", - "imgbb_api" : "imgbb api key", - "ptpimg_api" : "ptpimg api key", - "lensdump_api" : "lensdump api key", - "ptscreens_api" : "ptscreens api key", + "tmdb_api": "tmdb_api key", + "imgbb_api": "imgbb api key", + "ptpimg_api": "ptpimg api key", + "lensdump_api": "lensdump api key", + "ptscreens_api": "ptscreens api key", # Order of image hosts, and backup image hosts "img_host_1": "imgbb", @@ -21,35 +21,35 @@ "img_host_6": "ptscreens", - "screens" : "6", + "screens": "6", # Enable lossless PNG Compression (True/False) - "optimize_images" : True, + "optimize_images": True, # The name of your default torrent client, set in the torrent client sections below - "default_torrent_client" : "Client1", + "default_torrent_client": "Client1", # Play the bell sound effect when asking for confirmation - "sfx_on_prompt" : True, + "sfx_on_prompt": True, }, - "TRACKERS" : { + "TRACKERS": { # Which trackers do you want to upload to? # Available tracker: BLU, BHD, AITHER, STC, STT, SN, THR, R4E, HP, ACM, PTP, LCD, LST, PTER, NBL, ANT, MTV, CBR, RTF, HUNO, BHDTV, LT, PTER, TL, TDC, HDT, OE, RF, OTW, FNP, UTP, AL, HDB # Remove the ones not used to save being asked everytime - "default_trackers" : "BLU, BHD, AITHER, STC, STT, SN, THR, R4E, HP, ACM, PTP, LCD, LST, PTER, NBL, ANT, MTV, CBR, RTF, HUNO, BHDTV, LT, PTER, TL, TDC, HDT, OE, RF, OTW, FNP, UTP, AL, HDB", + "default_trackers": "BLU, BHD, AITHER, STC, STT, SN, THR, R4E, HP, ACM, PTP, LCD, LST, PTER, NBL, ANT, MTV, CBR, RTF, HUNO, BHDTV, LT, PTER, TL, TDC, HDT, OE, RF, OTW, FNP, UTP, AL, HDB", - "BLU" : { - "useAPI" : False, # Set to True if using BLU - "api_key" : "BLU api key", - "announce_url" : "https://blutopia.cc/announce/customannounceurl", + "BLU": { + "useAPI": False, # Set to True if using BLU + "api_key": "BLU api key", + "announce_url": "https://blutopia.cc/announce/customannounceurl", # "anon" : False }, - "BHD" : { - "api_key" : "BHD api key", - "announce_url" : "https://beyond-hd.me/announce/customannounceurl", - "draft_default" : "True", + "BHD": { + "api_key": "BHD api key", + "announce_url": "https://beyond-hd.me/announce/customannounceurl", + "draft_default": "True", # "anon" : False }, "BHDTV": { @@ -59,174 +59,174 @@ "my_announce_url": "https://trackerr.bit-hdtv.com/passkey/announce", # "anon" : "False" }, - "PTP" : { - "useAPI" : False, # Set to True if using PTP - "add_web_source_to_desc" : True, - "ApiUser" : "ptp api user", - "ApiKey" : 'ptp api key', - "username" : "", - "password" : "", - "announce_url" : "" - }, - "AITHER" :{ - "api_key" : "AITHER api key", - "announce_url" : "https://aither.cc/announce/customannounceurl", + "PTP": { + "useAPI": False, # Set to True if using PTP + "add_web_source_to_desc": True, + "ApiUser": "ptp api user", + "ApiKey": 'ptp api key', + "username": "", + "password": "", + "announce_url": "" + }, + "AITHER": { + "api_key": "AITHER api key", + "announce_url": "https://aither.cc/announce/customannounceurl", # "anon" : False }, - "R4E" :{ - "api_key" : "R4E api key", - "announce_url" : "https://racing4everyone.eu/announce/customannounceurl", + "R4E": { + "api_key": "R4E api key", + "announce_url": "https://racing4everyone.eu/announce/customannounceurl", # "anon" : False }, - "HUNO" : { - "api_key" : "HUNO api key", - "announce_url" : "https://hawke.uno/announce/customannounceurl", + "HUNO": { + "api_key": "HUNO api key", + "announce_url": "https://hawke.uno/announce/customannounceurl", # "anon" : False }, "MTV": { - 'api_key' : 'get from security page', - 'username' : '', - 'password' : '', - 'announce_url' : "get from https://www.morethantv.me/upload.php", - 'anon' : False, + 'api_key': 'get from security page', + 'username': '', + 'password': '', + 'announce_url': "get from https://www.morethantv.me/upload.php", + 'anon': False, # 'otp_uri' : 'OTP URI, read the following for more information https://github.com/google/google-authenticator/wiki/Key-Uri-Format' }, - "STC" :{ - "api_key" : "STC", - "announce_url" : "https://skipthecommericals.xyz/announce/customannounceurl", + "STC": { + "api_key": "STC", + "announce_url": "https://skipthecommericals.xyz/announce/customannounceurl", # "anon" : False }, - "STT" :{ - "api_key" : "STC", - "announce_url" : "https://stt.xyz/announce/customannounceurl", + "STT": { + "api_key": "STC", + "announce_url": "https://stt.xyz/announce/customannounceurl", # "anon" : False }, "SN": { "api_key": "SN", "announce_url": "https://tracker.swarmazon.club:8443//announce", }, - "HP" :{ - "api_key" : "HP", - "announce_url" : "https://hidden-palace.net/announce/customannounceurl", + "HP": { + "api_key": "HP", + "announce_url": "https://hidden-palace.net/announce/customannounceurl", # "anon" : False }, - "ACM" :{ - "api_key" : "ACM api key", - "announce_url" : "https://asiancinema.me/announce/customannounceurl", + "ACM": { + "api_key": "ACM api key", + "announce_url": "https://asiancinema.me/announce/customannounceurl", # "anon" : False, # FOR INTERNAL USE ONLY: # "internal" : True, # "internal_groups" : ["What", "Internal", "Groups", "Are", "You", "In"], }, - "NBL" : { - "api_key" : "NBL api key", - "announce_url" : "https://nebulance.io/customannounceurl", + "NBL": { + "api_key": "NBL api key", + "announce_url": "https://nebulance.io/customannounceurl", }, - "ANT" :{ - "api_key" : "ANT api key", - "announce_url" : "https://anthelion.me/announce/customannounceurl", + "ANT": { + "api_key": "ANT api key", + "announce_url": "https://anthelion.me/announce/customannounceurl", # "anon" : False }, - "THR" : { - "username" : "username", - "password" : "password", - "img_api" : "get this from the forum post", - "announce_url" : "http://www.torrenthr.org/announce.php?passkey=yourpasskeyhere", - "pronfo_api_key" : "pronfo api key", - "pronfo_theme" : "pronfo theme code", - "pronfo_rapi_id" : "pronfo remote api id", + "THR": { + "username": "username", + "password": "password", + "img_api": "get this from the forum post", + "announce_url": "http://www.torrenthr.org/announce.php?passkey=yourpasskeyhere", + "pronfo_api_key": "pronfo api key", + "pronfo_theme": "pronfo theme code", + "pronfo_rapi_id": "pronfo remote api id", # "anon" : False }, - "LCD" : { - "api_key" : "LCD api key", - "announce_url" : "https://locadora.cc/announce/customannounceurl", + "LCD": { + "api_key": "LCD api key", + "announce_url": "https://locadora.cc/announce/customannounceurl", # "anon" : False }, - "CBR" : { - "api_key" : "CBR api key", - "announce_url" : "https://capybarabr.com/announce/customannounceurl", + "CBR": { + "api_key": "CBR api key", + "announce_url": "https://capybarabr.com/announce/customannounceurl", # "anon" : False }, - "LST" : { - "api_key" : "LST api key", - "announce_url" : "https://lst.gg/announce/customannounceurl", + "LST": { + "api_key": "LST api key", + "announce_url": "https://lst.gg/announce/customannounceurl", # "anon" : False }, - "LT" : { - "api_key" : "LT api key", - "announce_url" : "https://lat-team.com/announce/customannounceurl", + "LT": { + "api_key": "LT api key", + "announce_url": "https://lat-team.com/announce/customannounceurl", # "anon" : False }, - "PTER" : { - "passkey":'passkey', - "img_rehost" : False, - "username" : "", - "password" : "", + "PTER": { + "passkey": 'passkey', + "img_rehost": False, + "username": "", + "password": "", "ptgen_api": "", "anon": True, }, "TL": { "announce_key": "TL announce key", }, - "TDC" :{ - "api_key" : "TDC api key", - "announce_url" : "https://thedarkcommunity.cc/announce/customannounceurl", + "TDC": { + "api_key": "TDC api key", + "announce_url": "https://thedarkcommunity.cc/announce/customannounceurl", # "anon" : "False" }, - "HDT" : { - "username" : "username", - "password" : "password", + "HDT": { + "username": "username", + "password": "password", "my_announce_url": "https://hdts-announce.ru/announce.php?pid=", # "anon" : "False" - "announce_url" : "https://hdts-announce.ru/announce.php", #DO NOT EDIT THIS LINE + "announce_url": "https://hdts-announce.ru/announce.php", #DO NOT EDIT THIS LINE }, - "OE" : { - "api_key" : "OE api key", - "announce_url" : "https://onlyencodes.cc/announce/customannounceurl", + "OE": { + "api_key": "OE api key", + "announce_url": "https://onlyencodes.cc/announce/customannounceurl", # "anon" : False }, "RTF": { - "username" : "username", - "password" : "password", + "username": "username", + "password": "password", "api_key": 'get_it_by_running_/api/ login command from https://retroflix.club/api/doc', "announce_url": "get from upload page", # "tag": "RetroFlix, nd", "anon": True }, - "RF" : { - "api_key" : "RF api key", - "announce_url" : "https://reelflix.xyz/announce/customannounceurl", + "RF": { + "api_key": "RF api key", + "announce_url": "https://reelflix.xyz/announce/customannounceurl", # "anon" : False }, - "OTW" : { - "api_key" : "OTW api key", - "announce_url" : "https://oldtoons.world/announce/customannounceurl", + "OTW": { + "api_key": "OTW api key", + "announce_url": "https://oldtoons.world/announce/customannounceurl", # "anon" : False }, - "FNP" :{ - "api_key" : "FNP api key", - "announce_url" : "https://fearnopeer.com/announce/customannounceurl", + "FNP": { + "api_key": "FNP api key", + "announce_url": "https://fearnopeer.com/announce/customannounceurl", # "anon" : "False" }, - "UTP" : { - "api_key" : "UTP api key", - "announce_url" : "https://UTP/announce/customannounceurl", + "UTP": { + "api_key": "UTP api key", + "announce_url": "https://UTP/announce/customannounceurl", # "anon" : False }, - "AL" : { - "api_key" : "AL api key", - "announce_url" : "https://animelovers.club/announce/customannounceurl", + "AL": { + "api_key": "AL api key", + "announce_url": "https://animelovers.club/announce/customannounceurl", # "anon" : False }, - "HDB" : { - "useAPI" : True, - "username" : "HDB username", - "passkey" : "HDB passkey", - "announce_url" : "https://hdbits.org/announce/Custom_Announce_URL", - "anon" : False, + "HDB": { + "useAPI": True, + "username": "HDB username", + "passkey": "HDB passkey", + "announce_url": "https://hdbits.org/announce/Custom_Announce_URL", + "anon": False, }, - "MANUAL" : { + "MANUAL": { # Uncomment and replace link with filebrowser (https://github.com/filebrowser/filebrowser) link to the Upload-Assistant directory, this will link to your filebrowser instead of uploading to uguu.se # "filebrowser" : "https://domain.tld/filebrowser/files/Upload-Assistant/" }, @@ -235,36 +235,36 @@ # enable_search to true will automatically try and find a suitable hash to save having to rehash when creating torrents # Should use the qbit API, but will also use the torrent_storage_dir to find suitable hashes # If you find issue, use the "--debug" command option to print out some related details - "TORRENT_CLIENTS" : { + "TORRENT_CLIENTS": { # Name your torrent clients here, for example, this example is named "Client1" and is set as default_torrent_client above # All options relate to the webui, make sure you have the webui secured if it has WAN access # See https://github.com/Audionut/Upload-Assistant/wiki - "Client1" : { - "torrent_client" : "qbit", - # "enable_search" : True, - "qbit_url" : "http://127.0.0.1", - "qbit_port" : "8080", - "qbit_user" : "username", - "qbit_pass" : "password", - # "torrent_storage_dir" : "path/to/BT_backup folder" + "Client1": { + "torrent_client": "qbit", + # "enable_search": True, + "qbit_url": "http://127.0.0.1", + "qbit_port": "8080", + "qbit_user": "username", + "qbit_pass": "password", + # "torrent_storage_dir": "path/to/BT_backup folder" # Remote path mapping (docker/etc.) CASE SENSITIVE - # "local_path" : "/LocalPath", - # "remote_path" : "/RemotePath" - }, - "qbit_sample" : { - "torrent_client" : "qbit", - "enable_search" : True, - "qbit_url" : "http://127.0.0.1", - "qbit_port" : "8080", - "qbit_user" : "username", - "qbit_pass" : "password", - # "torrent_storage_dir" : "path/to/BT_backup folder" - # "qbit_tag" : "tag", - # "qbit_cat" : "category" + # "local_path": "/LocalPath", + # "remote_path": "/RemotePath" + }, + "qbit_sample": { + "torrent_client": "qbit", + "enable_search": True, + "qbit_url": "http://127.0.0.1", + "qbit_port": "8080", + "qbit_user": "username", + "qbit_pass": "password", + # "torrent_storage_dir": "path/to/BT_backup folder" + # "qbit_tag": "tag", + # "qbit_cat": "category" # Content Layout for adding .torrents: "Original"(recommended)/"Subfolder"/"NoSubfolder" - "content_layout" : "Original" + "content_layout": "Original" # Enable automatic torrent management if listed path(s) are present in the path # If using remote path mapping, use remote path @@ -278,9 +278,9 @@ # "VERIFY_WEBUI_CERTIFICATE" : True }, - "rtorrent_sample" : { - "torrent_client" : "rtorrent", - "rtorrent_url" : "https://user:password@server.host.tld:443/username/rutorrent/plugins/httprpc/action.php", + "rtorrent_sample": { + "torrent_client": "rtorrent", + "rtorrent_url": "https://user:password@server.host.tld:443/username/rutorrent/plugins/httprpc/action.php", # "torrent_storage_dir" : "path/to/session folder", # "rtorrent_label" : "Add this label to all uploads" @@ -289,48 +289,47 @@ # "remote_path" : "/RemotePath" }, - "deluge_sample" : { - "torrent_client" : "deluge", - "deluge_url" : "localhost", - "deluge_port" : "8080", - "deluge_user" : "username", - "deluge_pass" : "password", + "deluge_sample": { + "torrent_client": "deluge", + "deluge_url": "localhost", + "deluge_port": "8080", + "deluge_user": "username", + "deluge_pass": "password", # "torrent_storage_dir" : "path/to/session folder", # Remote path mapping (docker/etc.) CASE SENSITIVE # "local_path" : "/LocalPath", # "remote_path" : "/RemotePath" }, - "watch_sample" : { - "torrent_client" : "watch", - "watch_folder" : "/Path/To/Watch/Folder" + "watch_sample": { + "torrent_client": "watch", + "watch_folder": "/Path/To/Watch/Folder" }, }, - "DISCORD" :{ - "discord_bot_token" : "discord bot token", - "discord_bot_description" : "L4G's Upload Assistant", - "command_prefix" : "!", - "discord_channel_id" : "discord channel id for use", - "admin_id" : "your discord user id", + "DISCORD": { + "discord_bot_token": "discord bot token", + "discord_bot_description": "L4G's Upload Assistant", + "command_prefix": "!", + "discord_channel_id": "discord channel id for use", + "admin_id": "your discord user id", - "search_dir" : "Path/to/downloads/folder/ this is used for search", + "search_dir": "Path/to/downloads/folder/ this is used for search", # Alternatively, search multiple folders: # "search_dir" : [ # "/downloads/dir1", # "/data/dir2", # ] - "discord_emojis" : { + "discord_emojis": { "BLU": "💙", "BHD": "🎉", "AITHER": "🛫", "STC": "📺", "ACM": "🍙", - "MANUAL" : "📩", - "UPLOAD" : "✅", - "CANCEL" : "🚫" + "MANUAL": "📩", + "UPLOAD": "✅", + "CANCEL": "🚫" } } -} - +} \ No newline at end of file diff --git a/src/prep.py b/src/prep.py index c06a8b4b5..87dc6d138 100644 --- a/src/prep.py +++ b/src/prep.py @@ -1967,7 +1967,6 @@ def get_distributor(self, distributor_in): distributor_out = each return distributor_out - def get_video_codec(self, bdinfo): codecs = { "MPEG-2 Video" : "MPEG-2", @@ -2019,7 +2018,6 @@ def get_video_encode(self, mi, type, bdinfo): video_codec = f"MPEG-{mi['media']['track'][1].get('Format_Version')}" return video_encode, video_codec, has_encode_settings, bit_depth - def get_edition(self, video, bdinfo, filelist, manual_edition): if video.lower().startswith('dc'): video = video.replace('dc', '', 1) @@ -2227,7 +2225,6 @@ def create_base_from_existing_torrent(self, torrentpath, base_dir, uuid): base_torrent.private = True Torrent.copy(base_torrent).write(f"{base_dir}/tmp/{uuid}/BASE.torrent", overwrite=True) - """ Upload Screenshots """ @@ -2498,7 +2495,6 @@ async def get_name(self, meta): name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {edition} {repack} {resolution} {source} {audio} {video_encode}" potential_missing = [] - try: name = ' '.join(name.split()) except: @@ -2513,9 +2509,6 @@ async def get_name(self, meta): clean_name = self.clean_filename(name) return name_notag, name, clean_name, potential_missing - - - async def get_season_episode(self, video, meta): if meta['category'] == 'TV': filelist = meta['filelist'] @@ -2681,16 +2674,6 @@ async def get_season_episode(self, video, meta): console.print(f"[bold yellow]{meta['title']} does not exist on thexem, guessing {season}") console.print(f"[bold yellow]If [green]{season}[/green] is incorrect, use --season to correct") await asyncio.sleep(3) - # try: - # version = parsed['release_version'] - # if int(version) == 2: - # meta['repack'] = "REPACK" - # elif int(version) > 2: - # meta['repack'] = f"REPACK{int(version) - 1}" - # # version = f"v{version}" - # except Exception: - # # version = "" - # pass if meta.get('manual_season', None) == None: meta['season'] = season @@ -2722,7 +2705,6 @@ async def get_season_episode(self, video, meta): return meta - def get_service(self, video, tag, audio, guess_title): service = guessit(video).get('streaming_service', "") services = { @@ -2771,8 +2753,7 @@ def get_service(self, video, tag, audio, guess_title): 'W Network': 'WNET', 'WWEN': 'WWEN', 'WWE Network': 'WWEN', 'XBOX': 'XBOX', 'Xbox Video': 'XBOX', 'YHOO': 'YHOO', 'Yahoo': 'YHOO', 'YT': 'YT', 'ZDF': 'ZDF', 'iP': 'iP', 'BBC iPlayer': 'iP', 'iQIYI': 'iQIYI', 'iT': 'iT', 'iTunes': 'iT' } - - + video_name = re.sub(r"[.()]", " ", video.replace(tag, '').replace(guess_title, '')) if "DTS-HD MA" in audio: video_name = video_name.replace("DTS-HD.MA.", "").replace("DTS-HD MA ", "") @@ -2789,8 +2770,6 @@ def get_service(self, video, tag, audio, guess_title): service_longname = "Amazon" return service, service_longname - - def stream_optimized(self, stream_opt): if stream_opt == True: stream = 1 @@ -2826,15 +2805,13 @@ async def upload_image(self, session, url, data, headers, files): async with session.post(url=url, data=data, headers=headers, files=files) as resp: response = await resp.json() return response - - + def clean_filename(self, name): invalid = '<>:"/\|?*' for char in invalid: name = name.replace(char, '-') return name - async def gen_desc(self, meta): desclink = meta.get('desclink', None) descfile = meta.get('descfile', None) @@ -2892,7 +2869,7 @@ async def gen_desc(self, meta): description.write(requests.get(raw).text) description.write("\n") meta['description'] = "CUSTOM" - + if descfile != None: if os.path.isfile(descfile) == True: text = open(descfile, 'r').read() @@ -2904,7 +2881,7 @@ async def gen_desc(self, meta): meta['description'] = "CUSTOM" description.write("\n") return meta - + async def tag_override(self, meta): with open(f"{meta['base_dir']}/data/tags.json", 'r', encoding="utf-8") as f: tags = json.load(f) diff --git a/src/trackers/BHD.py b/src/trackers/BHD.py index d9e73acdf..8909f3f46 100644 --- a/src/trackers/BHD.py +++ b/src/trackers/BHD.py @@ -24,7 +24,7 @@ def __init__(self, config): self.tracker = 'BHD' self.source_flag = 'BHD' self.upload_url = 'https://beyond-hd.me/api/upload/' - self.signature = f"\n[center][url=https://beyond-hd.me/forums/topic/toolpython-l4gs-upload-assistant.5456]Created by L4G's Upload Assistant[/url][/center]" + self.signature = f"\n[center][url=https://beyond-hd.me/forums/topic/toolpython-l4gs-upload-assistant.5456/post/138087#post-138087]Created by L4G's Upload Assistant[/url][/center]" self.banned_groups = ['Sicario', 'TOMMY', 'x0r', 'nikt0', 'FGT', 'd3g', 'MeGusta', 'YIFY', 'tigole', 'TEKNO3D', 'C4K', 'RARBG', '4K4U', 'EASports', 'ReaLHD'] pass @@ -43,12 +43,12 @@ async def upload(self, meta): anon = 0 else: anon = 1 - + if meta['bdinfo'] != None: mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8') else: mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt", 'r', encoding='utf-8') - + desc = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'r').read() torrent_file = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent" files = { @@ -58,18 +58,18 @@ async def upload(self, meta): open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent", 'rb') files['file'] = open_torrent.read() open_torrent.close() - + data = { - 'name' : bhd_name, - 'category_id' : cat_id, - 'type' : type_id, + 'name': bhd_name, + 'category_id': cat_id, + 'type': type_id, 'source': source_id, - 'imdb_id' : meta['imdb_id'].replace('tt', ''), - 'tmdb_id' : meta['tmdb'], - 'description' : desc, - 'anon' : anon, - 'sd' : meta.get('sd', 0), - 'live' : draft + 'imdb_id': meta['imdb_id'].replace('tt', ''), + 'tmdb_id': meta['tmdb'], + 'description': desc, + 'anon': anon, + 'sd': meta.get('sd', 0), + 'live': draft # 'internal' : 0, # 'featured' : 0, # 'free' : 0, @@ -80,7 +80,7 @@ async def upload(self, meta): if self.config['TRACKERS'][self.tracker].get('internal', False) == True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 - + if meta.get('tv_pack', 0) == 1: data['pack'] = 1 if meta.get('season', None) == "S00": @@ -96,7 +96,7 @@ async def upload(self, meta): headers = { 'User-Agent': f'Upload Assistant/2.1 ({platform.system()} {platform.release()})' } - + url = self.upload_url + self.config['TRACKERS'][self.tracker]['api_key'].strip() if meta['debug'] == False: response = requests.post(url=url, files=files, data=data, headers=headers) @@ -118,12 +118,6 @@ async def upload(self, meta): else: console.print(f"[cyan]Request Data:") console.print(data) - - - - - - async def get_cat_id(self, category_name): category_id = { @@ -144,7 +138,7 @@ async def get_source(self, source): "NTSC" : "DVD", "NTSC DVD" : "DVD", "PAL" : "DVD", "PAL DVD": "DVD", } - + source_id = sources.get(source) return source_id @@ -184,8 +178,6 @@ async def get_type(self, meta): else: type_id = "Other" return type_id - - async def edit_desc(self, meta): base = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'r').read() @@ -220,8 +212,6 @@ async def edit_desc(self, meta): desc.write(self.signature) desc.close() return - - async def search_existing(self, meta): dupes = [] diff --git a/src/trackers/HDB.py b/src/trackers/HDB.py index 893ae9a25..f71187d72 100644 --- a/src/trackers/HDB.py +++ b/src/trackers/HDB.py @@ -264,7 +264,7 @@ async def upload(self, meta): else: torrentFileName = unidecode(os.path.basename(meta['path']).replace(' ', '.')) files = { - 'file': (f"{torrentFileName}.torrent", torrentFile, "application/x-bittorent") + 'file': (f"{torrentFileName}.torrent", torrentFile, "application/x-bittorrent") } data = { 'name': hdb_name, From ded208f6dc75975121255924e8cbac83acec4091 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 12:41:29 +1000 Subject: [PATCH 09/33] Auto search ID and description from BLU Also auto pulls image links todo: Probably kill the derived description text as I don't believe it applies anywhere else. --- src/prep.py | 53 ++++++--- src/trackers/AITHER.py | 12 +- src/trackers/BLU.py | 116 +++++++++---------- src/trackers/COMMON.py | 245 +++++++++++++++++++++-------------------- src/trackers/HDB.py | 2 +- 5 files changed, 218 insertions(+), 210 deletions(-) diff --git a/src/prep.py b/src/prep.py index 87dc6d138..922c56096 100644 --- a/src/prep.py +++ b/src/prep.py @@ -56,9 +56,6 @@ exit() - - - class Prep(): """ Prepare for upload: @@ -77,15 +74,14 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met tracker_key = tracker_name.lower() manual_key = f"{tracker_key}_manual" found_match = False - + console.print(f"[cyan]Attempting to search {tracker_name} with search_term: {search_term}[/cyan]") - + if meta.get(tracker_key) is not None: meta[manual_key] = meta[tracker_key] console.print(f"[cyan]{tracker_name} ID found in meta, reusing existing ID: {meta[tracker_key]}[/cyan]") if tracker_name == "BLU": - blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, meta[tracker_key]) - # Check if we got valid data + blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, id=meta[tracker_key]) if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: console.print(f"[green]Valid data found on {tracker_name}, setting meta values[/green]") if blu_tmdb not in [None, '0']: @@ -102,6 +98,8 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() if meta.get('image_list', []) == []: meta['image_list'] = blu_imagelist + if blu_filename: + meta['blu_filename'] = blu_filename # Store the filename in meta for later use found_match = True # Set flag if any relevant data is found else: console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") @@ -114,15 +112,36 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met console.print(f"[yellow]No IMDb ID found on {tracker_name}[/yellow]") else: console.print(f"[cyan]Searching {tracker_name} using search_term: {search_term}[/cyan]") + imdb, tracker_id = None, None # Initialize variables if tracker_name == "PTP": imdb, tracker_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) elif tracker_name == "HDB": console.print(f"[cyan]HDB search using folder/file: {search_term}[/cyan]") - # Pass search_term as a string, not a list imdb, tvdb_id, hdb_name, meta['ext_torrenthash'], tracker_id = await tracker_instance.search_filename(search_term, search_file_folder) - meta['tvdb_id'] = str(tvdb_id) if tvdb_id else meta.get('tvdb_id') meta['hdb_name'] = hdb_name + elif tracker_name == "BLU": + # Attempt to search using the file name if ID is not available + blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, file_name=search_term) + if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: + console.print(f"[green]Valid data found on {tracker_name} using file name, setting meta values[/green]") + if blu_tmdb not in [None, '0']: + meta['tmdb_manual'] = blu_tmdb + if blu_imdb not in [None, '0']: + meta['imdb'] = str(blu_imdb) + if blu_tvdb not in [None, '0']: + meta['tvdb_id'] = blu_tvdb + if blu_mal not in [None, '0']: + meta['mal'] = blu_mal + if blu_desc not in [None, '0', '']: + meta['blu_desc'] = blu_desc + if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: + meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() + if meta.get('image_list', []) == []: + meta['image_list'] = blu_imagelist + if blu_filename: + meta['blu_filename'] = blu_filename # Store the filename in meta for later use + found_match = True else: imdb = tracker_id = None @@ -132,7 +151,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met found_match = True if tracker_id: meta[tracker_key] = tracker_id - + return meta, found_match async def gather_prep(self, meta, mode): @@ -146,7 +165,7 @@ async def gather_prep(self, meta, mode): meta['uuid'] = folder_id if not os.path.exists(f"{base_dir}/tmp/{meta['uuid']}"): Path(f"{base_dir}/tmp/{meta['uuid']}").mkdir(parents=True, exist_ok=True) - + if meta['debug']: console.print(f"[cyan]ID: {meta['uuid']}") @@ -201,7 +220,7 @@ async def gather_prep(self, meta, mode): meta['mediainfo'] = mi else: mi = meta['mediainfo'] - + meta['dvd_size'] = await self.get_dvd_size(meta['discs']) meta['resolution'] = self.get_resolution(guessit(video), meta['uuid'], base_dir) meta['sd'] = self.is_sd(meta['resolution']) @@ -239,7 +258,7 @@ async def gather_prep(self, meta, mode): meta['search_year'] = guessit(video)['year'] except Exception: meta['search_year'] = "" - + if not meta.get('edit', False): mi = self.exportInfo(videopath, meta['isdir'], meta['uuid'], base_dir, export_text=True) meta['mediainfo'] = mi @@ -249,20 +268,20 @@ async def gather_prep(self, meta, mode): if meta.get('resolution', None) is None: meta['resolution'] = self.get_resolution(guessit(video), meta['uuid'], base_dir) meta['sd'] = self.is_sd(meta['resolution']) - + if " AKA " in filename.replace('.', ' '): filename = filename.split('AKA')[0] meta['filename'] = filename meta['bdinfo'] = bdinfo - + # Debugging information after population console.print(f"Debug: meta['filelist'] after population: {meta.get('filelist', 'Not Set')}") # Reuse information from trackers with fallback if search_term: # Ensure there's a valid search term found_match = False - + if str(self.config['TRACKERS'].get('PTP', {}).get('useAPI')).lower() == "true": ptp = PTP(config=self.config) console.print(f"[cyan]Attempting to search PTP with search_term: {search_term}[/cyan]") @@ -3174,4 +3193,4 @@ async def search_tvmaze(self, filename, year, imdbID, tvdbID): if int(tvdbID) == 0: if show.get('externals', {}).get('tvdb', '0') != None: tvdbID = show.get('externals', {}).get('tvdb', '0') - return tvmazeID, imdbID, tvdbID \ No newline at end of file + return tvmazeID, imdbID, tvdbID diff --git a/src/trackers/AITHER.py b/src/trackers/AITHER.py index 382ae92d2..3bf94eea9 100644 --- a/src/trackers/AITHER.py +++ b/src/trackers/AITHER.py @@ -30,7 +30,7 @@ def __init__(self, config): 'QxR', 'Ralphy', 'RARBG', 'RetroPeeps', 'SAMPA', 'Sicario', 'Silence', 'SkipTT', 'SPDVD', 'STUTTERSHIT', 'SWTYBLZ', 'TAoE', 'TGx', 'Tigole', 'TSP', 'TSPxL', 'VXT', 'Weasley[HONE]', 'Will1869', 'x0r', 'YIFY'] pass - + async def upload(self, meta): common = COMMON(config=self.config) await common.edit_torrent(meta, self.tracker, self.source_flag) @@ -103,15 +103,13 @@ async def upload(self, meta): console.print(data) open_torrent.close() - - async def edit_name(self, meta): aither_name = meta['name'] has_eng_audio = False if meta['is_disc'] != "BDMV": with open(f"{meta.get('base_dir')}/tmp/{meta.get('uuid')}/MediaInfo.json", 'r', encoding='utf-8') as f: mi = json.load(f) - + for track in mi['media']['track']: if track['@type'] == "Audio": if track.get('Language', 'None').startswith('en'): @@ -166,10 +164,6 @@ async def get_res_id(self, resolution): }.get(resolution, '10') return resolution_id - - - - async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") @@ -198,4 +192,4 @@ async def search_existing(self, meta): console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) - return dupes \ No newline at end of file + return dupes diff --git a/src/trackers/BLU.py b/src/trackers/BLU.py index cf9d4a14d..96fbe2f36 100644 --- a/src/trackers/BLU.py +++ b/src/trackers/BLU.py @@ -2,13 +2,13 @@ # import discord import asyncio import requests -import os import platform from str2bool import str2bool from src.trackers.COMMON import COMMON from src.console import console + class BLU(): """ Edit for Tracker: @@ -23,21 +23,20 @@ def __init__(self, config): self.source_flag = 'BLU' self.search_url = 'https://blutopia.cc/api/torrents/filter' self.torrent_url = 'https://blutopia.cc/api/torrents/' - self.upload_url = 'https://blutopia.cc/api/torrents/upload' - self.signature = f"\n[center][url=https://blutopia.cc/forums/topics/3087]Created by L4G's Upload Assistant[/url][/center]" + self.upload_url = 'https://blutopia.cc/api/torrents/upload' + self.signature = "\n[center][url=https://blutopia.cc/forums/topics/3087/posts/42941]Created by L4G's Upload Assistant[/url][/center]" self.banned_groups = [ - '[Oj]', '3LTON', '4yEo', 'ADE', 'AFG', 'AniHLS', 'AnimeRG', 'AniURL', 'AROMA', 'aXXo', 'Brrip', 'CHD', 'CM8', 'CrEwSaDe', 'd3g', 'DeadFish', 'DNL', 'ELiTE', 'eSc', 'FaNGDiNG0', 'FGT', 'Flights', + '[Oj]', '3LTON', '4yEo', 'ADE', 'AFG', 'AniHLS', 'AnimeRG', 'AniURL', 'AROMA', 'aXXo', 'Brrip', 'CHD', 'CM8', 'CrEwSaDe', 'd3g', 'DeadFish', 'DNL', 'ELiTE', 'eSc', 'FaNGDiNG0', 'FGT', 'Flights', 'FRDS', 'FUM', 'HAiKU', 'HD2DVD', 'HDS', 'HDTime', 'Hi10', 'ION10', 'iPlanet', 'JIVE', 'KiNGDOM', 'Leffe', 'LEGi0N', 'LOAD', 'MeGusta', 'mHD', 'mSD', 'NhaNc3', 'nHD', 'nikt0', 'NOIVTC', 'OFT', 'nSD', 'PiRaTeS', 'playBD', 'PlaySD', 'playXD', 'PRODJi', 'RAPiDCOWS', 'RARBG', 'RetroPeeps', 'RDN', 'REsuRRecTioN', 'RMTeam', 'SANTi', 'SicFoI', 'SPASM', 'SPDVD', 'STUTTERSHIT', 'Telly', 'TM', 'TRiToN', 'UPiNSMOKE', 'URANiME', 'WAF', 'x0r', 'xRed', 'XS', 'YIFY', 'ZKBL', 'ZmN', 'ZMNT', 'AOC', ['EVO', 'Raw Content Only'], ['TERMiNAL', 'Raw Content Only'], ['ViSION', 'Note the capitalization and characters used'], ['CMRG', 'Raw Content Only'] ] - + pass - + async def upload(self, meta): common = COMMON(config=self.config) - blu_name = meta['name'] desc_header = "" if meta.get('webdv', False): @@ -64,34 +63,34 @@ async def upload(self, meta): open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[BLU]{meta['clean_name']}.torrent", 'rb') files = {'torrent': ("placeholder.torrent", open_torrent, "application/x-bittorrent")} data = { - 'name' : blu_name, - 'description' : desc, - 'mediainfo' : mi_dump, - 'bdinfo' : bd_dump, - 'category_id' : cat_id, - 'type_id' : type_id, - 'resolution_id' : resolution_id, - 'tmdb' : meta['tmdb'], - 'imdb' : meta['imdb_id'].replace('tt', ''), - 'tvdb' : meta['tvdb_id'], - 'mal' : meta['mal_id'], - 'igdb' : 0, - 'anonymous' : anon, - 'stream' : meta['stream'], - 'sd' : meta['sd'], - 'keywords' : meta['keywords'], - 'personal_release' : int(meta.get('personalrelease', False)), - 'internal' : 0, - 'featured' : 0, - 'free' : 0, - 'doubleup' : 0, - 'sticky' : 0, + 'name': blu_name, + 'description': desc, + 'mediainfo': mi_dump, + 'bdinfo': bd_dump, + 'category_id': cat_id, + 'type_id': type_id, + 'resolution_id': resolution_id, + 'tmdb': meta['tmdb'], + 'imdb': meta['imdb_id'].replace('tt', ''), + 'tvdb': meta['tvdb_id'], + 'mal': meta['mal_id'], + 'igdb': 0, + 'anonymous': anon, + 'stream': meta['stream'], + 'sd': meta['sd'], + 'keywords': meta['keywords'], + 'personal_release': int(meta.get('personalrelease', False)), + 'internal': 0, + 'featured': 0, + 'free': 0, + 'doubleup': 0, + 'sticky': 0, } # Internal if self.config['TRACKERS'][self.tracker].get('internal', False) == True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 - + if region_id != 0: data['region_id'] = region_id if distributor_id != 0: @@ -105,28 +104,24 @@ async def upload(self, meta): params = { 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip() } - + if meta['debug'] == False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) except: console.print("It may have uploaded, go check") - - return + + return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) open_torrent.close() - - - - async def get_cat_id(self, category_name, edition): category_id = { - 'MOVIE': '1', - 'TV': '2', + 'MOVIE': '1', + 'TV': '2', 'FANRES': '3' }.get(category_name, '0') if category_name == 'MOVIE' and 'FANRES' in edition: @@ -135,10 +130,10 @@ async def get_cat_id(self, category_name, edition): async def get_type_id(self, type): type_id = { - 'DISC': '1', + 'DISC': '1', 'REMUX': '3', - 'WEBDL': '4', - 'WEBRIP': '5', + 'WEBDL': '4', + 'WEBRIP': '5', 'HDTV': '6', 'ENCODE': '12' }.get(type, '0') @@ -146,16 +141,16 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { - '8640p':'10', - '4320p': '11', - '2160p': '1', - '1440p' : '2', + '8640p':'10', + '4320p': '11', + '2160p': '1', + '1440p': '2', '1080p': '2', - '1080i':'3', - '720p': '5', - '576p': '6', + '1080i':'3', + '720p': '5', + '576p': '6', '576i': '7', - '480p': '8', + '480p': '8', '480i': '9' }.get(resolution, '10') return resolution_id @@ -173,12 +168,12 @@ async def derived_dv_layer(self, meta): if cli_ui.ask_yes_no("Is the DV Layer sourced from the same service as the video?"): ask_comp = False desc_header = "[code]This release contains a derived Dolby Vision profile 8 layer. Comparisons not required as DV and HDR are from same provider.[/code]" - + if ask_comp: while desc_header == "": desc_input = cli_ui.ask_string("Please provide comparisons between HDR masters. (link or bbcode)", default="") desc_header = f"[code]This release contains a derived Dolby Vision profile 8 layer. Comparisons between HDR masters: {desc_input}[/code]" - + if "hybrid" not in name.lower(): if "REPACK" in name: name = name.replace('REPACK', 'Hybrid REPACK') @@ -186,17 +181,16 @@ async def derived_dv_layer(self, meta): name = name.replace(meta['resolution'], f"Hybrid {meta['resolution']}") return name, desc_header - async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'tmdbId' : meta['tmdb'], - 'categories[]' : await self.get_cat_id(meta['category'], meta.get('edition', '')), - 'types[]' : await self.get_type_id(meta['type']), - 'resolutions[]' : await self.get_res_id(meta['resolution']), - 'name' : "" + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'tmdbId': meta['tmdb'], + 'categories[]': await self.get_cat_id(meta['category'], meta.get('edition', '')), + 'types[]': await self.get_type_id(meta['type']), + 'resolutions[]': await self.get_res_id(meta['resolution']), + 'name': "" } if meta['category'] == 'TV': params['name'] = params['name'] + f" {meta.get('season', '')}{meta.get('episode', '')}" @@ -214,4 +208,4 @@ async def search_existing(self, meta): console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) - return dupes \ No newline at end of file + return dupes diff --git a/src/trackers/COMMON.py b/src/trackers/COMMON.py index 75b5689f1..9a3a7849e 100644 --- a/src/trackers/COMMON.py +++ b/src/trackers/COMMON.py @@ -8,6 +8,7 @@ from src.bbcode import BBCODE from src.console import console + class COMMON(): def __init__(self, config): self.config = config @@ -37,7 +38,7 @@ async def unit3d_edit_desc(self, meta, tracker, signature, comparison=False, des with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}]DESCRIPTION.txt", 'w', encoding='utf8') as descfile: if desc_header != "": descfile.write(desc_header) - + bbcode = BBCODE() if meta.get('discs', []) != []: discs = meta['discs'] @@ -60,13 +61,13 @@ async def unit3d_edit_desc(self, meta, tracker, signature, comparison=False, des desc = base desc = bbcode.convert_pre_to_code(desc) desc = bbcode.convert_hide_to_spoiler(desc) - if comparison == False: + if comparison is False: desc = bbcode.convert_comparison_to_collapse(desc, 1000) - + desc = desc.replace('[img]', '[img=300]') descfile.write(desc) images = meta['image_list'] - if len(images) > 0: + if len(images) > 0: descfile.write("[center]") for each in range(len(images[:int(meta['screens'])])): web_url = images[each]['web_url'] @@ -77,97 +78,123 @@ async def unit3d_edit_desc(self, meta, tracker, signature, comparison=False, des if signature != None: descfile.write(signature) descfile.close() - return + return async def unit3d_region_ids(self, region): region_id = { - 'AFG': 1, 'AIA': 2, 'ALA': 3, 'ALG': 4, 'AND': 5, 'ANG': 6, 'ARG': 7, 'ARM': 8, 'ARU': 9, - 'ASA': 10, 'ATA': 11, 'ATF': 12, 'ATG': 13, 'AUS': 14, 'AUT': 15, 'AZE': 16, 'BAH': 17, - 'BAN': 18, 'BDI': 19, 'BEL': 20, 'BEN': 21, 'BER': 22, 'BES': 23, 'BFA': 24, 'BHR': 25, - 'BHU': 26, 'BIH': 27, 'BLM': 28, 'BLR': 29, 'BLZ': 30, 'BOL': 31, 'BOT': 32, 'BRA': 33, - 'BRB': 34, 'BRU': 35, 'BVT': 36, 'CAM': 37, 'CAN': 38, 'CAY': 39, 'CCK': 40, 'CEE': 41, - 'CGO': 42, 'CHA': 43, 'CHI': 44, 'CHN': 45, 'CIV': 46, 'CMR': 47, 'COD': 48, 'COK': 49, - 'COL': 50, 'COM': 51, 'CPV': 52, 'CRC': 53, 'CRO': 54, 'CTA': 55, 'CUB': 56, 'CUW': 57, - 'CXR': 58, 'CYP': 59, 'DJI': 60, 'DMA': 61, 'DOM': 62, 'ECU': 63, 'EGY': 64, 'ENG': 65, - 'EQG': 66, 'ERI': 67, 'ESH': 68, 'ESP': 69, 'ETH': 70, 'FIJ': 71, 'FLK': 72, 'FRA': 73, - 'FRO': 74, 'FSM': 75, 'GAB': 76, 'GAM': 77, 'GBR': 78, 'GEO': 79, 'GER': 80, 'GGY': 81, - 'GHA': 82, 'GIB': 83, 'GLP': 84, 'GNB': 85, 'GRE': 86, 'GRL': 87, 'GRN': 88, 'GUA': 89, - 'GUF': 90, 'GUI': 91, 'GUM': 92, 'GUY': 93, 'HAI': 94, 'HKG': 95, 'HMD': 96, 'HON': 97, - 'HUN': 98, 'IDN': 99, 'IMN': 100, 'IND': 101, 'IOT': 102, 'IRL': 103, 'IRN': 104, 'IRQ': 105, - 'ISL': 106, 'ISR': 107, 'ITA': 108, 'JAM': 109, 'JEY': 110, 'JOR': 111, 'JPN': 112, 'KAZ': 113, - 'KEN': 114, 'KGZ': 115, 'KIR': 116, 'KNA': 117, 'KOR': 118, 'KSA': 119, 'KUW': 120, 'KVX': 121, - 'LAO': 122, 'LBN': 123, 'LBR': 124, 'LBY': 125, 'LCA': 126, 'LES': 127, 'LIE': 128, 'LKA': 129, - 'LUX': 130, 'MAC': 131, 'MAD': 132, 'MAF': 133, 'MAR': 134, 'MAS': 135, 'MDA': 136, 'MDV': 137, - 'MEX': 138, 'MHL': 139, 'MKD': 140, 'MLI': 141, 'MLT': 142, 'MNG': 143, 'MNP': 144, 'MON': 145, - 'MOZ': 146, 'MRI': 147, 'MSR': 148, 'MTN': 149, 'MTQ': 150, 'MWI': 151, 'MYA': 152, 'MYT': 153, - 'NAM': 154, 'NCA': 155, 'NCL': 156, 'NEP': 157, 'NFK': 158, 'NIG': 159, 'NIR': 160, 'NIU': 161, - 'NLD': 162, 'NOR': 163, 'NRU': 164, 'NZL': 165, 'OMA': 166, 'PAK': 167, 'PAN': 168, 'PAR': 169, - 'PCN': 170, 'PER': 171, 'PHI': 172, 'PLE': 173, 'PLW': 174, 'PNG': 175, 'POL': 176, 'POR': 177, - 'PRK': 178, 'PUR': 179, 'QAT': 180, 'REU': 181, 'ROU': 182, 'RSA': 183, 'RUS': 184, 'RWA': 185, - 'SAM': 186, 'SCO': 187, 'SDN': 188, 'SEN': 189, 'SEY': 190, 'SGS': 191, 'SHN': 192, 'SIN': 193, - 'SJM': 194, 'SLE': 195, 'SLV': 196, 'SMR': 197, 'SOL': 198, 'SOM': 199, 'SPM': 200, 'SRB': 201, - 'SSD': 202, 'STP': 203, 'SUI': 204, 'SUR': 205, 'SWZ': 206, 'SXM': 207, 'SYR': 208, 'TAH': 209, - 'TAN': 210, 'TCA': 211, 'TGA': 212, 'THA': 213, 'TJK': 214, 'TKL': 215, 'TKM': 216, 'TLS': 217, - 'TOG': 218, 'TRI': 219, 'TUN': 220, 'TUR': 221, 'TUV': 222, 'TWN': 223, 'UAE': 224, 'UGA': 225, - 'UKR': 226, 'UMI': 227, 'URU': 228, 'USA': 229, 'UZB': 230, 'VAN': 231, 'VAT': 232, 'VEN': 233, - 'VGB': 234, 'VIE': 235, 'VIN': 236, 'VIR': 237, 'WAL': 238, 'WLF': 239, 'YEM': 240, 'ZAM': 241, - 'ZIM': 242, 'EUR' : 243 + 'AFG': 1, 'AIA': 2, 'ALA': 3, 'ALG': 4, 'AND': 5, 'ANG': 6, 'ARG': 7, 'ARM': 8, 'ARU': 9, + 'ASA': 10, 'ATA': 11, 'ATF': 12, 'ATG': 13, 'AUS': 14, 'AUT': 15, 'AZE': 16, 'BAH': 17, + 'BAN': 18, 'BDI': 19, 'BEL': 20, 'BEN': 21, 'BER': 22, 'BES': 23, 'BFA': 24, 'BHR': 25, + 'BHU': 26, 'BIH': 27, 'BLM': 28, 'BLR': 29, 'BLZ': 30, 'BOL': 31, 'BOT': 32, 'BRA': 33, + 'BRB': 34, 'BRU': 35, 'BVT': 36, 'CAM': 37, 'CAN': 38, 'CAY': 39, 'CCK': 40, 'CEE': 41, + 'CGO': 42, 'CHA': 43, 'CHI': 44, 'CHN': 45, 'CIV': 46, 'CMR': 47, 'COD': 48, 'COK': 49, + 'COL': 50, 'COM': 51, 'CPV': 52, 'CRC': 53, 'CRO': 54, 'CTA': 55, 'CUB': 56, 'CUW': 57, + 'CXR': 58, 'CYP': 59, 'DJI': 60, 'DMA': 61, 'DOM': 62, 'ECU': 63, 'EGY': 64, 'ENG': 65, + 'EQG': 66, 'ERI': 67, 'ESH': 68, 'ESP': 69, 'ETH': 70, 'FIJ': 71, 'FLK': 72, 'FRA': 73, + 'FRO': 74, 'FSM': 75, 'GAB': 76, 'GAM': 77, 'GBR': 78, 'GEO': 79, 'GER': 80, 'GGY': 81, + 'GHA': 82, 'GIB': 83, 'GLP': 84, 'GNB': 85, 'GRE': 86, 'GRL': 87, 'GRN': 88, 'GUA': 89, + 'GUF': 90, 'GUI': 91, 'GUM': 92, 'GUY': 93, 'HAI': 94, 'HKG': 95, 'HMD': 96, 'HON': 97, + 'HUN': 98, 'IDN': 99, 'IMN': 100, 'IND': 101, 'IOT': 102, 'IRL': 103, 'IRN': 104, 'IRQ': 105, + 'ISL': 106, 'ISR': 107, 'ITA': 108, 'JAM': 109, 'JEY': 110, 'JOR': 111, 'JPN': 112, 'KAZ': 113, + 'KEN': 114, 'KGZ': 115, 'KIR': 116, 'KNA': 117, 'KOR': 118, 'KSA': 119, 'KUW': 120, 'KVX': 121, + 'LAO': 122, 'LBN': 123, 'LBR': 124, 'LBY': 125, 'LCA': 126, 'LES': 127, 'LIE': 128, 'LKA': 129, + 'LUX': 130, 'MAC': 131, 'MAD': 132, 'MAF': 133, 'MAR': 134, 'MAS': 135, 'MDA': 136, 'MDV': 137, + 'MEX': 138, 'MHL': 139, 'MKD': 140, 'MLI': 141, 'MLT': 142, 'MNG': 143, 'MNP': 144, 'MON': 145, + 'MOZ': 146, 'MRI': 147, 'MSR': 148, 'MTN': 149, 'MTQ': 150, 'MWI': 151, 'MYA': 152, 'MYT': 153, + 'NAM': 154, 'NCA': 155, 'NCL': 156, 'NEP': 157, 'NFK': 158, 'NIG': 159, 'NIR': 160, 'NIU': 161, + 'NLD': 162, 'NOR': 163, 'NRU': 164, 'NZL': 165, 'OMA': 166, 'PAK': 167, 'PAN': 168, 'PAR': 169, + 'PCN': 170, 'PER': 171, 'PHI': 172, 'PLE': 173, 'PLW': 174, 'PNG': 175, 'POL': 176, 'POR': 177, + 'PRK': 178, 'PUR': 179, 'QAT': 180, 'REU': 181, 'ROU': 182, 'RSA': 183, 'RUS': 184, 'RWA': 185, + 'SAM': 186, 'SCO': 187, 'SDN': 188, 'SEN': 189, 'SEY': 190, 'SGS': 191, 'SHN': 192, 'SIN': 193, + 'SJM': 194, 'SLE': 195, 'SLV': 196, 'SMR': 197, 'SOL': 198, 'SOM': 199, 'SPM': 200, 'SRB': 201, + 'SSD': 202, 'STP': 203, 'SUI': 204, 'SUR': 205, 'SWZ': 206, 'SXM': 207, 'SYR': 208, 'TAH': 209, + 'TAN': 210, 'TCA': 211, 'TGA': 212, 'THA': 213, 'TJK': 214, 'TKL': 215, 'TKM': 216, 'TLS': 217, + 'TOG': 218, 'TRI': 219, 'TUN': 220, 'TUR': 221, 'TUV': 222, 'TWN': 223, 'UAE': 224, 'UGA': 225, + 'UKR': 226, 'UMI': 227, 'URU': 228, 'USA': 229, 'UZB': 230, 'VAN': 231, 'VAT': 232, 'VEN': 233, + 'VGB': 234, 'VIE': 235, 'VIN': 236, 'VIR': 237, 'WAL': 238, 'WLF': 239, 'YEM': 240, 'ZAM': 241, + 'ZIM': 242, 'EUR': 243 }.get(region, 0) return region_id async def unit3d_distributor_ids(self, distributor): distributor_id = { - '01 DISTRIBUTION': 1, '100 DESTINATIONS TRAVEL FILM': 2, '101 FILMS': 3, '1FILMS': 4, '2 ENTERTAIN VIDEO': 5, '20TH CENTURY FOX': 6, '2L': 7, '3D CONTENT HUB': 8, '3D MEDIA': 9, '3L FILM': 10, '4DIGITAL': 11, '4DVD': 12, '4K ULTRA HD MOVIES': 13, '4K UHD': 13, '8-FILMS': 14, '84 ENTERTAINMENT': 15, '88 FILMS': 16, '@ANIME': 17, 'ANIME': 17, 'A CONTRACORRIENTE': 18, 'A CONTRACORRIENTE FILMS': 19, 'A&E HOME VIDEO': 20, 'A&E': 20, 'A&M RECORDS': 21, 'A+E NETWORKS': 22, 'A+R': 23, 'A-FILM': 24, 'AAA': 25, 'AB VIDÉO': 26, 'AB VIDEO': 26, 'ABC - (AUSTRALIAN BROADCASTING CORPORATION)': 27, 'ABC': 27, 'ABKCO': 28, 'ABSOLUT MEDIEN': 29, 'ABSOLUTE': 30, 'ACCENT FILM ENTERTAINMENT': 31, 'ACCENTUS': 32, 'ACORN MEDIA': 33, 'AD VITAM': 34, 'ADA': 35, 'ADITYA VIDEOS': 36, 'ADSO FILMS': 37, 'AFM RECORDS': 38, 'AGFA': 39, 'AIX RECORDS': 40, 'ALAMODE FILM': 41, 'ALBA RECORDS': 42, 'ALBANY RECORDS': 43, 'ALBATROS': 44, 'ALCHEMY': 45, 'ALIVE': 46, 'ALL ANIME': 47, 'ALL INTERACTIVE ENTERTAINMENT': 48, 'ALLEGRO': 49, 'ALLIANCE': 50, 'ALPHA MUSIC': 51, 'ALTERDYSTRYBUCJA': 52, 'ALTERED INNOCENCE': 53, 'ALTITUDE FILM DISTRIBUTION': 54, 'ALUCARD RECORDS': 55, 'AMAZING D.C.': 56, 'AMAZING DC': 56, 'AMMO CONTENT': 57, 'AMUSE SOFT ENTERTAINMENT': 58, 'ANCONNECT': 59, 'ANEC': 60, 'ANIMATSU': 61, 'ANIME HOUSE': 62, 'ANIME LTD': 63, 'ANIME WORKS': 64, 'ANIMEIGO': 65, 'ANIPLEX': 66, 'ANOLIS ENTERTAINMENT': 67, 'ANOTHER WORLD ENTERTAINMENT': 68, 'AP INTERNATIONAL': 69, 'APPLE': 70, 'ARA MEDIA': 71, 'ARBELOS': 72, 'ARC ENTERTAINMENT': 73, 'ARP SÉLECTION': 74, 'ARP SELECTION': 74, 'ARROW': 75, 'ART SERVICE': 76, 'ART VISION': 77, 'ARTE ÉDITIONS': 78, 'ARTE EDITIONS': 78, 'ARTE VIDÉO': 79, 'ARTE VIDEO': 79, 'ARTHAUS MUSIK': 80, 'ARTIFICIAL EYE': 81, 'ARTSPLOITATION FILMS': 82, 'ARTUS FILMS': 83, 'ASCOT ELITE HOME ENTERTAINMENT': 84, 'ASIA VIDEO': 85, 'ASMIK ACE': 86, 'ASTRO RECORDS & FILMWORKS': 87, 'ASYLUM': 88, 'ATLANTIC FILM': 89, 'ATLANTIC RECORDS': 90, 'ATLAS FILM': 91, 'AUDIO VISUAL ENTERTAINMENT': 92, 'AURO-3D CREATIVE LABEL': 93, 'AURUM': 94, 'AV VISIONEN': 95, 'AV-JET': 96, 'AVALON': 97, 'AVENTI': 98, 'AVEX TRAX': 99, 'AXIOM': 100, 'AXIS RECORDS': 101, 'AYNGARAN': 102, 'BAC FILMS': 103, 'BACH FILMS': 104, 'BANDAI VISUAL': 105, 'BARCLAY': 106, 'BBC': 107, 'BRITISH BROADCASTING CORPORATION': 107, 'BBI FILMS': 108, 'BBI': 108, 'BCI HOME ENTERTAINMENT': 109, 'BEGGARS BANQUET': 110, 'BEL AIR CLASSIQUES': 111, 'BELGA FILMS': 112, 'BELVEDERE': 113, 'BENELUX FILM DISTRIBUTORS': 114, 'BENNETT-WATT MEDIA': 115, 'BERLIN CLASSICS': 116, 'BERLINER PHILHARMONIKER RECORDINGS': 117, 'BEST ENTERTAINMENT': 118, 'BEYOND HOME ENTERTAINMENT': 119, 'BFI VIDEO': 120, 'BFI': 120, 'BRITISH FILM INSTITUTE': 120, 'BFS ENTERTAINMENT': 121, 'BFS': 121, 'BHAVANI': 122, 'BIBER RECORDS': 123, 'BIG HOME VIDEO': 124, 'BILDSTÖRUNG': 125, 'BILDSTORUNG': 125, 'BILL ZEBUB': 126, 'BIRNENBLATT': 127, 'BIT WEL': 128, 'BLACK BOX': 129, 'BLACK HILL PICTURES': 130, 'BLACK HILL': 130, 'BLACK HOLE RECORDINGS': 131, 'BLACK HOLE': 131, 'BLAQOUT': 132, 'BLAUFIELD MUSIC': 133, 'BLAUFIELD': 133, 'BLOCKBUSTER ENTERTAINMENT': 134, 'BLOCKBUSTER': 134, 'BLU PHASE MEDIA': 135, 'BLU-RAY ONLY': 136, 'BLU-RAY': 136, 'BLURAY ONLY': 136, 'BLURAY': 136, 'BLUE GENTIAN RECORDS': 137, 'BLUE KINO': 138, 'BLUE UNDERGROUND': 139, 'BMG/ARISTA': 140, 'BMG': 140, 'BMGARISTA': 140, 'BMG ARISTA': 140, 'ARISTA': - 140, 'ARISTA/BMG': 140, 'ARISTABMG': 140, 'ARISTA BMG': 140, 'BONTON FILM': 141, 'BONTON': 141, 'BOOMERANG PICTURES': 142, 'BOOMERANG': 142, 'BQHL ÉDITIONS': 143, 'BQHL EDITIONS': 143, 'BQHL': 143, 'BREAKING GLASS': 144, 'BRIDGESTONE': 145, 'BRINK': 146, 'BROAD GREEN PICTURES': 147, 'BROAD GREEN': 147, 'BUSCH MEDIA GROUP': 148, 'BUSCH': 148, 'C MAJOR': 149, 'C.B.S.': 150, 'CAICHANG': 151, 'CALIFÓRNIA FILMES': 152, 'CALIFORNIA FILMES': 152, 'CALIFORNIA': 152, 'CAMEO': 153, 'CAMERA OBSCURA': 154, 'CAMERATA': 155, 'CAMP MOTION PICTURES': 156, 'CAMP MOTION': 156, 'CAPELIGHT PICTURES': 157, 'CAPELIGHT': 157, 'CAPITOL': 159, 'CAPITOL RECORDS': 159, 'CAPRICCI': 160, 'CARGO RECORDS': 161, 'CARLOTTA FILMS': 162, 'CARLOTTA': 162, 'CARLOTA': 162, 'CARMEN FILM': 163, 'CASCADE': 164, 'CATCHPLAY': 165, 'CAULDRON FILMS': 166, 'CAULDRON': 166, 'CBS TELEVISION STUDIOS': 167, 'CBS': 167, 'CCTV': 168, 'CCV ENTERTAINMENT': 169, 'CCV': 169, 'CD BABY': 170, 'CD LAND': 171, 'CECCHI GORI': 172, 'CENTURY MEDIA': 173, 'CHUAN XUN SHI DAI MULTIMEDIA': 174, 'CINE-ASIA': 175, 'CINÉART': 176, 'CINEART': 176, 'CINEDIGM': 177, 'CINEFIL IMAGICA': 178, 'CINEMA EPOCH': 179, 'CINEMA GUILD': 180, 'CINEMA LIBRE STUDIOS': 181, 'CINEMA MONDO': 182, 'CINEMATIC VISION': 183, 'CINEPLOIT RECORDS': 184, 'CINESTRANGE EXTREME': 185, 'CITEL VIDEO': 186, 'CITEL': 186, 'CJ ENTERTAINMENT': 187, 'CJ': 187, 'CLASSIC MEDIA': 188, 'CLASSICFLIX': 189, 'CLASSICLINE': 190, 'CLAUDIO RECORDS': 191, 'CLEAR VISION': 192, 'CLEOPATRA': 193, 'CLOSE UP': 194, 'CMS MEDIA LIMITED': 195, 'CMV LASERVISION': 196, 'CN ENTERTAINMENT': 197, 'CODE RED': 198, 'COHEN MEDIA GROUP': 199, 'COHEN': 199, 'COIN DE MIRE CINÉMA': 200, 'COIN DE MIRE CINEMA': 200, 'COLOSSEO FILM': 201, 'COLUMBIA': 203, 'COLUMBIA PICTURES': 203, 'COLUMBIA/TRI-STAR': 204, 'TRI-STAR': 204, 'COMMERCIAL MARKETING': 205, 'CONCORD MUSIC GROUP': 206, 'CONCORDE VIDEO': 207, 'CONDOR': 208, 'CONSTANTIN FILM': 209, 'CONSTANTIN': 209, 'CONSTANTINO FILMES': 210, 'CONSTANTINO': 210, 'CONSTRUCTIVE MEDIA SERVICE': 211, 'CONSTRUCTIVE': 211, 'CONTENT ZONE': 212, 'CONTENTS GATE': 213, 'COQUEIRO VERDE': 214, 'CORNERSTONE MEDIA': 215, 'CORNERSTONE': 215, 'CP DIGITAL': 216, 'CREST MOVIES': 217, 'CRITERION': 218, 'CRITERION COLLECTION': - 218, 'CC': 218, 'CRYSTAL CLASSICS': 219, 'CULT EPICS': 220, 'CULT FILMS': 221, 'CULT VIDEO': 222, 'CURZON FILM WORLD': 223, 'D FILMS': 224, "D'AILLY COMPANY": 225, 'DAILLY COMPANY': 225, 'D AILLY COMPANY': 225, "D'AILLY": 225, 'DAILLY': 225, 'D AILLY': 225, 'DA CAPO': 226, 'DA MUSIC': 227, "DALL'ANGELO PICTURES": 228, 'DALLANGELO PICTURES': 228, "DALL'ANGELO": 228, 'DALL ANGELO PICTURES': 228, 'DALL ANGELO': 228, 'DAREDO': 229, 'DARK FORCE ENTERTAINMENT': 230, 'DARK FORCE': 230, 'DARK SIDE RELEASING': 231, 'DARK SIDE': 231, 'DAZZLER MEDIA': 232, 'DAZZLER': 232, 'DCM PICTURES': 233, 'DCM': 233, 'DEAPLANETA': 234, 'DECCA': 235, 'DEEPJOY': 236, 'DEFIANT SCREEN ENTERTAINMENT': 237, 'DEFIANT SCREEN': 237, 'DEFIANT': 237, 'DELOS': 238, 'DELPHIAN RECORDS': 239, 'DELPHIAN': 239, 'DELTA MUSIC & ENTERTAINMENT': 240, 'DELTA MUSIC AND ENTERTAINMENT': 240, 'DELTA MUSIC ENTERTAINMENT': 240, 'DELTA MUSIC': 240, 'DELTAMAC CO. LTD.': 241, 'DELTAMAC CO LTD': 241, 'DELTAMAC CO': 241, 'DELTAMAC': 241, 'DEMAND MEDIA': 242, 'DEMAND': 242, 'DEP': 243, 'DEUTSCHE GRAMMOPHON': 244, 'DFW': 245, 'DGM': 246, 'DIAPHANA': 247, 'DIGIDREAMS STUDIOS': 248, 'DIGIDREAMS': 248, 'DIGITAL ENVIRONMENTS': 249, 'DIGITAL': 249, 'DISCOTEK MEDIA': 250, 'DISCOVERY CHANNEL': 251, 'DISCOVERY': 251, 'DISK KINO': 252, 'DISNEY / BUENA VISTA': 253, 'DISNEY': 253, 'BUENA VISTA': 253, 'DISNEY BUENA VISTA': 253, 'DISTRIBUTION SELECT': 254, 'DIVISA': 255, 'DNC ENTERTAINMENT': 256, 'DNC': 256, 'DOGWOOF': 257, 'DOLMEN HOME VIDEO': 258, 'DOLMEN': 258, 'DONAU FILM': 259, 'DONAU': 259, 'DORADO FILMS': 260, 'DORADO': 260, 'DRAFTHOUSE FILMS': 261, 'DRAFTHOUSE': 261, 'DRAGON FILM ENTERTAINMENT': 262, 'DRAGON ENTERTAINMENT': 262, 'DRAGON FILM': 262, 'DRAGON': 262, 'DREAMWORKS': 263, 'DRIVE ON RECORDS': 264, 'DRIVE ON': 264, 'DRIVE-ON': 264, 'DRIVEON': 264, 'DS MEDIA': 265, 'DTP ENTERTAINMENT AG': 266, 'DTP ENTERTAINMENT': 266, 'DTP AG': 266, 'DTP': 266, 'DTS ENTERTAINMENT': 267, 'DTS': 267, 'DUKE MARKETING': 268, 'DUKE VIDEO DISTRIBUTION': 269, 'DUKE': 269, 'DUTCH FILMWORKS': 270, 'DUTCH': 270, 'DVD INTERNATIONAL': 271, 'DVD': 271, 'DYBEX': 272, 'DYNAMIC': 273, 'DYNIT': 274, 'E1 ENTERTAINMENT': 275, 'E1': 275, 'EAGLE ENTERTAINMENT': 276, 'EAGLE HOME ENTERTAINMENT PVT.LTD.': - 277, 'EAGLE HOME ENTERTAINMENT PVTLTD': 277, 'EAGLE HOME ENTERTAINMENT PVT LTD': 277, 'EAGLE HOME ENTERTAINMENT': 277, 'EAGLE PICTURES': 278, 'EAGLE ROCK ENTERTAINMENT': 279, 'EAGLE ROCK': 279, 'EAGLE VISION MEDIA': 280, 'EAGLE VISION': 280, 'EARMUSIC': 281, 'EARTH ENTERTAINMENT': 282, 'EARTH': 282, 'ECHO BRIDGE ENTERTAINMENT': 283, 'ECHO BRIDGE': 283, 'EDEL GERMANY GMBH': 284, 'EDEL GERMANY': 284, 'EDEL RECORDS': 285, 'EDITION TONFILM': 286, 'EDITIONS MONTPARNASSE': 287, 'EDKO FILMS LTD.': 288, 'EDKO FILMS LTD': 288, 'EDKO FILMS': 288, 'EDKO': 288, "EIN'S M&M CO": 289, 'EINS M&M CO': 289, "EIN'S M&M": 289, 'EINS M&M': 289, 'ELEA-MEDIA': 290, 'ELEA MEDIA': 290, 'ELEA': 290, 'ELECTRIC PICTURE': 291, 'ELECTRIC': 291, 'ELEPHANT FILMS': 292, 'ELEPHANT': 292, 'ELEVATION': 293, 'EMI': 294, 'EMON': 295, 'EMS': 296, 'EMYLIA': 297, 'ENE MEDIA': 298, 'ENE': 298, 'ENTERTAINMENT IN VIDEO': 299, 'ENTERTAINMENT IN': 299, 'ENTERTAINMENT ONE': 300, 'ENTERTAINMENT ONE FILMS CANADA INC.': 301, 'ENTERTAINMENT ONE FILMS CANADA INC': 301, 'ENTERTAINMENT ONE FILMS CANADA': 301, 'ENTERTAINMENT ONE CANADA INC': 301, - 'ENTERTAINMENT ONE CANADA': 301, 'ENTERTAINMENTONE': 302, 'EONE': 303, 'EOS': 304, 'EPIC PICTURES': 305, 'EPIC': 305, 'EPIC RECORDS': 306, 'ERATO': 307, 'EROS': 308, 'ESC EDITIONS': 309, 'ESCAPI MEDIA BV': 310, 'ESOTERIC RECORDINGS': 311, 'ESPN FILMS': 312, 'EUREKA ENTERTAINMENT': 313, 'EUREKA': 313, 'EURO PICTURES': 314, 'EURO VIDEO': 315, 'EUROARTS': 316, 'EUROPA FILMES': 317, 'EUROPA': 317, 'EUROPACORP': 318, 'EUROZOOM': 319, 'EXCEL': 320, 'EXPLOSIVE MEDIA': 321, 'EXPLOSIVE': 321, 'EXTRALUCID FILMS': 322, 'EXTRALUCID': 322, 'EYE SEE MOVIES': 323, 'EYE SEE': 323, 'EYK MEDIA': 324, 'EYK': 324, 'FABULOUS FILMS': 325, 'FABULOUS': 325, 'FACTORIS FILMS': 326, 'FACTORIS': 326, 'FARAO RECORDS': 327, 'FARBFILM HOME ENTERTAINMENT': 328, 'FARBFILM ENTERTAINMENT': 328, 'FARBFILM HOME': 328, 'FARBFILM': 328, 'FEELGOOD ENTERTAINMENT': 329, 'FEELGOOD': 329, 'FERNSEHJUWELEN': 330, 'FILM CHEST': 331, 'FILM MEDIA': 332, 'FILM MOVEMENT': 333, 'FILM4': 334, 'FILMART': 335, 'FILMAURO': 336, 'FILMAX': 337, 'FILMCONFECT HOME ENTERTAINMENT': 338, 'FILMCONFECT ENTERTAINMENT': 338, 'FILMCONFECT HOME': 338, 'FILMCONFECT': 338, 'FILMEDIA': 339, 'FILMJUWELEN': 340, 'FILMOTEKA NARODAWA': 341, 'FILMRISE': 342, 'FINAL CUT ENTERTAINMENT': 343, 'FINAL CUT': 343, 'FIREHOUSE 12 RECORDS': 344, 'FIREHOUSE 12': 344, 'FIRST INTERNATIONAL PRODUCTION': 345, 'FIRST INTERNATIONAL': 345, 'FIRST LOOK STUDIOS': 346, 'FIRST LOOK': 346, 'FLAGMAN TRADE': 347, 'FLASHSTAR FILMES': 348, 'FLASHSTAR': 348, 'FLICKER ALLEY': 349, 'FNC ADD CULTURE': 350, 'FOCUS FILMES': 351, 'FOCUS': 351, 'FOKUS MEDIA': 352, 'FOKUSA': 352, 'FOX PATHE EUROPA': 353, 'FOX PATHE': 353, 'FOX EUROPA': 353, 'FOX/MGM': 354, 'FOX MGM': 354, 'MGM': 354, 'MGM/FOX': 354, 'FOX': 354, 'FPE': 355, 'FRANCE TÉLÉVISIONS DISTRIBUTION': 356, 'FRANCE TELEVISIONS DISTRIBUTION': 356, 'FRANCE TELEVISIONS': 356, 'FRANCE': 356, 'FREE DOLPHIN ENTERTAINMENT': 357, 'FREE DOLPHIN': 357, 'FREESTYLE DIGITAL MEDIA': 358, 'FREESTYLE DIGITAL': 358, 'FREESTYLE': 358, 'FREMANTLE HOME ENTERTAINMENT': 359, 'FREMANTLE ENTERTAINMENT': 359, 'FREMANTLE HOME': 359, 'FREMANTL': 359, 'FRENETIC FILMS': 360, 'FRENETIC': 360, 'FRONTIER WORKS': 361, 'FRONTIER': 361, 'FRONTIERS MUSIC': 362, 'FRONTIERS RECORDS': 363, 'FS FILM OY': 364, 'FS FILM': - 364, 'FULL MOON FEATURES': 365, 'FULL MOON': 365, 'FUN CITY EDITIONS': 366, 'FUN CITY': 366, 'FUNIMATION ENTERTAINMENT': 367, 'FUNIMATION': 367, 'FUSION': 368, 'FUTUREFILM': 369, 'G2 PICTURES': 370, 'G2': 370, 'GAGA COMMUNICATIONS': 371, 'GAGA': 371, 'GAIAM': 372, 'GALAPAGOS': 373, 'GAMMA HOME ENTERTAINMENT': 374, 'GAMMA ENTERTAINMENT': 374, 'GAMMA HOME': 374, 'GAMMA': 374, 'GARAGEHOUSE PICTURES': 375, 'GARAGEHOUSE': 375, 'GARAGEPLAY (車庫娛樂)': 376, '車庫娛樂': 376, 'GARAGEPLAY (Che Ku Yu Le )': 376, 'GARAGEPLAY': 376, 'Che Ku Yu Le': 376, 'GAUMONT': 377, 'GEFFEN': 378, 'GENEON ENTERTAINMENT': 379, 'GENEON': 379, 'GENEON UNIVERSAL ENTERTAINMENT': 380, 'GENERAL VIDEO RECORDING': 381, 'GLASS DOLL FILMS': 382, 'GLASS DOLL': 382, 'GLOBE MUSIC MEDIA': 383, 'GLOBE MUSIC': 383, 'GLOBE MEDIA': 383, 'GLOBE': 383, 'GO ENTERTAIN': 384, 'GO': 384, 'GOLDEN HARVEST': 385, 'GOOD!MOVIES': 386, - 'GOOD! MOVIES': 386, 'GOOD MOVIES': 386, 'GRAPEVINE VIDEO': 387, 'GRAPEVINE': 387, 'GRASSHOPPER FILM': 388, 'GRASSHOPPER FILMS': 388, 'GRASSHOPPER': 388, 'GRAVITAS VENTURES': 389, 'GRAVITAS': 389, 'GREAT MOVIES': 390, 'GREAT': 390, - 'GREEN APPLE ENTERTAINMENT': 391, 'GREEN ENTERTAINMENT': 391, 'GREEN APPLE': 391, 'GREEN': 391, 'GREENNARAE MEDIA': 392, 'GREENNARAE': 392, 'GRINDHOUSE RELEASING': 393, 'GRINDHOUSE': 393, 'GRIND HOUSE': 393, 'GRYPHON ENTERTAINMENT': 394, 'GRYPHON': 394, 'GUNPOWDER & SKY': 395, 'GUNPOWDER AND SKY': 395, 'GUNPOWDER SKY': 395, 'GUNPOWDER + SKY': 395, 'GUNPOWDER': 395, 'HANABEE ENTERTAINMENT': 396, 'HANABEE': 396, 'HANNOVER HOUSE': 397, 'HANNOVER': 397, 'HANSESOUND': 398, 'HANSE SOUND': 398, 'HANSE': 398, 'HAPPINET': 399, 'HARMONIA MUNDI': 400, 'HARMONIA': 400, 'HBO': 401, 'HDC': 402, 'HEC': 403, 'HELL & BACK RECORDINGS': 404, 'HELL AND BACK RECORDINGS': 404, 'HELL & BACK': 404, 'HELL AND BACK': 404, "HEN'S TOOTH VIDEO": 405, 'HENS TOOTH VIDEO': 405, "HEN'S TOOTH": 405, 'HENS TOOTH': 405, 'HIGH FLIERS': 406, 'HIGHLIGHT': 407, 'HILLSONG': 408, 'HISTORY CHANNEL': 409, 'HISTORY': 409, 'HK VIDÉO': 410, 'HK VIDEO': 410, 'HK': 410, 'HMH HAMBURGER MEDIEN HAUS': 411, 'HAMBURGER MEDIEN HAUS': 411, 'HMH HAMBURGER MEDIEN': 411, 'HMH HAMBURGER': 411, 'HMH': 411, 'HOLLYWOOD CLASSIC ENTERTAINMENT': 412, 'HOLLYWOOD CLASSIC': 412, 'HOLLYWOOD PICTURES': 413, 'HOLLYWOOD': 413, 'HOPSCOTCH ENTERTAINMENT': 414, 'HOPSCOTCH': 414, 'HPM': 415, 'HÄNNSLER CLASSIC': 416, 'HANNSLER CLASSIC': 416, 'HANNSLER': 416, 'I-CATCHER': 417, 'I CATCHER': 417, 'ICATCHER': 417, 'I-ON NEW MEDIA': 418, 'I ON NEW MEDIA': 418, 'ION NEW MEDIA': 418, 'ION MEDIA': 418, 'I-ON': 418, 'ION': 418, 'IAN PRODUCTIONS': 419, 'IAN': 419, 'ICESTORM': 420, 'ICON FILM DISTRIBUTION': 421, 'ICON DISTRIBUTION': 421, 'ICON FILM': 421, 'ICON': 421, 'IDEALE AUDIENCE': 422, 'IDEALE': 422, 'IFC FILMS': 423, 'IFC': 423, 'IFILM': 424, 'ILLUSIONS UNLTD.': 425, 'ILLUSIONS UNLTD': 425, 'ILLUSIONS': 425, 'IMAGE ENTERTAINMENT': 426, 'IMAGE': 426, - 'IMAGEM FILMES': 427, 'IMAGEM': 427, 'IMOVISION': 428, 'IMPERIAL CINEPIX': 429, 'IMPRINT': 430, 'IMPULS HOME ENTERTAINMENT': 431, 'IMPULS ENTERTAINMENT': 431, 'IMPULS HOME': 431, 'IMPULS': 431, 'IN-AKUSTIK': 432, 'IN AKUSTIK': 432, 'INAKUSTIK': 432, 'INCEPTION MEDIA GROUP': 433, 'INCEPTION MEDIA': 433, 'INCEPTION GROUP': 433, 'INCEPTION': 433, 'INDEPENDENT': 434, 'INDICAN': 435, 'INDIE RIGHTS': 436, 'INDIE': 436, 'INDIGO': 437, 'INFO': 438, 'INJOINGAN': 439, 'INKED PICTURES': 440, 'INKED': 440, 'INSIDE OUT MUSIC': 441, 'INSIDE MUSIC': 441, 'INSIDE OUT': 441, 'INSIDE': 441, 'INTERCOM': 442, 'INTERCONTINENTAL VIDEO': 443, 'INTERCONTINENTAL': 443, 'INTERGROOVE': 444, - 'INTERSCOPE': 445, 'INVINCIBLE PICTURES': 446, 'INVINCIBLE': 446, 'ISLAND/MERCURY': 447, 'ISLAND MERCURY': 447, 'ISLANDMERCURY': 447, 'ISLAND & MERCURY': 447, 'ISLAND AND MERCURY': 447, 'ISLAND': 447, 'ITN': 448, 'ITV DVD': 449, 'ITV': 449, 'IVC': 450, 'IVE ENTERTAINMENT': 451, 'IVE': 451, 'J&R ADVENTURES': 452, 'J&R': 452, 'JR': 452, 'JAKOB': 453, 'JONU MEDIA': 454, 'JONU': 454, 'JRB PRODUCTIONS': 455, 'JRB': 455, 'JUST BRIDGE ENTERTAINMENT': 456, 'JUST BRIDGE': 456, 'JUST ENTERTAINMENT': 456, 'JUST': 456, 'KABOOM ENTERTAINMENT': 457, 'KABOOM': 457, 'KADOKAWA ENTERTAINMENT': 458, 'KADOKAWA': 458, 'KAIROS': 459, 'KALEIDOSCOPE ENTERTAINMENT': 460, 'KALEIDOSCOPE': 460, 'KAM & RONSON ENTERPRISES': 461, 'KAM & RONSON': 461, 'KAM&RONSON ENTERPRISES': 461, 'KAM&RONSON': 461, 'KAM AND RONSON ENTERPRISES': 461, 'KAM AND RONSON': 461, 'KANA HOME VIDEO': 462, 'KARMA FILMS': 463, 'KARMA': 463, 'KATZENBERGER': 464, 'KAZE': 465, 'KBS MEDIA': 466, 'KBS': 466, 'KD MEDIA': 467, 'KD': 467, 'KING MEDIA': 468, 'KING': 468, 'KING RECORDS': 469, 'KINO LORBER': 470, 'KINO': 470, 'KINO SWIAT': 471, 'KINOKUNIYA': 472, 'KINOWELT HOME ENTERTAINMENT/DVD': 473, 'KINOWELT HOME ENTERTAINMENT': 473, 'KINOWELT ENTERTAINMENT': 473, 'KINOWELT HOME DVD': 473, 'KINOWELT ENTERTAINMENT/DVD': 473, 'KINOWELT DVD': 473, 'KINOWELT': 473, 'KIT PARKER FILMS': 474, 'KIT PARKER': 474, 'KITTY MEDIA': 475, 'KNM HOME ENTERTAINMENT': 476, 'KNM ENTERTAINMENT': 476, 'KNM HOME': 476, 'KNM': 476, 'KOBA FILMS': 477, 'KOBA': 477, 'KOCH ENTERTAINMENT': 478, 'KOCH MEDIA': 479, 'KOCH': 479, 'KRAKEN RELEASING': 480, 'KRAKEN': 480, 'KSCOPE': 481, 'KSM': 482, 'KULTUR': 483, "L'ATELIER D'IMAGES": 484, "LATELIER D'IMAGES": 484, "L'ATELIER DIMAGES": 484, 'LATELIER DIMAGES': 484, "L ATELIER D'IMAGES": 484, "L'ATELIER D IMAGES": 484, - 'L ATELIER D IMAGES': 484, "L'ATELIER": 484, 'L ATELIER': 484, 'LATELIER': 484, 'LA AVENTURA AUDIOVISUAL': 485, 'LA AVENTURA': 485, 'LACE GROUP': 486, 'LACE': 486, 'LASER PARADISE': 487, 'LAYONS': 488, 'LCJ EDITIONS': 489, 'LCJ': 489, 'LE CHAT QUI FUME': 490, 'LE PACTE': 491, 'LEDICK FILMHANDEL': 492, 'LEGEND': 493, 'LEOMARK STUDIOS': 494, 'LEOMARK': 494, 'LEONINE FILMS': 495, 'LEONINE': 495, 'LICHTUNG MEDIA LTD': 496, 'LICHTUNG LTD': 496, 'LICHTUNG MEDIA LTD.': 496, 'LICHTUNG LTD.': 496, 'LICHTUNG MEDIA': 496, 'LICHTUNG': 496, 'LIGHTHOUSE HOME ENTERTAINMENT': 497, 'LIGHTHOUSE ENTERTAINMENT': 497, 'LIGHTHOUSE HOME': 497, 'LIGHTHOUSE': 497, 'LIGHTYEAR': 498, 'LIONSGATE FILMS': 499, 'LIONSGATE': 499, 'LIZARD CINEMA TRADE': 500, 'LLAMENTOL': 501, 'LOBSTER FILMS': 502, 'LOBSTER': 502, 'LOGON': 503, 'LORBER FILMS': 504, 'LORBER': 504, 'LOS BANDITOS FILMS': 505, 'LOS BANDITOS': 505, 'LOUD & PROUD RECORDS': 506, 'LOUD AND PROUD RECORDS': 506, 'LOUD & PROUD': 506, 'LOUD AND PROUD': 506, 'LSO LIVE': 507, 'LUCASFILM': 508, 'LUCKY RED': 509, 'LUMIÈRE HOME ENTERTAINMENT': 510, 'LUMIERE HOME ENTERTAINMENT': 510, 'LUMIERE ENTERTAINMENT': 510, 'LUMIERE HOME': 510, 'LUMIERE': 510, 'M6 VIDEO': 511, 'M6': 511, 'MAD DIMENSION': 512, 'MADMAN ENTERTAINMENT': 513, 'MADMAN': 513, 'MAGIC BOX': 514, 'MAGIC PLAY': 515, 'MAGNA HOME ENTERTAINMENT': 516, 'MAGNA ENTERTAINMENT': 516, 'MAGNA HOME': 516, 'MAGNA': 516, 'MAGNOLIA PICTURES': 517, 'MAGNOLIA': 517, 'MAIDEN JAPAN': 518, 'MAIDEN': 518, 'MAJENG MEDIA': 519, 'MAJENG': 519, 'MAJESTIC HOME ENTERTAINMENT': 520, 'MAJESTIC ENTERTAINMENT': 520, 'MAJESTIC HOME': 520, 'MAJESTIC': 520, 'MANGA HOME ENTERTAINMENT': 521, 'MANGA ENTERTAINMENT': 521, 'MANGA HOME': 521, 'MANGA': 521, 'MANTA LAB': 522, 'MAPLE STUDIOS': 523, 'MAPLE': 523, 'MARCO POLO PRODUCTION': - 524, 'MARCO POLO': 524, 'MARIINSKY': 525, 'MARVEL STUDIOS': 526, 'MARVEL': 526, 'MASCOT RECORDS': 527, 'MASCOT': 527, 'MASSACRE VIDEO': 528, 'MASSACRE': 528, 'MATCHBOX': 529, 'MATRIX D': 530, 'MAXAM': 531, 'MAYA HOME ENTERTAINMENT': 532, 'MAYA ENTERTAINMENT': 532, 'MAYA HOME': 532, 'MAYAT': 532, 'MDG': 533, 'MEDIA BLASTERS': 534, 'MEDIA FACTORY': 535, 'MEDIA TARGET DISTRIBUTION': 536, 'MEDIA TARGET': 536, 'MEDIAINVISION': 537, 'MEDIATOON': 538, 'MEDIATRES ESTUDIO': 539, 'MEDIATRES STUDIO': 539, 'MEDIATRES': 539, 'MEDICI ARTS': 540, 'MEDICI CLASSICS': 541, 'MEDIUMRARE ENTERTAINMENT': 542, 'MEDIUMRARE': 542, 'MEDUSA': 543, 'MEGASTAR': 544, 'MEI AH': 545, 'MELI MÉDIAS': 546, 'MELI MEDIAS': 546, 'MEMENTO FILMS': 547, 'MEMENTO': 547, 'MENEMSHA FILMS': 548, 'MENEMSHA': 548, 'MERCURY': 549, 'MERCURY STUDIOS': 550, 'MERGE SOFT PRODUCTIONS': 551, 'MERGE PRODUCTIONS': 551, 'MERGE SOFT': 551, 'MERGE': 551, 'METAL BLADE RECORDS': 552, 'METAL BLADE': 552, 'METEOR': 553, 'METRO-GOLDWYN-MAYER': 554, 'METRO GOLDWYN MAYER': 554, 'METROGOLDWYNMAYER': 554, 'METRODOME VIDEO': 555, 'METRODOME': 555, 'METROPOLITAN': 556, 'MFA+': - 557, 'MFA': 557, 'MIG FILMGROUP': 558, 'MIG': 558, 'MILESTONE': 559, 'MILL CREEK ENTERTAINMENT': 560, 'MILL CREEK': 560, 'MILLENNIUM MEDIA': 561, 'MILLENNIUM': 561, 'MIRAGE ENTERTAINMENT': 562, 'MIRAGE': 562, 'MIRAMAX': 563, - 'MISTERIYA ZVUKA': 564, 'MK2': 565, 'MODE RECORDS': 566, 'MODE': 566, 'MOMENTUM PICTURES': 567, 'MONDO HOME ENTERTAINMENT': 568, 'MONDO ENTERTAINMENT': 568, 'MONDO HOME': 568, 'MONDO MACABRO': 569, 'MONGREL MEDIA': 570, 'MONOLIT': 571, 'MONOLITH VIDEO': 572, 'MONOLITH': 572, 'MONSTER PICTURES': 573, 'MONSTER': 573, 'MONTEREY VIDEO': 574, 'MONTEREY': 574, 'MONUMENT RELEASING': 575, 'MONUMENT': 575, 'MORNINGSTAR': 576, 'MORNING STAR': 576, 'MOSERBAER': 577, 'MOVIEMAX': 578, 'MOVINSIDE': 579, 'MPI MEDIA GROUP': 580, 'MPI MEDIA': 580, 'MPI': 580, 'MR. BONGO FILMS': 581, 'MR BONGO FILMS': 581, 'MR BONGO': 581, 'MRG (MERIDIAN)': 582, 'MRG MERIDIAN': 582, 'MRG': 582, 'MERIDIAN': 582, 'MUBI': 583, 'MUG SHOT PRODUCTIONS': 584, 'MUG SHOT': 584, 'MULTIMUSIC': 585, 'MULTI-MUSIC': 585, 'MULTI MUSIC': 585, 'MUSE': 586, 'MUSIC BOX FILMS': 587, 'MUSIC BOX': 587, 'MUSICBOX': 587, 'MUSIC BROKERS': 588, 'MUSIC THEORIES': 589, 'MUSIC VIDEO DISTRIBUTORS': 590, 'MUSIC VIDEO': 590, 'MUSTANG ENTERTAINMENT': 591, 'MUSTANG': 591, 'MVD VISUAL': 592, 'MVD': 592, 'MVD/VSC': 593, 'MVL': 594, 'MVM ENTERTAINMENT': 595, 'MVM': 595, 'MYNDFORM': 596, 'MYSTIC NIGHT PICTURES': 597, 'MYSTIC NIGHT': 597, 'NAMELESS MEDIA': 598, 'NAMELESS': 598, 'NAPALM RECORDS': 599, 'NAPALM': 599, 'NATIONAL ENTERTAINMENT MEDIA': 600, 'NATIONAL ENTERTAINMENT': 600, 'NATIONAL MEDIA': 600, 'NATIONAL FILM ARCHIVE': 601, 'NATIONAL ARCHIVE': 601, 'NATIONAL FILM': 601, 'NATIONAL GEOGRAPHIC': 602, 'NAT GEO TV': 602, 'NAT GEO': 602, 'NGO': 602, 'NAXOS': 603, 'NBCUNIVERSAL ENTERTAINMENT JAPAN': 604, 'NBC UNIVERSAL ENTERTAINMENT JAPAN': 604, 'NBCUNIVERSAL JAPAN': 604, 'NBC UNIVERSAL JAPAN': 604, 'NBC JAPAN': 604, 'NBO ENTERTAINMENT': 605, 'NBO': 605, 'NEOS': 606, 'NETFLIX': 607, 'NETWORK': 608, 'NEW BLOOD': 609, 'NEW DISC': 610, 'NEW KSM': 611, 'NEW LINE CINEMA': 612, 'NEW LINE': 612, 'NEW MOVIE TRADING CO. LTD': 613, 'NEW MOVIE TRADING CO LTD': 613, 'NEW MOVIE TRADING CO': 613, 'NEW MOVIE TRADING': 613, 'NEW WAVE FILMS': 614, 'NEW WAVE': 614, 'NFI': 615, - 'NHK': 616, 'NIPPONART': 617, 'NIS AMERICA': 618, 'NJUTAFILMS': 619, 'NOBLE ENTERTAINMENT': 620, 'NOBLE': 620, 'NORDISK FILM': 621, 'NORDISK': 621, 'NORSK FILM': 622, 'NORSK': 622, 'NORTH AMERICAN MOTION PICTURES': 623, 'NOS AUDIOVISUAIS': 624, 'NOTORIOUS PICTURES': 625, 'NOTORIOUS': 625, 'NOVA MEDIA': 626, 'NOVA': 626, 'NOVA SALES AND DISTRIBUTION': 627, 'NOVA SALES & DISTRIBUTION': 627, 'NSM': 628, 'NSM RECORDS': 629, 'NUCLEAR BLAST': 630, 'NUCLEUS FILMS': 631, 'NUCLEUS': 631, 'OBERLIN MUSIC': 632, 'OBERLIN': 632, 'OBRAS-PRIMAS DO CINEMA': 633, 'OBRAS PRIMAS DO CINEMA': 633, 'OBRASPRIMAS DO CINEMA': 633, 'OBRAS-PRIMAS CINEMA': 633, 'OBRAS PRIMAS CINEMA': 633, 'OBRASPRIMAS CINEMA': 633, 'OBRAS-PRIMAS': 633, 'OBRAS PRIMAS': 633, 'OBRASPRIMAS': 633, 'ODEON': 634, 'OFDB FILMWORKS': 635, 'OFDB': 635, 'OLIVE FILMS': 636, 'OLIVE': 636, 'ONDINE': 637, 'ONSCREEN FILMS': 638, 'ONSCREEN': 638, 'OPENING DISTRIBUTION': 639, 'OPERA AUSTRALIA': 640, 'OPTIMUM HOME ENTERTAINMENT': 641, 'OPTIMUM ENTERTAINMENT': 641, 'OPTIMUM HOME': 641, 'OPTIMUM': 641, 'OPUS ARTE': 642, 'ORANGE STUDIO': 643, 'ORANGE': 643, 'ORLANDO EASTWOOD FILMS': 644, 'ORLANDO FILMS': 644, 'ORLANDO EASTWOOD': 644, 'ORLANDO': 644, 'ORUSTAK PICTURES': 645, 'ORUSTAK': 645, 'OSCILLOSCOPE PICTURES': 646, 'OSCILLOSCOPE': 646, 'OUTPLAY': 647, 'PALISADES TARTAN': 648, 'PAN VISION': 649, 'PANVISION': 649, 'PANAMINT CINEMA': 650, 'PANAMINT': 650, 'PANDASTORM ENTERTAINMENT': 651, 'PANDA STORM ENTERTAINMENT': 651, 'PANDASTORM': 651, 'PANDA STORM': 651, 'PANDORA FILM': 652, 'PANDORA': 652, 'PANEGYRIC': 653, 'PANORAMA': 654, 'PARADE DECK FILMS': 655, 'PARADE DECK': 655, 'PARADISE': 656, 'PARADISO FILMS': 657, 'PARADOX': 658, 'PARAMOUNT PICTURES': 659, 'PARAMOUNT': 659, 'PARIS FILMES': 660, 'PARIS FILMS': 660, 'PARIS': 660, 'PARK CIRCUS': 661, 'PARLOPHONE': 662, 'PASSION RIVER': 663, 'PATHE DISTRIBUTION': 664, 'PATHE': 664, 'PBS': 665, 'PEACE ARCH TRINITY': 666, 'PECCADILLO PICTURES': 667, 'PEPPERMINT': 668, 'PHASE 4 FILMS': 669, 'PHASE 4': 669, 'PHILHARMONIA BAROQUE': 670, 'PICTURE HOUSE ENTERTAINMENT': 671, 'PICTURE ENTERTAINMENT': 671, 'PICTURE HOUSE': 671, 'PICTURE': 671, 'PIDAX': 672, 'PINK FLOYD RECORDS': 673, 'PINK FLOYD': 673, 'PINNACLE FILMS': 674, 'PINNACLE': 674, 'PLAIN': 675, 'PLATFORM ENTERTAINMENT LIMITED': 676, 'PLATFORM ENTERTAINMENT LTD': 676, 'PLATFORM ENTERTAINMENT LTD.': 676, 'PLATFORM ENTERTAINMENT': 676, 'PLATFORM': 676, 'PLAYARTE': 677, 'PLG UK CLASSICS': 678, 'PLG UK': - 678, 'PLG': 678, 'POLYBAND & TOPPIC VIDEO/WVG': 679, 'POLYBAND AND TOPPIC VIDEO/WVG': 679, 'POLYBAND & TOPPIC VIDEO WVG': 679, 'POLYBAND & TOPPIC VIDEO AND WVG': 679, 'POLYBAND & TOPPIC VIDEO & WVG': 679, 'POLYBAND AND TOPPIC VIDEO WVG': 679, 'POLYBAND AND TOPPIC VIDEO AND WVG': 679, 'POLYBAND AND TOPPIC VIDEO & WVG': 679, 'POLYBAND & TOPPIC VIDEO': 679, 'POLYBAND AND TOPPIC VIDEO': 679, 'POLYBAND & TOPPIC': 679, 'POLYBAND AND TOPPIC': 679, 'POLYBAND': 679, 'WVG': 679, 'POLYDOR': 680, 'PONY': 681, 'PONY CANYON': 682, 'POTEMKINE': 683, 'POWERHOUSE FILMS': 684, 'POWERHOUSE': 684, 'POWERSTATIOM': 685, 'PRIDE & JOY': 686, 'PRIDE AND JOY': 686, 'PRINZ MEDIA': 687, 'PRINZ': 687, 'PRIS AUDIOVISUAIS': 688, 'PRO VIDEO': 689, 'PRO-VIDEO': 689, 'PRO-MOTION': 690, 'PRO MOTION': 690, 'PROD. JRB': 691, 'PROD JRB': 691, 'PRODISC': 692, 'PROKINO': 693, 'PROVOGUE RECORDS': 694, 'PROVOGUE': 694, 'PROWARE': 695, 'PULP VIDEO': 696, 'PULP': 696, 'PULSE VIDEO': 697, 'PULSE': 697, 'PURE AUDIO RECORDINGS': 698, 'PURE AUDIO': 698, 'PURE FLIX ENTERTAINMENT': 699, 'PURE FLIX': 699, 'PURE ENTERTAINMENT': 699, 'PYRAMIDE VIDEO': 700, 'PYRAMIDE': 700, 'QUALITY FILMS': 701, 'QUALITY': 701, 'QUARTO VALLEY RECORDS': 702, 'QUARTO VALLEY': 702, 'QUESTAR': 703, 'R SQUARED FILMS': 704, 'R SQUARED': 704, 'RAPID EYE MOVIES': 705, 'RAPID EYE': 705, 'RARO VIDEO': 706, 'RARO': 706, 'RAROVIDEO U.S.': 707, 'RAROVIDEO US': 707, 'RARO VIDEO US': 707, 'RARO VIDEO U.S.': 707, 'RARO U.S.': 707, 'RARO US': 707, 'RAVEN BANNER RELEASING': 708, 'RAVEN BANNER': 708, 'RAVEN': 708, 'RAZOR DIGITAL ENTERTAINMENT': 709, 'RAZOR DIGITAL': 709, 'RCA': 710, 'RCO LIVE': 711, 'RCO': 711, 'RCV': 712, 'REAL GONE MUSIC': 713, 'REAL GONE': 713, 'REANIMEDIA': 714, 'REANI MEDIA': 714, 'REDEMPTION': 715, 'REEL': 716, 'RELIANCE HOME VIDEO & GAMES': 717, 'RELIANCE HOME VIDEO AND GAMES': 717, 'RELIANCE HOME VIDEO': 717, 'RELIANCE VIDEO': 717, 'RELIANCE HOME': 717, 'RELIANCE': 717, 'REM CULTURE': 718, 'REMAIN IN LIGHT': 719, 'REPRISE': 720, 'RESEN': 721, 'RETROMEDIA': 722, 'REVELATION FILMS LTD.': 723, 'REVELATION FILMS LTD': 723, 'REVELATION FILMS': 723, 'REVELATION LTD.': 723, 'REVELATION LTD': 723, 'REVELATION': 723, 'REVOLVER ENTERTAINMENT': 724, 'REVOLVER': 724, 'RHINO MUSIC': 725, 'RHINO': 725, 'RHV': 726, 'RIGHT STUF': 727, 'RIMINI EDITIONS': 728, 'RISING SUN MEDIA': 729, 'RLJ ENTERTAINMENT': 730, 'RLJ': 730, 'ROADRUNNER RECORDS': 731, 'ROADSHOW ENTERTAINMENT': 732, 'ROADSHOW': 732, 'RONE': 733, 'RONIN FLIX': 734, 'ROTANA HOME ENTERTAINMENT': 735, 'ROTANA ENTERTAINMENT': 735, 'ROTANA HOME': 735, 'ROTANA': 735, 'ROUGH TRADE': 736, 'ROUNDER': 737, 'SAFFRON HILL FILMS': 738, 'SAFFRON HILL': 738, 'SAFFRON': 738, 'SAMUEL GOLDWYN FILMS': 739, 'SAMUEL GOLDWYN': 739, 'SAN FRANCISCO SYMPHONY': 740, 'SANDREW METRONOME': 741, 'SAPHRANE': 742, 'SAVOR': 743, 'SCANBOX ENTERTAINMENT': 744, 'SCANBOX': 744, 'SCENIC LABS': 745, 'SCHRÖDERMEDIA': 746, 'SCHRODERMEDIA': 746, 'SCHRODER MEDIA': 746, 'SCORPION RELEASING': 747, 'SCORPION': 747, 'SCREAM TEAM RELEASING': 748, 'SCREAM TEAM': 748, 'SCREEN MEDIA': 749, 'SCREEN': 749, 'SCREENBOUND PICTURES': 750, 'SCREENBOUND': 750, 'SCREENWAVE MEDIA': 751, 'SCREENWAVE': 751, 'SECOND RUN': 752, 'SECOND SIGHT': 753, 'SEEDSMAN GROUP': 754, 'SELECT VIDEO': 755, 'SELECTA VISION': 756, 'SENATOR': 757, 'SENTAI FILMWORKS': 758, 'SENTAI': 758, 'SEVEN7': 759, 'SEVERIN FILMS': 760, 'SEVERIN': 760, 'SEVILLE': 761, 'SEYONS ENTERTAINMENT': 762, 'SEYONS': 762, 'SF STUDIOS': 763, 'SGL ENTERTAINMENT': 764, 'SGL': 764, 'SHAMELESS': 765, 'SHAMROCK MEDIA': 766, 'SHAMROCK': 766, 'SHANGHAI EPIC MUSIC ENTERTAINMENT': 767, 'SHANGHAI EPIC ENTERTAINMENT': 767, 'SHANGHAI EPIC MUSIC': 767, 'SHANGHAI MUSIC ENTERTAINMENT': 767, 'SHANGHAI ENTERTAINMENT': 767, 'SHANGHAI MUSIC': 767, 'SHANGHAI': 767, 'SHEMAROO': 768, 'SHOCHIKU': 769, 'SHOCK': 770, 'SHOGAKU KAN': 771, 'SHOUT FACTORY': 772, 'SHOUT! FACTORY': 772, 'SHOUT': 772, 'SHOUT!': 772, 'SHOWBOX': 773, 'SHOWTIME ENTERTAINMENT': 774, 'SHOWTIME': 774, 'SHRIEK SHOW': 775, 'SHUDDER': 776, 'SIDONIS': 777, 'SIDONIS CALYSTA': 778, 'SIGNAL ONE ENTERTAINMENT': 779, 'SIGNAL ONE': 779, 'SIGNATURE ENTERTAINMENT': 780, 'SIGNATURE': 780, 'SILVER VISION': 781, 'SINISTER FILM': 782, 'SINISTER': 782, 'SIREN VISUAL ENTERTAINMENT': 783, 'SIREN VISUAL': 783, 'SIREN ENTERTAINMENT': 783, 'SIREN': 783, 'SKANI': 784, 'SKY DIGI': 785, 'SLASHER // VIDEO': 786, 'SLASHER / VIDEO': 786, 'SLASHER VIDEO': 786, 'SLASHER': 786, 'SLOVAK FILM INSTITUTE': 787, 'SLOVAK FILM': 787, - 'SFI': 787, 'SM LIFE DESIGN GROUP': 788, 'SMOOTH PICTURES': 789, 'SMOOTH': 789, 'SNAPPER MUSIC': 790, 'SNAPPER': 790, 'SODA PICTURES': 791, 'SODA': 791, 'SONO LUMINUS': 792, 'SONY MUSIC': 793, 'SONY PICTURES': 794, 'SONY': 794, 'SONY PICTURES CLASSICS': 795, 'SONY CLASSICS': 795, 'SOUL MEDIA': 796, 'SOUL': 796, 'SOULFOOD MUSIC DISTRIBUTION': 797, 'SOULFOOD DISTRIBUTION': 797, 'SOULFOOD MUSIC': 797, 'SOULFOOD': 797, 'SOYUZ': 798, 'SPECTRUM': 799, - 'SPENTZOS FILM': 800, 'SPENTZOS': 800, 'SPIRIT ENTERTAINMENT': 801, 'SPIRIT': 801, 'SPIRIT MEDIA GMBH': 802, 'SPIRIT MEDIA': 802, 'SPLENDID ENTERTAINMENT': 803, 'SPLENDID FILM': 804, 'SPO': 805, 'SQUARE ENIX': 806, 'SRI BALAJI VIDEO': 807, 'SRI BALAJI': 807, 'SRI': 807, 'SRI VIDEO': 807, 'SRS CINEMA': 808, 'SRS': 808, 'SSO RECORDINGS': 809, 'SSO': 809, 'ST2 MUSIC': 810, 'ST2': 810, 'STAR MEDIA ENTERTAINMENT': 811, 'STAR ENTERTAINMENT': 811, 'STAR MEDIA': 811, 'STAR': 811, 'STARLIGHT': 812, 'STARZ / ANCHOR BAY': 813, 'STARZ ANCHOR BAY': 813, 'STARZ': 813, 'ANCHOR BAY': 813, 'STER KINEKOR': 814, 'STERLING ENTERTAINMENT': 815, 'STERLING': 815, 'STINGRAY': 816, 'STOCKFISCH RECORDS': 817, 'STOCKFISCH': 817, 'STRAND RELEASING': 818, 'STRAND': 818, 'STUDIO 4K': 819, 'STUDIO CANAL': 820, 'STUDIO GHIBLI': 821, 'GHIBLI': 821, 'STUDIO HAMBURG ENTERPRISES': 822, 'HAMBURG ENTERPRISES': 822, 'STUDIO HAMBURG': 822, 'HAMBURG': 822, 'STUDIO S': 823, 'SUBKULTUR ENTERTAINMENT': 824, 'SUBKULTUR': 824, 'SUEVIA FILMS': 825, 'SUEVIA': 825, 'SUMMIT ENTERTAINMENT': 826, 'SUMMIT': 826, 'SUNFILM ENTERTAINMENT': 827, 'SUNFILM': 827, 'SURROUND RECORDS': 828, 'SURROUND': 828, 'SVENSK FILMINDUSTRI': 829, 'SVENSK': 829, 'SWEN FILMES': 830, 'SWEN FILMS': 830, 'SWEN': 830, 'SYNAPSE FILMS': 831, 'SYNAPSE': 831, 'SYNDICADO': 832, 'SYNERGETIC': 833, 'T- SERIES': 834, 'T-SERIES': 834, 'T SERIES': 834, 'TSERIES': 834, 'T.V.P.': 835, 'TVP': 835, 'TACET RECORDS': 836, 'TACET': 836, 'TAI SENG': 837, 'TAI SHENG': 838, 'TAKEONE': 839, 'TAKESHOBO': 840, 'TAMASA DIFFUSION': 841, 'TC ENTERTAINMENT': 842, 'TC': 842, 'TDK': 843, 'TEAM MARKETING': 844, 'TEATRO REAL': 845, 'TEMA DISTRIBUCIONES': 846, 'TEMPE DIGITAL': 847, 'TF1 VIDÉO': 848, 'TF1 VIDEO': 848, 'TF1': 848, 'THE BLU': 849, 'BLU': 849, 'THE ECSTASY OF FILMS': 850, 'THE FILM DETECTIVE': 851, 'FILM DETECTIVE': 851, 'THE JOKERS': 852, 'JOKERS': 852, 'THE ON': 853, 'ON': 853, 'THIMFILM': 854, 'THIM FILM': 854, 'THIM': 854, 'THIRD WINDOW FILMS': 855, 'THIRD WINDOW': 855, '3RD WINDOW FILMS': 855, '3RD WINDOW': 855, 'THUNDERBEAN ANIMATION': 856, 'THUNDERBEAN': 856, 'THUNDERBIRD RELEASING': 857, 'THUNDERBIRD': 857, 'TIBERIUS FILM': 858, 'TIME LIFE': 859, 'TIMELESS MEDIA GROUP': 860, 'TIMELESS MEDIA': 860, 'TIMELESS GROUP': 860, 'TIMELESS': 860, 'TLA RELEASING': 861, 'TLA': 861, 'TOBIS FILM': 862, 'TOBIS': 862, 'TOEI': 863, 'TOHO': 864, 'TOKYO SHOCK': 865, 'TOKYO': 865, 'TONPOOL MEDIEN GMBH': 866, 'TONPOOL MEDIEN': 866, 'TOPICS ENTERTAINMENT': 867, 'TOPICS': 867, 'TOUCHSTONE PICTURES': 868, 'TOUCHSTONE': 868, 'TRANSMISSION FILMS': 869, 'TRANSMISSION': 869, 'TRAVEL VIDEO STORE': 870, 'TRIART': 871, 'TRIGON FILM': 872, 'TRIGON': 872, 'TRINITY HOME ENTERTAINMENT': 873, 'TRINITY ENTERTAINMENT': 873, 'TRINITY HOME': 873, 'TRINITY': 873, 'TRIPICTURES': 874, 'TRI-PICTURES': 874, 'TRI PICTURES': 874, 'TROMA': 875, 'TURBINE MEDIEN': 876, 'TURTLE RECORDS': 877, 'TURTLE': 877, 'TVA FILMS': 878, 'TVA': 878, 'TWILIGHT TIME': 879, 'TWILIGHT': 879, 'TT': 879, 'TWIN CO., LTD.': 880, 'TWIN CO, LTD.': 880, 'TWIN CO., LTD': 880, 'TWIN CO, LTD': 880, 'TWIN CO LTD': 880, 'TWIN LTD': 880, 'TWIN CO.': 880, 'TWIN CO': 880, 'TWIN': 880, 'UCA': 881, 'UDR': 882, 'UEK': 883, 'UFA/DVD': 884, 'UFA DVD': 884, 'UFADVD': 884, 'UGC PH': 885, 'ULTIMATE3DHEAVEN': 886, 'ULTRA': 887, 'UMBRELLA ENTERTAINMENT': 888, 'UMBRELLA': 888, 'UMC': 889, "UNCORK'D ENTERTAINMENT": 890, 'UNCORKD ENTERTAINMENT': 890, 'UNCORK D ENTERTAINMENT': 890, "UNCORK'D": 890, 'UNCORK D': 890, 'UNCORKD': 890, 'UNEARTHED FILMS': 891, 'UNEARTHED': 891, 'UNI DISC': 892, 'UNIMUNDOS': 893, 'UNITEL': 894, 'UNIVERSAL MUSIC': 895, 'UNIVERSAL SONY PICTURES HOME ENTERTAINMENT': 896, 'UNIVERSAL SONY PICTURES ENTERTAINMENT': 896, 'UNIVERSAL SONY PICTURES HOME': 896, 'UNIVERSAL SONY PICTURES': 896, 'UNIVERSAL HOME ENTERTAINMENT': - 896, 'UNIVERSAL ENTERTAINMENT': 896, 'UNIVERSAL HOME': 896, 'UNIVERSAL STUDIOS': 897, 'UNIVERSAL': 897, 'UNIVERSE LASER & VIDEO CO.': 898, 'UNIVERSE LASER AND VIDEO CO.': 898, 'UNIVERSE LASER & VIDEO CO': 898, 'UNIVERSE LASER AND VIDEO CO': 898, 'UNIVERSE LASER CO.': 898, 'UNIVERSE LASER CO': 898, 'UNIVERSE LASER': 898, 'UNIVERSUM FILM': 899, 'UNIVERSUM': 899, 'UTV': 900, 'VAP': 901, 'VCI': 902, 'VENDETTA FILMS': 903, 'VENDETTA': 903, 'VERSÁTIL HOME VIDEO': 904, 'VERSÁTIL VIDEO': 904, 'VERSÁTIL HOME': 904, 'VERSÁTIL': 904, 'VERSATIL HOME VIDEO': 904, 'VERSATIL VIDEO': 904, 'VERSATIL HOME': 904, 'VERSATIL': 904, 'VERTICAL ENTERTAINMENT': 905, 'VERTICAL': 905, 'VÉRTICE 360º': 906, 'VÉRTICE 360': 906, 'VERTICE 360o': 906, 'VERTICE 360': 906, 'VERTIGO BERLIN': 907, 'VÉRTIGO FILMS': 908, 'VÉRTIGO': 908, 'VERTIGO FILMS': 908, 'VERTIGO': 908, 'VERVE PICTURES': 909, 'VIA VISION ENTERTAINMENT': 910, 'VIA VISION': 910, 'VICOL ENTERTAINMENT': 911, 'VICOL': 911, 'VICOM': 912, 'VICTOR ENTERTAINMENT': 913, 'VICTOR': 913, 'VIDEA CDE': 914, 'VIDEO FILM EXPRESS': 915, 'VIDEO FILM': 915, 'VIDEO EXPRESS': 915, 'VIDEO MUSIC, INC.': 916, 'VIDEO MUSIC, INC': 916, 'VIDEO MUSIC INC.': 916, 'VIDEO MUSIC INC': 916, 'VIDEO MUSIC': 916, 'VIDEO SERVICE CORP.': 917, 'VIDEO SERVICE CORP': 917, 'VIDEO SERVICE': 917, 'VIDEO TRAVEL': 918, 'VIDEOMAX': 919, 'VIDEO MAX': 919, 'VII PILLARS ENTERTAINMENT': 920, 'VII PILLARS': 920, 'VILLAGE FILMS': 921, 'VINEGAR SYNDROME': 922, 'VINEGAR': 922, 'VS': 922, 'VINNY MOVIES': 923, 'VINNY': 923, 'VIRGIL FILMS & ENTERTAINMENT': 924, 'VIRGIL FILMS AND ENTERTAINMENT': 924, 'VIRGIL ENTERTAINMENT': 924, 'VIRGIL FILMS': 924, 'VIRGIL': 924, 'VIRGIN RECORDS': 925, 'VIRGIN': 925, 'VISION FILMS': 926, 'VISION': 926, 'VISUAL ENTERTAINMENT GROUP': 927, 'VISUAL GROUP': 927, 'VISUAL ENTERTAINMENT': 927, 'VISUAL': 927, 'VIVENDI VISUAL ENTERTAINMENT': 928, 'VIVENDI VISUAL': 928, 'VIVENDI': 928, 'VIZ PICTURES': 929, 'VIZ': 929, 'VLMEDIA': 930, 'VL MEDIA': 930, 'VL': 930, 'VOLGA': 931, 'VVS FILMS': 932, - 'VVS': 932, 'VZ HANDELS GMBH': 933, 'VZ HANDELS': 933, 'WARD RECORDS': 934, 'WARD': 934, 'WARNER BROS.': 935, 'WARNER BROS': 935, 'WARNER ARCHIVE': 935, 'WARNER ARCHIVE COLLECTION': 935, 'WAC': 935, 'WARNER': 935, 'WARNER MUSIC': 936, 'WEA': 937, 'WEINSTEIN COMPANY': 938, 'WEINSTEIN': 938, 'WELL GO USA': 939, 'WELL GO': 939, 'WELTKINO FILMVERLEIH': 940, 'WEST VIDEO': 941, 'WEST': 941, 'WHITE PEARL MOVIES': 942, 'WHITE PEARL': 942, + '01 DISTRIBUTION': 1, '100 DESTINATIONS TRAVEL FILM': 2, '101 FILMS': 3, '1FILMS': 4, '2 ENTERTAIN VIDEO': 5, '20TH CENTURY FOX': 6, '2L': 7, '3D CONTENT HUB': 8, '3D MEDIA': 9, '3L FILM': 10, '4DIGITAL': 11, '4DVD': 12, '4K ULTRA HD MOVIES': 13, '4K UHD': 13, '8-FILMS': 14, '84 ENTERTAINMENT': 15, '88 FILMS': 16, '@ANIME': 17, 'ANIME': 17, 'A CONTRACORRIENTE': 18, 'A CONTRACORRIENTE FILMS': 19, 'A&E HOME VIDEO': 20, 'A&E': 20, 'A&M RECORDS': 21, 'A+E NETWORKS': 22, 'A+R': 23, 'A-FILM': 24, 'AAA': 25, 'AB VIDÉO': 26, 'AB VIDEO': 26, 'ABC - (AUSTRALIAN BROADCASTING CORPORATION)': 27, 'ABC': 27, 'ABKCO': 28, 'ABSOLUT MEDIEN': 29, 'ABSOLUTE': 30, 'ACCENT FILM ENTERTAINMENT': 31, 'ACCENTUS': 32, 'ACORN MEDIA': 33, 'AD VITAM': 34, 'ADA': 35, 'ADITYA VIDEOS': 36, 'ADSO FILMS': 37, 'AFM RECORDS': 38, 'AGFA': 39, 'AIX RECORDS': 40, 'ALAMODE FILM': 41, 'ALBA RECORDS': 42, 'ALBANY RECORDS': 43, 'ALBATROS': 44, 'ALCHEMY': 45, 'ALIVE': 46, 'ALL ANIME': 47, 'ALL INTERACTIVE ENTERTAINMENT': 48, 'ALLEGRO': 49, 'ALLIANCE': 50, 'ALPHA MUSIC': 51, 'ALTERDYSTRYBUCJA': 52, 'ALTERED INNOCENCE': 53, 'ALTITUDE FILM DISTRIBUTION': 54, 'ALUCARD RECORDS': 55, 'AMAZING D.C.': 56, 'AMAZING DC': 56, 'AMMO CONTENT': 57, 'AMUSE SOFT ENTERTAINMENT': 58, 'ANCONNECT': 59, 'ANEC': 60, 'ANIMATSU': 61, 'ANIME HOUSE': 62, 'ANIME LTD': 63, 'ANIME WORKS': 64, 'ANIMEIGO': 65, 'ANIPLEX': 66, 'ANOLIS ENTERTAINMENT': 67, 'ANOTHER WORLD ENTERTAINMENT': 68, 'AP INTERNATIONAL': 69, 'APPLE': 70, 'ARA MEDIA': 71, 'ARBELOS': 72, 'ARC ENTERTAINMENT': 73, 'ARP SÉLECTION': 74, 'ARP SELECTION': 74, 'ARROW': 75, 'ART SERVICE': 76, 'ART VISION': 77, 'ARTE ÉDITIONS': 78, 'ARTE EDITIONS': 78, 'ARTE VIDÉO': 79, 'ARTE VIDEO': 79, 'ARTHAUS MUSIK': 80, 'ARTIFICIAL EYE': 81, 'ARTSPLOITATION FILMS': 82, 'ARTUS FILMS': 83, 'ASCOT ELITE HOME ENTERTAINMENT': 84, 'ASIA VIDEO': 85, 'ASMIK ACE': 86, 'ASTRO RECORDS & FILMWORKS': 87, 'ASYLUM': 88, 'ATLANTIC FILM': 89, 'ATLANTIC RECORDS': 90, 'ATLAS FILM': 91, 'AUDIO VISUAL ENTERTAINMENT': 92, 'AURO-3D CREATIVE LABEL': 93, 'AURUM': 94, 'AV VISIONEN': 95, 'AV-JET': 96, 'AVALON': 97, 'AVENTI': 98, 'AVEX TRAX': 99, 'AXIOM': 100, 'AXIS RECORDS': 101, 'AYNGARAN': 102, 'BAC FILMS': 103, 'BACH FILMS': 104, 'BANDAI VISUAL': 105, 'BARCLAY': 106, 'BBC': 107, 'BRITISH BROADCASTING CORPORATION': 107, 'BBI FILMS': 108, 'BBI': 108, 'BCI HOME ENTERTAINMENT': 109, 'BEGGARS BANQUET': 110, 'BEL AIR CLASSIQUES': 111, 'BELGA FILMS': 112, 'BELVEDERE': 113, 'BENELUX FILM DISTRIBUTORS': 114, 'BENNETT-WATT MEDIA': 115, 'BERLIN CLASSICS': 116, 'BERLINER PHILHARMONIKER RECORDINGS': 117, 'BEST ENTERTAINMENT': 118, 'BEYOND HOME ENTERTAINMENT': 119, 'BFI VIDEO': 120, 'BFI': 120, 'BRITISH FILM INSTITUTE': 120, 'BFS ENTERTAINMENT': 121, 'BFS': 121, 'BHAVANI': 122, 'BIBER RECORDS': 123, 'BIG HOME VIDEO': 124, 'BILDSTÖRUNG': 125, 'BILDSTORUNG': 125, 'BILL ZEBUB': 126, 'BIRNENBLATT': 127, 'BIT WEL': 128, 'BLACK BOX': 129, 'BLACK HILL PICTURES': 130, 'BLACK HILL': 130, 'BLACK HOLE RECORDINGS': 131, 'BLACK HOLE': 131, 'BLAQOUT': 132, 'BLAUFIELD MUSIC': 133, 'BLAUFIELD': 133, 'BLOCKBUSTER ENTERTAINMENT': 134, 'BLOCKBUSTER': 134, 'BLU PHASE MEDIA': 135, 'BLU-RAY ONLY': 136, 'BLU-RAY': 136, 'BLURAY ONLY': 136, 'BLURAY': 136, 'BLUE GENTIAN RECORDS': 137, 'BLUE KINO': 138, 'BLUE UNDERGROUND': 139, 'BMG/ARISTA': 140, 'BMG': 140, 'BMGARISTA': 140, 'BMG ARISTA': 140, 'ARISTA': + 140, 'ARISTA/BMG': 140, 'ARISTABMG': 140, 'ARISTA BMG': 140, 'BONTON FILM': 141, 'BONTON': 141, 'BOOMERANG PICTURES': 142, 'BOOMERANG': 142, 'BQHL ÉDITIONS': 143, 'BQHL EDITIONS': 143, 'BQHL': 143, 'BREAKING GLASS': 144, 'BRIDGESTONE': 145, 'BRINK': 146, 'BROAD GREEN PICTURES': 147, 'BROAD GREEN': 147, 'BUSCH MEDIA GROUP': 148, 'BUSCH': 148, 'C MAJOR': 149, 'C.B.S.': 150, 'CAICHANG': 151, 'CALIFÓRNIA FILMES': 152, 'CALIFORNIA FILMES': 152, 'CALIFORNIA': 152, 'CAMEO': 153, 'CAMERA OBSCURA': 154, 'CAMERATA': 155, 'CAMP MOTION PICTURES': 156, 'CAMP MOTION': 156, 'CAPELIGHT PICTURES': 157, 'CAPELIGHT': 157, 'CAPITOL': 159, 'CAPITOL RECORDS': 159, 'CAPRICCI': 160, 'CARGO RECORDS': 161, 'CARLOTTA FILMS': 162, 'CARLOTTA': 162, 'CARLOTA': 162, 'CARMEN FILM': 163, 'CASCADE': 164, 'CATCHPLAY': 165, 'CAULDRON FILMS': 166, 'CAULDRON': 166, 'CBS TELEVISION STUDIOS': 167, 'CBS': 167, 'CCTV': 168, 'CCV ENTERTAINMENT': 169, 'CCV': 169, 'CD BABY': 170, 'CD LAND': 171, 'CECCHI GORI': 172, 'CENTURY MEDIA': 173, 'CHUAN XUN SHI DAI MULTIMEDIA': 174, 'CINE-ASIA': 175, 'CINÉART': 176, 'CINEART': 176, 'CINEDIGM': 177, 'CINEFIL IMAGICA': 178, 'CINEMA EPOCH': 179, 'CINEMA GUILD': 180, 'CINEMA LIBRE STUDIOS': 181, 'CINEMA MONDO': 182, 'CINEMATIC VISION': 183, 'CINEPLOIT RECORDS': 184, 'CINESTRANGE EXTREME': 185, 'CITEL VIDEO': 186, 'CITEL': 186, 'CJ ENTERTAINMENT': 187, 'CJ': 187, 'CLASSIC MEDIA': 188, 'CLASSICFLIX': 189, 'CLASSICLINE': 190, 'CLAUDIO RECORDS': 191, 'CLEAR VISION': 192, 'CLEOPATRA': 193, 'CLOSE UP': 194, 'CMS MEDIA LIMITED': 195, 'CMV LASERVISION': 196, 'CN ENTERTAINMENT': 197, 'CODE RED': 198, 'COHEN MEDIA GROUP': 199, 'COHEN': 199, 'COIN DE MIRE CINÉMA': 200, 'COIN DE MIRE CINEMA': 200, 'COLOSSEO FILM': 201, 'COLUMBIA': 203, 'COLUMBIA PICTURES': 203, 'COLUMBIA/TRI-STAR': 204, 'TRI-STAR': 204, 'COMMERCIAL MARKETING': 205, 'CONCORD MUSIC GROUP': 206, 'CONCORDE VIDEO': 207, 'CONDOR': 208, 'CONSTANTIN FILM': 209, 'CONSTANTIN': 209, 'CONSTANTINO FILMES': 210, 'CONSTANTINO': 210, 'CONSTRUCTIVE MEDIA SERVICE': 211, 'CONSTRUCTIVE': 211, 'CONTENT ZONE': 212, 'CONTENTS GATE': 213, 'COQUEIRO VERDE': 214, 'CORNERSTONE MEDIA': 215, 'CORNERSTONE': 215, 'CP DIGITAL': 216, 'CREST MOVIES': 217, 'CRITERION': 218, 'CRITERION COLLECTION': + 218, 'CC': 218, 'CRYSTAL CLASSICS': 219, 'CULT EPICS': 220, 'CULT FILMS': 221, 'CULT VIDEO': 222, 'CURZON FILM WORLD': 223, 'D FILMS': 224, "D'AILLY COMPANY": 225, 'DAILLY COMPANY': 225, 'D AILLY COMPANY': 225, "D'AILLY": 225, 'DAILLY': 225, 'D AILLY': 225, 'DA CAPO': 226, 'DA MUSIC': 227, "DALL'ANGELO PICTURES": 228, 'DALLANGELO PICTURES': 228, "DALL'ANGELO": 228, 'DALL ANGELO PICTURES': 228, 'DALL ANGELO': 228, 'DAREDO': 229, 'DARK FORCE ENTERTAINMENT': 230, 'DARK FORCE': 230, 'DARK SIDE RELEASING': 231, 'DARK SIDE': 231, 'DAZZLER MEDIA': 232, 'DAZZLER': 232, 'DCM PICTURES': 233, 'DCM': 233, 'DEAPLANETA': 234, 'DECCA': 235, 'DEEPJOY': 236, 'DEFIANT SCREEN ENTERTAINMENT': 237, 'DEFIANT SCREEN': 237, 'DEFIANT': 237, 'DELOS': 238, 'DELPHIAN RECORDS': 239, 'DELPHIAN': 239, 'DELTA MUSIC & ENTERTAINMENT': 240, 'DELTA MUSIC AND ENTERTAINMENT': 240, 'DELTA MUSIC ENTERTAINMENT': 240, 'DELTA MUSIC': 240, 'DELTAMAC CO. LTD.': 241, 'DELTAMAC CO LTD': 241, 'DELTAMAC CO': 241, 'DELTAMAC': 241, 'DEMAND MEDIA': 242, 'DEMAND': 242, 'DEP': 243, 'DEUTSCHE GRAMMOPHON': 244, 'DFW': 245, 'DGM': 246, 'DIAPHANA': 247, 'DIGIDREAMS STUDIOS': 248, 'DIGIDREAMS': 248, 'DIGITAL ENVIRONMENTS': 249, 'DIGITAL': 249, 'DISCOTEK MEDIA': 250, 'DISCOVERY CHANNEL': 251, 'DISCOVERY': 251, 'DISK KINO': 252, 'DISNEY / BUENA VISTA': 253, 'DISNEY': 253, 'BUENA VISTA': 253, 'DISNEY BUENA VISTA': 253, 'DISTRIBUTION SELECT': 254, 'DIVISA': 255, 'DNC ENTERTAINMENT': 256, 'DNC': 256, 'DOGWOOF': 257, 'DOLMEN HOME VIDEO': 258, 'DOLMEN': 258, 'DONAU FILM': 259, 'DONAU': 259, 'DORADO FILMS': 260, 'DORADO': 260, 'DRAFTHOUSE FILMS': 261, 'DRAFTHOUSE': 261, 'DRAGON FILM ENTERTAINMENT': 262, 'DRAGON ENTERTAINMENT': 262, 'DRAGON FILM': 262, 'DRAGON': 262, 'DREAMWORKS': 263, 'DRIVE ON RECORDS': 264, 'DRIVE ON': 264, 'DRIVE-ON': 264, 'DRIVEON': 264, 'DS MEDIA': 265, 'DTP ENTERTAINMENT AG': 266, 'DTP ENTERTAINMENT': 266, 'DTP AG': 266, 'DTP': 266, 'DTS ENTERTAINMENT': 267, 'DTS': 267, 'DUKE MARKETING': 268, 'DUKE VIDEO DISTRIBUTION': 269, 'DUKE': 269, 'DUTCH FILMWORKS': 270, 'DUTCH': 270, 'DVD INTERNATIONAL': 271, 'DVD': 271, 'DYBEX': 272, 'DYNAMIC': 273, 'DYNIT': 274, 'E1 ENTERTAINMENT': 275, 'E1': 275, 'EAGLE ENTERTAINMENT': 276, 'EAGLE HOME ENTERTAINMENT PVT.LTD.': + 277, 'EAGLE HOME ENTERTAINMENT PVTLTD': 277, 'EAGLE HOME ENTERTAINMENT PVT LTD': 277, 'EAGLE HOME ENTERTAINMENT': 277, 'EAGLE PICTURES': 278, 'EAGLE ROCK ENTERTAINMENT': 279, 'EAGLE ROCK': 279, 'EAGLE VISION MEDIA': 280, 'EAGLE VISION': 280, 'EARMUSIC': 281, 'EARTH ENTERTAINMENT': 282, 'EARTH': 282, 'ECHO BRIDGE ENTERTAINMENT': 283, 'ECHO BRIDGE': 283, 'EDEL GERMANY GMBH': 284, 'EDEL GERMANY': 284, 'EDEL RECORDS': 285, 'EDITION TONFILM': 286, 'EDITIONS MONTPARNASSE': 287, 'EDKO FILMS LTD.': 288, 'EDKO FILMS LTD': 288, 'EDKO FILMS': 288, 'EDKO': 288, "EIN'S M&M CO": 289, 'EINS M&M CO': 289, "EIN'S M&M": 289, 'EINS M&M': 289, 'ELEA-MEDIA': 290, 'ELEA MEDIA': 290, 'ELEA': 290, 'ELECTRIC PICTURE': 291, 'ELECTRIC': 291, 'ELEPHANT FILMS': 292, 'ELEPHANT': 292, 'ELEVATION': 293, 'EMI': 294, 'EMON': 295, 'EMS': 296, 'EMYLIA': 297, 'ENE MEDIA': 298, 'ENE': 298, 'ENTERTAINMENT IN VIDEO': 299, 'ENTERTAINMENT IN': 299, 'ENTERTAINMENT ONE': 300, 'ENTERTAINMENT ONE FILMS CANADA INC.': 301, 'ENTERTAINMENT ONE FILMS CANADA INC': 301, 'ENTERTAINMENT ONE FILMS CANADA': 301, 'ENTERTAINMENT ONE CANADA INC': 301, + 'ENTERTAINMENT ONE CANADA': 301, 'ENTERTAINMENTONE': 302, 'EONE': 303, 'EOS': 304, 'EPIC PICTURES': 305, 'EPIC': 305, 'EPIC RECORDS': 306, 'ERATO': 307, 'EROS': 308, 'ESC EDITIONS': 309, 'ESCAPI MEDIA BV': 310, 'ESOTERIC RECORDINGS': 311, 'ESPN FILMS': 312, 'EUREKA ENTERTAINMENT': 313, 'EUREKA': 313, 'EURO PICTURES': 314, 'EURO VIDEO': 315, 'EUROARTS': 316, 'EUROPA FILMES': 317, 'EUROPA': 317, 'EUROPACORP': 318, 'EUROZOOM': 319, 'EXCEL': 320, 'EXPLOSIVE MEDIA': 321, 'EXPLOSIVE': 321, 'EXTRALUCID FILMS': 322, 'EXTRALUCID': 322, 'EYE SEE MOVIES': 323, 'EYE SEE': 323, 'EYK MEDIA': 324, 'EYK': 324, 'FABULOUS FILMS': 325, 'FABULOUS': 325, 'FACTORIS FILMS': 326, 'FACTORIS': 326, 'FARAO RECORDS': 327, 'FARBFILM HOME ENTERTAINMENT': 328, 'FARBFILM ENTERTAINMENT': 328, 'FARBFILM HOME': 328, 'FARBFILM': 328, 'FEELGOOD ENTERTAINMENT': 329, 'FEELGOOD': 329, 'FERNSEHJUWELEN': 330, 'FILM CHEST': 331, 'FILM MEDIA': 332, 'FILM MOVEMENT': 333, 'FILM4': 334, 'FILMART': 335, 'FILMAURO': 336, 'FILMAX': 337, 'FILMCONFECT HOME ENTERTAINMENT': 338, 'FILMCONFECT ENTERTAINMENT': 338, 'FILMCONFECT HOME': 338, 'FILMCONFECT': 338, 'FILMEDIA': 339, 'FILMJUWELEN': 340, 'FILMOTEKA NARODAWA': 341, 'FILMRISE': 342, 'FINAL CUT ENTERTAINMENT': 343, 'FINAL CUT': 343, 'FIREHOUSE 12 RECORDS': 344, 'FIREHOUSE 12': 344, 'FIRST INTERNATIONAL PRODUCTION': 345, 'FIRST INTERNATIONAL': 345, 'FIRST LOOK STUDIOS': 346, 'FIRST LOOK': 346, 'FLAGMAN TRADE': 347, 'FLASHSTAR FILMES': 348, 'FLASHSTAR': 348, 'FLICKER ALLEY': 349, 'FNC ADD CULTURE': 350, 'FOCUS FILMES': 351, 'FOCUS': 351, 'FOKUS MEDIA': 352, 'FOKUSA': 352, 'FOX PATHE EUROPA': 353, 'FOX PATHE': 353, 'FOX EUROPA': 353, 'FOX/MGM': 354, 'FOX MGM': 354, 'MGM': 354, 'MGM/FOX': 354, 'FOX': 354, 'FPE': 355, 'FRANCE TÉLÉVISIONS DISTRIBUTION': 356, 'FRANCE TELEVISIONS DISTRIBUTION': 356, 'FRANCE TELEVISIONS': 356, 'FRANCE': 356, 'FREE DOLPHIN ENTERTAINMENT': 357, 'FREE DOLPHIN': 357, 'FREESTYLE DIGITAL MEDIA': 358, 'FREESTYLE DIGITAL': 358, 'FREESTYLE': 358, 'FREMANTLE HOME ENTERTAINMENT': 359, 'FREMANTLE ENTERTAINMENT': 359, 'FREMANTLE HOME': 359, 'FREMANTL': 359, 'FRENETIC FILMS': 360, 'FRENETIC': 360, 'FRONTIER WORKS': 361, 'FRONTIER': 361, 'FRONTIERS MUSIC': 362, 'FRONTIERS RECORDS': 363, 'FS FILM OY': 364, 'FS FILM': + 364, 'FULL MOON FEATURES': 365, 'FULL MOON': 365, 'FUN CITY EDITIONS': 366, 'FUN CITY': 366, 'FUNIMATION ENTERTAINMENT': 367, 'FUNIMATION': 367, 'FUSION': 368, 'FUTUREFILM': 369, 'G2 PICTURES': 370, 'G2': 370, 'GAGA COMMUNICATIONS': 371, 'GAGA': 371, 'GAIAM': 372, 'GALAPAGOS': 373, 'GAMMA HOME ENTERTAINMENT': 374, 'GAMMA ENTERTAINMENT': 374, 'GAMMA HOME': 374, 'GAMMA': 374, 'GARAGEHOUSE PICTURES': 375, 'GARAGEHOUSE': 375, 'GARAGEPLAY (車庫娛樂)': 376, '車庫娛樂': 376, 'GARAGEPLAY (Che Ku Yu Le )': 376, 'GARAGEPLAY': 376, 'Che Ku Yu Le': 376, 'GAUMONT': 377, 'GEFFEN': 378, 'GENEON ENTERTAINMENT': 379, 'GENEON': 379, 'GENEON UNIVERSAL ENTERTAINMENT': 380, 'GENERAL VIDEO RECORDING': 381, 'GLASS DOLL FILMS': 382, 'GLASS DOLL': 382, 'GLOBE MUSIC MEDIA': 383, 'GLOBE MUSIC': 383, 'GLOBE MEDIA': 383, 'GLOBE': 383, 'GO ENTERTAIN': 384, 'GO': 384, 'GOLDEN HARVEST': 385, 'GOOD!MOVIES': 386, + 'GOOD! MOVIES': 386, 'GOOD MOVIES': 386, 'GRAPEVINE VIDEO': 387, 'GRAPEVINE': 387, 'GRASSHOPPER FILM': 388, 'GRASSHOPPER FILMS': 388, 'GRASSHOPPER': 388, 'GRAVITAS VENTURES': 389, 'GRAVITAS': 389, 'GREAT MOVIES': 390, 'GREAT': 390, + 'GREEN APPLE ENTERTAINMENT': 391, 'GREEN ENTERTAINMENT': 391, 'GREEN APPLE': 391, 'GREEN': 391, 'GREENNARAE MEDIA': 392, 'GREENNARAE': 392, 'GRINDHOUSE RELEASING': 393, 'GRINDHOUSE': 393, 'GRIND HOUSE': 393, 'GRYPHON ENTERTAINMENT': 394, 'GRYPHON': 394, 'GUNPOWDER & SKY': 395, 'GUNPOWDER AND SKY': 395, 'GUNPOWDER SKY': 395, 'GUNPOWDER + SKY': 395, 'GUNPOWDER': 395, 'HANABEE ENTERTAINMENT': 396, 'HANABEE': 396, 'HANNOVER HOUSE': 397, 'HANNOVER': 397, 'HANSESOUND': 398, 'HANSE SOUND': 398, 'HANSE': 398, 'HAPPINET': 399, 'HARMONIA MUNDI': 400, 'HARMONIA': 400, 'HBO': 401, 'HDC': 402, 'HEC': 403, 'HELL & BACK RECORDINGS': 404, 'HELL AND BACK RECORDINGS': 404, 'HELL & BACK': 404, 'HELL AND BACK': 404, "HEN'S TOOTH VIDEO": 405, 'HENS TOOTH VIDEO': 405, "HEN'S TOOTH": 405, 'HENS TOOTH': 405, 'HIGH FLIERS': 406, 'HIGHLIGHT': 407, 'HILLSONG': 408, 'HISTORY CHANNEL': 409, 'HISTORY': 409, 'HK VIDÉO': 410, 'HK VIDEO': 410, 'HK': 410, 'HMH HAMBURGER MEDIEN HAUS': 411, 'HAMBURGER MEDIEN HAUS': 411, 'HMH HAMBURGER MEDIEN': 411, 'HMH HAMBURGER': 411, 'HMH': 411, 'HOLLYWOOD CLASSIC ENTERTAINMENT': 412, 'HOLLYWOOD CLASSIC': 412, 'HOLLYWOOD PICTURES': 413, 'HOLLYWOOD': 413, 'HOPSCOTCH ENTERTAINMENT': 414, 'HOPSCOTCH': 414, 'HPM': 415, 'HÄNNSLER CLASSIC': 416, 'HANNSLER CLASSIC': 416, 'HANNSLER': 416, 'I-CATCHER': 417, 'I CATCHER': 417, 'ICATCHER': 417, 'I-ON NEW MEDIA': 418, 'I ON NEW MEDIA': 418, 'ION NEW MEDIA': 418, 'ION MEDIA': 418, 'I-ON': 418, 'ION': 418, 'IAN PRODUCTIONS': 419, 'IAN': 419, 'ICESTORM': 420, 'ICON FILM DISTRIBUTION': 421, 'ICON DISTRIBUTION': 421, 'ICON FILM': 421, 'ICON': 421, 'IDEALE AUDIENCE': 422, 'IDEALE': 422, 'IFC FILMS': 423, 'IFC': 423, 'IFILM': 424, 'ILLUSIONS UNLTD.': 425, 'ILLUSIONS UNLTD': 425, 'ILLUSIONS': 425, 'IMAGE ENTERTAINMENT': 426, 'IMAGE': 426, + 'IMAGEM FILMES': 427, 'IMAGEM': 427, 'IMOVISION': 428, 'IMPERIAL CINEPIX': 429, 'IMPRINT': 430, 'IMPULS HOME ENTERTAINMENT': 431, 'IMPULS ENTERTAINMENT': 431, 'IMPULS HOME': 431, 'IMPULS': 431, 'IN-AKUSTIK': 432, 'IN AKUSTIK': 432, 'INAKUSTIK': 432, 'INCEPTION MEDIA GROUP': 433, 'INCEPTION MEDIA': 433, 'INCEPTION GROUP': 433, 'INCEPTION': 433, 'INDEPENDENT': 434, 'INDICAN': 435, 'INDIE RIGHTS': 436, 'INDIE': 436, 'INDIGO': 437, 'INFO': 438, 'INJOINGAN': 439, 'INKED PICTURES': 440, 'INKED': 440, 'INSIDE OUT MUSIC': 441, 'INSIDE MUSIC': 441, 'INSIDE OUT': 441, 'INSIDE': 441, 'INTERCOM': 442, 'INTERCONTINENTAL VIDEO': 443, 'INTERCONTINENTAL': 443, 'INTERGROOVE': 444, + 'INTERSCOPE': 445, 'INVINCIBLE PICTURES': 446, 'INVINCIBLE': 446, 'ISLAND/MERCURY': 447, 'ISLAND MERCURY': 447, 'ISLANDMERCURY': 447, 'ISLAND & MERCURY': 447, 'ISLAND AND MERCURY': 447, 'ISLAND': 447, 'ITN': 448, 'ITV DVD': 449, 'ITV': 449, 'IVC': 450, 'IVE ENTERTAINMENT': 451, 'IVE': 451, 'J&R ADVENTURES': 452, 'J&R': 452, 'JR': 452, 'JAKOB': 453, 'JONU MEDIA': 454, 'JONU': 454, 'JRB PRODUCTIONS': 455, 'JRB': 455, 'JUST BRIDGE ENTERTAINMENT': 456, 'JUST BRIDGE': 456, 'JUST ENTERTAINMENT': 456, 'JUST': 456, 'KABOOM ENTERTAINMENT': 457, 'KABOOM': 457, 'KADOKAWA ENTERTAINMENT': 458, 'KADOKAWA': 458, 'KAIROS': 459, 'KALEIDOSCOPE ENTERTAINMENT': 460, 'KALEIDOSCOPE': 460, 'KAM & RONSON ENTERPRISES': 461, 'KAM & RONSON': 461, 'KAM&RONSON ENTERPRISES': 461, 'KAM&RONSON': 461, 'KAM AND RONSON ENTERPRISES': 461, 'KAM AND RONSON': 461, 'KANA HOME VIDEO': 462, 'KARMA FILMS': 463, 'KARMA': 463, 'KATZENBERGER': 464, 'KAZE': 465, 'KBS MEDIA': 466, 'KBS': 466, 'KD MEDIA': 467, 'KD': 467, 'KING MEDIA': 468, 'KING': 468, 'KING RECORDS': 469, 'KINO LORBER': 470, 'KINO': 470, 'KINO SWIAT': 471, 'KINOKUNIYA': 472, 'KINOWELT HOME ENTERTAINMENT/DVD': 473, 'KINOWELT HOME ENTERTAINMENT': 473, 'KINOWELT ENTERTAINMENT': 473, 'KINOWELT HOME DVD': 473, 'KINOWELT ENTERTAINMENT/DVD': 473, 'KINOWELT DVD': 473, 'KINOWELT': 473, 'KIT PARKER FILMS': 474, 'KIT PARKER': 474, 'KITTY MEDIA': 475, 'KNM HOME ENTERTAINMENT': 476, 'KNM ENTERTAINMENT': 476, 'KNM HOME': 476, 'KNM': 476, 'KOBA FILMS': 477, 'KOBA': 477, 'KOCH ENTERTAINMENT': 478, 'KOCH MEDIA': 479, 'KOCH': 479, 'KRAKEN RELEASING': 480, 'KRAKEN': 480, 'KSCOPE': 481, 'KSM': 482, 'KULTUR': 483, "L'ATELIER D'IMAGES": 484, "LATELIER D'IMAGES": 484, "L'ATELIER DIMAGES": 484, 'LATELIER DIMAGES': 484, "L ATELIER D'IMAGES": 484, "L'ATELIER D IMAGES": 484, + 'L ATELIER D IMAGES': 484, "L'ATELIER": 484, 'L ATELIER': 484, 'LATELIER': 484, 'LA AVENTURA AUDIOVISUAL': 485, 'LA AVENTURA': 485, 'LACE GROUP': 486, 'LACE': 486, 'LASER PARADISE': 487, 'LAYONS': 488, 'LCJ EDITIONS': 489, 'LCJ': 489, 'LE CHAT QUI FUME': 490, 'LE PACTE': 491, 'LEDICK FILMHANDEL': 492, 'LEGEND': 493, 'LEOMARK STUDIOS': 494, 'LEOMARK': 494, 'LEONINE FILMS': 495, 'LEONINE': 495, 'LICHTUNG MEDIA LTD': 496, 'LICHTUNG LTD': 496, 'LICHTUNG MEDIA LTD.': 496, 'LICHTUNG LTD.': 496, 'LICHTUNG MEDIA': 496, 'LICHTUNG': 496, 'LIGHTHOUSE HOME ENTERTAINMENT': 497, 'LIGHTHOUSE ENTERTAINMENT': 497, 'LIGHTHOUSE HOME': 497, 'LIGHTHOUSE': 497, 'LIGHTYEAR': 498, 'LIONSGATE FILMS': 499, 'LIONSGATE': 499, 'LIZARD CINEMA TRADE': 500, 'LLAMENTOL': 501, 'LOBSTER FILMS': 502, 'LOBSTER': 502, 'LOGON': 503, 'LORBER FILMS': 504, 'LORBER': 504, 'LOS BANDITOS FILMS': 505, 'LOS BANDITOS': 505, 'LOUD & PROUD RECORDS': 506, 'LOUD AND PROUD RECORDS': 506, 'LOUD & PROUD': 506, 'LOUD AND PROUD': 506, 'LSO LIVE': 507, 'LUCASFILM': 508, 'LUCKY RED': 509, 'LUMIÈRE HOME ENTERTAINMENT': 510, 'LUMIERE HOME ENTERTAINMENT': 510, 'LUMIERE ENTERTAINMENT': 510, 'LUMIERE HOME': 510, 'LUMIERE': 510, 'M6 VIDEO': 511, 'M6': 511, 'MAD DIMENSION': 512, 'MADMAN ENTERTAINMENT': 513, 'MADMAN': 513, 'MAGIC BOX': 514, 'MAGIC PLAY': 515, 'MAGNA HOME ENTERTAINMENT': 516, 'MAGNA ENTERTAINMENT': 516, 'MAGNA HOME': 516, 'MAGNA': 516, 'MAGNOLIA PICTURES': 517, 'MAGNOLIA': 517, 'MAIDEN JAPAN': 518, 'MAIDEN': 518, 'MAJENG MEDIA': 519, 'MAJENG': 519, 'MAJESTIC HOME ENTERTAINMENT': 520, 'MAJESTIC ENTERTAINMENT': 520, 'MAJESTIC HOME': 520, 'MAJESTIC': 520, 'MANGA HOME ENTERTAINMENT': 521, 'MANGA ENTERTAINMENT': 521, 'MANGA HOME': 521, 'MANGA': 521, 'MANTA LAB': 522, 'MAPLE STUDIOS': 523, 'MAPLE': 523, 'MARCO POLO PRODUCTION': + 524, 'MARCO POLO': 524, 'MARIINSKY': 525, 'MARVEL STUDIOS': 526, 'MARVEL': 526, 'MASCOT RECORDS': 527, 'MASCOT': 527, 'MASSACRE VIDEO': 528, 'MASSACRE': 528, 'MATCHBOX': 529, 'MATRIX D': 530, 'MAXAM': 531, 'MAYA HOME ENTERTAINMENT': 532, 'MAYA ENTERTAINMENT': 532, 'MAYA HOME': 532, 'MAYAT': 532, 'MDG': 533, 'MEDIA BLASTERS': 534, 'MEDIA FACTORY': 535, 'MEDIA TARGET DISTRIBUTION': 536, 'MEDIA TARGET': 536, 'MEDIAINVISION': 537, 'MEDIATOON': 538, 'MEDIATRES ESTUDIO': 539, 'MEDIATRES STUDIO': 539, 'MEDIATRES': 539, 'MEDICI ARTS': 540, 'MEDICI CLASSICS': 541, 'MEDIUMRARE ENTERTAINMENT': 542, 'MEDIUMRARE': 542, 'MEDUSA': 543, 'MEGASTAR': 544, 'MEI AH': 545, 'MELI MÉDIAS': 546, 'MELI MEDIAS': 546, 'MEMENTO FILMS': 547, 'MEMENTO': 547, 'MENEMSHA FILMS': 548, 'MENEMSHA': 548, 'MERCURY': 549, 'MERCURY STUDIOS': 550, 'MERGE SOFT PRODUCTIONS': 551, 'MERGE PRODUCTIONS': 551, 'MERGE SOFT': 551, 'MERGE': 551, 'METAL BLADE RECORDS': 552, 'METAL BLADE': 552, 'METEOR': 553, 'METRO-GOLDWYN-MAYER': 554, 'METRO GOLDWYN MAYER': 554, 'METROGOLDWYNMAYER': 554, 'METRODOME VIDEO': 555, 'METRODOME': 555, 'METROPOLITAN': 556, 'MFA+': + 557, 'MFA': 557, 'MIG FILMGROUP': 558, 'MIG': 558, 'MILESTONE': 559, 'MILL CREEK ENTERTAINMENT': 560, 'MILL CREEK': 560, 'MILLENNIUM MEDIA': 561, 'MILLENNIUM': 561, 'MIRAGE ENTERTAINMENT': 562, 'MIRAGE': 562, 'MIRAMAX': 563, + 'MISTERIYA ZVUKA': 564, 'MK2': 565, 'MODE RECORDS': 566, 'MODE': 566, 'MOMENTUM PICTURES': 567, 'MONDO HOME ENTERTAINMENT': 568, 'MONDO ENTERTAINMENT': 568, 'MONDO HOME': 568, 'MONDO MACABRO': 569, 'MONGREL MEDIA': 570, 'MONOLIT': 571, 'MONOLITH VIDEO': 572, 'MONOLITH': 572, 'MONSTER PICTURES': 573, 'MONSTER': 573, 'MONTEREY VIDEO': 574, 'MONTEREY': 574, 'MONUMENT RELEASING': 575, 'MONUMENT': 575, 'MORNINGSTAR': 576, 'MORNING STAR': 576, 'MOSERBAER': 577, 'MOVIEMAX': 578, 'MOVINSIDE': 579, 'MPI MEDIA GROUP': 580, 'MPI MEDIA': 580, 'MPI': 580, 'MR. BONGO FILMS': 581, 'MR BONGO FILMS': 581, 'MR BONGO': 581, 'MRG (MERIDIAN)': 582, 'MRG MERIDIAN': 582, 'MRG': 582, 'MERIDIAN': 582, 'MUBI': 583, 'MUG SHOT PRODUCTIONS': 584, 'MUG SHOT': 584, 'MULTIMUSIC': 585, 'MULTI-MUSIC': 585, 'MULTI MUSIC': 585, 'MUSE': 586, 'MUSIC BOX FILMS': 587, 'MUSIC BOX': 587, 'MUSICBOX': 587, 'MUSIC BROKERS': 588, 'MUSIC THEORIES': 589, 'MUSIC VIDEO DISTRIBUTORS': 590, 'MUSIC VIDEO': 590, 'MUSTANG ENTERTAINMENT': 591, 'MUSTANG': 591, 'MVD VISUAL': 592, 'MVD': 592, 'MVD/VSC': 593, 'MVL': 594, 'MVM ENTERTAINMENT': 595, 'MVM': 595, 'MYNDFORM': 596, 'MYSTIC NIGHT PICTURES': 597, 'MYSTIC NIGHT': 597, 'NAMELESS MEDIA': 598, 'NAMELESS': 598, 'NAPALM RECORDS': 599, 'NAPALM': 599, 'NATIONAL ENTERTAINMENT MEDIA': 600, 'NATIONAL ENTERTAINMENT': 600, 'NATIONAL MEDIA': 600, 'NATIONAL FILM ARCHIVE': 601, 'NATIONAL ARCHIVE': 601, 'NATIONAL FILM': 601, 'NATIONAL GEOGRAPHIC': 602, 'NAT GEO TV': 602, 'NAT GEO': 602, 'NGO': 602, 'NAXOS': 603, 'NBCUNIVERSAL ENTERTAINMENT JAPAN': 604, 'NBC UNIVERSAL ENTERTAINMENT JAPAN': 604, 'NBCUNIVERSAL JAPAN': 604, 'NBC UNIVERSAL JAPAN': 604, 'NBC JAPAN': 604, 'NBO ENTERTAINMENT': 605, 'NBO': 605, 'NEOS': 606, 'NETFLIX': 607, 'NETWORK': 608, 'NEW BLOOD': 609, 'NEW DISC': 610, 'NEW KSM': 611, 'NEW LINE CINEMA': 612, 'NEW LINE': 612, 'NEW MOVIE TRADING CO. LTD': 613, 'NEW MOVIE TRADING CO LTD': 613, 'NEW MOVIE TRADING CO': 613, 'NEW MOVIE TRADING': 613, 'NEW WAVE FILMS': 614, 'NEW WAVE': 614, 'NFI': 615, + 'NHK': 616, 'NIPPONART': 617, 'NIS AMERICA': 618, 'NJUTAFILMS': 619, 'NOBLE ENTERTAINMENT': 620, 'NOBLE': 620, 'NORDISK FILM': 621, 'NORDISK': 621, 'NORSK FILM': 622, 'NORSK': 622, 'NORTH AMERICAN MOTION PICTURES': 623, 'NOS AUDIOVISUAIS': 624, 'NOTORIOUS PICTURES': 625, 'NOTORIOUS': 625, 'NOVA MEDIA': 626, 'NOVA': 626, 'NOVA SALES AND DISTRIBUTION': 627, 'NOVA SALES & DISTRIBUTION': 627, 'NSM': 628, 'NSM RECORDS': 629, 'NUCLEAR BLAST': 630, 'NUCLEUS FILMS': 631, 'NUCLEUS': 631, 'OBERLIN MUSIC': 632, 'OBERLIN': 632, 'OBRAS-PRIMAS DO CINEMA': 633, 'OBRAS PRIMAS DO CINEMA': 633, 'OBRASPRIMAS DO CINEMA': 633, 'OBRAS-PRIMAS CINEMA': 633, 'OBRAS PRIMAS CINEMA': 633, 'OBRASPRIMAS CINEMA': 633, 'OBRAS-PRIMAS': 633, 'OBRAS PRIMAS': 633, 'OBRASPRIMAS': 633, 'ODEON': 634, 'OFDB FILMWORKS': 635, 'OFDB': 635, 'OLIVE FILMS': 636, 'OLIVE': 636, 'ONDINE': 637, 'ONSCREEN FILMS': 638, 'ONSCREEN': 638, 'OPENING DISTRIBUTION': 639, 'OPERA AUSTRALIA': 640, 'OPTIMUM HOME ENTERTAINMENT': 641, 'OPTIMUM ENTERTAINMENT': 641, 'OPTIMUM HOME': 641, 'OPTIMUM': 641, 'OPUS ARTE': 642, 'ORANGE STUDIO': 643, 'ORANGE': 643, 'ORLANDO EASTWOOD FILMS': 644, 'ORLANDO FILMS': 644, 'ORLANDO EASTWOOD': 644, 'ORLANDO': 644, 'ORUSTAK PICTURES': 645, 'ORUSTAK': 645, 'OSCILLOSCOPE PICTURES': 646, 'OSCILLOSCOPE': 646, 'OUTPLAY': 647, 'PALISADES TARTAN': 648, 'PAN VISION': 649, 'PANVISION': 649, 'PANAMINT CINEMA': 650, 'PANAMINT': 650, 'PANDASTORM ENTERTAINMENT': 651, 'PANDA STORM ENTERTAINMENT': 651, 'PANDASTORM': 651, 'PANDA STORM': 651, 'PANDORA FILM': 652, 'PANDORA': 652, 'PANEGYRIC': 653, 'PANORAMA': 654, 'PARADE DECK FILMS': 655, 'PARADE DECK': 655, 'PARADISE': 656, 'PARADISO FILMS': 657, 'PARADOX': 658, 'PARAMOUNT PICTURES': 659, 'PARAMOUNT': 659, 'PARIS FILMES': 660, 'PARIS FILMS': 660, 'PARIS': 660, 'PARK CIRCUS': 661, 'PARLOPHONE': 662, 'PASSION RIVER': 663, 'PATHE DISTRIBUTION': 664, 'PATHE': 664, 'PBS': 665, 'PEACE ARCH TRINITY': 666, 'PECCADILLO PICTURES': 667, 'PEPPERMINT': 668, 'PHASE 4 FILMS': 669, 'PHASE 4': 669, 'PHILHARMONIA BAROQUE': 670, 'PICTURE HOUSE ENTERTAINMENT': 671, 'PICTURE ENTERTAINMENT': 671, 'PICTURE HOUSE': 671, 'PICTURE': 671, 'PIDAX': 672, 'PINK FLOYD RECORDS': 673, 'PINK FLOYD': 673, 'PINNACLE FILMS': 674, 'PINNACLE': 674, 'PLAIN': 675, 'PLATFORM ENTERTAINMENT LIMITED': 676, 'PLATFORM ENTERTAINMENT LTD': 676, 'PLATFORM ENTERTAINMENT LTD.': 676, 'PLATFORM ENTERTAINMENT': 676, 'PLATFORM': 676, 'PLAYARTE': 677, 'PLG UK CLASSICS': 678, 'PLG UK': + 678, 'PLG': 678, 'POLYBAND & TOPPIC VIDEO/WVG': 679, 'POLYBAND AND TOPPIC VIDEO/WVG': 679, 'POLYBAND & TOPPIC VIDEO WVG': 679, 'POLYBAND & TOPPIC VIDEO AND WVG': 679, 'POLYBAND & TOPPIC VIDEO & WVG': 679, 'POLYBAND AND TOPPIC VIDEO WVG': 679, 'POLYBAND AND TOPPIC VIDEO AND WVG': 679, 'POLYBAND AND TOPPIC VIDEO & WVG': 679, 'POLYBAND & TOPPIC VIDEO': 679, 'POLYBAND AND TOPPIC VIDEO': 679, 'POLYBAND & TOPPIC': 679, 'POLYBAND AND TOPPIC': 679, 'POLYBAND': 679, 'WVG': 679, 'POLYDOR': 680, 'PONY': 681, 'PONY CANYON': 682, 'POTEMKINE': 683, 'POWERHOUSE FILMS': 684, 'POWERHOUSE': 684, 'POWERSTATIOM': 685, 'PRIDE & JOY': 686, 'PRIDE AND JOY': 686, 'PRINZ MEDIA': 687, 'PRINZ': 687, 'PRIS AUDIOVISUAIS': 688, 'PRO VIDEO': 689, 'PRO-VIDEO': 689, 'PRO-MOTION': 690, 'PRO MOTION': 690, 'PROD. JRB': 691, 'PROD JRB': 691, 'PRODISC': 692, 'PROKINO': 693, 'PROVOGUE RECORDS': 694, 'PROVOGUE': 694, 'PROWARE': 695, 'PULP VIDEO': 696, 'PULP': 696, 'PULSE VIDEO': 697, 'PULSE': 697, 'PURE AUDIO RECORDINGS': 698, 'PURE AUDIO': 698, 'PURE FLIX ENTERTAINMENT': 699, 'PURE FLIX': 699, 'PURE ENTERTAINMENT': 699, 'PYRAMIDE VIDEO': 700, 'PYRAMIDE': 700, 'QUALITY FILMS': 701, 'QUALITY': 701, 'QUARTO VALLEY RECORDS': 702, 'QUARTO VALLEY': 702, 'QUESTAR': 703, 'R SQUARED FILMS': 704, 'R SQUARED': 704, 'RAPID EYE MOVIES': 705, 'RAPID EYE': 705, 'RARO VIDEO': 706, 'RARO': 706, 'RAROVIDEO U.S.': 707, 'RAROVIDEO US': 707, 'RARO VIDEO US': 707, 'RARO VIDEO U.S.': 707, 'RARO U.S.': 707, 'RARO US': 707, 'RAVEN BANNER RELEASING': 708, 'RAVEN BANNER': 708, 'RAVEN': 708, 'RAZOR DIGITAL ENTERTAINMENT': 709, 'RAZOR DIGITAL': 709, 'RCA': 710, 'RCO LIVE': 711, 'RCO': 711, 'RCV': 712, 'REAL GONE MUSIC': 713, 'REAL GONE': 713, 'REANIMEDIA': 714, 'REANI MEDIA': 714, 'REDEMPTION': 715, 'REEL': 716, 'RELIANCE HOME VIDEO & GAMES': 717, 'RELIANCE HOME VIDEO AND GAMES': 717, 'RELIANCE HOME VIDEO': 717, 'RELIANCE VIDEO': 717, 'RELIANCE HOME': 717, 'RELIANCE': 717, 'REM CULTURE': 718, 'REMAIN IN LIGHT': 719, 'REPRISE': 720, 'RESEN': 721, 'RETROMEDIA': 722, 'REVELATION FILMS LTD.': 723, 'REVELATION FILMS LTD': 723, 'REVELATION FILMS': 723, 'REVELATION LTD.': 723, 'REVELATION LTD': 723, 'REVELATION': 723, 'REVOLVER ENTERTAINMENT': 724, 'REVOLVER': 724, 'RHINO MUSIC': 725, 'RHINO': 725, 'RHV': 726, 'RIGHT STUF': 727, 'RIMINI EDITIONS': 728, 'RISING SUN MEDIA': 729, 'RLJ ENTERTAINMENT': 730, 'RLJ': 730, 'ROADRUNNER RECORDS': 731, 'ROADSHOW ENTERTAINMENT': 732, 'ROADSHOW': 732, 'RONE': 733, 'RONIN FLIX': 734, 'ROTANA HOME ENTERTAINMENT': 735, 'ROTANA ENTERTAINMENT': 735, 'ROTANA HOME': 735, 'ROTANA': 735, 'ROUGH TRADE': 736, 'ROUNDER': 737, 'SAFFRON HILL FILMS': 738, 'SAFFRON HILL': 738, 'SAFFRON': 738, 'SAMUEL GOLDWYN FILMS': 739, 'SAMUEL GOLDWYN': 739, 'SAN FRANCISCO SYMPHONY': 740, 'SANDREW METRONOME': 741, 'SAPHRANE': 742, 'SAVOR': 743, 'SCANBOX ENTERTAINMENT': 744, 'SCANBOX': 744, 'SCENIC LABS': 745, 'SCHRÖDERMEDIA': 746, 'SCHRODERMEDIA': 746, 'SCHRODER MEDIA': 746, 'SCORPION RELEASING': 747, 'SCORPION': 747, 'SCREAM TEAM RELEASING': 748, 'SCREAM TEAM': 748, 'SCREEN MEDIA': 749, 'SCREEN': 749, 'SCREENBOUND PICTURES': 750, 'SCREENBOUND': 750, 'SCREENWAVE MEDIA': 751, 'SCREENWAVE': 751, 'SECOND RUN': 752, 'SECOND SIGHT': 753, 'SEEDSMAN GROUP': 754, 'SELECT VIDEO': 755, 'SELECTA VISION': 756, 'SENATOR': 757, 'SENTAI FILMWORKS': 758, 'SENTAI': 758, 'SEVEN7': 759, 'SEVERIN FILMS': 760, 'SEVERIN': 760, 'SEVILLE': 761, 'SEYONS ENTERTAINMENT': 762, 'SEYONS': 762, 'SF STUDIOS': 763, 'SGL ENTERTAINMENT': 764, 'SGL': 764, 'SHAMELESS': 765, 'SHAMROCK MEDIA': 766, 'SHAMROCK': 766, 'SHANGHAI EPIC MUSIC ENTERTAINMENT': 767, 'SHANGHAI EPIC ENTERTAINMENT': 767, 'SHANGHAI EPIC MUSIC': 767, 'SHANGHAI MUSIC ENTERTAINMENT': 767, 'SHANGHAI ENTERTAINMENT': 767, 'SHANGHAI MUSIC': 767, 'SHANGHAI': 767, 'SHEMAROO': 768, 'SHOCHIKU': 769, 'SHOCK': 770, 'SHOGAKU KAN': 771, 'SHOUT FACTORY': 772, 'SHOUT! FACTORY': 772, 'SHOUT': 772, 'SHOUT!': 772, 'SHOWBOX': 773, 'SHOWTIME ENTERTAINMENT': 774, 'SHOWTIME': 774, 'SHRIEK SHOW': 775, 'SHUDDER': 776, 'SIDONIS': 777, 'SIDONIS CALYSTA': 778, 'SIGNAL ONE ENTERTAINMENT': 779, 'SIGNAL ONE': 779, 'SIGNATURE ENTERTAINMENT': 780, 'SIGNATURE': 780, 'SILVER VISION': 781, 'SINISTER FILM': 782, 'SINISTER': 782, 'SIREN VISUAL ENTERTAINMENT': 783, 'SIREN VISUAL': 783, 'SIREN ENTERTAINMENT': 783, 'SIREN': 783, 'SKANI': 784, 'SKY DIGI': 785, 'SLASHER // VIDEO': 786, 'SLASHER / VIDEO': 786, 'SLASHER VIDEO': 786, 'SLASHER': 786, 'SLOVAK FILM INSTITUTE': 787, 'SLOVAK FILM': 787, + 'SFI': 787, 'SM LIFE DESIGN GROUP': 788, 'SMOOTH PICTURES': 789, 'SMOOTH': 789, 'SNAPPER MUSIC': 790, 'SNAPPER': 790, 'SODA PICTURES': 791, 'SODA': 791, 'SONO LUMINUS': 792, 'SONY MUSIC': 793, 'SONY PICTURES': 794, 'SONY': 794, 'SONY PICTURES CLASSICS': 795, 'SONY CLASSICS': 795, 'SOUL MEDIA': 796, 'SOUL': 796, 'SOULFOOD MUSIC DISTRIBUTION': 797, 'SOULFOOD DISTRIBUTION': 797, 'SOULFOOD MUSIC': 797, 'SOULFOOD': 797, 'SOYUZ': 798, 'SPECTRUM': 799, + 'SPENTZOS FILM': 800, 'SPENTZOS': 800, 'SPIRIT ENTERTAINMENT': 801, 'SPIRIT': 801, 'SPIRIT MEDIA GMBH': 802, 'SPIRIT MEDIA': 802, 'SPLENDID ENTERTAINMENT': 803, 'SPLENDID FILM': 804, 'SPO': 805, 'SQUARE ENIX': 806, 'SRI BALAJI VIDEO': 807, 'SRI BALAJI': 807, 'SRI': 807, 'SRI VIDEO': 807, 'SRS CINEMA': 808, 'SRS': 808, 'SSO RECORDINGS': 809, 'SSO': 809, 'ST2 MUSIC': 810, 'ST2': 810, 'STAR MEDIA ENTERTAINMENT': 811, 'STAR ENTERTAINMENT': 811, 'STAR MEDIA': 811, 'STAR': 811, 'STARLIGHT': 812, 'STARZ / ANCHOR BAY': 813, 'STARZ ANCHOR BAY': 813, 'STARZ': 813, 'ANCHOR BAY': 813, 'STER KINEKOR': 814, 'STERLING ENTERTAINMENT': 815, 'STERLING': 815, 'STINGRAY': 816, 'STOCKFISCH RECORDS': 817, 'STOCKFISCH': 817, 'STRAND RELEASING': 818, 'STRAND': 818, 'STUDIO 4K': 819, 'STUDIO CANAL': 820, 'STUDIO GHIBLI': 821, 'GHIBLI': 821, 'STUDIO HAMBURG ENTERPRISES': 822, 'HAMBURG ENTERPRISES': 822, 'STUDIO HAMBURG': 822, 'HAMBURG': 822, 'STUDIO S': 823, 'SUBKULTUR ENTERTAINMENT': 824, 'SUBKULTUR': 824, 'SUEVIA FILMS': 825, 'SUEVIA': 825, 'SUMMIT ENTERTAINMENT': 826, 'SUMMIT': 826, 'SUNFILM ENTERTAINMENT': 827, 'SUNFILM': 827, 'SURROUND RECORDS': 828, 'SURROUND': 828, 'SVENSK FILMINDUSTRI': 829, 'SVENSK': 829, 'SWEN FILMES': 830, 'SWEN FILMS': 830, 'SWEN': 830, 'SYNAPSE FILMS': 831, 'SYNAPSE': 831, 'SYNDICADO': 832, 'SYNERGETIC': 833, 'T- SERIES': 834, 'T-SERIES': 834, 'T SERIES': 834, 'TSERIES': 834, 'T.V.P.': 835, 'TVP': 835, 'TACET RECORDS': 836, 'TACET': 836, 'TAI SENG': 837, 'TAI SHENG': 838, 'TAKEONE': 839, 'TAKESHOBO': 840, 'TAMASA DIFFUSION': 841, 'TC ENTERTAINMENT': 842, 'TC': 842, 'TDK': 843, 'TEAM MARKETING': 844, 'TEATRO REAL': 845, 'TEMA DISTRIBUCIONES': 846, 'TEMPE DIGITAL': 847, 'TF1 VIDÉO': 848, 'TF1 VIDEO': 848, 'TF1': 848, 'THE BLU': 849, 'BLU': 849, 'THE ECSTASY OF FILMS': 850, 'THE FILM DETECTIVE': 851, 'FILM DETECTIVE': 851, 'THE JOKERS': 852, 'JOKERS': 852, 'THE ON': 853, 'ON': 853, 'THIMFILM': 854, 'THIM FILM': 854, 'THIM': 854, 'THIRD WINDOW FILMS': 855, 'THIRD WINDOW': 855, '3RD WINDOW FILMS': 855, '3RD WINDOW': 855, 'THUNDERBEAN ANIMATION': 856, 'THUNDERBEAN': 856, 'THUNDERBIRD RELEASING': 857, 'THUNDERBIRD': 857, 'TIBERIUS FILM': 858, 'TIME LIFE': 859, 'TIMELESS MEDIA GROUP': 860, 'TIMELESS MEDIA': 860, 'TIMELESS GROUP': 860, 'TIMELESS': 860, 'TLA RELEASING': 861, 'TLA': 861, 'TOBIS FILM': 862, 'TOBIS': 862, 'TOEI': 863, 'TOHO': 864, 'TOKYO SHOCK': 865, 'TOKYO': 865, 'TONPOOL MEDIEN GMBH': 866, 'TONPOOL MEDIEN': 866, 'TOPICS ENTERTAINMENT': 867, 'TOPICS': 867, 'TOUCHSTONE PICTURES': 868, 'TOUCHSTONE': 868, 'TRANSMISSION FILMS': 869, 'TRANSMISSION': 869, 'TRAVEL VIDEO STORE': 870, 'TRIART': 871, 'TRIGON FILM': 872, 'TRIGON': 872, 'TRINITY HOME ENTERTAINMENT': 873, 'TRINITY ENTERTAINMENT': 873, 'TRINITY HOME': 873, 'TRINITY': 873, 'TRIPICTURES': 874, 'TRI-PICTURES': 874, 'TRI PICTURES': 874, 'TROMA': 875, 'TURBINE MEDIEN': 876, 'TURTLE RECORDS': 877, 'TURTLE': 877, 'TVA FILMS': 878, 'TVA': 878, 'TWILIGHT TIME': 879, 'TWILIGHT': 879, 'TT': 879, 'TWIN CO., LTD.': 880, 'TWIN CO, LTD.': 880, 'TWIN CO., LTD': 880, 'TWIN CO, LTD': 880, 'TWIN CO LTD': 880, 'TWIN LTD': 880, 'TWIN CO.': 880, 'TWIN CO': 880, 'TWIN': 880, 'UCA': 881, 'UDR': 882, 'UEK': 883, 'UFA/DVD': 884, 'UFA DVD': 884, 'UFADVD': 884, 'UGC PH': 885, 'ULTIMATE3DHEAVEN': 886, 'ULTRA': 887, 'UMBRELLA ENTERTAINMENT': 888, 'UMBRELLA': 888, 'UMC': 889, "UNCORK'D ENTERTAINMENT": 890, 'UNCORKD ENTERTAINMENT': 890, 'UNCORK D ENTERTAINMENT': 890, "UNCORK'D": 890, 'UNCORK D': 890, 'UNCORKD': 890, 'UNEARTHED FILMS': 891, 'UNEARTHED': 891, 'UNI DISC': 892, 'UNIMUNDOS': 893, 'UNITEL': 894, 'UNIVERSAL MUSIC': 895, 'UNIVERSAL SONY PICTURES HOME ENTERTAINMENT': 896, 'UNIVERSAL SONY PICTURES ENTERTAINMENT': 896, 'UNIVERSAL SONY PICTURES HOME': 896, 'UNIVERSAL SONY PICTURES': 896, 'UNIVERSAL HOME ENTERTAINMENT': + 896, 'UNIVERSAL ENTERTAINMENT': 896, 'UNIVERSAL HOME': 896, 'UNIVERSAL STUDIOS': 897, 'UNIVERSAL': 897, 'UNIVERSE LASER & VIDEO CO.': 898, 'UNIVERSE LASER AND VIDEO CO.': 898, 'UNIVERSE LASER & VIDEO CO': 898, 'UNIVERSE LASER AND VIDEO CO': 898, 'UNIVERSE LASER CO.': 898, 'UNIVERSE LASER CO': 898, 'UNIVERSE LASER': 898, 'UNIVERSUM FILM': 899, 'UNIVERSUM': 899, 'UTV': 900, 'VAP': 901, 'VCI': 902, 'VENDETTA FILMS': 903, 'VENDETTA': 903, 'VERSÁTIL HOME VIDEO': 904, 'VERSÁTIL VIDEO': 904, 'VERSÁTIL HOME': 904, 'VERSÁTIL': 904, 'VERSATIL HOME VIDEO': 904, 'VERSATIL VIDEO': 904, 'VERSATIL HOME': 904, 'VERSATIL': 904, 'VERTICAL ENTERTAINMENT': 905, 'VERTICAL': 905, 'VÉRTICE 360º': 906, 'VÉRTICE 360': 906, 'VERTICE 360o': 906, 'VERTICE 360': 906, 'VERTIGO BERLIN': 907, 'VÉRTIGO FILMS': 908, 'VÉRTIGO': 908, 'VERTIGO FILMS': 908, 'VERTIGO': 908, 'VERVE PICTURES': 909, 'VIA VISION ENTERTAINMENT': 910, 'VIA VISION': 910, 'VICOL ENTERTAINMENT': 911, 'VICOL': 911, 'VICOM': 912, 'VICTOR ENTERTAINMENT': 913, 'VICTOR': 913, 'VIDEA CDE': 914, 'VIDEO FILM EXPRESS': 915, 'VIDEO FILM': 915, 'VIDEO EXPRESS': 915, 'VIDEO MUSIC, INC.': 916, 'VIDEO MUSIC, INC': 916, 'VIDEO MUSIC INC.': 916, 'VIDEO MUSIC INC': 916, 'VIDEO MUSIC': 916, 'VIDEO SERVICE CORP.': 917, 'VIDEO SERVICE CORP': 917, 'VIDEO SERVICE': 917, 'VIDEO TRAVEL': 918, 'VIDEOMAX': 919, 'VIDEO MAX': 919, 'VII PILLARS ENTERTAINMENT': 920, 'VII PILLARS': 920, 'VILLAGE FILMS': 921, 'VINEGAR SYNDROME': 922, 'VINEGAR': 922, 'VS': 922, 'VINNY MOVIES': 923, 'VINNY': 923, 'VIRGIL FILMS & ENTERTAINMENT': 924, 'VIRGIL FILMS AND ENTERTAINMENT': 924, 'VIRGIL ENTERTAINMENT': 924, 'VIRGIL FILMS': 924, 'VIRGIL': 924, 'VIRGIN RECORDS': 925, 'VIRGIN': 925, 'VISION FILMS': 926, 'VISION': 926, 'VISUAL ENTERTAINMENT GROUP': 927, 'VISUAL GROUP': 927, 'VISUAL ENTERTAINMENT': 927, 'VISUAL': 927, 'VIVENDI VISUAL ENTERTAINMENT': 928, 'VIVENDI VISUAL': 928, 'VIVENDI': 928, 'VIZ PICTURES': 929, 'VIZ': 929, 'VLMEDIA': 930, 'VL MEDIA': 930, 'VL': 930, 'VOLGA': 931, 'VVS FILMS': 932, + 'VVS': 932, 'VZ HANDELS GMBH': 933, 'VZ HANDELS': 933, 'WARD RECORDS': 934, 'WARD': 934, 'WARNER BROS.': 935, 'WARNER BROS': 935, 'WARNER ARCHIVE': 935, 'WARNER ARCHIVE COLLECTION': 935, 'WAC': 935, 'WARNER': 935, 'WARNER MUSIC': 936, 'WEA': 937, 'WEINSTEIN COMPANY': 938, 'WEINSTEIN': 938, 'WELL GO USA': 939, 'WELL GO': 939, 'WELTKINO FILMVERLEIH': 940, 'WEST VIDEO': 941, 'WEST': 941, 'WHITE PEARL MOVIES': 942, 'WHITE PEARL': 942, 'WICKED-VISION MEDIA': 943, 'WICKED VISION MEDIA': 943, 'WICKEDVISION MEDIA': 943, 'WICKED-VISION': 943, 'WICKED VISION': 943, 'WICKEDVISION': 943, 'WIENERWORLD': 944, 'WILD BUNCH': 945, 'WILD EYE RELEASING': 946, 'WILD EYE': 946, 'WILD SIDE VIDEO': 947, 'WILD SIDE': 947, 'WME': 948, 'WOLFE VIDEO': 949, 'WOLFE': 949, 'WORD ON FIRE': 950, 'WORKS FILM GROUP': 951, 'WORLD WRESTLING': 952, 'WVG MEDIEN': 953, 'WWE STUDIOS': 954, 'WWE': 954, 'X RATED KULT': 955, 'X-RATED KULT': 955, 'X RATED CULT': 955, 'X-RATED CULT': 955, 'X RATED': 955, 'X-RATED': 955, 'XCESS': 956, 'XLRATOR': 957, 'XT VIDEO': 958, 'XT': 958, 'YAMATO VIDEO': 959, 'YAMATO': 959, 'YASH RAJ FILMS': 960, 'YASH RAJS': 960, 'ZEITGEIST FILMS': 961, 'ZEITGEIST': 961, 'ZENITH PICTURES': 962, 'ZENITH': 962, 'ZIMA': 963, 'ZYLO': 964, 'ZYX MUSIC': 965, 'ZYX': 965 }.get(distributor, 0) return distributor_id - async def unit3d_torrent_info(self, tracker, torrent_url, id): - tmdb = imdb = tvdb = description = category = infohash = mal = None + async def unit3d_torrent_info(self, tracker, torrent_url, search_url, id=None, file_name=None): + tmdb = imdb = tvdb = description = category = infohash = mal = files = None imagelist = [] - params = {'api_token' : self.config['TRACKERS'][tracker].get('api_key', '')} - url = f"{torrent_url}{id}" + + # Build the params for the API request + params = {'api_token': self.config['TRACKERS'][tracker].get('api_key', '')} + + # Determine the URL based on whether we're searching by ID or file name + if id: + url = f"{torrent_url}{id}" + console.print(f"[green]Searching {tracker} by ID: [bold yellow]{id}[/bold yellow]") + elif file_name: + url = f"{search_url}?file_name={file_name}" + console.print(f"[green]Searching {tracker} by file name: [bold yellow]{file_name}[/bold yellow]") + else: + console.print(f"[red]No ID or file name provided for search.[/red]") + return None, None, None, None, None, None, None, None, None + response = requests.get(url=url, params=params) - console.print(f"[green]Searching {tracker} for: [bold yellow]{filename}[/bold yellow]") + try: + # console.print(f"[green]Raw response from {tracker}: {response.text}[/green]") response = response.json() - attributes = response['attributes'] - category = attributes.get('category') - description = attributes.get('description') - tmdb = attributes.get('tmdb_id') - tvdb = attributes.get('tvdb_id') - mal = attributes.get('mal_id') - imdb = attributes.get('imdb_id') - infohash = attributes.get('info_hash') - - bbcode = BBCODE() - description, imagelist = bbcode.clean_unit3d_description(description, torrent_url) - console.print(f"[green]Successfully grabbed description from {tracker}") - except Exception: - console.print(traceback.print_exc()) - console.print(f"[yellow]Invalid Response from {tracker} API.") - + data = response.get('data', []) + if data: + attributes = data[0].get('attributes', {}) + + # Extract data from the attributes + category = attributes.get('category') + description = attributes.get('description') + tmdb = attributes.get('tmdb_id') + tvdb = attributes.get('tvdb_id') + mal = attributes.get('mal_id') + imdb = attributes.get('imdb_id') + infohash = attributes.get('info_hash') - return tmdb, imdb, tvdb, mal, description, category, infohash, imagelist + console.print(f"[blue]Extracted description: {description}[/blue]") + + # Process the description and imagelist if the description exists + if description: + bbcode = BBCODE() + description, imagelist = bbcode.clean_unit3d_description(description, torrent_url) + console.print(f"[green]Successfully grabbed description from {tracker}") + else: + console.print(f"[yellow]No description found for {tracker}.[/yellow]") + else: + console.print(f"[yellow]No data found in the response for {tracker}.[/yellow]") + + except Exception as e: + console.print_exception() + console.print(f"[yellow]Invalid Response from {tracker} API. Error: {str(e)}[/yellow]") + + return tmdb, imdb, tvdb, mal, description, category, infohash, imagelist, file_name async def parseCookieFile(self, cookiefile): """Parse a cookies.txt file and return a dictionary of key value pairs @@ -232,32 +259,6 @@ async def ptgen(self, meta, ptgen_site="", ptgen_retry=3): return "" return ptgen - - - # async def ptgen(self, meta): - # ptgen = "" - # url = "https://api.iyuu.cn/App.Movie.Ptgen" - # params = {} - # if int(meta.get('imdb_id', '0')) != 0: - # params['url'] = f"tt{meta['imdb_id']}" - # else: - # console.print("[red]No IMDb id was found.") - # params['url'] = console.input(f"[red]Please enter [yellow]Douban[/yellow] link: ") - # try: - # ptgen = requests.get(url, params=params) - # ptgen = ptgen.json() - # ptgen = ptgen['data']['format'] - # if "[/img]" in ptgen: - # ptgen = ptgen.split("[/img]")[1] - # ptgen = f"[img]{meta.get('imdb_info', {}).get('cover', meta.get('cover', ''))}[/img]{ptgen}" - # except: - # console.print_exception() - # console.print("[bold red]There was an error getting the ptgen") - # console.print(ptgen) - # return ptgen - - - async def filter_dupes(self, dupes, meta): if meta['debug']: console.log("[cyan]Pre-filtered dupes") @@ -270,35 +271,35 @@ async def filter_dupes(self, dupes, meta): remove_set = set({meta['resolution']}) search_combos = [ { - 'search' : meta['hdr'], - 'search_for' : {'HDR', 'PQ10'}, - 'update' : {'HDR|PQ10'} + 'search': meta['hdr'], + 'search_for': {'HDR', 'PQ10'}, + 'update': {'HDR|PQ10'} }, { - 'search' : meta['hdr'], - 'search_for' : {'DV'}, - 'update' : {'DV|DoVi'} + 'search': meta['hdr'], + 'search_for': {'DV'}, + 'update': {'DV|DoVi'} }, { - 'search' : meta['hdr'], - 'search_not' : {'DV', 'DoVi', 'HDR', 'PQ10'}, - 'update' : {'!(DV)|(DoVi)|(HDR)|(PQ10)'} + 'search': meta['hdr'], + 'search_not': {'DV', 'DoVi', 'HDR', 'PQ10'}, + 'update': {'!(DV)|(DoVi)|(HDR)|(PQ10)'} }, { - 'search' : str(meta.get('tv_pack', 0)), - 'search_for' : '1', - 'update' : {f"{meta['season']}(?!E\d+)"} + 'search': str(meta.get('tv_pack', 0)), + 'search_for': '1', + 'update': {f"{meta['season']}(?!E\d+)"} }, { - 'search' : meta['episode'], - 'search_for' : meta['episode'], - 'update' : {meta['season'], meta['episode']} + 'search': meta['episode'], + 'search_for': meta['episode'], + 'update': {meta['season'], meta['episode']} } ] search_matches = [ { - 'if' : {'REMUX', 'WEBDL', 'WEBRip', 'HDTV'}, - 'in' : meta['type'] + 'if': {'REMUX', 'WEBDL', 'WEBRip', 'HDTV'}, + 'in': meta['type'] } ] for s in search_combos: @@ -333,4 +334,4 @@ async def filter_dupes(self, dupes, meta): allow = False if allow and each not in new_dupes: new_dupes.append(each) - return new_dupes \ No newline at end of file + return new_dupes diff --git a/src/trackers/HDB.py b/src/trackers/HDB.py index f71187d72..86bb0eb28 100644 --- a/src/trackers/HDB.py +++ b/src/trackers/HDB.py @@ -561,4 +561,4 @@ async def search_filename(self, search_term, search_file_folder): console.print(f"[red]Failed to get info from HDB. Status code: {response.status_code}, Reason: {response.reason}[/red]") console.print(f'[yellow]Could not find a matching release on HDB[/yellow]') - return hdb_imdb, hdb_tvdb, hdb_name, hdb_torrenthash, hdb_id \ No newline at end of file + return hdb_imdb, hdb_tvdb, hdb_name, hdb_torrenthash, hdb_id From 26956c900f57de46bdebb8b2453ea8fe28f3ab9d Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 17:07:32 +1000 Subject: [PATCH 10/33] More linter --- .flake8 | 2 +- .github/workflows/.flake8 | 2 - data/example-config.py | 20 +++---- src/args.py | 27 +-------- src/bbcode.py | 33 ++--------- src/clients.py | 119 +++++++++++++++++++------------------- src/console.py | 4 +- src/discparse.py | 74 +++++++++++------------- src/exceptions.py | 19 +++--- src/search.py | 11 ++-- src/trackers/ACM.py | 23 +------- src/trackers/AITHER.py | 103 ++++++++++++++++----------------- src/trackers/HDB.py | 1 + 13 files changed, 189 insertions(+), 249 deletions(-) delete mode 100644 .github/workflows/.flake8 diff --git a/.flake8 b/.flake8 index 905c8bbe4..0cb611f43 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,2 @@ [flake8] -max-line-length = 160 \ No newline at end of file +max-line-length = 220 \ No newline at end of file diff --git a/.github/workflows/.flake8 b/.github/workflows/.flake8 deleted file mode 100644 index 905c8bbe4..000000000 --- a/.github/workflows/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 160 \ No newline at end of file diff --git a/data/example-config.py b/data/example-config.py index 1e2a23f5d..78fa1ccec 100644 --- a/data/example-config.py +++ b/data/example-config.py @@ -41,7 +41,7 @@ "default_trackers": "BLU, BHD, AITHER, STC, STT, SN, THR, R4E, HP, ACM, PTP, LCD, LST, PTER, NBL, ANT, MTV, CBR, RTF, HUNO, BHDTV, LT, PTER, TL, TDC, HDT, OE, RF, OTW, FNP, UTP, AL, HDB", "BLU": { - "useAPI": False, # Set to True if using BLU + "useAPI": False, # Set to True if using BLU "api_key": "BLU api key", "announce_url": "https://blutopia.cc/announce/customannounceurl", # "anon" : False @@ -55,7 +55,7 @@ "BHDTV": { "api_key": "found under https://www.bit-hdtv.com/my.php", "announce_url": "https://trackerr.bit-hdtv.com/announce", - #passkey found under https://www.bit-hdtv.com/my.php + # passkey found under https://www.bit-hdtv.com/my.php "my_announce_url": "https://trackerr.bit-hdtv.com/passkey/announce", # "anon" : "False" }, @@ -187,7 +187,7 @@ # "anon" : False }, "RTF": { - "username": "username", + "username": "username", "password": "password", "api_key": 'get_it_by_running_/api/ login command from https://retroflix.club/api/doc', "announce_url": "get from upload page", @@ -225,7 +225,7 @@ "passkey": "HDB passkey", "announce_url": "https://hdbits.org/announce/Custom_Announce_URL", "anon": False, - }, + }, "MANUAL": { # Uncomment and replace link with filebrowser (https://github.com/filebrowser/filebrowser) link to the Upload-Assistant directory, this will link to your filebrowser instead of uploading to uguu.se # "filebrowser" : "https://domain.tld/filebrowser/files/Upload-Assistant/" @@ -262,13 +262,13 @@ # "torrent_storage_dir": "path/to/BT_backup folder" # "qbit_tag": "tag", # "qbit_cat": "category" - + # Content Layout for adding .torrents: "Original"(recommended)/"Subfolder"/"NoSubfolder" "content_layout": "Original" - + # Enable automatic torrent management if listed path(s) are present in the path - # If using remote path mapping, use remote path - # For using multiple paths, use a list ["path1", "path2"] + # If using remote path mapping, use remote path + # For using multiple paths, use a list ["path1", "path2"] # "automatic_management_paths" : "" # Remote path mapping (docker/etc.) CASE SENSITIVE # "local_path" : "E:\\downloads\\tv", @@ -296,7 +296,7 @@ "deluge_user": "username", "deluge_pass": "password", # "torrent_storage_dir" : "path/to/session folder", - + # Remote path mapping (docker/etc.) CASE SENSITIVE # "local_path" : "/LocalPath", # "remote_path" : "/RemotePath" @@ -332,4 +332,4 @@ "CANCEL": "🚫" } } -} \ No newline at end of file +} diff --git a/src/args.py b/src/args.py index b1f55cf3b..d431cdbb3 100644 --- a/src/args.py +++ b/src/args.py @@ -3,7 +3,6 @@ import urllib.parse import os import datetime -import traceback from src.console import console @@ -15,9 +14,7 @@ class Args(): def __init__(self, config): self.config = config pass - - def parse(self, args, meta): input = args parser = argparse.ArgumentParser() @@ -81,7 +78,6 @@ def parse(self, args, meta): parser.add_argument('-ua', '--unattended', action='store_true', required=False, help=argparse.SUPPRESS) parser.add_argument('-vs', '--vapoursynth', action='store_true', required=False, help="Use vapoursynth for screens (requires vs install)") parser.add_argument('-cleanup', '--cleanup', action='store_true', required=False, help="Clean up tmp directory") - parser.add_argument('-fl', '--freeleech', nargs='*', required=False, help="Freeleech Percentage", default=0, dest="freeleech") args, before_args = parser.parse_known_args(input) args = vars(args) @@ -95,8 +91,8 @@ def parse(self, args, meta): break else: break - - if meta.get('tmdb_manual') != None or meta.get('imdb') != None: + + if meta.get('tmdb_manual') is not None or meta.get('imdb') is not None: meta['tmdb_manual'] = meta['imdb'] = None for key in args: value = args.get(key) @@ -104,7 +100,7 @@ def parse(self, args, meta): if isinstance(value, list): value2 = self.list_to_string(value) if key == 'type': - meta[key] = value2.upper().replace('-','') + meta[key] = value2.upper().replace('-', '') elif key == 'tag': meta[key] = f"-{value2}" elif key == 'screens': @@ -169,7 +165,6 @@ def parse(self, args, meta): # parser.print_help() return meta, parser, before_args - def list_to_string(self, list): if len(list) == 1: return str(list[0]) @@ -179,7 +174,6 @@ def list_to_string(self, list): result = "None" return result - def parse_tmdb_id(self, id, category): id = id.lower().lstrip() if id.startswith('tv'): @@ -191,18 +185,3 @@ def parse_tmdb_id(self, id, category): else: id = id return category, id - - - - - - - - - - - - - - - diff --git a/src/bbcode.py b/src/bbcode.py index 8ddff33c8..5c5886ba4 100644 --- a/src/bbcode.py +++ b/src/bbcode.py @@ -72,7 +72,6 @@ def clean_ptp_description(self, desc, is_disc): elif any(x in is_disc for x in ["BDMV", "DVD"]): return "" - # Convert Quote tags: desc = re.sub("\[quote.*?\]", "[code]", desc) desc = desc.replace("[/quote]", "[/code]") @@ -91,7 +90,6 @@ def clean_ptp_description(self, desc, is_disc): # Remove Staff tags desc = re.sub("\[staff[\s\S]*?\[\/staff\]", "", desc) - #Remove Movie/Person/User/hr/Indent remove_list = [ '[movie]', '[/movie]', @@ -103,7 +101,7 @@ def clean_ptp_description(self, desc, is_disc): ] for each in remove_list: desc = desc.replace(each, '') - + #Catch Stray Images comps = re.findall("\[comparison=[\s\S]*?\[\/comparison\]", desc) hides = re.findall("\[hide[\s\S]*?\[\/hide\]", desc) @@ -117,7 +115,6 @@ def clean_ptp_description(self, desc, is_disc): desc = desc.replace(comps[i], f"COMPARISON_PLACEHOLDER-{i} ") comp_placeholders.append(comps[i]) - # Remove Images in IMG tags: desc = re.sub("\[img\][\s\S]*?\[\/img\]", "", desc, flags=re.IGNORECASE) desc = re.sub("\[img=[\s\S]*?\]", "", desc, flags=re.IGNORECASE) @@ -145,7 +142,6 @@ def clean_ptp_description(self, desc, is_disc): if desc.replace('\n', '') == '': return "" return desc - def clean_unit3d_description(self, desc, site): # Unescape html @@ -175,7 +171,7 @@ def clean_unit3d_description(self, desc, site): nospoil = nospoil.replace(spoilers[i], '') desc = desc.replace(spoilers[i], f"SPOILER_PLACEHOLDER-{i} ") spoiler_placeholders.append(spoilers[i]) - + # Get Images from outside spoilers imagelist = [] url_tags = re.findall("\[url=[\s\S]*?\[\/url\]", desc) @@ -213,7 +209,7 @@ def clean_unit3d_description(self, desc, site): # Convert Comparison spoilers to [comparison=] desc = self.convert_collapse_to_comparison(desc, "spoiler", spoilers) - + # Strip blank lines: desc = desc.strip('\n') desc = re.sub("\n\n+", "\n\n", desc) @@ -225,32 +221,16 @@ def clean_unit3d_description(self, desc, site): return "", imagelist return desc, imagelist - - - - - - - - - - - - - - - def convert_pre_to_code(self, desc): desc = desc.replace('[pre]', '[code]') desc = desc.replace('[/pre]', '[/code]') return desc - def convert_hide_to_spoiler(self, desc): desc = desc.replace('[hide', '[spoiler') desc = desc.replace('[/hide]', '[/spoiler]') return desc - + def convert_spoiler_to_hide(self, desc): desc = desc.replace('[spoiler', '[hide') desc = desc.replace('[/spoiler]', '[/hide]') @@ -259,7 +239,7 @@ def convert_spoiler_to_hide(self, desc): def remove_spoiler(self, desc): desc = re.sub("\[\/?spoiler[\s\S]*?\]", "", desc, flags=re.IGNORECASE) return desc - + def convert_spoiler_to_code(self, desc): desc = desc.replace('[spoiler', '[code') desc = desc.replace('[/spoiler]', '[/code]') @@ -295,7 +275,6 @@ def convert_comparison_to_collapse(self, desc, max_width): desc = desc.replace(comp, new_bbcode) return desc - def convert_comparison_to_centered(self, desc, max_width): comparisons = re.findall("\[comparison=[\s\S]*?\[\/comparison\]", desc) for comp in comparisons: @@ -348,4 +327,4 @@ def convert_collapse_to_comparison(self, desc, spoiler_hide, collapses): final_sources = ', '.join(final_sources) spoil2comp = f"[comparison={final_sources}]{comp_images}[/comparison]" desc = desc.replace(tag, spoil2comp) - return desc \ No newline at end of file + return desc diff --git a/src/clients.py b/src/clients.py index 518babd80..9025845d8 100644 --- a/src/clients.py +++ b/src/clients.py @@ -4,7 +4,7 @@ import bencode import os import qbittorrentapi -from deluge_client import DelugeRPCClient, LocalDelugeRPCClient +from deluge_client import DelugeRPCClient import base64 from pyrobase.parts import Bunch import errno @@ -12,7 +12,8 @@ import ssl import shutil import time -from src.console import console +from src.console import console + class Clients(): """ @@ -21,30 +22,30 @@ class Clients(): def __init__(self, config): self.config = config pass - + async def add_to_client(self, meta, tracker): torrent_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}]{meta['clean_name']}.torrent" - if meta.get('no_seed', False) == True: - console.print(f"[bold red]--no-seed was passed, so the torrent will not be added to the client") - console.print(f"[bold yellow]Add torrent manually to the client") + if meta.get('no_seed', False) is True: + console.print("[bold red]--no-seed was passed, so the torrent will not be added to the client") + console.print("[bold yellow]Add torrent manually to the client") return if os.path.exists(torrent_path): torrent = Torrent.read(torrent_path) else: return - if meta.get('client', None) == None: + if meta.get('client', None) is None: default_torrent_client = self.config['DEFAULT']['default_torrent_client'] else: default_torrent_client = meta['client'] - if meta.get('client', None) == 'none': + if meta.get('client', None) is 'none': + return + if default_torrent_client is "none": return - if default_torrent_client == "none": - return client = self.config['TORRENT_CLIENTS'][default_torrent_client] torrent_client = client['torrent_client'] - + local_path, remote_path = await self.remote_path_map(meta) - + console.print(f"[bold green]Adding to {torrent_client}") if torrent_client.lower() == "rtorrent": self.rtorrent(meta['path'], torrent_path, torrent, meta, local_path, remote_path, client) @@ -57,33 +58,33 @@ async def add_to_client(self, meta, tracker): elif torrent_client.lower() == "watch": shutil.copy(torrent_path, client['watch_folder']) return - + async def find_existing_torrent(self, meta): - if meta.get('client', None) == None: + if meta.get('client', None) is None: default_torrent_client = self.config['DEFAULT']['default_torrent_client'] else: default_torrent_client = meta['client'] - if meta.get('client', None) == 'none' or default_torrent_client == 'none': + if meta.get('client', None) is 'none' or default_torrent_client is 'none': return None client = self.config['TORRENT_CLIENTS'][default_torrent_client] torrent_storage_dir = client.get('torrent_storage_dir', None) torrent_client = client.get('torrent_client', None).lower() - if torrent_storage_dir == None and torrent_client != "watch": + if torrent_storage_dir is None and torrent_client != "watch": console.print(f'[bold red]Missing torrent_storage_dir for {default_torrent_client}') return None elif not os.path.exists(str(torrent_storage_dir)) and torrent_client != "watch": console.print(f"[bold red]Invalid torrent_storage_dir path: [bold yellow]{torrent_storage_dir}") torrenthash = None - if torrent_storage_dir != None and os.path.exists(torrent_storage_dir): - if meta.get('torrenthash', None) != None: + if torrent_storage_dir is not None and os.path.exists(torrent_storage_dir): + if meta.get('torrenthash', None) is not None: valid, torrent_path = await self.is_valid_torrent(meta, f"{torrent_storage_dir}/{meta['torrenthash']}.torrent", meta['torrenthash'], torrent_client, print_err=True) if valid: torrenthash = meta['torrenthash'] - elif meta.get('ext_torrenthash', None) != None: + elif meta.get('ext_torrenthash', None) is not None: valid, torrent_path = await self.is_valid_torrent(meta, f"{torrent_storage_dir}/{meta['ext_torrenthash']}.torrent", meta['ext_torrenthash'], torrent_client, print_err=True) if valid: torrenthash = meta['ext_torrenthash'] - if torrent_client == 'qbit' and torrenthash == None and client.get('enable_search') == True: + if torrent_client == 'qbit' and torrenthash is None and client.get('enable_search') is True: torrenthash = await self.search_qbit_for_torrent(meta, client) if not torrenthash: console.print("[bold yellow]No Valid .torrent found") @@ -93,14 +94,14 @@ async def find_existing_torrent(self, meta): valid2, torrent_path = await self.is_valid_torrent(meta, torrent_path, torrenthash, torrent_client, print_err=False) if valid2: return torrent_path - + return None async def is_valid_torrent(self, meta, torrent_path, torrenthash, torrent_client, print_err=False): valid = False wrong_file = False err_print = "" - + # Normalize the torrent hash based on the client if torrent_client in ('qbit', 'deluge'): torrenthash = torrenthash.lower().strip() @@ -108,22 +109,22 @@ async def is_valid_torrent(self, meta, torrent_path, torrenthash, torrent_client elif torrent_client == 'rtorrent': torrenthash = torrenthash.upper().strip() torrent_path = torrent_path.replace(torrenthash.upper(), torrenthash) - + if meta['debug']: console.log(f"[DEBUG] Torrent path after normalization: {torrent_path}") - + # Check if torrent file exists if os.path.exists(torrent_path): torrent = Torrent.read(torrent_path) - + # Reuse if disc and basename matches or --keep-folder was specified - if meta.get('is_disc', None) != None or (meta['keep_folder'] and meta['isdir']): + if meta.get('is_disc', None) is not None or (meta['keep_folder'] and meta['isdir']): torrent_filepath = os.path.commonpath(torrent.files) if os.path.basename(meta['path']) in torrent_filepath: valid = True if meta['debug']: console.log(f"[DEBUG] Torrent is valid based on disc/basename or keep-folder: {valid}") - + # If one file, check for folder if len(torrent.files) == len(meta['filelist']) == 1: if os.path.basename(torrent.files[0]) == os.path.basename(meta['filelist'][0]): @@ -133,36 +134,36 @@ async def is_valid_torrent(self, meta, torrent_path, torrenthash, torrent_client wrong_file = True if meta['debug']: console.log(f"[DEBUG] Single file match status: valid={valid}, wrong_file={wrong_file}") - + # Check if number of files matches number of videos elif len(torrent.files) == len(meta['filelist']): torrent_filepath = os.path.commonpath(torrent.files) actual_filepath = os.path.commonpath(meta['filelist']) local_path, remote_path = await self.remote_path_map(meta) - + if local_path.lower() in meta['path'].lower() and local_path.lower() != remote_path.lower(): actual_filepath = torrent_path.replace(local_path, remote_path) actual_filepath = torrent_path.replace(os.sep, '/') - + if meta['debug']: console.log(f"[DEBUG] torrent_filepath: {torrent_filepath}") console.log(f"[DEBUG] actual_filepath: {actual_filepath}") - + if torrent_filepath in actual_filepath: valid = True if meta['debug']: console.log(f"[DEBUG] Multiple file match status: valid={valid}") - + else: console.print(f'[bold yellow]{torrent_path} was not found') - + # Additional checks if the torrent is valid so far if valid: if os.path.exists(torrent_path): reuse_torrent = Torrent.read(torrent_path) if meta['debug']: console.log(f"[DEBUG] Checking piece size and count: pieces={reuse_torrent.pieces}, piece_size={reuse_torrent.piece_size}") - + if (reuse_torrent.pieces >= 7000 and reuse_torrent.piece_size < 8388608) or (reuse_torrent.pieces >= 4000 and reuse_torrent.piece_size < 4194304): err_print = "[bold yellow]Too many pieces exist in current hash. REHASHING" valid = False @@ -178,11 +179,11 @@ async def is_valid_torrent(self, meta, torrent_path, torrenthash, torrent_client console.log(f"[DEBUG] Final validity after piece checks: valid={valid}") else: err_print = '[bold yellow]Unwanted Files/Folders Identified' - + # Print the error message if needed if print_err: console.print(err_print) - + return valid, torrent_path async def search_qbit_for_torrent(self, meta, client): @@ -193,7 +194,7 @@ async def search_qbit_for_torrent(self, meta, client): console.print(f"Torrent storage directory found: {torrent_storage_dir}") else: console.print("No torrent storage directory found.") - if torrent_storage_dir == None and client.get("torrent_client", None) != "watch": + if torrent_storage_dir is None and client.get("torrent_client", None) != "watch": console.print(f"[bold red]Missing torrent_storage_dir for {self.config['DEFAULT']['default_torrent_client']}") return None @@ -213,7 +214,7 @@ async def search_qbit_for_torrent(self, meta, client): if local_path.lower() in meta['path'].lower() and local_path.lower() != remote_path.lower(): remote_path_map = True if meta['debug']: - console.print(f"Remote path mapping found!") + console.print("Remote path mapping found!") console.print(f"Local path: {local_path}") console.print(f"Remote path: {remote_path}") @@ -251,7 +252,7 @@ def rtorrent(self, path, torrent_path, torrent, meta, local_path, remote_path, c except EnvironmentError as exc: console.print("[red]Error making fast-resume data (%s)" % (exc,)) raise - + new_meta = bencode.bencode(fast_resume) if new_meta != metainfo: fr_file = torrent_path.replace('.torrent', '-resume.torrent') @@ -261,7 +262,7 @@ def rtorrent(self, path, torrent_path, torrent, meta, local_path, remote_path, c isdir = os.path.isdir(path) # if meta['type'] == "DISC": # path = os.path.dirname(path) - #Remote path mount + # Remote path mount modified_fr = False if local_path.lower() in path.lower() and local_path.lower() != remote_path.lower(): path_dir = os.path.dirname(path) @@ -270,16 +271,16 @@ def rtorrent(self, path, torrent_path, torrent, meta, local_path, remote_path, c shutil.copy(fr_file, f"{path_dir}/fr.torrent") fr_file = f"{os.path.dirname(path)}/fr.torrent" modified_fr = True - if isdir == False: + if isdir is False: path = os.path.dirname(path) - + console.print("[bold yellow]Adding and starting torrent") rtorrent.load.start_verbose('', fr_file, f"d.directory_base.set={path}") time.sleep(1) # Add labels - if client.get('rtorrent_label', None) != None: + if client.get('rtorrent_label', None) is not None: rtorrent.d.custom1.set(torrent.infohash, client['rtorrent_label']) - if meta.get('rtorrent_label') != None: + if meta.get('rtorrent_label') is not None: rtorrent.d.custom1.set(torrent.infohash, meta['rtorrent_label']) # Delete modified fr_file location @@ -291,7 +292,7 @@ def rtorrent(self, path, torrent_path, torrent, meta, local_path, remote_path, c async def qbittorrent(self, path, torrent, local_path, remote_path, client, is_disc, filelist, meta): # infohash = torrent.infohash - #Remote path mount + # Remote path mount isdir = os.path.isdir(path) if not isdir and len(filelist) == 1: path = os.path.dirname(path) @@ -313,15 +314,15 @@ async def qbittorrent(self, path, torrent, local_path, remote_path, client, is_d am_config = client.get('automatic_management_paths', '') if isinstance(am_config, list): for each in am_config: - if os.path.normpath(each).lower() in os.path.normpath(path).lower(): + if os.path.normpath(each).lower() in os.path.normpath(path).lower(): auto_management = True else: - if os.path.normpath(am_config).lower() in os.path.normpath(path).lower() and am_config.strip() != "": + if os.path.normpath(am_config).lower() in os.path.normpath(path).lower() and am_config.strip() != "": auto_management = True qbt_category = client.get("qbit_cat") if not meta.get("qbit_cat") else meta.get('qbit_cat') content_layout = client.get('content_layout', 'Original') - + qbt_client.torrents_add(torrent_files=torrent.dump(), save_path=path, use_auto_torrent_management=auto_management, is_skip_checking=True, content_layout=content_layout, category=qbt_category) # Wait for up to 30 seconds for qbit to actually return the download # there's an async race conditiion within qbt that it will return ok before the torrent is actually added @@ -330,27 +331,27 @@ async def qbittorrent(self, path, torrent, local_path, remote_path, client, is_d break await asyncio.sleep(1) qbt_client.torrents_resume(torrent.infohash) - if client.get('qbit_tag', None) != None: + if client.get('qbit_tag', None) is not None: qbt_client.torrents_add_tags(tags=client.get('qbit_tag'), torrent_hashes=torrent.infohash) - if meta.get('qbit_tag') != None: + if meta.get('qbit_tag') is not None: qbt_client.torrents_add_tags(tags=meta.get('qbit_tag'), torrent_hashes=torrent.infohash) console.print(f"Added to: {path}") - + def deluge(self, path, torrent_path, torrent, local_path, remote_path, client, meta): client = DelugeRPCClient(client['deluge_url'], int(client['deluge_port']), client['deluge_user'], client['deluge_pass']) # client = LocalDelugeRPCClient() client.connect() - if client.connected == True: - console.print("Connected to Deluge") + if client.connected is True: + console.print("Connected to Deluge") isdir = os.path.isdir(path) - #Remote path mount + # Remote path mount if local_path.lower() in path.lower() and local_path.lower() != remote_path.lower(): path = path.replace(local_path, remote_path) path = path.replace(os.sep, '/') - + path = os.path.dirname(path) - client.call('core.add_torrent_file', torrent_path, base64.b64encode(torrent.dump()), {'download_location' : path, 'seed_mode' : True}) + client.call('core.add_torrent_file', torrent_path, base64.b64encode(torrent.dump()), {'download_location': path, 'seed_mode': True}) if meta['debug']: console.print(f"[cyan]Path: {path}") else: @@ -401,11 +402,11 @@ def add_fast_resume(self, metainfo, datapath, torrent): return metainfo async def remote_path_map(self, meta): - if meta.get('client', None) == None: + if meta.get('client', None) is None: torrent_client = self.config['DEFAULT']['default_torrent_client'] else: torrent_client = meta['client'] - local_path = list_local_path = self.config['TORRENT_CLIENTS'][torrent_client].get('local_path','/LocalPath') + local_path = list_local_path = self.config['TORRENT_CLIENTS'][torrent_client].get('local_path', '/LocalPath') remote_path = list_remote_path = self.config['TORRENT_CLIENTS'][torrent_client].get('remote_path', '/RemotePath') if isinstance(local_path, list): for i in range(len(local_path)): @@ -418,4 +419,4 @@ async def remote_path_map(self, meta): if local_path.endswith(os.sep): remote_path = remote_path + os.sep - return local_path, remote_path \ No newline at end of file + return local_path, remote_path diff --git a/src/console.py b/src/console.py index 61aeecb04..223c51181 100644 --- a/src/console.py +++ b/src/console.py @@ -1,2 +1,2 @@ -from rich.console import Console -console = Console() \ No newline at end of file +from rich.console import Console +console = Console() diff --git a/src/discparse.py b/src/discparse.py index 33d9b8c68..bc3ba2cfe 100644 --- a/src/discparse.py +++ b/src/discparse.py @@ -9,8 +9,8 @@ import json from src.console import console - - + + class DiscParse(): def __init__(self): pass @@ -54,7 +54,7 @@ async def get_bdinfo(self, discs, folder_id, base_dir, meta_discs): try: if bdinfo_text == "": for file in os.listdir(save_dir): - if file.startswith(f"BDINFO"): + if file.startswith("BDINFO"): bdinfo_text = save_dir + "/" + file with open(bdinfo_text, 'r') as f: text = f.read() @@ -64,7 +64,7 @@ async def get_bdinfo(self, discs, folder_id, base_dir, meta_discs): result = result2.split("********************", 1) bd_summary = result[0].rstrip(" \n") f.close() - with open(bdinfo_text, 'r') as f: # parse extended BDInfo + with open(bdinfo_text, 'r') as f: # parse extended BDInfo text = f.read() result = text.split("[code]", 3) result2 = result[2].rstrip(" \n") @@ -84,21 +84,19 @@ async def get_bdinfo(self, discs, folder_id, base_dir, meta_discs): with open(f"{save_dir}/BD_SUMMARY_{str(i).zfill(2)}.txt", 'w') as f: f.write(bd_summary.strip()) f.close() - with open(f"{save_dir}/BD_SUMMARY_EXT.txt", 'w') as f: # write extended BDInfo file + with open(f"{save_dir}/BD_SUMMARY_EXT.txt", 'w') as f: # write extended BDInfo file f.write(ext_bd_summary.strip()) f.close() - + bdinfo = self.parse_bdinfo(bd_summary, files[1], path) - + discs[i]['summary'] = bd_summary.strip() discs[i]['bdinfo'] = bdinfo # shutil.rmtree(f"{base_dir}/tmp") else: discs = meta_discs - + return discs, discs[0]['bdinfo'] - - def parse_bdinfo(self, bdinfo_input, files, path): bdinfo = dict() @@ -113,15 +111,15 @@ def parse_bdinfo(self, bdinfo_input, files, path): line = l.replace("*", "").strip().lower() if line.startswith("playlist:"): playlist = l.split(':', 1)[1] - bdinfo['playlist'] = playlist.split('.',1)[0].strip() + bdinfo['playlist'] = playlist.split('.', 1)[0].strip() if line.startswith("disc size:"): size = l.split(':', 1)[1] - size = size.split('bytes', 1)[0].replace(',','') + size = size.split('bytes', 1)[0].replace(',', '') size = float(size)/float(1<<30) bdinfo['size'] = size if line.startswith("length:"): length = l.split(':', 1)[1] - bdinfo['length'] = length.split('.',1)[0].strip() + bdinfo['length'] = length.split('.', 1)[0].strip() if line.startswith("video:"): split1 = l.split(':', 1)[1] split2 = split1.split('/', 12) @@ -142,16 +140,16 @@ def parse_bdinfo(self, bdinfo_input, files, path): hdr_dv = "" color = "" bdinfo['video'].append({ - 'codec': split2[0].strip(), - 'bitrate': split2[1].strip(), - 'res': split2[n+2].strip(), - 'fps': split2[n+3].strip(), - 'aspect_ratio' : split2[n+4].strip(), + 'codec': split2[0].strip(), + 'bitrate': split2[1].strip(), + 'res': split2[n+2].strip(), + 'fps': split2[n+3].strip(), + 'aspect_ratio': split2[n+4].strip(), 'profile': split2[n+5].strip(), - 'bit_depth' : bit_depth, - 'hdr_dv' : hdr_dv, - 'color' : color, - '3d' : three_dim, + 'bit_depth': bit_depth, + 'hdr_dv': hdr_dv, + 'color': color, + '3d': three_dim, }) elif line.startswith("audio:"): if "(" in l: @@ -170,12 +168,12 @@ def parse_bdinfo(self, bdinfo_input, files, path): except: bit_depth = "" bdinfo['audio'].append({ - 'language' : split2[0].strip(), - 'codec' : split2[1].strip(), - 'channels' : split2[n+2].strip(), - 'sample_rate' : split2[n+3].strip(), - 'bitrate' : split2[n+4].strip(), - 'bit_depth' : bit_depth, # Also DialNorm, but is not in use anywhere yet + 'language': split2[0].strip(), + 'codec': split2[1].strip(), + 'channels': split2[n+2].strip(), + 'sample_rate': split2[n+3].strip(), + 'bitrate': split2[n+4].strip(), + 'bit_depth': bit_depth, # Also DialNorm, but is not in use anywhere yet 'atmos_why_you_be_like_this': fuckatmos, }) elif line.startswith("disc title:"): @@ -205,8 +203,6 @@ def parse_bdinfo(self, bdinfo_input, files, path): except: pass return bdinfo - - """ Parse VIDEO_TS and get mediainfos @@ -215,7 +211,7 @@ async def get_dvdinfo(self, discs): for each in discs: path = each.get('path') os.chdir(path) - files = glob(f"VTS_*.VOB") + files = glob("VTS_*.VOB") files.sort() # Switch to ordered dictionary filesdict = OrderedDict() @@ -232,8 +228,7 @@ async def get_dvdinfo(self, discs): vob_set_mi = MediaInfo.parse(f"VTS_{vob_set[0][:2]}_0.IFO", output='JSON') vob_set_mi = json.loads(vob_set_mi) vob_set_duration = vob_set_mi['media']['track'][1]['Duration'] - - + # If the duration of the new vob set > main set by more than 10% then it's our new main set # This should make it so TV shows pick the first episode if (float(vob_set_duration) * 1.00) > (float(main_set_duration) * 1.10) or len(main_set) < 1: @@ -243,11 +238,10 @@ async def get_dvdinfo(self, discs): set = main_set[0][:2] each['vob'] = vob = f"{path}/VTS_{set}_1.VOB" each['ifo'] = ifo = f"{path}/VTS_{set}_0.IFO" - each['vob_mi'] = MediaInfo.parse(os.path.basename(vob), output='STRING', full=False, mediainfo_options={'inform_version' : '1'}).replace('\r\n', '\n') - each['ifo_mi'] = MediaInfo.parse(os.path.basename(ifo), output='STRING', full=False, mediainfo_options={'inform_version' : '1'}).replace('\r\n', '\n') - each['vob_mi_full'] = MediaInfo.parse(vob, output='STRING', full=False, mediainfo_options={'inform_version' : '1'}).replace('\r\n', '\n') - each['ifo_mi_full'] = MediaInfo.parse(ifo, output='STRING', full=False, mediainfo_options={'inform_version' : '1'}).replace('\r\n', '\n') - + each['vob_mi'] = MediaInfo.parse(os.path.basename(vob), output='STRING', full=False, mediainfo_options={'inform_version': '1'}).replace('\r\n', '\n') + each['ifo_mi'] = MediaInfo.parse(os.path.basename(ifo), output='STRING', full=False, mediainfo_options={'inform_version': '1'}).replace('\r\n', '\n') + each['vob_mi_full'] = MediaInfo.parse(vob, output='STRING', full=False, mediainfo_options={'inform_version': '1'}).replace('\r\n', '\n') + each['ifo_mi_full'] = MediaInfo.parse(ifo, output='STRING', full=False, mediainfo_options={'inform_version': '1'}).replace('\r\n', '\n') size = sum(os.path.getsize(f) for f in os.listdir('.') if os.path.isfile(f))/float(1<<30) if size <= 7.95: @@ -256,7 +250,7 @@ async def get_dvdinfo(self, discs): dvd_size = "DVD5" each['size'] = dvd_size return discs - + async def get_hddvd_info(self, discs): for each in discs: path = each.get('path') @@ -270,6 +264,6 @@ async def get_hddvd_info(self, discs): if file_size > size: largest = file size = file_size - each['evo_mi'] = MediaInfo.parse(os.path.basename(largest), output='STRING', full=False, mediainfo_options={'inform_version' : '1'}) + each['evo_mi'] = MediaInfo.parse(os.path.basename(largest), output='STRING', full=False, mediainfo_options={'inform_version': '1'}) each['largest_evo'] = os.path.abspath(f"{path}/{largest}") return discs diff --git a/src/exceptions.py b/src/exceptions.py index b4c6dbead..5aa798ce0 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -7,9 +7,10 @@ def __init__(self, *args, **kwargs): if args: # ... pass them to the super constructor super().__init__(*args, **kwargs) - else: # else, the exception was raised without arguments ... - # ... pass the default message to the super constructor - super().__init__(default_message, **kwargs) + else: # else, the exception was raised without arguments ... + # ... pass the default message to the super constructor + super().__init__(default_message, **kwargs) + class UploadException(Exception): def __init__(self, *args, **kwargs): @@ -20,14 +21,18 @@ def __init__(self, *args, **kwargs): if args: # ... pass them to the super constructor super().__init__(*args, **kwargs) - else: # else, the exception was raised without arguments ... - # ... pass the default message to the super constructor - super().__init__(default_message, **kwargs) + else: # else, the exception was raised without arguments ... + # ... pass the default message to the super constructor + super().__init__(default_message, **kwargs) class XEMNotFound(Exception): pass + + class WeirdSystem(Exception): pass + + class ManualDateException(Exception): - pass \ No newline at end of file + pass diff --git a/src/search.py b/src/search.py index 8e782ee7e..911636479 100644 --- a/src/search.py +++ b/src/search.py @@ -1,8 +1,8 @@ import platform -import asyncio import os from src.console import console + class Search(): """ Logic for searching @@ -11,7 +11,6 @@ def __init__(self, config): self.config = config pass - async def searchFile(self, filename): os_info = platform.platform() filename = filename.lower() @@ -54,6 +53,7 @@ async def searchFolder(self, foldername): return folders_found = False words = foldername.split() + async def search_dir(search_dir): console.print(f"Searching {search_dir}") folders_total_search = [] @@ -70,23 +70,24 @@ async def search_dir(search_dir): folders_total_search.append(root+'\\'+name) else: folders_total_search.append(root+'/'+name) - + return folders_total_search config_dir = self.config['DISCORD']['search_dir'] if isinstance(config_dir, list): for each in config_dir: folders = await search_dir(each) - + folders_total = folders_total + folders else: folders_total = await search_dir(config_dir) return folders_total return folders_total + async def file_search(self, name, name_words): check = True for word in name_words: if word not in name: check = False break - return check \ No newline at end of file + return check diff --git a/src/trackers/ACM.py b/src/trackers/ACM.py index 194a2d0a2..ec4276cd9 100644 --- a/src/trackers/ACM.py +++ b/src/trackers/ACM.py @@ -9,7 +9,6 @@ from src.console import console - class ACM(): """ Edit for Tracker: @@ -19,12 +18,6 @@ class ACM(): Upload """ - ############################################################### - ######## EDIT ME ######## - ############################################################### - - # ALSO EDIT CLASS NAME ABOVE - def __init__(self, config): self.config = config self.tracker = 'ACM' @@ -34,7 +27,7 @@ def __init__(self, config): self.signature = None self.banned_groups = [""] pass - + async def get_cat_id(self, category_name): category_id = { 'MOVIE': '1', @@ -179,7 +172,7 @@ def get_subtitles(self, meta): for lang, subID in sub_lang_map.items(): if language in lang and subID not in sub_langs: sub_langs.append(subID) - + # if sub_langs == []: # sub_langs = [44] # No Subtitle return sub_langs @@ -193,10 +186,6 @@ def get_subs_tag(self, subs): return ' [No Eng subs]' return f" [{subs[0]} subs only]" - ############################################################### - ###### STOP HERE UNLESS EXTRA MODIFICATION IS NEEDED ###### - ############################################################### - async def upload(self, meta): common = COMMON(config=self.config) await common.edit_torrent(meta, self.tracker, self.source_flag) @@ -264,7 +253,7 @@ async def upload(self, meta): params = { 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip() } - + if meta['debug'] == False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: @@ -277,10 +266,6 @@ async def upload(self, meta): console.print(data) open_torrent.close() - - - - async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") @@ -348,8 +333,6 @@ async def edit_name(self, meta): name = name + self.get_subs_tag(subs) return name - - async def edit_desc(self, meta): base = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'r').read() with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w') as descfile: diff --git a/src/trackers/AITHER.py b/src/trackers/AITHER.py index 3bf94eea9..6a9b709de 100644 --- a/src/trackers/AITHER.py +++ b/src/trackers/AITHER.py @@ -2,15 +2,14 @@ # import discord import asyncio import requests -from difflib import SequenceMatcher from str2bool import str2bool import json -import os import platform from src.trackers.COMMON import COMMON from src.console import console + class AITHER(): """ Edit for Tracker: @@ -25,7 +24,7 @@ def __init__(self, config): self.source_flag = 'Aither' self.search_url = 'https://aither.cc/api/torrents/filter' self.upload_url = 'https://aither.cc/api/torrents/upload' - self.signature = f"\n[center][url=https://aither.cc/forums/topics/1349/posts/24958]Created by L4G's Upload Assistant[/url][/center]" + self.signature = "\n[center][url=https://aither.cc/forums/topics/1349/posts/24958]Created by L4G's Upload Assistant[/url][/center]" self.banned_groups = ['4K4U', 'AROMA', 'd3g', 'edge2020', 'EMBER', 'EVO', 'FGT', 'FreetheFish', 'Hi10', 'HiQVE', 'ION10', 'iVy', 'Judas', 'LAMA', 'MeGusta', 'nikt0', 'OEPlus', 'OFT', 'OsC', 'PYC', 'QxR', 'Ralphy', 'RARBG', 'RetroPeeps', 'SAMPA', 'Sicario', 'Silence', 'SkipTT', 'SPDVD', 'STUTTERSHIT', 'SWTYBLZ', 'TAoE', 'TGx', 'Tigole', 'TSP', 'TSPxL', 'VXT', 'Weasley[HONE]', 'Will1869', 'x0r', 'YIFY'] @@ -39,11 +38,11 @@ async def upload(self, meta): type_id = await self.get_type_id(meta['type']) resolution_id = await self.get_res_id(meta['resolution']) name = await self.edit_name(meta) - if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) == False: + if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) is False: anon = 0 else: anon = 1 - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: @@ -53,28 +52,28 @@ async def upload(self, meta): open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent", 'rb') files = {'torrent': open_torrent} data = { - 'name' : name, - 'description' : desc, - 'mediainfo' : mi_dump, - 'bdinfo' : bd_dump, - 'category_id' : cat_id, - 'type_id' : type_id, - 'resolution_id' : resolution_id, - 'tmdb' : meta['tmdb'], - 'imdb' : meta['imdb_id'].replace('tt', ''), - 'tvdb' : meta['tvdb_id'], - 'mal' : meta['mal_id'], - 'igdb' : 0, - 'anonymous' : anon, - 'stream' : meta['stream'], - 'sd' : meta['sd'], - 'keywords' : meta['keywords'], - 'personal_release' : int(meta.get('personalrelease', False)), - 'internal' : 0, - 'featured' : 0, - 'free' : 0, - 'doubleup' : 0, - 'sticky' : 0, + 'name': name, + 'description': desc, + 'mediainfo': mi_dump, + 'bdinfo': bd_dump, + 'category_id': cat_id, + 'type_id': type_id, + 'resolution_id': resolution_id, + 'tmdb': meta['tmdb'], + 'imdb': meta['imdb_id'].replace('tt', ''), + 'tvdb': meta['tvdb_id'], + 'mal': meta['mal_id'], + 'igdb': 0, + 'anonymous': anon, + 'stream': meta['stream'], + 'sd': meta['sd'], + 'keywords': meta['keywords'], + 'personal_release': int(meta.get('personalrelease', False)), + 'internal': 0, + 'featured': 0, + 'free': 0, + 'doubleup': 0, + 'sticky': 0, } headers = { 'User-Agent': f'Upload Assistant/2.1 ({platform.system()} {platform.release()})' @@ -84,22 +83,22 @@ async def upload(self, meta): } # Internal - if self.config['TRACKERS'][self.tracker].get('internal', False) == True: + if self.config['TRACKERS'][self.tracker].get('internal', False) is True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 - + if meta.get('category') == "TV": data['season_number'] = meta.get('season_int', '0') data['episode_number'] = meta.get('episode_int', '0') - if meta['debug'] == False: + if meta['debug'] is False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) except: console.print("It may have uploaded, go check") - return + return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) open_torrent.close() @@ -132,17 +131,17 @@ async def edit_name(self, meta): async def get_cat_id(self, category_name): category_id = { - 'MOVIE': '1', - 'TV': '2', + 'MOVIE': '1', + 'TV': '2', }.get(category_name, '0') return category_id async def get_type_id(self, type): type_id = { - 'DISC': '1', + 'DISC': '1', 'REMUX': '2', - 'WEBDL': '4', - 'WEBRIP': '5', + 'WEBDL': '4', + 'WEBRIP': '5', 'HDTV': '6', 'ENCODE': '3' }.get(type, '0') @@ -150,16 +149,16 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { - '8640p':'10', - '4320p': '1', - '2160p': '2', - '1440p' : '3', + '8640p': '10', + '4320p': '1', + '2160p': '2', + '1440p': '3', '1080p': '3', - '1080i':'4', - '720p': '5', - '576p': '6', + '1080i': '4', + '720p': '5', + '576p': '6', '576i': '7', - '480p': '8', + '480p': '8', '480i': '9' }.get(resolution, '10') return resolution_id @@ -168,18 +167,18 @@ async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'tmdbId' : meta['tmdb'], - 'categories[]' : await self.get_cat_id(meta['category']), - 'types[]' : await self.get_type_id(meta['type']), - 'resolutions[]' : await self.get_res_id(meta['resolution']), - 'name' : "" + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'tmdbId': meta['tmdb'], + 'categories[]': await self.get_cat_id(meta['category']), + 'types[]': await self.get_type_id(meta['type']), + 'resolutions[]': await self.get_res_id(meta['resolution']), + 'name': "" } if meta['category'] == 'TV': params['name'] = params['name'] + f" {meta.get('season', '')}{meta.get('episode', '')}" if meta.get('edition', "") != "": params['name'] = params['name'] + f" {meta['edition']}" - + try: response = requests.get(url=self.search_url, params=params) response = response.json() diff --git a/src/trackers/HDB.py b/src/trackers/HDB.py index 86bb0eb28..25136f5dc 100644 --- a/src/trackers/HDB.py +++ b/src/trackers/HDB.py @@ -13,6 +13,7 @@ from src.exceptions import * from src.console import console from datetime import datetime, date +from torf import Torrent class HDB(): From 5caa9f5b4c90de0b642d7659d7d3bdd4e5c1badf Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 18:11:07 +1000 Subject: [PATCH 11/33] Print links and allow skip ID --- data/example-config.py | 6 +-- src/prep.py | 108 ++++++++++++++++++++++++++--------------- src/trackers/HDB.py | 2 +- 3 files changed, 72 insertions(+), 44 deletions(-) diff --git a/data/example-config.py b/data/example-config.py index 78fa1ccec..4bff88201 100644 --- a/data/example-config.py +++ b/data/example-config.py @@ -60,7 +60,7 @@ # "anon" : "False" }, "PTP": { - "useAPI": False, # Set to True if using PTP + "useAPI": False, # Set to True if using PTP "add_web_source_to_desc": True, "ApiUser": "ptp api user", "ApiKey": 'ptp api key', @@ -147,7 +147,7 @@ "api_key": "CBR api key", "announce_url": "https://capybarabr.com/announce/customannounceurl", # "anon" : False - }, + }, "LST": { "api_key": "LST api key", "announce_url": "https://lst.gg/announce/customannounceurl", @@ -179,7 +179,7 @@ "password": "password", "my_announce_url": "https://hdts-announce.ru/announce.php?pid=", # "anon" : "False" - "announce_url": "https://hdts-announce.ru/announce.php", #DO NOT EDIT THIS LINE + "announce_url": "https://hdts-announce.ru/announce.php", # DO NOT EDIT THIS LINE }, "OE": { "api_key": "OE api key", diff --git a/src/prep.py b/src/prep.py index 922c56096..33fa57016 100644 --- a/src/prep.py +++ b/src/prep.py @@ -70,6 +70,22 @@ def __init__(self, screens, img_host, config): self.img_host = img_host.lower() tmdb.API_KEY = config['DEFAULT']['tmdb_api'] + async def prompt_user_for_id_selection(self, blu_tmdb=None, blu_imdb=None, blu_tvdb=None, blu_filename=None, imdb=None): + if imdb: + imdb = str(imdb).zfill(7) # Convert to string and ensure IMDb ID is 7 characters long by adding leading zeros + console.print(f"[cyan]Found IMDb ID: https://www.imdb.com/title/tt{imdb}[/cyan]") + if blu_tmdb or blu_imdb or blu_tvdb: + if blu_imdb: + blu_imdb = str(blu_imdb).zfill(7) # Convert to string and ensure IMDb ID is 7 characters long by adding leading zeros + console.print(f"[cyan]Found the following IDs on BLU:[/cyan]") + console.print(f"TMDb ID: {blu_tmdb}") + console.print(f"IMDb ID: https://www.imdb.com/title/tt{blu_imdb}") + console.print(f"TVDb ID: {blu_tvdb}") + console.print(f"Filename: {blu_filename}") + + selection = input("Do you want to use this ID? (y/n): ").strip().lower() + return selection == 'y' + async def update_metadata_from_tracker(self, tracker_name, tracker_instance, meta, search_term, search_file_folder): tracker_key = tracker_name.lower() manual_key = f"{tracker_key}_manual" @@ -84,30 +100,37 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, id=meta[tracker_key]) if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: console.print(f"[green]Valid data found on {tracker_name}, setting meta values[/green]") - if blu_tmdb not in [None, '0']: - meta['tmdb_manual'] = blu_tmdb - if blu_imdb not in [None, '0']: - meta['imdb'] = str(blu_imdb) - if blu_tvdb not in [None, '0']: - meta['tvdb_id'] = blu_tvdb - if blu_mal not in [None, '0']: - meta['mal'] = blu_mal - if blu_desc not in [None, '0', '']: - meta['blu_desc'] = blu_desc - if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: - meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() - if meta.get('image_list', []) == []: - meta['image_list'] = blu_imagelist - if blu_filename: - meta['blu_filename'] = blu_filename # Store the filename in meta for later use - found_match = True # Set flag if any relevant data is found + # Prompt user for selection + if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): + if blu_tmdb not in [None, '0']: + meta['tmdb_manual'] = blu_tmdb + if blu_imdb not in [None, '0']: + meta['imdb'] = str(blu_imdb) + if blu_tvdb not in [None, '0']: + meta['tvdb_id'] = blu_tvdb + if blu_mal not in [None, '0']: + meta['mal'] = blu_mal + if blu_desc not in [None, '0', '']: + meta['blu_desc'] = blu_desc + if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: + meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() + if meta.get('image_list', []) == []: + meta['image_list'] = blu_imagelist + if blu_filename: + meta['blu_filename'] = blu_filename # Store the filename in meta for later use + found_match = True # Set flag if any relevant data is found + else: + console.print(f"[yellow]User skipped the found ID on {tracker_name}[/yellow]") else: console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") else: meta['imdb'], meta['ext_torrenthash'] = await tracker_instance.get_imdb_from_torrent_id(meta[tracker_key]) if meta['imdb']: - console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}[/green]") - found_match = True + if await self.prompt_user_for_id_selection(imdb=meta['imdb']): + console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}[/green]") + found_match = True + else: + console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}[/yellow]") else: console.print(f"[yellow]No IMDb ID found on {tracker_name}[/yellow]") else: @@ -121,34 +144,39 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met meta['tvdb_id'] = str(tvdb_id) if tvdb_id else meta.get('tvdb_id') meta['hdb_name'] = hdb_name elif tracker_name == "BLU": - # Attempt to search using the file name if ID is not available blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, file_name=search_term) if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: console.print(f"[green]Valid data found on {tracker_name} using file name, setting meta values[/green]") - if blu_tmdb not in [None, '0']: - meta['tmdb_manual'] = blu_tmdb - if blu_imdb not in [None, '0']: - meta['imdb'] = str(blu_imdb) - if blu_tvdb not in [None, '0']: - meta['tvdb_id'] = blu_tvdb - if blu_mal not in [None, '0']: - meta['mal'] = blu_mal - if blu_desc not in [None, '0', '']: - meta['blu_desc'] = blu_desc - if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: - meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() - if meta.get('image_list', []) == []: - meta['image_list'] = blu_imagelist - if blu_filename: - meta['blu_filename'] = blu_filename # Store the filename in meta for later use - found_match = True + if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): + if blu_tmdb not in [None, '0']: + meta['tmdb_manual'] = blu_tmdb + if blu_imdb not in [None, '0']: + meta['imdb'] = str(blu_imdb) + if blu_tvdb not in [None, '0']: + meta['tvdb_id'] = blu_tvdb + if blu_mal not in [None, '0']: + meta['mal'] = blu_mal + if blu_desc not in [None, '0', '']: + meta['blu_desc'] = blu_desc + if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: + meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() + if meta.get('image_list', []) == []: + meta['image_list'] = blu_imagelist + if blu_filename: + meta['blu_filename'] = blu_filename # Store the filename in meta for later use + found_match = True + else: + console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") else: imdb = tracker_id = None if imdb: - console.print(f"[green]{tracker_name} IMDb ID found: {imdb}[/green]") - meta['imdb'] = str(imdb) - found_match = True + if await self.prompt_user_for_id_selection(imdb=imdb): + console.print(f"[green]{tracker_name} IMDb ID found: {imdb}[/green]") + meta['imdb'] = str(imdb) + found_match = True + else: + console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}[/yellow]") if tracker_id: meta[tracker_key] = tracker_id diff --git a/src/trackers/HDB.py b/src/trackers/HDB.py index 25136f5dc..a38ad377a 100644 --- a/src/trackers/HDB.py +++ b/src/trackers/HDB.py @@ -553,7 +553,7 @@ async def search_filename(self, search_term, search_file_folder): hdb_name = each['name'] hdb_torrenthash = each['hash'] hdb_id = each['id'] - console.print(f'[bold green]Matched release with HDB ID: [yellow]{hdb_id}[/yellow][/bold green]') + console.print(f'[bold green]Matched release with HDB ID: [yellow]https://hdbits.org/details.php?id={hdb_id}[/yellow][/bold green]') return hdb_imdb, hdb_tvdb, hdb_name, hdb_torrenthash, hdb_id except Exception as e: console.print_exception() From e330ae07ef4f9f036de270e6b7418cd8231ff07e Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 19:58:49 +1000 Subject: [PATCH 12/33] More linting --- cogs/commands.py | 58 +++++----- src/args.py | 2 +- src/bbcode.py | 92 +++++++-------- src/clients.py | 12 +- src/discparse.py | 12 +- src/exceptions.py | 8 +- src/prep.py | 7 +- src/search.py | 5 +- src/trackers/ANT.py | 30 ++--- src/trackers/BHD.py | 76 ++++++------- src/trackers/BLU.py | 14 +-- src/trackers/COMMON.py | 25 ++-- src/trackers/FL.py | 50 ++++---- src/trackers/HDB.py | 195 ++++++++++++++++---------------- src/trackers/UNIT3D_TEMPLATE.py | 107 +++++++++--------- src/vs.py | 14 +-- upload.py | 151 +++++++++++++------------ 17 files changed, 413 insertions(+), 445 deletions(-) diff --git a/cogs/commands.py b/cogs/commands.py index ffc7af74f..230de6b73 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -1,4 +1,3 @@ -from discord.ext.commands.errors import CommandInvokeError from src.prep import Prep from src.args import Args from src.clients import Clients @@ -247,11 +246,11 @@ def check(reaction, user): # await ctx.send(folders_total) return - async def send_embed_and_upload(self,ctx,meta): + async def send_embed_and_upload(self, ctx, meta): prep = Prep(screens=meta['screens'], img_host=meta['imghost'], config=config) meta['name_notag'], meta['name'], meta['clean_name'], meta['potential_missing'] = await prep.get_name(meta) - if meta.get('uploaded_screens', False) == False: + if meta.get('uploaded_screens', False) is False: if meta.get('embed_msg_id', '0') != '0': message = await ctx.fetch_message(meta['embed_msg_id']) await message.edit(embed=discord.Embed(title="Uploading Screenshots", color=0xffff00)) @@ -261,17 +260,16 @@ async def send_embed_and_upload(self,ctx,meta): channel = message.channel.id return_dict = multiprocessing.Manager().dict() - u = multiprocessing.Process(target = prep.upload_screens, args=(meta, meta['screens'], 1, 0, meta['screens'], [], return_dict)) + u = multiprocessing.Process(target=prep.upload_screens, args=(meta, meta['screens'], 1, 0, meta['screens'], [], return_dict)) u.start() - while u.is_alive() == True: + while u.is_alive() is True: await asyncio.sleep(3) meta['image_list'] = return_dict['image_list'] if meta['debug']: print(meta['image_list']) meta['uploaded_screens'] = True - #Create base .torrent - + # Create base .torrent if len(glob(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent")) == 0: if meta.get('embed_msg_id', '0') != '0': message = await ctx.fetch_message(int(meta['embed_msg_id'])) @@ -280,15 +278,15 @@ async def send_embed_and_upload(self,ctx,meta): message = await ctx.send(embed=discord.Embed(title="Creating .torrent", color=0xffff00)) meta['embed_msg_id'] = message.id channel = message.channel - if meta['nohash'] == False: - if meta.get('torrenthash', None) != None: + if meta['nohash'] is False: + if meta.get('torrenthash', None) is not None: reuse_torrent = await client.find_existing_torrent(meta) - if reuse_torrent != None: + if reuse_torrent is not None: prep.create_base_from_existing_torrent(reuse_torrent, meta['base_dir'], meta['uuid']) - p = multiprocessing.Process(target = prep.create_torrent, args=(meta, Path(meta['path']))) + p = multiprocessing.Process(target=prep.create_torrent, args=(meta, Path(meta['path']))) p.start() - while p.is_alive() == True: + while p.is_alive() is True: await asyncio.sleep(5) if int(meta.get('randomized', 0)) >= 1: @@ -296,7 +294,7 @@ async def send_embed_and_upload(self,ctx,meta): else: meta['client'] = 'none' - #Format for embed + # Format for embed if meta['tag'] == "": tag = "" else: @@ -322,12 +320,12 @@ async def send_embed_and_upload(self,ctx,meta): embed.add_field(name=f"POTENTIALLY MISSING INFORMATION:", value="\n".join(missing), inline=False) embed.set_thumbnail(url=f"https://image.tmdb.org/t/p/original{meta['poster']}") embed.set_footer(text=meta['uuid']) - embed.set_author(name="L4G's Upload Assistant", url="https://github.com/L4GSP1KE/Upload-Assistant", icon_url="https://images2.imgbox.com/6e/da/dXfdgNYs_o.png") - + embed.set_author(name="L4G's Upload Assistant", url="https://github.com/Audionut/Upload-Assistant", icon_url="https://images2.imgbox.com/6e/da/dXfdgNYs_o.png") + message = await ctx.fetch_message(meta['embed_msg_id']) await message.edit(embed=embed) - if meta.get('trackers', None) != None: + if meta.get('trackers', None) is not None: trackers = meta['trackers'] else: trackers = config['TRACKERS']['default_trackers'] @@ -350,14 +348,14 @@ async def send_embed_and_upload(self,ctx,meta): await asyncio.sleep(0.3) if "CBR" in each.replace(' ', ''): await message.add_reaction(config['DISCORD']['discord_emojis']['CBR']) - await asyncio.sleep(0.3) + await asyncio.sleep(0.3) await message.add_reaction(config['DISCORD']['discord_emojis']['MANUAL']) await asyncio.sleep(0.3) await message.add_reaction(config['DISCORD']['discord_emojis']['CANCEL']) await asyncio.sleep(0.3) await message.add_reaction(config['DISCORD']['discord_emojis']['UPLOAD']) - #Save meta to json + # Save meta to json with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as f: json.dump(meta, f, indent=4) f.close() @@ -393,7 +391,7 @@ def check(reaction, user): return else: - #Check which are selected and upload to them + # Check which are selected and upload to them msg = await ctx.fetch_message(message.id) tracker_list = list() tracker_emojis = config['DISCORD']['discord_emojis'] @@ -434,29 +432,29 @@ def check(reaction, user): await cbr.edit_desc(meta) archive_url = await prep.package(meta) upload_embed_description = upload_embed_description.replace('MANUAL', '~~MANUAL~~') - if archive_url == False: + if archive_url is False: upload_embed = discord.Embed(title=f"Uploaded `{meta['name']}` to:", description=upload_embed_description, color=0xff0000) upload_embed.add_field(name="Unable to upload prep files", value=f"The files can be found at `tmp/{meta['title']}.tar`") await msg.edit(embed=upload_embed) else: upload_embed = discord.Embed(title=f"Uploaded `{meta['name']}` to:", description=upload_embed_description, color=0x00ff40) - upload_embed.add_field(name="Files can be found at:",value=f"{archive_url} or `tmp/{meta['uuid']}`") + upload_embed.add_field(name="Files can be found at:", value=f"{archive_url} or `tmp/{meta['uuid']}`") await msg.edit(embed=upload_embed) if "BLU" in tracker_list: blu = BLU(config=config) dupes = await blu.search_existing(meta) meta = await self.dupe_embed(dupes, meta, tracker_emojis, channel) - if meta['upload'] == True: + if meta['upload'] is True: await blu.upload(meta) await client.add_to_client(meta, "BLU") upload_embed_description = upload_embed_description.replace('BLU', '~~BLU~~') upload_embed = discord.Embed(title=f"Uploaded `{meta['name']}` to:", description=upload_embed_description, color=0x00ff40) - await msg.edit(embed=upload_embed) + await msg.edit(embed=upload_embed) if "BHD" in tracker_list: bhd = BHD(config=config) dupes = await bhd.search_existing(meta) meta = await self.dupe_embed(dupes, meta, tracker_emojis, channel) - if meta['upload'] == True: + if meta['upload'] is True: await bhd.upload(meta) await client.add_to_client(meta, "BHD") upload_embed_description = upload_embed_description.replace('BHD', '~~BHD~~') @@ -466,7 +464,7 @@ def check(reaction, user): aither = AITHER(config=config) dupes = await aither.search_existing(meta) meta = await self.dupe_embed(dupes, meta, tracker_emojis, channel) - if meta['upload'] == True: + if meta['upload'] is True: await aither.upload(meta) await client.add_to_client(meta, "AITHER") upload_embed_description = upload_embed_description.replace('AITHER', '~~AITHER~~') @@ -476,7 +474,7 @@ def check(reaction, user): stc = STC(config=config) dupes = await stc.search_existing(meta) meta = await self.dupe_embed(dupes, meta, tracker_emojis, channel) - if meta['upload'] == True: + if meta['upload'] is True: await stc.upload(meta) await client.add_to_client(meta, "STC") upload_embed_description = upload_embed_description.replace('STC', '~~STC~~') @@ -486,7 +484,7 @@ def check(reaction, user): lcd = LCD(config=config) dupes = await lcd.search_existing(meta) meta = await self.dupe_embed(dupes, meta, tracker_emojis, channel) - if meta['upload'] == True: + if meta['upload'] is True: await lcd.upload(meta) await client.add_to_client(meta, "LCD") upload_embed_description = upload_embed_description.replace('LCD', '~~LCD~~') @@ -496,7 +494,7 @@ def check(reaction, user): cbr = CBR(config=config) dupes = await cbr.search_existing(meta) meta = await self.dupe_embed(dupes, meta, tracker_emojis, channel) - if meta['upload'] == True: + if meta['upload'] is True: await cbr.upload(meta) await client.add_to_client(meta, "CBR") upload_embed_description = upload_embed_description.replace('CBR', '~~CBR~~') @@ -562,8 +560,10 @@ async def get_missing(self, meta): def setup(bot): bot.add_cog(Commands(bot)) + class CancelException(Exception): pass + class ManualException(Exception): - pass \ No newline at end of file + pass diff --git a/src/args.py b/src/args.py index d431cdbb3..e3bf8651b 100644 --- a/src/args.py +++ b/src/args.py @@ -60,7 +60,7 @@ def parse(self, args, meta): parser.add_argument('-webdv', '--webdv', action='store_true', required=False, help="Contains a Dolby Vision layer converted using dovi_tool") parser.add_argument('-hc', '--hardcoded-subs', action='store_true', required=False, help="Contains hardcoded subs", dest="hardcoded-subs") parser.add_argument('-pr', '--personalrelease', action='store_true', required=False, help="Personal Release") - parser.add_argument('-sdc','--skip-dupe-check', action='store_true', required=False, help="Pass if you know this is a dupe (Skips dupe check)", dest="dupe") + parser.add_argument('-sdc', '--skip-dupe-check', action='store_true', required=False, help="Pass if you know this is a dupe (Skips dupe check)", dest="dupe") parser.add_argument('-debug', '--debug', action='store_true', required=False, help="Debug Mode, will run through all the motions providing extra info, but will not upload to trackers.") parser.add_argument('-ffdebug', '--ffdebug', action='store_true', required=False, help="Will show info from ffmpeg while taking screenshots.") parser.add_argument('-m', '--manual', action='store_true', required=False, help="Manual Mode. Returns link to ddl screens/base.torrent") diff --git a/src/bbcode.py b/src/bbcode.py index 5c5886ba4..51bdacbab 100644 --- a/src/bbcode.py +++ b/src/bbcode.py @@ -45,13 +45,13 @@ def clean_ptp_description(self, desc, is_disc): desc = desc.replace('\r\n', '\n') # Remove url tags with PTP/HDB links - url_tags = re.findall("(\[url[\=\]]https?:\/\/passthepopcorn\.m[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) - url_tags = url_tags + re.findall("(\[url[\=\]]https?:\/\/hdbits\.o[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) + url_tags = re.findall(r"(\[url[\=\]]https?:\/\/passthepopcorn\.m[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) + url_tags = url_tags + re.findall(r"(\[url[\=\]]https?:\/\/hdbits\.o[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) if url_tags != []: for url_tag in url_tags: url_tag = ''.join(url_tag) - url_tag_removed = re.sub("(\[url[\=\]]https?:\/\/passthepopcorn\.m[^\]]+])", "", url_tag, flags=re.IGNORECASE) - url_tag_removed = re.sub("(\[url[\=\]]https?:\/\/hdbits\.o[^\]]+])", "", url_tag_removed, flags=re.IGNORECASE) + url_tag_removed = re.sub(r"(\[url[\=\]]https?:\/\/passthepopcorn\.m[^\]]+])", "", url_tag, flags=re.IGNORECASE) + url_tag_removed = re.sub(r"(\[url[\=\]]https?:\/\/hdbits\.o[^\]]+])", "", url_tag_removed, flags=re.IGNORECASE) url_tag_removed = url_tag_removed.replace("[/url]", "") desc = desc.replace(url_tag, url_tag_removed) @@ -60,37 +60,37 @@ def clean_ptp_description(self, desc, is_disc): desc = desc.replace('http://hdbits.org', 'HDB').replace('https://hdbits.org', 'HDB') # Remove Mediainfo Tags / Attempt to regex out mediainfo - mediainfo_tags = re.findall("\[mediainfo\][\s\S]*?\[\/mediainfo\]", desc) + mediainfo_tags = re.findall(r"\[mediainfo\][\s\S]*?\[\/mediainfo\]", desc) if len(mediainfo_tags) >= 1: - desc = re.sub("\[mediainfo\][\s\S]*?\[\/mediainfo\]", "", desc) + desc = re.sub(r"\[mediainfo\][\s\S]*?\[\/mediainfo\]", "", desc) elif is_disc != "BDMV": - desc = re.sub("(^general\nunique)(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) - desc = re.sub("(^general\ncomplete)(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) - desc = re.sub("(^(Format[\s]{2,}:))(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) - desc = re.sub("(^(video|audio|text)( #\d+)?\nid)(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) - desc = re.sub("(^(menu)( #\d+)?\n)(.*?)^$", "", f"{desc}\n\n", flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) + desc = re.sub(r"(^general\nunique)(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) + desc = re.sub(r"(^general\ncomplete)(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) + desc = re.sub(r"(^(Format[\s]{2,}:))(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) + desc = re.sub(r"(^(video|audio|text)( #\d+)?\nid)(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) + desc = re.sub(r"(^(menu)( #\d+)?\n)(.*?)^$", "", f"{desc}\n\n", flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) elif any(x in is_disc for x in ["BDMV", "DVD"]): return "" # Convert Quote tags: - desc = re.sub("\[quote.*?\]", "[code]", desc) + desc = re.sub(r"\[quote.*?\]", "[code]", desc) desc = desc.replace("[/quote]", "[/code]") - + # Remove Alignments: - desc = re.sub("\[align=.*?\]", "", desc) + desc = re.sub(r"\[align=.*?\]", "", desc) desc = desc.replace("[/align]", "") # Remove size tags - desc = re.sub("\[size=.*?\]", "", desc) + desc = re.sub(r"\[size=.*?\]", "", desc) desc = desc.replace("[/size]", "") # Remove Videos - desc = re.sub("\[video\][\s\S]*?\[\/video\]", "", desc) + desc = re.sub(r"\[video\][\s\S]*?\[\/video\]", "", desc) # Remove Staff tags - desc = re.sub("\[staff[\s\S]*?\[\/staff\]", "", desc) + desc = re.sub(r"\[staff[\s\S]*?\[\/staff\]", "", desc) - #Remove Movie/Person/User/hr/Indent + # Remove Movie/Person/User/hr/Indent remove_list = [ '[movie]', '[/movie]', '[artist]', '[/artist]', @@ -102,9 +102,9 @@ def clean_ptp_description(self, desc, is_disc): for each in remove_list: desc = desc.replace(each, '') - #Catch Stray Images - comps = re.findall("\[comparison=[\s\S]*?\[\/comparison\]", desc) - hides = re.findall("\[hide[\s\S]*?\[\/hide\]", desc) + # Catch Stray Images + comps = re.findall(r"\[comparison=[\s\S]*?\[\/comparison\]", desc) + hides = re.findall(r"\[hide[\s\S]*?\[\/hide\]", desc) comps.extend(hides) nocomp = desc comp_placeholders = [] @@ -116,17 +116,17 @@ def clean_ptp_description(self, desc, is_disc): comp_placeholders.append(comps[i]) # Remove Images in IMG tags: - desc = re.sub("\[img\][\s\S]*?\[\/img\]", "", desc, flags=re.IGNORECASE) - desc = re.sub("\[img=[\s\S]*?\]", "", desc, flags=re.IGNORECASE) + desc = re.sub(r"\[img\][\s\S]*?\[\/img\]", "", desc, flags=re.IGNORECASE) + desc = re.sub(r"\[img=[\s\S]*?\]", "", desc, flags=re.IGNORECASE) # Replace Images - loose_images = re.findall("(https?:\/\/.*\.(?:png|jpg))", nocomp, flags=re.IGNORECASE) + loose_images = re.findall(r"(https?:\/\/.*\.(?:png|jpg))", nocomp, flags=re.IGNORECASE) if len(loose_images) >= 1: for image in loose_images: desc = desc.replace(image, '') # Re-place comparisons if comp_placeholders != []: for i, comp in enumerate(comp_placeholders): - comp = re.sub("\[\/?img[\s\S]*?\]", "",comp, flags=re.IGNORECASE) + comp = re.sub(r"\[\/?img[\s\S]*?\]", "",comp, flags=re.IGNORECASE) desc = desc.replace(f"COMPARISON_PLACEHOLDER-{i} ", comp) # Convert hides with multiple images to comparison @@ -142,7 +142,7 @@ def clean_ptp_description(self, desc, is_disc): if desc.replace('\n', '') == '': return "" return desc - + def clean_unit3d_description(self, desc, site): # Unescape html desc = html.unescape(desc) @@ -151,12 +151,12 @@ def clean_unit3d_description(self, desc, site): # Remove links to site site_netloc = urllib.parse.urlparse(site).netloc - site_regex = f"(\[url[\=\]]https?:\/\/{site_netloc}/[^\]]+])([^\[]+)(\[\/url\])?" + site_regex = rf"(\[url[\=\]]https?:\/\/{site_netloc}/[^\]]+])([^\[]+)(\[\/url\])?" site_url_tags = re.findall(site_regex, desc) if site_url_tags != []: for site_url_tag in site_url_tags: site_url_tag = ''.join(site_url_tag) - url_tag_regex = f"(\[url[\=\]]https?:\/\/{site_netloc}[^\]]+])" + url_tag_regex = rf"(\[url[\=\]]https?:\/\/{site_netloc}[^\]]+])" url_tag_removed = re.sub(url_tag_regex, "", site_url_tag) url_tag_removed = url_tag_removed.replace("[/url]", "") desc = desc.replace(site_url_tag, url_tag_removed) @@ -164,7 +164,7 @@ def clean_unit3d_description(self, desc, site): desc = desc.replace(site_netloc, site_netloc.split('.')[0]) # Temporarily hide spoiler tags - spoilers = re.findall("\[spoiler[\s\S]*?\[\/spoiler\]", desc) + spoilers = re.findall(r"\[spoiler[\s\S]*?\[\/spoiler\]", desc) nospoil = desc spoiler_placeholders = [] for i in range(len(spoilers)): @@ -174,22 +174,22 @@ def clean_unit3d_description(self, desc, site): # Get Images from outside spoilers imagelist = [] - url_tags = re.findall("\[url=[\s\S]*?\[\/url\]", desc) + url_tags = re.findall(r"\[url=[\s\S]*?\[\/url\]", desc) if url_tags != []: for tag in url_tags: - image = re.findall("\[img[\s\S]*?\[\/img\]", tag) + image = re.findall(r"\[img[\s\S]*?\[\/img\]", tag) if len(image) == 1: image_dict = {} img_url = image[0].lower().replace('[img]', '').replace('[/img]', '') - image_dict['img_url'] = image_dict['raw_url'] = re.sub("\[img[\s\S]*\]", "", img_url) + image_dict['img_url'] = image_dict['raw_url'] = re.sub(r"\[img[\s\S]*\]", "", img_url) url_tag = tag.replace(image[0], '') - image_dict['web_url'] = re.match("\[url=[\s\S]*?\]", url_tag, flags=re.IGNORECASE)[0].lower().replace('[url=', '')[:-1] + image_dict['web_url'] = re.match(r"\[url=[\s\S]*?\]", url_tag, flags=re.IGNORECASE)[0].lower().replace('[url=', '')[:-1] imagelist.append(image_dict) desc = desc.replace(tag, '') # Remove bot signatures desc = desc.replace("[img=35]https://blutopia/favicon.ico[/img] [b]Uploaded Using [url=https://github.com/HDInnovations/UNIT3D]UNIT3D[/url] Auto Uploader[/b] [img=35]https://blutopia/favicon.ico[/img]", '') - desc = re.sub("\[center\].*Created by L4G's Upload Assistant.*\[\/center\]", "", desc, flags=re.IGNORECASE) + desc = re.sub(r"\[center\].*Created by L4G's Upload Assistant.*\[\/center\]", "", desc, flags=re.IGNORECASE) # Replace spoiler tags if spoiler_placeholders != []: @@ -197,7 +197,7 @@ def clean_unit3d_description(self, desc, site): desc = desc.replace(f"SPOILER_PLACEHOLDER-{i} ", spoiler) # Check for empty [center] tags - centers = re.findall("\[center[\s\S]*?\[\/center\]", desc) + centers = re.findall(r"\[center[\s\S]*?\[\/center\]", desc) if centers != []: for center in centers: full_center = center @@ -225,7 +225,7 @@ def convert_pre_to_code(self, desc): desc = desc.replace('[pre]', '[code]') desc = desc.replace('[/pre]', '[/code]') return desc - + def convert_hide_to_spoiler(self, desc): desc = desc.replace('[hide', '[spoiler') desc = desc.replace('[/hide]', '[/spoiler]') @@ -237,7 +237,7 @@ def convert_spoiler_to_hide(self, desc): return desc def remove_spoiler(self, desc): - desc = re.sub("\[\/?spoiler[\s\S]*?\]", "", desc, flags=re.IGNORECASE) + desc = re.sub(r"\[\/?spoiler[\s\S]*?\]", "", desc, flags=re.IGNORECASE) return desc def convert_spoiler_to_code(self, desc): @@ -249,15 +249,15 @@ def convert_code_to_quote(self, desc): desc = desc.replace('[code', '[quote') desc = desc.replace('[/code]', '[/quote]') return desc - + def convert_comparison_to_collapse(self, desc, max_width): - comparisons = re.findall("\[comparison=[\s\S]*?\[\/comparison\]", desc) + comparisons = re.findall(r"\[comparison=[\s\S]*?\[\/comparison\]", desc) for comp in comparisons: line = [] output = [] comp_sources = comp.split(']', 1)[0].replace('[comparison=', '').replace(' ', '').split(',') comp_images = comp.split(']', 1)[1].replace('[/comparison]', '').replace(',', '\n').replace(' ', '\n') - comp_images = re.findall("(https?:\/\/.*\.(?:png|jpg))", comp_images, flags=re.IGNORECASE) + comp_images = re.findall(r"(https?:\/\/.*\.(?:png|jpg))", comp_images, flags=re.IGNORECASE) screens_per_line = len(comp_sources) img_size = int(max_width / screens_per_line) if img_size > 350: @@ -276,13 +276,13 @@ def convert_comparison_to_collapse(self, desc, max_width): return desc def convert_comparison_to_centered(self, desc, max_width): - comparisons = re.findall("\[comparison=[\s\S]*?\[\/comparison\]", desc) + comparisons = re.findall(r"\[comparison=[\s\S]*?\[\/comparison\]", desc) for comp in comparisons: line = [] output = [] comp_sources = comp.split(']', 1)[0].replace('[comparison=', '').replace(' ', '').split(',') comp_images = comp.split(']', 1)[1].replace('[/comparison]', '').replace(',', '\n').replace(' ', '\n') - comp_images = re.findall("(https?:\/\/.*\.(?:png|jpg))", comp_images, flags=re.IGNORECASE) + comp_images = re.findall(r"(https?:\/\/.*\.(?:png|jpg))", comp_images, flags=re.IGNORECASE) screens_per_line = len(comp_sources) img_size = int(max_width / screens_per_line) if img_size > 350: @@ -305,17 +305,17 @@ def convert_collapse_to_comparison(self, desc, spoiler_hide, collapses): if collapses != []: for i in range(len(collapses)): tag = collapses[i] - images = re.findall("\[img[\s\S]*?\[\/img\]", tag, flags=re.IGNORECASE) + images = re.findall(r"\[img[\s\S]*?\[\/img\]", tag, flags=re.IGNORECASE) if len(images) >= 6: comp_images = [] final_sources = [] for image in images: - image_url = re.sub("\[img[\s\S]*\]", "", image.replace('[/img]', ''), flags=re.IGNORECASE) + image_url = re.sub(r"\[img[\s\S]*\]", "", image.replace('[/img]', ''), flags=re.IGNORECASE) comp_images.append(image_url) if spoiler_hide == "spoiler": - sources = re.match("\[spoiler[\s\S]*?\]", tag)[0].replace('[spoiler=', '')[:-1] + sources = re.match(r"\[spoiler[\s\S]*?\]", tag)[0].replace('[spoiler=', '')[:-1] elif spoiler_hide == "hide": - sources = re.match("\[hide[\s\S]*?\]", tag)[0].replace('[hide=', '')[:-1] + sources = re.match(r"\[hide[\s\S]*?\]", tag)[0].replace('[hide=', '')[:-1] sources = re.sub("comparison", "", sources, flags=re.IGNORECASE) for each in ['vs', ',', '|']: sources = sources.split(each) diff --git a/src/clients.py b/src/clients.py index 9025845d8..a0cc51ea8 100644 --- a/src/clients.py +++ b/src/clients.py @@ -37,9 +37,9 @@ async def add_to_client(self, meta, tracker): default_torrent_client = self.config['DEFAULT']['default_torrent_client'] else: default_torrent_client = meta['client'] - if meta.get('client', None) is 'none': + if meta.get('client', None) == 'none': return - if default_torrent_client is "none": + if default_torrent_client == "none": return client = self.config['TORRENT_CLIENTS'][default_torrent_client] torrent_client = client['torrent_client'] @@ -64,7 +64,7 @@ async def find_existing_torrent(self, meta): default_torrent_client = self.config['DEFAULT']['default_torrent_client'] else: default_torrent_client = meta['client'] - if meta.get('client', None) is 'none' or default_torrent_client is 'none': + if meta.get('client', None) == 'none' or default_torrent_client == 'none': return None client = self.config['TORRENT_CLIENTS'][default_torrent_client] torrent_storage_dir = client.get('torrent_storage_dir', None) @@ -394,8 +394,10 @@ def add_fast_resume(self, metainfo, datapath, torrent): resume["files"].append(dict( priority=1, mtime=int(os.path.getmtime(filepath)), - completed=(offset+fileinfo["length"]+piece_length-1) // piece_length - - offset // piece_length, + completed=( + (offset + fileinfo["length"] + piece_length - 1) // piece_length + - offset // piece_length + ), )) offset += fileinfo["length"] diff --git a/src/discparse.py b/src/discparse.py index bc3ba2cfe..c4db1499a 100644 --- a/src/discparse.py +++ b/src/discparse.py @@ -28,7 +28,7 @@ async def get_bdinfo(self, discs, folder_id, base_dir, meta_discs): for file in os.listdir(save_dir): if file == f"BD_SUMMARY_{str(i).zfill(2)}.txt": bdinfo_text = save_dir + "/" + file - if bdinfo_text == None or meta_discs == []: + if bdinfo_text is None or meta_discs == []: if os.path.exists(f"{save_dir}/BD_FULL_{str(i).zfill(2)}.txt"): bdinfo_text = os.path.abspath(f"{save_dir}/BD_FULL_{str(i).zfill(2)}.txt") else: @@ -115,7 +115,7 @@ def parse_bdinfo(self, bdinfo_input, files, path): if line.startswith("disc size:"): size = l.split(':', 1)[1] size = size.split('bytes', 1)[0].replace(',', '') - size = float(size)/float(1<<30) + size = float(size)/float(1 << 30) bdinfo['size'] = size if line.startswith("length:"): length = l.split(':', 1)[1] @@ -125,7 +125,7 @@ def parse_bdinfo(self, bdinfo_input, files, path): split2 = split1.split('/', 12) while len(split2) != 9: split2.append("") - n=0 + n = 0 if "Eye" in split2[2].strip(): n = 1 three_dim = split2[2].strip() @@ -203,7 +203,7 @@ def parse_bdinfo(self, bdinfo_input, files, path): except: pass return bdinfo - + """ Parse VIDEO_TS and get mediainfos """ @@ -230,7 +230,7 @@ async def get_dvdinfo(self, discs): vob_set_duration = vob_set_mi['media']['track'][1]['Duration'] # If the duration of the new vob set > main set by more than 10% then it's our new main set - # This should make it so TV shows pick the first episode + # This should make it so TV shows pick the first episode if (float(vob_set_duration) * 1.00) > (float(main_set_duration) * 1.10) or len(main_set) < 1: main_set = vob_set main_set_duration = vob_set_duration @@ -243,7 +243,7 @@ async def get_dvdinfo(self, discs): each['vob_mi_full'] = MediaInfo.parse(vob, output='STRING', full=False, mediainfo_options={'inform_version': '1'}).replace('\r\n', '\n') each['ifo_mi_full'] = MediaInfo.parse(ifo, output='STRING', full=False, mediainfo_options={'inform_version': '1'}).replace('\r\n', '\n') - size = sum(os.path.getsize(f) for f in os.listdir('.') if os.path.isfile(f))/float(1<<30) + size = sum(os.path.getsize(f) for f in os.listdir('.') if os.path.isfile(f))/float(1 << 30) if size <= 7.95: dvd_size = "DVD9" if size <= 4.37: diff --git a/src/exceptions.py b/src/exceptions.py index 5aa798ce0..e5de6f944 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -8,8 +8,8 @@ def __init__(self, *args, **kwargs): # ... pass them to the super constructor super().__init__(*args, **kwargs) else: # else, the exception was raised without arguments ... - # ... pass the default message to the super constructor - super().__init__(default_message, **kwargs) + # ... pass the default message to the super constructor + super().__init__(default_message, **kwargs) class UploadException(Exception): @@ -22,8 +22,8 @@ def __init__(self, *args, **kwargs): # ... pass them to the super constructor super().__init__(*args, **kwargs) else: # else, the exception was raised without arguments ... - # ... pass the default message to the super constructor - super().__init__(default_message, **kwargs) + # ... pass the default message to the super constructor + super().__init__(default_message, **kwargs) class XEMNotFound(Exception): diff --git a/src/prep.py b/src/prep.py index 33fa57016..1e8225493 100644 --- a/src/prep.py +++ b/src/prep.py @@ -9,14 +9,11 @@ try: import traceback - import nest_asyncio from src.discparse import DiscParse import multiprocessing import os - from os.path import basename import re import math - import sys from str2bool import str2bool import asyncio from guessit import guessit @@ -32,7 +29,7 @@ import pyimgbox from pymediainfo import MediaInfo import tmdbsimple as tmdb - from datetime import datetime, date + from datetime import datetime from difflib import SequenceMatcher import torf from torf import Torrent @@ -41,8 +38,6 @@ import anitopy import shutil from imdb import Cinemagoer - from subprocess import Popen - import subprocess import itertools import cli_ui from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn diff --git a/src/search.py b/src/search.py index 911636479..f8d7324c9 100644 --- a/src/search.py +++ b/src/search.py @@ -20,6 +20,7 @@ async def searchFile(self, filename): return file_found = False words = filename.split() + async def search_file(search_dir): files_total_search = [] console.print(f"Searching {search_dir}") @@ -30,7 +31,7 @@ async def search_file(search_dir): os_info = platform.platform() if await self.file_search(l_name, words): file_found = True - if('Windows' in os_info): + if ('Windows' in os_info): files_total_search.append(root+'\\'+name) else: files_total_search.append(root+'/'+name) @@ -66,7 +67,7 @@ async def search_dir(search_dir): if await self.file_search(l_name, words): folder_found = True - if('Windows' in os_info): + if ('Windows' in os_info): folders_total_search.append(root+'\\'+name) else: folders_total_search.append(root+'/'+name) diff --git a/src/trackers/ANT.py b/src/trackers/ANT.py index 4aeb241e6..941639824 100644 --- a/src/trackers/ANT.py +++ b/src/trackers/ANT.py @@ -11,7 +11,7 @@ from pathlib import Path from src.trackers.COMMON import COMMON from src.console import console -from datetime import datetime, date + class ANT(): """ @@ -22,24 +22,18 @@ class ANT(): Upload """ - ############################################################### - # ####### EDIT ME ##### # - ############################################################### - - # ALSO EDIT CLASS NAME ABOVE - def __init__(self, config): self.config = config self.tracker = 'ANT' self.source_flag = 'ANT' self.search_url = 'https://anthelion.me/api.php' self.upload_url = 'https://anthelion.me/api.php' - self.banned_groups = ['3LTON', '4yEo', 'ADE', 'AFG', 'AniHLS', 'AnimeRG', 'AniURL', 'AROMA', 'aXXo', 'Brrip', 'CHD', 'CM8', - 'CrEwSaDe', 'd3g', 'DDR', 'DNL', 'DeadFish', 'ELiTE', 'eSc', 'FaNGDiNG0', 'FGT', 'Flights', 'FRDS', - 'FUM', 'HAiKU', 'HD2DVD', 'HDS', 'HDTime', 'Hi10', 'ION10', 'iPlanet', 'JIVE', 'KiNGDOM', 'Leffe', - 'LiGaS', 'LOAD', 'MeGusta', 'MkvCage', 'mHD', 'mSD', 'NhaNc3', 'nHD', 'NOIVTC', 'nSD', 'Oj', 'Ozlem', - 'PiRaTeS', 'PRoDJi', 'RAPiDCOWS', 'RARBG', 'RetroPeeps', 'RDN', 'REsuRRecTioN', 'RMTeam', 'SANTi', - 'SicFoI', 'SPASM', 'SPDVD', 'STUTTERSHIT', 'TBS', 'Telly', 'TM', 'UPiNSMOKE', 'URANiME', 'WAF', 'xRed', + self.banned_groups = ['3LTON', '4yEo', 'ADE', 'AFG', 'AniHLS', 'AnimeRG', 'AniURL', 'AROMA', 'aXXo', 'Brrip', 'CHD', 'CM8', + 'CrEwSaDe', 'd3g', 'DDR', 'DNL', 'DeadFish', 'ELiTE', 'eSc', 'FaNGDiNG0', 'FGT', 'Flights', 'FRDS', + 'FUM', 'HAiKU', 'HD2DVD', 'HDS', 'HDTime', 'Hi10', 'ION10', 'iPlanet', 'JIVE', 'KiNGDOM', 'Leffe', + 'LiGaS', 'LOAD', 'MeGusta', 'MkvCage', 'mHD', 'mSD', 'NhaNc3', 'nHD', 'NOIVTC', 'nSD', 'Oj', 'Ozlem', + 'PiRaTeS', 'PRoDJi', 'RAPiDCOWS', 'RARBG', 'RetroPeeps', 'RDN', 'REsuRRecTioN', 'RMTeam', 'SANTi', + 'SicFoI', 'SPASM', 'SPDVD', 'STUTTERSHIT', 'TBS', 'Telly', 'TM', 'UPiNSMOKE', 'URANiME', 'WAF', 'xRed', 'XS', 'YIFY', 'YTS', 'Zeus', 'ZKBL', 'ZmN', 'ZMNT'] self.signature = None pass @@ -66,10 +60,6 @@ async def get_flags(self, meta): flags.append('Remux') return flags - ############################################################### - # #### STOP HERE UNLESS EXTRA MODIFICATION IS NEEDED ### # - ############################################################### - async def upload(self, meta): common = COMMON(config=self.config) torrent_filename = "BASE" @@ -90,7 +80,7 @@ def calculate_pieces_and_file_size(total_size, piece_size): console.print("[yellow]Regenerating torrent to fit within 1000-2000 pieces and 100 KiB .torrent size limit needed for ANT.") from src.prep import Prep prep = Prep(screens=meta['screens'], img_host=meta['imghost'], config=self.config) - + # Call create_torrent with the default piece size calculation prep.create_torrent(meta, Path(meta['path']), "ANT") torrent_filename = "ANT" @@ -137,7 +127,7 @@ def calculate_pieces_and_file_size(total_size, piece_size): headers = { 'User-Agent': f'Upload Assistant/2.1 ({platform.system()} {platform.release()})' } - + try: if not meta['debug']: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers) @@ -184,4 +174,4 @@ async def search_existing(self, meta): console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) - return dupes \ No newline at end of file + return dupes diff --git a/src/trackers/BHD.py b/src/trackers/BHD.py index 8909f3f46..46cb0129a 100644 --- a/src/trackers/BHD.py +++ b/src/trackers/BHD.py @@ -4,13 +4,13 @@ import requests from difflib import SequenceMatcher from str2bool import str2bool -import urllib import os import platform from src.trackers.COMMON import COMMON from src.console import console + class BHD(): """ Edit for Tracker: @@ -24,10 +24,10 @@ def __init__(self, config): self.tracker = 'BHD' self.source_flag = 'BHD' self.upload_url = 'https://beyond-hd.me/api/upload/' - self.signature = f"\n[center][url=https://beyond-hd.me/forums/topic/toolpython-l4gs-upload-assistant.5456/post/138087#post-138087]Created by L4G's Upload Assistant[/url][/center]" + self.signature = "\n[center][url=https://beyond-hd.me/forums/topic/toolpython-l4gs-upload-assistant.5456/post/138087#post-138087]Created by L4G's Upload Assistant[/url][/center]" self.banned_groups = ['Sicario', 'TOMMY', 'x0r', 'nikt0', 'FGT', 'd3g', 'MeGusta', 'YIFY', 'tigole', 'TEKNO3D', 'C4K', 'RARBG', '4K4U', 'EASports', 'ReaLHD'] pass - + async def upload(self, meta): common = COMMON(config=self.config) await common.edit_torrent(meta, self.tracker, self.source_flag) @@ -39,12 +39,12 @@ async def upload(self, meta): tags = await self.get_tags(meta) custom, edition = await self.get_edition(meta, tags) bhd_name = await self.edit_name(meta) - if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) == False: + if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) is False: anon = 0 else: anon = 1 - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8') else: mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt", 'r', encoding='utf-8') @@ -52,7 +52,7 @@ async def upload(self, meta): desc = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'r').read() torrent_file = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent" files = { - 'mediainfo' : mi_dump, + 'mediainfo': mi_dump, } if os.path.exists(torrent_file): open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent", 'rb') @@ -64,12 +64,12 @@ async def upload(self, meta): 'category_id': cat_id, 'type': type_id, 'source': source_id, - 'imdb_id': meta['imdb_id'].replace('tt', ''), + 'imdb_id': meta['imdb_id'].replace('tt', ''), 'tmdb_id': meta['tmdb'], 'description': desc, 'anon': anon, 'sd': meta.get('sd', 0), - 'live': draft + 'live': draft # 'internal' : 0, # 'featured' : 0, # 'free' : 0, @@ -77,7 +77,7 @@ async def upload(self, meta): # 'sticky' : 0, } # Internal - if self.config['TRACKERS'][self.tracker].get('internal', False) == True: + if self.config['TRACKERS'][self.tracker].get('internal', False) is True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 @@ -87,7 +87,7 @@ async def upload(self, meta): data['special'] = 1 if meta.get('region', "") != "": data['region'] = meta['region'] - if custom == True: + if custom is True: data['custom_edition'] = edition elif edition != "": data['edition'] = edition @@ -98,7 +98,7 @@ async def upload(self, meta): } url = self.upload_url + self.config['TRACKERS'][self.tracker]['api_key'].strip() - if meta['debug'] == False: + if meta['debug'] is False: response = requests.post(url=url, files=files, data=data, headers=headers) try: response = response.json() @@ -114,29 +114,29 @@ async def upload(self, meta): console.print(response) except: console.print("It may have uploaded, go check") - return + return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) async def get_cat_id(self, category_name): category_id = { - 'MOVIE': '1', - 'TV': '2', + 'MOVIE': '1', + 'TV': '2', }.get(category_name, '1') return category_id async def get_source(self, source): sources = { - "Blu-ray" : "Blu-ray", - "BluRay" : "Blu-ray", - "HDDVD" : "HD-DVD", - "HD DVD" : "HD-DVD", - "Web" : "WEB", - "HDTV" : "HDTV", - "UHDTV" : "HDTV", - "NTSC" : "DVD", "NTSC DVD" : "DVD", - "PAL" : "DVD", "PAL DVD": "DVD", + "Blu-ray": "Blu-ray", + "BluRay": "Blu-ray", + "HDDVD": "HD-DVD", + "HD DVD": "HD-DVD", + "Web": "WEB", + "HDTV": "HDTV", + "UHDTV": "HDTV", + "NTSC": "DVD", "NTSC DVD" : "DVD", + "PAL": "DVD", "PAL DVD": "DVD", } source_id = sources.get(source) @@ -160,7 +160,7 @@ async def get_type(self, meta): if "DVD5" in meta['dvd_size']: type_id = "DVD 5" elif "DVD9" in meta['dvd_size']: - type_id = "DVD 9" + type_id = "DVD 9" else: if meta['type'] == "REMUX": if meta['source'] == "BluRay": @@ -178,7 +178,7 @@ async def get_type(self, meta): else: type_id = "Other" return type_id - + async def edit_desc(self, meta): base = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'r').read() with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w') as desc: @@ -202,7 +202,7 @@ async def edit_desc(self, meta): desc.write("\n") desc.write(base.replace("[img]", "[img width=300]")) images = meta['image_list'] - if len(images) > 0: + if len(images) > 0: desc.write("[center]") for each in range(len(images[:int(meta['screens'])])): web_url = images[each]['web_url'] @@ -220,9 +220,9 @@ async def search_existing(self, meta): if category == 'MOVIE': category = "Movies" data = { - 'tmdb_id' : meta['tmdb'], - 'categories' : category, - 'types' : await self.get_type(meta), + 'tmdb_id': meta['tmdb'], + 'categories': category, + 'types': await self.get_type(meta), } # Search all releases if SD if meta['sd'] == 1: @@ -244,16 +244,16 @@ async def search_existing(self, meta): dupes.append(result) else: console.print(f"[yellow]{response.get('status_message')}") - await asyncio.sleep(5) + await asyncio.sleep(5) except: console.print('[bold red]Unable to search for existing torrents on site. Most likely the site is down.') await asyncio.sleep(5) return dupes - async def get_live(self, meta): + async def get_live(self, meta): draft = self.config['TRACKERS'][self.tracker]['draft_default'].strip() - draft = bool(str2bool(str(draft))) #0 for send to draft, 1 for live + draft = bool(str2bool(str(draft))) # 0 for send to draft, 1 for live if draft: draft_int = 0 else: @@ -274,7 +274,7 @@ async def get_edition(self, meta, tags): elif edition == "": edition = "" else: - custom = True + custom = True return custom, edition async def get_tags(self, meta): @@ -291,13 +291,13 @@ async def get_tags(self, meta): tags.append('EnglishDub') if "Open Matte" in meta.get('edition', ""): tags.append("OpenMatte") - if meta.get('scene', False) == True: + if meta.get('scene', False) is True: tags.append("Scene") - if meta.get('personalrelease', False) == True: + if meta.get('personalrelease', False) is True: tags.append('Personal') if "hybrid" in meta.get('edition', "").lower(): tags.append('Hybrid') - if meta.get('has_commentary', False) == True: + if meta.get('has_commentary', False) is True: tags.append('Commentary') if "DV" in meta.get('hdr', ''): tags.append('DV') @@ -321,4 +321,4 @@ async def edit_name(self, meta): # name = name.replace('H.264', 'x264') if meta['category'] == "TV" and meta.get('tv_pack', 0) == 0 and meta.get('episode_title_storage', '').strip() != '' and meta['episode'].strip() != '': name = name.replace(meta['episode'], f"{meta['episode']} {meta['episode_title_storage']}", 1) - return name \ No newline at end of file + return name diff --git a/src/trackers/BLU.py b/src/trackers/BLU.py index 96fbe2f36..a0d0040b0 100644 --- a/src/trackers/BLU.py +++ b/src/trackers/BLU.py @@ -48,12 +48,12 @@ async def upload(self, meta): resolution_id = await self.get_res_id(meta['resolution']) region_id = await common.unit3d_region_ids(meta.get('region')) distributor_id = await common.unit3d_distributor_ids(meta.get('distributor')) - if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) == False: + if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) is False: anon = 0 else: anon = 1 - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: @@ -66,7 +66,7 @@ async def upload(self, meta): 'name': blu_name, 'description': desc, 'mediainfo': mi_dump, - 'bdinfo': bd_dump, + 'bdinfo': bd_dump, 'category_id': cat_id, 'type_id': type_id, 'resolution_id': resolution_id, @@ -87,7 +87,7 @@ async def upload(self, meta): 'sticky': 0, } # Internal - if self.config['TRACKERS'][self.tracker].get('internal', False) == True: + if self.config['TRACKERS'][self.tracker].get('internal', False) is True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 @@ -105,7 +105,7 @@ async def upload(self, meta): 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip() } - if meta['debug'] == False: + if meta['debug'] is False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) @@ -141,12 +141,12 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { - '8640p':'10', + '8640p': '10', '4320p': '11', '2160p': '1', '1440p': '2', '1080p': '2', - '1080i':'3', + '1080i': '3', '720p': '5', '576p': '6', '576i': '7', diff --git a/src/trackers/COMMON.py b/src/trackers/COMMON.py index 9a3a7849e..2a2f317b9 100644 --- a/src/trackers/COMMON.py +++ b/src/trackers/COMMON.py @@ -1,6 +1,5 @@ from torf import Torrent import os -import traceback import requests import re import json @@ -75,7 +74,7 @@ async def unit3d_edit_desc(self, meta, tracker, signature, comparison=False, des descfile.write(f"[url={web_url}][img=350]{raw_url}[/img][/url]") descfile.write("[/center]") - if signature != None: + if signature is not None: descfile.write(signature) descfile.close() return @@ -157,7 +156,7 @@ async def unit3d_torrent_info(self, tracker, torrent_url, search_url, id=None, f url = f"{search_url}?file_name={file_name}" console.print(f"[green]Searching {tracker} by file name: [bold yellow]{file_name}[/bold yellow]") else: - console.print(f"[red]No ID or file name provided for search.[/red]") + console.print("[red]No ID or file name provided for search.[/red]") return None, None, None, None, None, None, None, None, None response = requests.get(url=url, params=params) @@ -215,33 +214,33 @@ async def ptgen(self, meta, ptgen_site="", ptgen_retry=3): if ptgen_site != '': url = ptgen_site params = {} - data={} - #get douban url + data = {} + # get douban url if int(meta.get('imdb_id', '0')) != 0: data['search'] = f"tt{meta['imdb_id']}" ptgen = requests.get(url, params=data) - if ptgen.json()["error"] != None: + if ptgen.json()["error"] is not None: for retry in range(ptgen_retry): try: ptgen = requests.get(url, params=params) - if ptgen.json()["error"] == None: + if ptgen.json()["error"] is None: break except requests.exceptions.JSONDecodeError: continue try: - params['url'] = ptgen.json()['data'][0]['link'] + params['url'] = ptgen.json()['data'][0]['link'] except Exception: console.print("[red]Unable to get data from ptgen using IMDb") - params['url'] = console.input(f"[red]Please enter [yellow]Douban[/yellow] link: ") + params['url'] = console.input("[red]Please enter [yellow]Douban[/yellow] link: ") else: console.print("[red]No IMDb id was found.") - params['url'] = console.input(f"[red]Please enter [yellow]Douban[/yellow] link: ") + params['url'] = console.input("[red]Please enter [yellow]Douban[/yellow] link: ") try: ptgen = requests.get(url, params=params) - if ptgen.json()["error"] != None: + if ptgen.json()["error"] is not None: for retry in range(ptgen_retry): ptgen = requests.get(url, params=params) - if ptgen.json()["error"] == None: + if ptgen.json()["error"] is None: break ptgen = ptgen.json() meta['ptgen'] = ptgen @@ -288,7 +287,7 @@ async def filter_dupes(self, dupes, meta): { 'search': str(meta.get('tv_pack', 0)), 'search_for': '1', - 'update': {f"{meta['season']}(?!E\d+)"} + 'update': {rf"{meta['season']}(?!E\d+)"} }, { 'search': meta['episode'], diff --git a/src/trackers/FL.py b/src/trackers/FL.py index 5813f469a..d2cd9ae31 100644 --- a/src/trackers/FL.py +++ b/src/trackers/FL.py @@ -2,13 +2,11 @@ import asyncio import re import os -from pathlib import Path from str2bool import str2bool -import json import glob import pickle from unidecode import unidecode -from urllib.parse import urlparse, quote +from urllib.parse import urlparse import cli_ui from bs4 import BeautifulSoup @@ -16,6 +14,7 @@ from src.exceptions import * from src.console import console + class FL(): def __init__(self, config): @@ -28,7 +27,6 @@ def __init__(self, config): self.uploader_name = config['TRACKERS'][self.tracker].get('uploader_name') self.signature = None self.banned_groups = [""] - async def get_category_id(self, meta): has_ro_audio, has_ro_sub = await self.get_ro_tracks(meta) @@ -51,7 +49,7 @@ async def get_category_id(self, meta): if has_ro_sub and meta.get('sd', 0) == 0 and meta['resolution'] != '2160p': # 19 = Movie + RO cat_id = 19 - + if meta['category'] == 'TV': # 21 = TV HD cat_id = 21 @@ -61,7 +59,7 @@ async def get_category_id(self, meta): elif meta.get('sd', 0) == 1: # 23 = TV SD cat_id = 23 - + if meta['is_disc'] == "DVD": # 2 = DVD cat_id = 2 @@ -102,7 +100,7 @@ async def edit_name(self, meta): fl_name = fl_name.replace(' ', '.').replace('..', '.') return fl_name - + ############################################################### ###### STOP HERE UNLESS EXTRA MODIFICATION IS NEEDED ###### ############################################################### @@ -114,7 +112,7 @@ async def upload(self, meta): fl_name = await self.edit_name(meta) cat_id = await self.get_category_id(meta) has_ro_audio, has_ro_sub = await self.get_ro_tracks(meta) - + # Confirm the correct naming order for FL cli_ui.info(f"Filelist name: {fl_name}") if meta.get('unattended', False) == False: @@ -184,7 +182,7 @@ async def upload(self, meta): session.cookies.update(pickle.load(cf)) up = session.post(url=url, data=data, files=files) torrentFile.close() - + # Match url to verify successful upload match = re.match(r".*?filelist\.io/details\.php\?id=(\d+)&uploaded=(\d+)", up.url) if match: @@ -197,7 +195,6 @@ async def upload(self, meta): raise UploadException(f"Upload to FL Failed: result URL {up.url} ({up.status_code}) was not expected", 'red') return - async def search_existing(self, meta): dupes = [] with requests.Session() as session: @@ -208,15 +205,15 @@ async def search_existing(self, meta): search_url = f"https://filelist.io/browse.php" if int(meta['imdb_id'].replace('tt', '')) != 0: params = { - 'search' : meta['imdb_id'], - 'cat' : await self.get_category_id(meta), - 'searchin' : '3' + 'search': meta['imdb_id'], + 'cat': await self.get_category_id(meta), + 'searchin': '3' } else: params = { - 'search' : meta['title'], - 'cat' : await self.get_category_id(meta), - 'searchin' : '0' + 'search': meta['title'], + 'cat': await self.get_category_id(meta), + 'searchin': '0' } r = session.get(search_url, params=params) @@ -229,9 +226,6 @@ async def search_existing(self, meta): return dupes - - - async def validate_credentials(self, meta): cookiefile = os.path.abspath(f"{meta['base_dir']}/data/cookies/FL.pkl") if not os.path.exists(cookiefile): @@ -249,8 +243,7 @@ async def validate_credentials(self, meta): else: return False return True - - + async def validate_cookies(self, meta, cookiefile): url = "https://filelist.io/index.php" if os.path.exists(cookiefile): @@ -268,7 +261,7 @@ async def validate_cookies(self, meta, cookiefile): return False else: return False - + async def login(self, cookiefile): with requests.Session() as session: r = session.get("https://filelist.io/login.php") @@ -276,10 +269,10 @@ async def login(self, cookiefile): soup = BeautifulSoup(r.text, 'html.parser') validator = soup.find('input', {'name' : 'validator'}).get('value') data = { - 'validator' : validator, - 'username' : self.username, - 'password' : self.password, - 'unlock' : '1', + 'validator': validator, + 'username': self.username, + 'password': self.password, + 'unlock': '1', } response = session.post('https://filelist.io/takelogin.php', data=data) await asyncio.sleep(0.5) @@ -306,14 +299,12 @@ async def download_new_torrent(self, session, id, torrent_path): console.print(r.text) return - - async def edit_desc(self, meta): base = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'r').read() with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w', newline='') as descfile: from src.bbcode import BBCODE bbcode = BBCODE() - + desc = base desc = bbcode.remove_spoiler(desc) desc = bbcode.convert_code_to_quote(desc) @@ -353,7 +344,6 @@ async def edit_desc(self, meta): if self.signature != None: descfile.write(self.signature) descfile.close() - async def get_ro_tracks(self, meta): has_ro_audio = has_ro_sub = False diff --git a/src/trackers/HDB.py b/src/trackers/HDB.py index a38ad377a..1d76ef09a 100644 --- a/src/trackers/HDB.py +++ b/src/trackers/HDB.py @@ -3,18 +3,17 @@ import re import os from pathlib import Path -import traceback import json import glob from unidecode import unidecode from urllib.parse import urlparse, quote from src.trackers.COMMON import COMMON -from src.bbcode import BBCODE from src.exceptions import * from src.console import console -from datetime import datetime, date +from datetime import datetime from torf import Torrent + class HDB(): def __init__(self, config): @@ -26,7 +25,7 @@ def __init__(self, config): self.rehost_images = config['TRACKERS']['HDB'].get('img_rehost', False) self.signature = None self.banned_groups = [""] - + async def get_type_category_id(self, meta): cat_id = "EXIT" # 6 = Audio Track @@ -47,12 +46,12 @@ async def get_type_category_id(self, meta): async def get_type_codec_id(self, meta): codecmap = { - "AVC" : 1, "H.264" : 1, - "HEVC" : 5, "H.265" : 5, - "MPEG-2" : 2, - "VC-1" : 3, - "XviD" : 4, - "VP9" : 6 + "AVC": 1, "H.264": 1, + "HEVC": 5, "H.265": 5, + "MPEG-2": 2, + "VC-1": 3, + "XviD": 4, + "VP9": 6 } searchcodec = meta.get('video_codec', meta.get('video_encode')) codec_id = codecmap.get(searchcodec, "EXIT") @@ -66,8 +65,8 @@ async def get_type_medium_id(self, meta): # 4 = Capture if meta.get('type', '') == "HDTV": medium_id = 4 - if meta.get('has_encode_settings', False) == True: - medium_id = 3 + if meta.get('has_encode_settings', False) is True: + medium_id = 3 # 3 = Encode if meta.get('type', '') in ("ENCODE", "WEBRIP"): medium_id = 3 @@ -81,16 +80,16 @@ async def get_type_medium_id(self, meta): async def get_res_id(self, resolution): resolution_id = { - '8640p':'10', - '4320p': '1', - '2160p': '2', - '1440p' : '3', + '8640p': '10', + '4320p': '1', + '2160p': '2', + '1440p': '3', '1080p': '3', - '1080i':'4', - '720p': '5', - '576p': '6', + '1080i': '4', + '720p': '5', + '576p': '6', '576i': '7', - '480p': '8', + '480p': '8', '480i': '9' }.get(resolution, '10') return resolution_id @@ -100,27 +99,27 @@ async def get_tags(self, meta): # Web Services: service_dict = { - "AMZN" : 28, - "NF" : 29, - "HULU" : 34, - "DSNP" : 33, - "HMAX" : 30, - "ATVP" : 27, - "iT" : 38, - "iP" : 56, - "STAN" : 32, - "PCOK" : 31, - "CR" : 72, - "PMTP" : 69, - "MA" : 77, - "SHO" : 76, - "BCORE" : 66, "CORE" : 66, - "CRKL" : 73, - "FUNI" : 74, - "HLMK" : 71, - "HTSR" : 79, - "CRAV" : 80, - 'MAX' : 88 + "AMZN": 28, + "NF": 29, + "HULU": 34, + "DSNP": 33, + "HMAX": 30, + "ATVP": 27, + "iT": 38, + "iP": 56, + "STAN": 32, + "PCOK": 31, + "CR": 72, + "PMTP": 69, + "MA": 77, + "SHO": 76, + "BCORE": 66, "CORE": 66, + "CRKL": 73, + "FUNI": 74, + "HLMK": 71, + "HTSR": 79, + "CRAV": 80, + 'MAX': 88 } if meta.get('service') in service_dict.keys(): tags.append(service_dict.get(meta['service'])) @@ -128,19 +127,18 @@ async def get_tags(self, meta): # Collections # Masters of Cinema, The Criterion Collection, Warner Archive Collection distributor_dict = { - "WARNER ARCHIVE" : 68, "WARNER ARCHIVE COLLECTION" : 68, "WAC" : 68, - "CRITERION" : 18, "CRITERION COLLECTION" : 18, "CC" : 18, - "MASTERS OF CINEMA" : 19, "MOC" : 19, - "KINO LORBER" : 55, "KINO" : 55, - "BFI VIDEO" : 63, "BFI" : 63, "BRITISH FILM INSTITUTE" : 63, - "STUDIO CANAL" : 65, - "ARROW" : 64 + "WARNER ARCHIVE": 68, "WARNER ARCHIVE COLLECTION": 68, "WAC": 68, + "CRITERION": 18, "CRITERION COLLECTION": 18, "CC": 18, + "MASTERS OF CINEMA": 19, "MOC": 19, + "KINO LORBER": 55, "KINO": 55, + "BFI VIDEO": 63, "BFI": 63, "BRITISH FILM INSTITUTE": 63, + "STUDIO CANAL":65, + "ARROW": 64 } if meta.get('distributor') in distributor_dict.keys(): tags.append(distributor_dict.get(meta['distributor'])) - - # 4K Remaster, + # 4K Remaster, if "IMAX" in meta.get('edition', ''): tags.append(14) if "OPEN MATTE" in meta.get('edition', '').upper(): @@ -152,20 +150,20 @@ async def get_tags(self, meta): tags.append(7) if "Atmos" in meta['audio']: tags.append(5) - if meta.get('silent', False) == True: - console.print('[yellow]zxx audio track found, suggesting you tag as silent') #57 + if meta.get('silent', False) is True: + console.print('[yellow]zxx audio track found, suggesting you tag as silent') # 57 # Video Metadata - # HDR10, HDR10+, Dolby Vision, 10-bit, + # HDR10, HDR10+, Dolby Vision, 10-bit, if "HDR" in meta.get('hdr', ''): if "HDR10+" in meta['hdr']: - tags.append(25) #HDR10+ + tags.append(25) # HDR10+ else: - tags.append(9) #HDR10 + tags.append(9) # HDR10 if "DV" in meta.get('hdr', ''): - tags.append(6) #DV + tags.append(6) # DV if "HLG" in meta.get('hdr', ''): - tags.append(10) #HLG + tags.append(10) # HLG return tags @@ -196,7 +194,7 @@ async def edit_name(self, meta): hdb_name = re.sub(r"[^0-9a-zA-ZÀ-ÿ. :&+'\-\[\]]+", "", hdb_name) hdb_name = hdb_name.replace(' .', '.').replace('..', '.') - return hdb_name + return hdb_name async def upload(self, meta): common = COMMON(config=self.config) @@ -248,7 +246,7 @@ async def upload(self, meta): comment="Created by L4G's Upload Assistant", created_by="L4G's Upload Assistant" ) - + # Explicitly set the piece size and update metainfo new_torrent.piece_size = 16777216 # 16 MiB in bytes new_torrent.metainfo['info']['piece length'] = 16777216 # Ensure 'piece length' is set @@ -279,7 +277,7 @@ async def upload(self, meta): } # If internal, set 1 - if self.config['TRACKERS'][self.tracker].get('internal', False) == True: + if self.config['TRACKERS'][self.tracker].get('internal', False) is True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 # If not BDMV fill mediainfo @@ -324,12 +322,12 @@ async def search_existing(self, meta): console.print("[yellow]Searching for existing torrents on site...") url = "https://hdbits.org/api/torrents" data = { - 'username' : self.username, - 'passkey' : self.passkey, - 'category' : await self.get_type_category_id(meta), - 'codec' : await self.get_type_codec_id(meta), - 'medium' : await self.get_type_medium_id(meta), - 'search' : meta['resolution'] + 'username': self.username, + 'passkey': self.passkey, + 'category': await self.get_type_category_id(meta), + 'codec': await self.get_type_codec_id(meta), + 'medium': await self.get_type_medium_id(meta), + 'search': meta['resolution'] } if int(meta.get('imdb_id', '0').replace('tt', '0')) != 0: data['imdb'] = {'id' : meta['imdb_id']} @@ -348,21 +346,21 @@ async def search_existing(self, meta): return dupes async def validate_credentials(self, meta): - vapi = await self.validate_api() + vapi = await self.validate_api() vcookie = await self.validate_cookies(meta) - if vapi != True: + if vapi is not True: console.print('[red]Failed to validate API. Please confirm that the site is up and your passkey is valid.') return False - if vcookie != True: + if vcookie is not True: console.print('[red]Failed to validate cookies. Please confirm that the site is up and your passkey is valid.') return False return True - + async def validate_api(self): url = "https://hdbits.org/api/test" data = { - 'username' : self.username, - 'passkey' : self.passkey + 'username': self.username, + 'passkey': self.passkey } try: r = requests.post(url, data=json.dumps(data)).json() @@ -371,7 +369,7 @@ async def validate_api(self): return False except: return False - + async def validate_cookies(self, meta): common = COMMON(config=self.config) url = "https://hdbits.org" @@ -397,9 +395,9 @@ async def download_new_torrent(self, id, torrent_path): # Get HDB .torrent filename api_url = "https://hdbits.org/api/torrents" data = { - 'username' : self.username, - 'passkey' : self.passkey, - 'id' : id + 'username': self.username, + 'passkey': self.passkey, + 'id': id } r = requests.get(url=api_url, data=json.dumps(data)) filename = r.json()['data'][0]['filename'] @@ -407,8 +405,8 @@ async def download_new_torrent(self, id, torrent_path): # Download new .torrent download_url = f"https://hdbits.org/download.php/{quote(filename)}" params = { - 'passkey' : self.passkey, - 'id' : id + 'passkey': self.passkey, + 'id': id } r = requests.get(url=download_url, params=params) @@ -421,7 +419,7 @@ async def edit_desc(self, meta): with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w') as descfile: from src.bbcode import BBCODE # Add This line for all web-dls - if meta['type'] == 'WEBDL' and meta.get('service_longname', '') != '' and meta.get('description', None) == None: + if meta['type'] == 'WEBDL' and meta.get('service_longname', '') != '' and meta.get('description', None) is None: descfile.write(f"[center][quote]This release is sourced from {meta['service_longname']}[/quote][/center]") bbcode = BBCODE() if meta.get('discs', []) != []: @@ -448,40 +446,39 @@ async def edit_desc(self, meta): desc = bbcode.convert_comparison_to_centered(desc, 1000) desc = re.sub(r"(\[img=\d+)]", "[img]", desc, flags=re.IGNORECASE) descfile.write(desc) - if self.rehost_images == True: + if self.rehost_images is True: console.print("[green]Rehosting Images...") hdbimg_bbcode = await self.hdbimg_upload(meta) descfile.write(f"{hdbimg_bbcode}") else: images = meta['image_list'] - if len(images) > 0: + if len(images) > 0: descfile.write("[center]") for each in range(len(images[:int(meta['screens'])])): img_url = images[each]['img_url'] web_url = images[each]['web_url'] descfile.write(f"[url={web_url}][img]{img_url}[/img][/url]") descfile.write("[/center]") - if self.signature != None: + if self.signature is not None: descfile.write(self.signature) descfile.close() - async def hdbimg_upload(self, meta): images = glob.glob(f"{meta['base_dir']}/tmp/{meta['uuid']}/{meta['filename']}-*.png") url = "https://img.hdbits.org/upload_api.php" data = { - 'username' : self.username, - 'passkey' : self.passkey, - 'galleryoption' : 1, - 'galleryname' : meta['name'], - 'thumbsize' : 'w300' + 'username': self.username, + 'passkey': self.passkey, + 'galleryoption': 1, + 'galleryname': meta['name'], + 'thumbsize': 'w300' } files = {} # Set maximum screenshots to 3 for tv singles and 6 for everthing else - hdbimg_screen_count = 3 if meta['category'] == "TV" and meta.get('tv_pack', 0) == 0 else 6 + hdbimg_screen_count = 3 if meta['category'] == "TV" and meta.get('tv_pack', 0) == 0 else 6 if len(images) < hdbimg_screen_count: - hdbimg_screen_count = len(images) + hdbimg_screen_count = len(images) for i in range(hdbimg_screen_count): files[f'images_files[{i}]'] = open(images[i], 'rb') r = requests.post(url=url, data=data, files=files) @@ -492,17 +489,17 @@ async def get_info_from_torrent_id(self, hdb_id): hdb_imdb = hdb_name = hdb_torrenthash = None url = "https://hdbits.org/api/torrents" data = { - "username" : self.username, - "passkey" : self.passkey, - "id" : hdb_id + "username": self.username, + "passkey": self.passkey, + "id": hdb_id } response = requests.get(url, json=data) if response.ok: try: response = response.json() if response['data'] != []: - hdb_imdb = response['data'][0].get('imdb', {'id' : None}).get('id') - hdb_tvdb = response['data'][0].get('tvdb', {'id' : None}).get('id') + hdb_imdb = response['data'][0].get('imdb', {'id': None}).get('id') + hdb_tvdb = response['data'][0].get('tvdb', {'id': None}).get('id') hdb_name = response['data'][0]['name'] hdb_torrenthash = response['data'][0]['hash'] @@ -515,7 +512,7 @@ async def get_info_from_torrent_id(self, hdb_id): async def search_filename(self, search_term, search_file_folder): hdb_imdb = hdb_tvdb = hdb_name = hdb_torrenthash = hdb_id = None url = "https://hdbits.org/api/torrents" - + if search_file_folder == 'folder': # Handling disc case data = { "username": self.username, @@ -534,7 +531,7 @@ async def search_filename(self, search_term, search_file_folder): console.print(f"[green]Searching HDB for file: [bold yellow]{os.path.basename(search_term)}[/bold yellow]") response = requests.get(url, json=data) - + if response.ok: try: response_json = response.json() @@ -560,6 +557,6 @@ async def search_filename(self, search_term, search_file_folder): console.print(f"[red]Failed to parse HDB API response. Error: {str(e)}[/red]") else: console.print(f"[red]Failed to get info from HDB. Status code: {response.status_code}, Reason: {response.reason}[/red]") - - console.print(f'[yellow]Could not find a matching release on HDB[/yellow]') + + console.print('[yellow]Could not find a matching release on HDB[/yellow]') return hdb_imdb, hdb_tvdb, hdb_name, hdb_torrenthash, hdb_id diff --git a/src/trackers/UNIT3D_TEMPLATE.py b/src/trackers/UNIT3D_TEMPLATE.py index c77e758a7..9d84b6dae 100644 --- a/src/trackers/UNIT3D_TEMPLATE.py +++ b/src/trackers/UNIT3D_TEMPLATE.py @@ -2,7 +2,6 @@ # import discord import asyncio import requests -import os import platform from str2bool import str2bool @@ -34,20 +33,20 @@ def __init__(self, config): self.signature = None self.banned_groups = [""] pass - + async def get_cat_id(self, category_name): category_id = { - 'MOVIE': '1', - 'TV': '2', + 'MOVIE': '1', + 'TV': '2', }.get(category_name, '0') return category_id async def get_type_id(self, type): type_id = { - 'DISC': '1', + 'DISC': '1', 'REMUX': '2', - 'WEBDL': '4', - 'WEBRIP': '5', + 'WEBDL': '4', + 'WEBRIP': '5', 'HDTV': '6', 'ENCODE': '3' }.get(type, '0') @@ -55,16 +54,16 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { - '8640p':'10', - '4320p': '1', - '2160p': '2', - '1440p' : '3', + '8640p':'10', + '4320p': '1', + '2160p': '2', + '1440p': '3', '1080p': '3', - '1080i':'4', - '720p': '5', - '576p': '6', + '1080i': '4', + '720p': '5', + '576p': '6', '576i': '7', - '480p': '8', + '480p': '8', '480i': '9' }.get(resolution, '10') return resolution_id @@ -82,12 +81,12 @@ async def upload(self, meta): await common.unit3d_edit_desc(meta, self.tracker, self.signature) region_id = await common.unit3d_region_ids(meta.get('region')) distributor_id = await common.unit3d_distributor_ids(meta.get('distributor')) - if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) == False: + if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) is False: anon = 0 else: anon = 1 - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: @@ -97,34 +96,34 @@ async def upload(self, meta): open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent", 'rb') files = {'torrent': open_torrent} data = { - 'name' : meta['name'], - 'description' : desc, - 'mediainfo' : mi_dump, - 'bdinfo' : bd_dump, - 'category_id' : cat_id, - 'type_id' : type_id, - 'resolution_id' : resolution_id, - 'tmdb' : meta['tmdb'], - 'imdb' : meta['imdb_id'].replace('tt', ''), - 'tvdb' : meta['tvdb_id'], - 'mal' : meta['mal_id'], - 'igdb' : 0, - 'anonymous' : anon, - 'stream' : meta['stream'], - 'sd' : meta['sd'], - 'keywords' : meta['keywords'], - 'personal_release' : int(meta.get('personalrelease', False)), - 'internal' : 0, - 'featured' : 0, - 'free' : 0, - 'doubleup' : 0, - 'sticky' : 0, + 'name': meta['name'], + 'description': desc, + 'mediainfo': mi_dump, + 'bdinfo': bd_dump, + 'category_id': cat_id, + 'type_id': type_id, + 'resolution_id': resolution_id, + 'tmdb': meta['tmdb'], + 'imdb': meta['imdb_id'].replace('tt', ''), + 'tvdb': meta['tvdb_id'], + 'mal': meta['mal_id'], + 'igdb': 0, + 'anonymous': anon, + 'stream': meta['stream'], + 'sd': meta['sd'], + 'keywords': meta['keywords'], + 'personal_release': int(meta.get('personalrelease', False)), + 'internal': 0, + 'featured': 0, + 'free': 0, + 'doubleup': 0, + 'sticky': 0, } # Internal - if self.config['TRACKERS'][self.tracker].get('internal', False) == True: + if self.config['TRACKERS'][self.tracker].get('internal', False) is True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 - + if region_id != 0: data['region_id'] = region_id if distributor_id != 0: @@ -136,35 +135,31 @@ async def upload(self, meta): 'User-Agent': f'Upload Assistant/2.1 ({platform.system()} {platform.release()})' } params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip() + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip() } - - if meta['debug'] == False: + + if meta['debug'] is False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) except: console.print("It may have uploaded, go check") - return + return else: console.print(f"[cyan]Request Data:") console.print(data) open_torrent.close() - - - - async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'tmdbId' : meta['tmdb'], - 'categories[]' : await self.get_cat_id(meta['category']), - 'types[]' : await self.get_type_id(meta['type']), - 'resolutions[]' : await self.get_res_id(meta['resolution']), - 'name' : "" + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'tmdbId': meta['tmdb'], + 'categories[]': await self.get_cat_id(meta['category']), + 'types[]': await self.get_type_id(meta['type']), + 'resolutions[]': await self.get_res_id(meta['resolution']), + 'name': "" } if meta.get('edition', "") != "": params['name'] = params['name'] + f" {meta['edition']}" @@ -180,4 +175,4 @@ async def search_existing(self, meta): console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) - return dupes \ No newline at end of file + return dupes diff --git a/src/vs.py b/src/vs.py index 6684ca6ea..f9d00dbd4 100644 --- a/src/vs.py +++ b/src/vs.py @@ -2,14 +2,10 @@ core = vs.core from awsmfunc import ScreenGen, DynamicTonemap, FrameInfo, zresize import random -import argparse -from typing import Union, List -from pathlib import Path -import os, sys -import platform -import multiprocessing +import os from functools import partial + def CustomFrameInfo(clip, text): def FrameProps(n, f, clip): # Modify the frame properties extraction here to avoid the decode issue @@ -20,6 +16,7 @@ def FrameProps(n, f, clip): # Apply FrameProps to each frame return core.std.FrameEval(clip, partial(FrameProps, clip=clip), prop_src=clip) + def optimize_images(image, config): import platform # Ensure platform is imported here if config.get('optimize_images', True): @@ -27,7 +24,7 @@ def optimize_images(image, config): try: pyver = platform.python_version_tuple() if int(pyver[0]) == 3 and int(pyver[1]) >= 7: - import oxipng + import oxipng if os.path.getsize(image) >= 16000000: oxipng.optimize(image, level=6) else: @@ -36,6 +33,7 @@ def optimize_images(image, config): print(f"Image optimization failed: {e}") return + def vs_screengn(source, encode=None, filter_b_frames=False, num=5, dir=".", config=None): if config is None: config = {'optimize_images': True} # Default configuration @@ -130,4 +128,4 @@ def vs_screengn(source, encode=None, filter_b_frames=False, num=5, dir=".", conf # Optimize images for i in range(1, num + 1): image_path = os.path.join(dir, f"{str(i).zfill(2)}a.png") - optimize_images(image_path, config) \ No newline at end of file + optimize_images(image_path, config) diff --git a/upload.py b/upload.py index 848f7f811..d003baff1 100644 --- a/upload.py +++ b/upload.py @@ -44,8 +44,6 @@ import os import sys import platform -import multiprocessing -import logging import shutil import glob import cli_ui @@ -55,7 +53,6 @@ from rich.style import Style - cli_ui.setup(color='always', title="L4G's Upload Assistant") import traceback @@ -73,8 +70,8 @@ with open(f"{base_dir}/data/config.py", 'w') as f: f.write(f"config = {json.dumps(json_config, indent=4)}") f.close() - cli_ui.info(cli_ui.green, "Successfully updated config from .json to .py") - cli_ui.info(cli_ui.green, "It is now safe for you to delete", cli_ui.yellow, "data/config.json", "if you wish") + cli_ui.info(cli_ui.green, "Successfully updated config from .json to .py") + cli_ui.info(cli_ui.green, "It is now safe for you to delete", cli_ui.yellow, "data/config.json", "if you wish") from data.config import config else: raise NotImplementedError @@ -82,7 +79,7 @@ cli_ui.info(cli_ui.red, "We have switched from .json to .py for config to have a much more lenient experience") cli_ui.info(cli_ui.red, "Looks like the auto updater didnt work though") cli_ui.info(cli_ui.red, "Updating is just 2 easy steps:") - cli_ui.info(cli_ui.red, "1: Rename", cli_ui.yellow, os.path.abspath(f"{base_dir}/data/config.json"), cli_ui.red, "to", cli_ui.green, os.path.abspath(f"{base_dir}/data/config.py") ) + cli_ui.info(cli_ui.red, "1: Rename", cli_ui.yellow, os.path.abspath(f"{base_dir}/data/config.json"), cli_ui.red, "to", cli_ui.green, os.path.abspath(f"{base_dir}/data/config.py")) cli_ui.info(cli_ui.red, "2: Add", cli_ui.green, "config = ", cli_ui.red, "to the beginning of", cli_ui.green, os.path.abspath(f"{base_dir}/data/config.py")) exit() else: @@ -90,6 +87,7 @@ client = Clients(config=config) parser = Args(config) + async def do_the_thing(base_dir): meta = dict() meta['base_dir'] = base_dir @@ -111,8 +109,8 @@ async def do_the_thing(base_dir): path = path[:-1] queue = [] if os.path.exists(path): - meta, help, before_args = parser.parse(tuple(' '.join(sys.argv[1:]).split(' ')), meta) - queue = [path] + meta, help, before_args = parser.parse(tuple(' '.join(sys.argv[1:]).split(' ')), meta) + queue = [path] else: # Search glob if dirname exists if os.path.exists(os.path.dirname(path)) and len(paths) <= 1: @@ -126,7 +124,7 @@ async def do_the_thing(base_dir): console.print("\n\n") else: console.print(f"[red]Path: [bold red]{path}[/bold red] does not exist") - + elif os.path.exists(os.path.dirname(path)) and len(paths) != 1: queue = paths md_text = "\n - ".join(queue) @@ -153,10 +151,10 @@ async def do_the_thing(base_dir): console.print("\n[bold green]Queuing these files:[/bold green]", end='') console.print(Markdown(f"- {md_text.rstrip()}\n\n", style=Style(color='cyan'))) console.print("\n\n") - + else: # Add Search Here - console.print(f"[red]There was an issue with your input. If you think this was not an issue, please make a report that includes the full command used.") + console.print("[red]There was an issue with your input. If you think this was not an issue, please make a report that includes the full command used.") exit() base_meta = {k: v for k, v in meta.items()} @@ -169,8 +167,8 @@ async def do_the_thing(base_dir): saved_meta = json.load(f) for key, value in saved_meta.items(): overwrite_list = [ - 'trackers', 'dupe', 'debug', 'anon', 'category', 'type', 'screens', 'nohash', 'manual_edition', 'imdb', 'tmdb_manual', 'mal', 'manual', - 'hdb', 'ptp', 'blu', 'no_season', 'no_aka', 'no_year', 'no_dub', 'no_tag', 'no_seed', 'client', 'desclink', 'descfile', 'desc', 'draft', 'region', 'freeleech', + 'trackers', 'dupe', 'debug', 'anon', 'category', 'type', 'screens', 'nohash', 'manual_edition', 'imdb', 'tmdb_manual', 'mal', 'manual', + 'hdb', 'ptp', 'blu', 'no_season', 'no_aka', 'no_year', 'no_dub', 'no_tag', 'no_seed', 'client', 'desclink', 'descfile', 'desc', 'draft', 'region', 'freeleech', 'personalrelease', 'unattended', 'season', 'episode', 'torrent_creation', 'qbit_tag', 'qbit_cat', 'skip_imghost_upload', 'imghost', 'manual_source', 'webdv', 'hardcoded-subs' ] if meta.get(key, None) != value and key in overwrite_list: @@ -180,7 +178,7 @@ async def do_the_thing(base_dir): except FileNotFoundError: pass console.print(f"[green]Gathering info for {os.path.basename(path)}") - if meta['imghost'] == None: + if meta['imghost'] is None: meta['imghost'] = config['DEFAULT']['img_host_1'] if not meta['unattended']: ua = config['DEFAULT'].get('auto_mode', False) @@ -188,44 +186,44 @@ async def do_the_thing(base_dir): meta['unattended'] = True console.print("[yellow]Running in Auto Mode") prep = Prep(screens=meta['screens'], img_host=meta['imghost'], config=config) - meta = await prep.gather_prep(meta=meta, mode='cli') + meta = await prep.gather_prep(meta=meta, mode='cli') meta['name_notag'], meta['name'], meta['clean_name'], meta['potential_missing'] = await prep.get_name(meta) - if meta.get('image_list', False) in (False, []) and meta.get('skip_imghost_upload', False) == False: + if meta.get('image_list', False) in (False, []) and meta.get('skip_imghost_upload', False) is False: return_dict = {} - meta['image_list'], dummy_var = prep.upload_screens(meta, meta['screens'], 1, 0, meta['screens'],[], return_dict) + meta['image_list'], dummy_var = prep.upload_screens(meta, meta['screens'], 1, 0, meta['screens'], [], return_dict) if meta['debug']: console.print(meta['image_list']) # meta['uploaded_screens'] = True - elif meta.get('skip_imghost_upload', False) == True and meta.get('image_list', False) == False: + elif meta.get('skip_imghost_upload', False) == True and meta.get('image_list', False) is False: meta['image_list'] = [] if not os.path.exists(os.path.abspath(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent")): reuse_torrent = None - if meta.get('rehash', False) == False: + if meta.get('rehash', False) is False: reuse_torrent = await client.find_existing_torrent(meta) - if reuse_torrent != None: + if reuse_torrent is not None: prep.create_base_from_existing_torrent(reuse_torrent, meta['base_dir'], meta['uuid']) - if meta['nohash'] == False and reuse_torrent == None: + if meta['nohash'] is False and reuse_torrent is None: prep.create_torrent(meta, Path(meta['path']), "BASE") if meta['nohash']: meta['client'] = "none" - elif os.path.exists(os.path.abspath(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent")) and meta.get('rehash', False) == True and meta['nohash'] == False: + elif os.path.exists(os.path.abspath(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent")) and meta.get('rehash', False) == True and meta['nohash'] is False: prep.create_torrent(meta, Path(meta['path']), "BASE") if int(meta.get('randomized', 0)) >= 1: prep.create_random_torrents(meta['base_dir'], meta['uuid'], meta['randomized'], meta['path']) - if meta.get('trackers', None) != None: + if meta.get('trackers', None) is not None: trackers = meta['trackers'] else: trackers = config['TRACKERS']['default_trackers'] if "," in trackers: trackers = trackers.split(',') - with open (f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as f: + with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as f: json.dump(meta, f, indent=4) f.close() - confirm = get_confirmation(meta) - while confirm == False: + confirm = get_confirmation(meta) + while confirm is False: # help.print_help() editargs = cli_ui.ask_string("Input args that need correction e.g.(--tag NTb --category tv --tmdb 12345)") editargs = (meta['path'],) + tuple(editargs.split()) @@ -234,26 +232,26 @@ async def do_the_thing(base_dir): meta, help, before_args = parser.parse(editargs, meta) # meta = await prep.tmdb_other_meta(meta) meta['edit'] = True - meta = await prep.gather_prep(meta=meta, mode='cli') + meta = await prep.gather_prep(meta=meta, mode='cli') meta['name_notag'], meta['name'], meta['clean_name'], meta['potential_missing'] = await prep.get_name(meta) confirm = get_confirmation(meta) - - if isinstance(trackers, list) == False: + + if isinstance(trackers, list) is False: trackers = [trackers] trackers = [s.strip().upper() for s in trackers] if meta.get('manual', False): trackers.insert(0, "MANUAL") - + #################################### ####### Upload to Trackers ####### #################################### common = COMMON(config=config) - api_trackers = ['BLU', 'AITHER', 'STC', 'R4E', 'STT', 'RF', 'ACM','LCD','LST','HUNO', 'SN', 'LT', 'NBL', 'ANT', 'JPTV', 'TDC', 'OE', 'BHDTV', 'RTF', 'OTW', 'FNP', 'CBR', 'UTP', 'AL', 'HDB'] + api_trackers = ['BLU', 'AITHER', 'STC', 'R4E', 'STT', 'RF', 'ACM', 'LCD', 'LST', 'HUNO', 'SN', 'LT', 'NBL', 'ANT', 'JPTV', 'TDC', 'OE', 'BHDTV', 'RTF', 'OTW', 'FNP', 'CBR', 'UTP', 'AL', 'HDB'] http_trackers = ['TTG', 'FL', 'PTER', 'HDT', 'MTV'] tracker_class_map = { - 'BLU' : BLU, 'BHD': BHD, 'AITHER' : AITHER, 'STC' : STC, 'R4E' : R4E, 'THR' : THR, 'STT' : STT, 'HP' : HP, 'PTP' : PTP, 'RF' : RF, 'SN' : SN, - 'ACM' : ACM, 'HDB' : HDB, 'LCD': LCD, 'TTG' : TTG, 'LST' : LST, 'HUNO': HUNO, 'FL' : FL, 'LT' : LT, 'NBL' : NBL, 'ANT' : ANT, 'PTER': PTER, 'JPTV' : JPTV, - 'TL' : TL, 'TDC' : TDC, 'HDT' : HDT, 'MTV': MTV, 'OE': OE, 'BHDTV': BHDTV, 'RTF':RTF, 'OTW': OTW, 'FNP': FNP, 'CBR': CBR, 'UTP': UTP, 'AL':AL} + 'BLU': BLU, 'BHD': BHD, 'AITHER': AITHER, 'STC': STC, 'R4E': R4E, 'THR': THR, 'STT': STT, 'HP': HP, 'PTP': PTP, 'RF': RF, 'SN': SN, + 'ACM': ACM, 'HDB': HDB, 'LCD': LCD, 'TTG': TTG, 'LST': LST, 'HUNO': HUNO, 'FL': FL, 'LT': LT, 'NBL': NBL, 'ANT': ANT, 'PTER': PTER, 'JPTV': JPTV, + 'TL': TL, 'TDC': TDC, 'HDT': HDT, 'MTV': MTV, 'OE': OE, 'BHDTV': BHDTV, 'RTF': RTF, 'OTW': OTW, 'FNP': FNP, 'CBR': CBR, 'UTP': UTP, 'AL': AL} for tracker in trackers: if meta['name'].endswith('DUPE?'): @@ -263,7 +261,7 @@ async def do_the_thing(base_dir): debug = "(DEBUG)" else: debug = "" - + if tracker in api_trackers: tracker_class = tracker_class_map[tracker](config=config) if meta['unattended']: @@ -280,12 +278,12 @@ async def do_the_thing(base_dir): dupes = await common.filter_dupes(dupes, meta) # note BHDTV does not have search implemented. meta = dupe_check(dupes, meta) - if meta['upload'] == True: + if meta['upload'] is True: await tracker_class.upload(meta) if tracker == 'SN': await asyncio.sleep(16) await client.add_to_client(meta, tracker_class.tracker) - + if tracker in http_trackers: tracker_class = tracker_class_map[tracker](config=config) if meta['unattended']: @@ -296,19 +294,19 @@ async def do_the_thing(base_dir): console.print(f"Uploading to {tracker}") if check_banned_group(tracker_class.tracker, tracker_class.banned_groups, meta): continue - if await tracker_class.validate_credentials(meta) == True: + if await tracker_class.validate_credentials(meta) is True: dupes = await tracker_class.search_existing(meta) dupes = await common.filter_dupes(dupes, meta) meta = dupe_check(dupes, meta) - if meta['upload'] == True: + if meta['upload'] is True: await tracker_class.upload(meta) await client.add_to_client(meta, tracker_class.tracker) if tracker == "MANUAL": - if meta['unattended']: + if meta['unattended']: do_manual = True else: - do_manual = cli_ui.ask_yes_no(f"Get files for manual upload?", default=True) + do_manual = cli_ui.ask_yes_no("Get files for manual upload?", default=True) if do_manual: for manual_tracker in trackers: if manual_tracker != 'MANUAL': @@ -319,11 +317,11 @@ async def do_the_thing(base_dir): else: await tracker_class.edit_desc(meta) url = await prep.package(meta) - if url == False: + if url is False: console.print(f"[yellow]Unable to upload prep files, they can be found at `tmp/{meta['uuid']}") else: console.print(f"[green]{meta['name']}") - console.print(f"[green]Files can be found at: [yellow]{url}[/yellow]") + console.print(f"[green]Files can be found at: [yellow]{url}[/yellow]") if tracker == "BHD": bhd = BHD(config=config) @@ -343,10 +341,10 @@ async def do_the_thing(base_dir): dupes = await bhd.search_existing(meta) dupes = await common.filter_dupes(dupes, meta) meta = dupe_check(dupes, meta) - if meta['upload'] == True: + if meta['upload'] is True: await bhd.upload(meta) await client.add_to_client(meta, "BHD") - + if tracker == "THR": if meta['unattended']: upload_to_thr = True @@ -354,11 +352,11 @@ async def do_the_thing(base_dir): upload_to_thr = cli_ui.ask_yes_no(f"Upload to THR? {debug}", default=meta['unattended']) if upload_to_thr: console.print("Uploading to THR") - #Unable to get IMDB id/Youtube Link + # nable to get IMDB id/Youtube Link if meta.get('imdb_id', '0') == '0': imdb_id = cli_ui.ask_string("Unable to find IMDB id, please enter e.g.(tt1234567)") meta['imdb_id'] = imdb_id.replace('tt', '').zfill(7) - if meta.get('youtube', None) == None: + if meta.get('youtube', None) is None: youtube = cli_ui.ask_string("Unable to find youtube trailer, please link one e.g.(https://www.youtube.com/watch?v=dQw4w9WgXcQ)") meta['youtube'] = youtube thr = THR(config=config) @@ -370,7 +368,7 @@ async def do_the_thing(base_dir): dupes = thr.search_existing(session, meta.get('imdb_id')) dupes = await common.filter_dupes(dupes, meta) meta = dupe_check(dupes, meta) - if meta['upload'] == True: + if meta['upload'] is True: await thr.upload(session, meta) await client.add_to_client(meta, "THR") except: @@ -392,9 +390,9 @@ async def do_the_thing(base_dir): try: console.print("[yellow]Searching for Group ID") groupID = await ptp.get_group_by_imdb(meta['imdb_id']) - if groupID == None: + if groupID is None: console.print("[yellow]No Existing Group found") - if meta.get('youtube', None) == None or "youtube" not in str(meta.get('youtube', '')): + if meta.get('youtube', None) is None or "youtube" not in str(meta.get('youtube', '')): youtube = cli_ui.ask_string("Unable to find youtube trailer, please link one e.g.(https://www.youtube.com/watch?v=dQw4w9WgXcQ)", default="") meta['youtube'] = youtube meta['upload'] = True @@ -405,7 +403,7 @@ async def do_the_thing(base_dir): meta = dupe_check(dupes, meta) if meta.get('imdb_info', {}) == {}: meta['imdb_info'] = await prep.get_imdb_info(meta['imdb_id'], meta) - if meta['upload'] == True: + if meta['upload'] is True: ptpUrl, ptpData = await ptp.fill_upload_form(groupID, meta) await ptp.upload(meta, ptpUrl, ptpData) await asyncio.sleep(5) @@ -424,11 +422,11 @@ async def do_the_thing(base_dir): if check_banned_group(tracker_class.tracker, tracker_class.banned_groups, meta): continue await tracker_class.upload(meta) - await client.add_to_client(meta, tracker_class.tracker) + await client.add_to_client(meta, tracker_class.tracker) def get_confirmation(meta): - if meta['debug'] == True: + if meta['debug'] is True: console.print("[bold red]DEBUG: True") console.print(f"Prep material saved to {meta['base_dir']}/tmp/{meta['uuid']}") console.print() @@ -450,7 +448,7 @@ def get_confirmation(meta): if int(meta.get('freeleech', '0')) != 0: cli_ui.info(f"Freeleech: {meta['freeleech']}") if meta['tag'] == "": - tag = "" + tag = "" else: tag = f" / {meta['tag'][1:]}" if meta['is_disc'] == "DVD": @@ -459,28 +457,28 @@ def get_confirmation(meta): res = meta['resolution'] cli_ui.info(f"{res} / {meta['type']}{tag}") - if meta.get('personalrelease', False) == True: + if meta.get('personalrelease', False) is True: cli_ui.info("Personal Release!") console.print() - if meta.get('unattended', False) == False: + if meta.get('unattended', False) is False: get_missing(meta) - ring_the_bell = "\a" if config['DEFAULT'].get("sfx_on_prompt", True) == True else "" # \a rings the bell + ring_the_bell = "\a" if config['DEFAULT'].get("sfx_on_prompt", True) is True else "" # \a rings the bell cli_ui.info(ring_the_bell) # Handle the 'keep_folder' logic based on 'is disc' and 'isdir' if meta.get('is disc', False): meta['keep_folder'] = False # Ensure 'keep_folder' is False if 'is disc' is True - + if meta['isdir']: if 'keep_folder' in meta: if meta['keep_folder']: - cli_ui.info_section(cli_ui.yellow, f"Uploading with --keep-folder") + cli_ui.info_section(cli_ui.yellow, "Uploading with --keep-folder") kf_confirm = cli_ui.ask_yes_no("You specified --keep-folder. Uploading in folders might not be allowed. Are you sure you want to proceed?", default=False) if not kf_confirm: cli_ui.info('Aborting...') exit() - cli_ui.info_section(cli_ui.yellow, f"Is this correct?") + cli_ui.info_section(cli_ui.yellow, "Is this correct?") cli_ui.info(f"Name: {meta['name']}") confirm = cli_ui.ask_yes_no("Correct?", default=False) else: @@ -489,19 +487,20 @@ def get_confirmation(meta): return confirm + def dupe_check(dupes, meta): if not dupes: - console.print("[green]No dupes found") - meta['upload'] = True - return meta + console.print("[green]No dupes found") + meta['upload'] = True + return meta else: - console.print() + console.print() dupe_text = "\n".join(dupes) console.print() cli_ui.info_section(cli_ui.bold, "Check if these are actually dupes!") cli_ui.info(dupe_text) if meta['unattended']: - if meta.get('dupe', False) == False: + if meta.get('dupe', False) is False: console.print("[red]Found potential dupes. Aborting. If this is not a dupe, or you would like to upload anyways, pass --skip-dupe-check") upload = False else: @@ -509,11 +508,11 @@ def dupe_check(dupes, meta): upload = True console.print() if not meta['unattended']: - if meta.get('dupe', False) == False: + if meta.get('dupe', False) is False: upload = cli_ui.ask_yes_no("Upload Anyways?", default=False) else: upload = True - if upload == False: + if upload is False: meta['upload'] = False else: meta['upload'] = True @@ -545,14 +544,15 @@ def check_banned_group(tracker, banned_group_list, meta): return True return False + def get_missing(meta): info_notes = { - 'edition' : 'Special Edition/Release', - 'description' : "Please include Remux/Encode Notes if possible (either here or edit your upload)", - 'service' : "WEB Service e.g.(AMZN, NF)", - 'region' : "Disc Region", - 'imdb' : 'IMDb ID (tt1234567)', - 'distributor' : "Disc Distributor e.g.(BFI, Criterion, etc)" + 'edition': 'Special Edition/Release', + 'description': "Please include Remux/Encode Notes if possible (either here or edit your upload)", + 'service': "WEB Service e.g.(AMZN, NF)", + 'region': "Disc Region", + 'imdb': 'IMDb ID (tt1234567)', + 'distributor': "Disc Distributor e.g.(BFI, Criterion, etc)" } missing = [] if meta.get('imdb_id', '0') == '0': @@ -562,7 +562,7 @@ def get_missing(meta): for each in meta['potential_missing']: if str(meta.get(each, '')).replace(' ', '') in ["", "None", "0"]: if each == "imdb_id": - each = 'imdb' + each = 'imdb' missing.append(f"--{each} | {info_notes.get(each)}") if missing != []: cli_ui.info_section(cli_ui.yellow, "Potentially missing information:") @@ -575,6 +575,7 @@ def get_missing(meta): console.print() return + if __name__ == '__main__': pyver = platform.python_version_tuple() if int(pyver[0]) != 3: @@ -586,4 +587,4 @@ def get_missing(meta): loop = asyncio.get_event_loop() loop.run_until_complete(do_the_thing(base_dir)) else: - asyncio.run(do_the_thing(base_dir)) \ No newline at end of file + asyncio.run(do_the_thing(base_dir)) From 3b57da3f5f5cb6893d0e44789a2f66c1ec090566 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 20:09:17 +1000 Subject: [PATCH 13/33] Check lint action --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 95143e891..64b68d96d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ A simple tool to take the work out of uploading. ## Coming Soon: - Features - ## **Setup:** From 96e7b479f9465b3120d69917311fe091e0609bf5 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 20:10:32 +1000 Subject: [PATCH 14/33] Move flake8 back --- .github/workflows/.flake8 | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/workflows/.flake8 diff --git a/.github/workflows/.flake8 b/.github/workflows/.flake8 new file mode 100644 index 000000000..0cb611f43 --- /dev/null +++ b/.github/workflows/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 220 \ No newline at end of file From cbc7433333e72526ecc59a91eabaa31b27fdc03d Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 20:18:55 +1000 Subject: [PATCH 15/33] Add linter dispatch --- .github/workflows/lint.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index f0f37dbd9..5105bd60c 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,6 +8,7 @@ on: pull_request: branches: - master + workflow_dispatch: jobs: lint: From fc28d7ca65d6a346c7f71c2998a47bb546703089 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 20:22:07 +1000 Subject: [PATCH 16/33] Rename lint workflow --- .github/workflows/{lint.yaml => lint.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{lint.yaml => lint.yml} (100%) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yml similarity index 100% rename from .github/workflows/lint.yaml rename to .github/workflows/lint.yml From 1bf6a76688c69ca592d5cb88a7c3ac843e4c8477 Mon Sep 17 00:00:00 2001 From: Audionut Date: Fri, 30 Aug 2024 21:26:06 +1000 Subject: [PATCH 17/33] Only skip site, not function --- src/prep.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/prep.py b/src/prep.py index 1e8225493..21c01e95d 100644 --- a/src/prep.py +++ b/src/prep.py @@ -95,12 +95,11 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, id=meta[tracker_key]) if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: console.print(f"[green]Valid data found on {tracker_name}, setting meta values[/green]") - # Prompt user for selection if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): if blu_tmdb not in [None, '0']: meta['tmdb_manual'] = blu_tmdb if blu_imdb not in [None, '0']: - meta['imdb'] = str(blu_imdb) + meta['imdb'] = str(blu_imdb).zfill(7) # Pad IMDb ID with leading zeros if blu_tvdb not in [None, '0']: meta['tvdb_id'] = blu_tvdb if blu_mal not in [None, '0']: @@ -115,17 +114,20 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met meta['blu_filename'] = blu_filename # Store the filename in meta for later use found_match = True # Set flag if any relevant data is found else: - console.print(f"[yellow]User skipped the found ID on {tracker_name}[/yellow]") + console.print(f"[yellow]User skipped the found ID on {tracker_name}, moving to the next site.[/yellow]") + return meta, found_match # Return immediately to skip the current site else: console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") else: meta['imdb'], meta['ext_torrenthash'] = await tracker_instance.get_imdb_from_torrent_id(meta[tracker_key]) if meta['imdb']: + meta['imdb'] = str(meta['imdb']).zfill(7) # Pad IMDb ID with leading zeros if await self.prompt_user_for_id_selection(imdb=meta['imdb']): console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}[/green]") found_match = True else: - console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}[/yellow]") + console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.[/yellow]") + return meta, found_match # Return immediately to skip the current site else: console.print(f"[yellow]No IMDb ID found on {tracker_name}[/yellow]") else: @@ -146,7 +148,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met if blu_tmdb not in [None, '0']: meta['tmdb_manual'] = blu_tmdb if blu_imdb not in [None, '0']: - meta['imdb'] = str(blu_imdb) + meta['imdb'] = str(blu_imdb).zfill(7) # Pad IMDb ID with leading zeros if blu_tvdb not in [None, '0']: meta['tvdb_id'] = blu_tvdb if blu_mal not in [None, '0']: @@ -160,18 +162,23 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met if blu_filename: meta['blu_filename'] = blu_filename # Store the filename in meta for later use found_match = True + else: + console.print(f"[yellow]User skipped the found ID on {tracker_name}, moving to the next site.[/yellow]") + return meta, found_match # Return immediately to skip the current site else: console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") else: imdb = tracker_id = None if imdb: + imdb = str(imdb).zfill(7) # Pad IMDb ID with leading zeros if await self.prompt_user_for_id_selection(imdb=imdb): console.print(f"[green]{tracker_name} IMDb ID found: {imdb}[/green]") - meta['imdb'] = str(imdb) + meta['imdb'] = imdb found_match = True else: - console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}[/yellow]") + console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.[/yellow]") + return meta, found_match # Return immediately to skip the current site if tracker_id: meta[tracker_key] = tracker_id @@ -374,7 +381,6 @@ async def gather_prep(self, meta, mode): meta = await self.get_tmdb_from_imdb(meta, filename) else: meta['tmdb_manual'] = meta.get('tmdb', None) - # If no tmdb, use imdb for meta if int(meta['tmdb']) == 0: @@ -490,9 +496,6 @@ async def get_disc(self, meta): discs = sorted(discs, key=lambda d: d['name']) return is_disc, videoloc, bdinfo, discs - - - """ Get video files @@ -517,8 +520,6 @@ def get_video(self, videoloc, mode): filelist = sorted(filelist) return video, filelist - - """ Get and parse mediainfo """ @@ -1587,13 +1588,6 @@ def get_romaji(self, tmdb_name, mal): episodes = 0 return romaji, mal_id, eng_title, season_year, episodes - - - - - - - """ Mediainfo/Bdinfo > meta """ From 05145b6b8f2664de6d0dd9ee3f3f012dd3996b7b Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 12:08:18 +1000 Subject: [PATCH 18/33] Don't fail when sub title is empty --- src/trackers/PTP.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/trackers/PTP.py b/src/trackers/PTP.py index c35a082e0..758b87d6c 100644 --- a/src/trackers/PTP.py +++ b/src/trackers/PTP.py @@ -438,7 +438,8 @@ def get_subtitles(self, meta): if language == "en": if track.get('Forced', "") == "Yes": language = "en (Forced)" - if "intertitles" in track.get('Title', "").lower(): + title = track.get('Title', "") + if isinstance(title, str) and "intertitles" in title.lower(): language = "en (Intertitles)" for lang, subID in sub_lang_map.items(): if language in lang and subID not in sub_langs: @@ -448,7 +449,7 @@ def get_subtitles(self, meta): for lang, subID in sub_lang_map.items(): if language in lang and subID not in sub_langs: sub_langs.append(subID) - + if sub_langs == []: sub_langs = [44] # No Subtitle return sub_langs @@ -871,4 +872,4 @@ async def upload(self, meta, url, data): if match is None: console.print(url) console.print(data) - raise UploadException(f"Upload to PTP failed: result URL {response.url} ({response.status_code}) is not the expected one.") \ No newline at end of file + raise UploadException(f"Upload to PTP failed: result URL {response.url} ({response.status_code}) is not the expected one.") From 9016a4df5af914ad4bf61e27a5fa032b6dcc2651 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 13:01:28 +1000 Subject: [PATCH 19/33] More linting --- src/prep.py | 598 ++++++++++++++++++++++++++-------------------------- 1 file changed, 295 insertions(+), 303 deletions(-) diff --git a/src/prep.py b/src/prep.py index 21c01e95d..5408b2a53 100644 --- a/src/prep.py +++ b/src/prep.py @@ -72,12 +72,12 @@ async def prompt_user_for_id_selection(self, blu_tmdb=None, blu_imdb=None, blu_t if blu_tmdb or blu_imdb or blu_tvdb: if blu_imdb: blu_imdb = str(blu_imdb).zfill(7) # Convert to string and ensure IMDb ID is 7 characters long by adding leading zeros - console.print(f"[cyan]Found the following IDs on BLU:[/cyan]") + console.print("[cyan]Found the following IDs on BLU:[/cyan]") console.print(f"TMDb ID: {blu_tmdb}") console.print(f"IMDb ID: https://www.imdb.com/title/tt{blu_imdb}") console.print(f"TVDb ID: {blu_tvdb}") console.print(f"Filename: {blu_filename}") - + selection = input("Do you want to use this ID? (y/n): ").strip().lower() return selection == 'y' @@ -192,7 +192,7 @@ async def gather_prep(self, meta, mode): if meta.get('uuid', None) is None: folder_id = os.path.basename(meta['path']) - meta['uuid'] = folder_id + meta['uuid'] = folder_id if not os.path.exists(f"{base_dir}/tmp/{meta['uuid']}"): Path(f"{base_dir}/tmp/{meta['uuid']}").mkdir(parents=True, exist_ok=True) @@ -334,67 +334,67 @@ async def gather_prep(self, meta, mode): # Take Screenshots if meta['is_disc'] == "BDMV": - if meta.get('edit', False) == False: - if meta.get('vapoursynth', False) == True: + if meta.get('edit', False) is False: + if meta.get('vapoursynth', False) is True: use_vs = True else: use_vs = False try: ds = multiprocessing.Process(target=self.disc_screenshots, args=(filename, bdinfo, meta['uuid'], base_dir, use_vs, meta.get('image_list', []), meta.get('ffdebug', False), None)) ds.start() - while ds.is_alive() == True: + while ds.is_alive() is True: await asyncio.sleep(1) except KeyboardInterrupt: ds.terminate() elif meta['is_disc'] == "DVD": - if meta.get('edit', False) == False: + if meta.get('edit', False) is False: try: ds = multiprocessing.Process(target=self.dvd_screenshots, args=(meta, 0, None)) ds.start() - while ds.is_alive() == True: + while ds.is_alive() is True: await asyncio.sleep(1) except KeyboardInterrupt: ds.terminate() else: - if meta.get('edit', False) == False: + if meta.get('edit', False) is False: try: s = multiprocessing.Process(target=self.screenshots, args=(videopath, filename, meta['uuid'], base_dir, meta)) s.start() - while s.is_alive() == True: + while s.is_alive() is True: await asyncio.sleep(3) except KeyboardInterrupt: s.terminate() meta['tmdb'] = meta.get('tmdb_manual', None) - if meta.get('type', None) == None: + if meta.get('type', None) is None: meta['type'] = self.get_type(video, meta['scene'], meta['is_disc']) - if meta.get('category', None) == None: + if meta.get('category', None) is None: meta['category'] = self.get_cat(video) else: meta['category'] = meta['category'].upper() - if meta.get('tmdb', None) == None and meta.get('imdb', None) == None: + if meta.get('tmdb', None) is None and meta.get('imdb', None) is None: meta['category'], meta['tmdb'], meta['imdb'] = self.get_tmdb_imdb_from_mediainfo(mi, meta['category'], meta['is_disc'], meta['tmdb'], meta['imdb']) - if meta.get('tmdb', None) == None and meta.get('imdb', None) == None: + if meta.get('tmdb', None) is None and meta.get('imdb', None) is None: meta = await self.get_tmdb_id(filename, meta['search_year'], meta, meta['category'], untouched_filename) - elif meta.get('imdb', None) != None and meta.get('tmdb_manual', None) == None: + elif meta.get('imdb', None) is not None and meta.get('tmdb_manual', None) is None: meta['imdb_id'] = str(meta['imdb']).replace('tt', '') meta = await self.get_tmdb_from_imdb(meta, filename) else: meta['tmdb_manual'] = meta.get('tmdb', None) - + # If no tmdb, use imdb for meta if int(meta['tmdb']) == 0: meta = await self.imdb_other_meta(meta) else: meta = await self.tmdb_other_meta(meta) # Search tvmaze - meta['tvmaze_id'], meta['imdb_id'], meta['tvdb_id'] = await self.search_tvmaze(filename, meta['search_year'], meta.get('imdb_id','0'), meta.get('tvdb_id', 0)) + meta['tvmaze_id'], meta['imdb_id'], meta['tvdb_id'] = await self.search_tvmaze(filename, meta['search_year'], meta.get('imdb_id', '0'), meta.get('tvdb_id', 0)) # If no imdb, search for it - if meta.get('imdb_id', None) == None: + if meta.get('imdb_id', None) is None: meta['imdb_id'] = await self.search_imdb(filename, meta['search_year']) - if meta.get('imdb_info', None) == None and int(meta['imdb_id']) != 0: + if meta.get('imdb_info', None) is None and int(meta['imdb_id']) != 0: meta['imdb_info'] = await self.get_imdb_info(meta['imdb_id'], meta) - if meta.get('tag', None) == None: + if meta.get('tag', None) is None: meta['tag'] = self.get_tag(video, meta) else: if not meta['tag'].startswith('-') and meta['tag'] != "": @@ -415,18 +415,18 @@ async def gather_prep(self, meta, mode): meta['uhd'] = self.get_uhd(meta['type'], guessit(meta['path']), meta['resolution'], meta['path']) meta['hdr'] = self.get_hdr(mi, bdinfo) meta['distributor'] = self.get_distributor(meta['distributor']) - if meta.get('is_disc', None) == "BDMV": #Blu-ray Specific + if meta.get('is_disc', None) == "BDMV": # Blu-ray Specific meta['region'] = self.get_region(bdinfo, meta.get('region', None)) meta['video_codec'] = self.get_video_codec(bdinfo) else: meta['video_encode'], meta['video_codec'], meta['has_encode_settings'], meta['bit_depth'] = self.get_video_encode(mi, meta['type'], bdinfo) - + meta['edition'], meta['repack'] = self.get_edition(meta['path'], bdinfo, meta['filelist'], meta.get('manual_edition')) if "REPACK" in meta.get('edition', ""): meta['repack'] = re.search(r"REPACK[\d]?", meta['edition'])[0] meta['edition'] = re.sub(r"REPACK[\d]?", "", meta['edition']).strip().replace(' ', ' ') - - #WORK ON THIS + + # WORK ON THIS meta.get('stream', False) meta['stream'] = self.stream_optimized(meta['stream']) meta.get('anon', False) @@ -446,40 +446,40 @@ async def get_disc(self, meta): parse = DiscParse() for path, directories, files in os. walk(meta['path']): for each in directories: - if each.upper() == "BDMV": #BDMVs + if each.upper() == "BDMV": # BDMVs is_disc = "BDMV" disc = { - 'path' : f"{path}/{each}", - 'name' : os.path.basename(path), - 'type' : 'BDMV', - 'summary' : "", - 'bdinfo' : "" + 'path': f"{path}/{each}", + 'name': os.path.basename(path), + 'type': 'BDMV', + 'summary': "", + 'bdinfo': "" } discs.append(disc) - elif each == "VIDEO_TS": #DVDs + elif each == "VIDEO_TS": # DVDs is_disc = "DVD" disc = { - 'path' : f"{path}/{each}", - 'name' : os.path.basename(path), - 'type' : 'DVD', - 'vob_mi' : '', - 'ifo_mi' : '', - 'main_set' : [], - 'size' : "" + 'path': f"{path}/{each}", + 'name': os.path.basename(path), + 'type': 'DVD', + 'vob_mi': '', + 'ifo_mi': '', + 'main_set': [], + 'size': "" } discs.append(disc) elif each == "HVDVD_TS": is_disc = "HDDVD" disc = { - 'path' : f"{path}/{each}", - 'name' : os.path.basename(path), - 'type' : 'HDDVD', - 'evo_mi' : '', - 'largest_evo' : "" + 'path': f"{path}/{each}", + 'name': os.path.basename(path), + 'type': 'HDDVD', + 'evo_mi': '', + 'largest_evo': "" } discs.append(disc) if is_disc == "BDMV": - if meta.get('edit', False) == False: + if meta.get('edit', False) is False: discs, bdinfo = await parse.get_bdinfo(discs, meta['uuid'], meta['base_dir'], meta.get('discs', [])) else: discs, bdinfo = await parse.get_bdinfo(meta['discs'], meta['uuid'], meta['base_dir'], meta['discs']) @@ -509,7 +509,7 @@ def get_video(self, videoloc, mode): if not file.lower().endswith('sample.mkv') or "!sample" in file.lower(): filelist.append(os.path.abspath(f"{videoloc}{os.sep}{file}")) try: - video = sorted(filelist)[0] + video = sorted(filelist)[0] except IndexError: console.print("[bold red]No Video files found") if mode == 'cli': @@ -532,7 +532,7 @@ def filter_mediainfo(data): "track": [] } } - + for track in data["media"]["track"]: if track["@type"] == "General": filtered["media"]["track"].append({ @@ -678,7 +678,7 @@ def filter_mediainfo(data): "@type": track["@type"], "extra": track.get("extra"), }) - + return filtered if not os.path.exists(f"{base_dir}/tmp/{folder_id}/MEDIAINFO.txt") and export_text: @@ -701,9 +701,8 @@ def filter_mediainfo(data): with open(f"{base_dir}/tmp/{folder_id}/MediaInfo.json", 'r', encoding='utf-8') as f: mi = json.load(f) - - return mi + return mi """ Get Resolution @@ -750,56 +749,54 @@ def closest(self, lst, K): res = each break return res - + # return lst[min(range(len(lst)), key = lambda i: abs(lst[i]-K))] def mi_resolution(self, res, guess, width, scan, height, actual_height): res_map = { - "3840x2160p" : "2160p", "2160p" : "2160p", - "2560x1440p" : "1440p", "1440p" : "1440p", - "1920x1080p" : "1080p", "1080p" : "1080p", - "1920x1080i" : "1080i", "1080i" : "1080i", - "1280x720p" : "720p", "720p" : "720p", - "1280x540p" : "720p", "1280x576p" : "720p", - "1024x576p" : "576p", "576p" : "576p", - "1024x576i" : "576i", "576i" : "576i", - "854x480p" : "480p", "480p" : "480p", - "854x480i" : "480i", "480i" : "480i", - "720x576p" : "576p", "576p" : "576p", - "720x576i" : "576i", "576i" : "576i", - "720x480p" : "480p", "480p" : "480p", - "720x480i" : "480i", "480i" : "480i", - "15360x8640p" : "8640p", "8640p" : "8640p", - "7680x4320p" : "4320p", "4320p" : "4320p", - "OTHER" : "OTHER"} + "3840x2160p": "2160p", "2160p": "2160p", + "2560x1440p": "1440p", "1440p": "1440p", + "1920x1080p": "1080p", "1080p": "1080p", + "1920x1080i": "1080i", "1080i": "1080i", + "1280x720p": "720p", "720p": "720p", + "1280x540p": "720p", "1280x576p": "720p", + "1024x576p": "576p", "576p": "576p", + "1024x576i": "576i", "576i": "576i", + "854x480p": "480p", "480p": "480p", + "854x480i": "480i", "480i": "480i", + "720x576p": "576p", "576p": "576p", + "720x576i": "576i", "576i": "576i", + "720x480p": "480p", "480p": "480p", + "720x480i": "480i", "480i": "480i", + "15360x8640p": "8640p", "8640p": "8640p", + "7680x4320p": "4320p", "4320p": "4320p", + "OTHER": "OTHER"} resolution = res_map.get(res, None) if actual_height == 540: resolution = "OTHER" - if resolution == None: + if resolution is None: try: resolution = guess['screen_size'] except: width_map = { - '3840p' : '2160p', - '2560p' : '1550p', - '1920p' : '1080p', - '1920i' : '1080i', - '1280p' : '720p', - '1024p' : '576p', - '1024i' : '576i', - '854p' : '480p', - '854i' : '480i', - '720p' : '576p', - '720i' : '576i', - '15360p' : '4320p', - 'OTHERp' : 'OTHER' + '3840p': '2160p', + '2560p': '1550p', + '1920p': '1080p', + '1920i': '1080i', + '1280p': '720p', + '1024p': '576p', + '1024i': '576i', + '854p': '480p', + '854i': '480i', + '720p': '576p', + '720i': '576i', + '15360p': '4320p', + 'OTHERp': 'OTHER' } resolution = width_map.get(f"{width}{scan}", "OTHER") resolution = self.mi_resolution(resolution, guess, width, scan, height, actual_height) - + return resolution - - def is_sd(self, resolution): if resolution in ("480i", "480p", "576i", "576p", "540p"): @@ -825,7 +822,7 @@ def is_scene(self, video, imdb=None): scene = True r = requests.get(f"https://api.srrdb.com/v1/imdb/{base}") r = r.json() - if r['releases'] != [] and imdb == None: + if r['releases'] != [] and imdb is None: imdb = r['releases'][0].get('imdb', imdb) if r['releases'][0].get('imdb') is not None else imdb console.print(f"[green]SRRDB: Matched to {response['results'][0]['release']}") except Exception: @@ -839,11 +836,11 @@ def is_scene(self, video, imdb=None): """ def disc_screenshots(self, filename, bdinfo, folder_id, base_dir, use_vs, image_list, ffdebug, num_screens=None): - if num_screens == None: + if num_screens is None: num_screens = self.screens if num_screens == 0 or len(image_list) >= num_screens: return - #Get longest m2ts + # Get longest m2ts length = 0 for each in bdinfo['files']: int_length = sum(int(float(x)) * 60 ** i for i, x in enumerate(reversed(each['length'].split(':')))) @@ -853,25 +850,24 @@ def disc_screenshots(self, filename, bdinfo, folder_id, base_dir, use_vs, image_ for name in files: if name.lower() == each['file'].lower(): file = f"{root}/{name}" - - + if "VC-1" in bdinfo['video'][0]['codec'] or bdinfo['video'][0]['hdr_dv'] != "": keyframe = 'nokey' else: keyframe = 'none' - os.chdir(f"{base_dir}/tmp/{folder_id}") - i = len(glob.glob(f"{filename}-*.png")) + os.chdir(f"{base_dir}/tmp/{folder_id}") + i = len(glob.glob(f"{filename}-*.png")) if i >= num_screens: i = num_screens console.print('[bold green]Reusing screenshots') else: console.print("[bold yellow]Saving Screens...") - if use_vs == True: + if use_vs is True: from src.vs import vs_screengn vs_screengn(source=file, encode=None, filter_b_frames=False, num=num_screens, dir=f"{base_dir}/tmp/{folder_id}/") else: - if bool(ffdebug) == True: + if bool(ffdebug) is True: loglevel = 'verbose' debug = False else: @@ -899,7 +895,7 @@ def disc_screenshots(self, filename, bdinfo, folder_id, base_dir, use_vs, image_ ) except Exception: console.print(traceback.format_exc()) - + self.optimize_images(image) if os.path.getsize(Path(image)) <= 31000000 and self.img_host == "imgbb": i += 1 @@ -916,22 +912,22 @@ def disc_screenshots(self, filename, bdinfo, folder_id, base_dir, use_vs, image_ console.print("[red]Image too large for your image host, retaking") time.sleep(1) progress.advance(screen_task) - #remove smallest image + # remove smallest image smallest = "" smallestsize = 99 ** 99 - for screens in glob.glob1(f"{base_dir}/tmp/{folder_id}/", f"{filename}-*"): + for screens in glob.glob1(f"{base_dir}/tmp/{folder_id}/", f"{filename}-*"): screensize = os.path.getsize(screens) if screensize < smallestsize: smallestsize = screensize smallest = screens - os.remove(smallest) - + os.remove(smallest) + def dvd_screenshots(self, meta, disc_num, num_screens=None): - if num_screens == None: + if num_screens is None: num_screens = self.screens if num_screens == 0 or (len(meta.get('image_list', [])) >= num_screens and disc_num == 0): return - ifo_mi = MediaInfo.parse(f"{meta['discs'][disc_num]['path']}/VTS_{meta['discs'][disc_num]['main_set'][0][:2]}_0.IFO", mediainfo_options={'inform_version' : '1'}) + ifo_mi = MediaInfo.parse(f"{meta['discs'][disc_num]['path']}/VTS_{meta['discs'][disc_num]['main_set'][0][:2]}_0.IFO", mediainfo_options={'inform_version': '1'}) sar = 1 for track in ifo_mi.tracks: if track.track_type == "Video": @@ -950,7 +946,7 @@ def dvd_screenshots(self, meta, disc_num, num_screens=None): sar = par w_sar = sar h_sar = 1 - + main_set_length = len(meta['discs'][disc_num]['main_set']) if main_set_length >= 3: main_set = meta['discs'][disc_num]['main_set'][1:-1] @@ -960,12 +956,12 @@ def dvd_screenshots(self, meta, disc_num, num_screens=None): main_set = meta['discs'][disc_num]['main_set'] n = 0 os.chdir(f"{meta['base_dir']}/tmp/{meta['uuid']}") - i = 0 + i = 0 if len(glob.glob(f"{meta['base_dir']}/tmp/{meta['uuid']}/{meta['discs'][disc_num]['name']}-*.png")) >= num_screens: i = num_screens console.print('[bold green]Reusing screenshots') else: - if bool(meta.get('ffdebug', False)) == True: + if bool(meta.get('ffdebug', False)) is True: loglevel = 'verbose' debug = False looped = 0 @@ -984,13 +980,14 @@ def dvd_screenshots(self, meta, disc_num, num_screens=None): if n >= num_screens: n -= num_screens image = f"{meta['base_dir']}/tmp/{meta['uuid']}/{meta['discs'][disc_num]['name']}-{i}.png" - if not os.path.exists(image) or retake != False: + if not os.path.exists(image) or retake is not False: retake = False loglevel = 'quiet' debug = True if bool(meta.get('debug', False)): loglevel = 'error' debug = False + def _is_vob_good(n, loops, num_screens): voblength = 300 vob_mi = MediaInfo.parse(f"{meta['discs'][disc_num]['path']}/VTS_{main_set[n]}", output='JSON') @@ -1016,7 +1013,7 @@ def _is_vob_good(n, loops, num_screens): return 300, n try: voblength, n = _is_vob_good(n, 0, num_screens) - img_time = random.randint(round(voblength/5) , round(voblength - voblength/5)) + img_time = random.randint(round(voblength/5), round(voblength - voblength/5)) ss_times = self.valid_ss_time(ss_times, num_screens+1, voblength) ff = ffmpeg.input(f"{meta['discs'][disc_num]['path']}/VTS_{main_set[n]}", ss=ss_times[-1]) if w_sar != 1 or h_sar != 1: @@ -1032,7 +1029,7 @@ def _is_vob_good(n, loops, num_screens): console.print(traceback.format_exc()) self.optimize_images(image) n += 1 - try: + try: if os.path.getsize(Path(image)) <= 31000000 and self.img_host == "imgbb": i += 1 elif os.path.getsize(Path(image)) <= 10000000 and self.img_host in ["imgbox", 'pixhost']: @@ -1058,10 +1055,10 @@ def _is_vob_good(n, loops, num_screens): exit() looped += 1 progress.advance(screen_task) - #remove smallest image + # remove smallest image smallest = "" smallestsize = 99**99 - for screens in glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}/", f"{meta['discs'][disc_num]['name']}-*"): + for screens in glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}/", f"{meta['discs'][disc_num]['name']}-*"): screensize = os.path.getsize(screens) if screensize < smallestsize: smallestsize = screensize @@ -1069,10 +1066,10 @@ def _is_vob_good(n, loops, num_screens): os.remove(smallest) def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=None): - if num_screens == None: + if num_screens is None: num_screens = self.screens - len(meta.get('image_list', [])) if num_screens == 0: - # or len(meta.get('image_list', [])) >= num_screens: + # or len(meta.get('image_list', [])) >= num_screens: return with open(f"{base_dir}/tmp/{folder_id}/MediaInfo.json", encoding='utf-8') as f: mi = json.load(f) @@ -1091,7 +1088,7 @@ def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=Non w_sar = 1 h_sar = sar else: - sar = w_sar = par + sar = w_sar = par h_sar = 1 length = round(float(length)) os.chdir(f"{base_dir}/tmp/{folder_id}") @@ -1102,10 +1099,10 @@ def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=Non else: loglevel = 'quiet' debug = True - if bool(meta.get('ffdebug', False)) == True: + if bool(meta.get('ffdebug', False)) is True: loglevel = 'verbose' debug = False - if meta.get('vapoursynth', False) == True: + if meta.get('vapoursynth', False) is True: from src.vs import vs_screengn vs_screengn(source=path, encode=None, filter_b_frames=False, num=num_screens, dir=f"{base_dir}/tmp/{folder_id}/") else: @@ -1120,7 +1117,7 @@ def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=Non screen_task = progress.add_task("[green]Saving Screens...", total=num_screens + 1) for i in range(num_screens + 1): image = os.path.abspath(f"{base_dir}/tmp/{folder_id}/{filename}-{i}.png") - if not os.path.exists(image) or retake != False: + if not os.path.exists(image) or retake is not False: retake = False try: ss_times = self.valid_ss_time(ss_times, num_screens+1, length) @@ -1136,43 +1133,43 @@ def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=Non ) except Exception: console.print(traceback.format_exc()) - + self.optimize_images(image) if os.path.getsize(Path(image)) <= 75000: console.print("[yellow]Image is incredibly small, retaking") retake = True time.sleep(1) - if os.path.getsize(Path(image)) <= 31000000 and self.img_host == "imgbb" and retake == False: + if os.path.getsize(Path(image)) <= 31000000 and self.img_host == "imgbb" and retake is False: i += 1 - elif os.path.getsize(Path(image)) <= 10000000 and self.img_host in ["imgbox", 'pixhost'] and retake == False: + elif os.path.getsize(Path(image)) <= 10000000 and self.img_host in ["imgbox", 'pixhost'] and retake is False: i += 1 - elif self.img_host in ["ptpimg", "lensdump", "ptscreens"] and retake == False: + elif self.img_host in ["ptpimg", "lensdump", "ptscreens"] and retake is False: i += 1 elif self.img_host == "freeimage.host": console.print("[bold red]Support for freeimage.host has been removed. Please remove from your config") exit() - elif retake == True: + elif retake is True: pass else: console.print("[red]Image too large for your image host, retaking") retake = True - time.sleep(1) + time.sleep(1) else: i += 1 progress.advance(screen_task) - #remove smallest image + # remove smallest image smallest = "" smallestsize = 99 ** 99 - for screens in glob.glob1(f"{base_dir}/tmp/{folder_id}/", f"{filename}-*"): + for screens in glob.glob1(f"{base_dir}/tmp/{folder_id}/", f"{filename}-*"): screensize = os.path.getsize(screens) if screensize < smallestsize: smallestsize = screensize smallest = screens - os.remove(smallest) + os.remove(smallest) def valid_ss_time(self, ss_times, num_screens, length): valid_time = False - while valid_time != True: + while valid_time is not True: valid_time = True if ss_times != []: sst = random.randint(round(length/5), round(length/2)) @@ -1180,19 +1177,19 @@ def valid_ss_time(self, ss_times, num_screens, length): tolerance = length / 10 / num_screens if abs(sst - each) <= tolerance: valid_time = False - if valid_time == True: + if valid_time is True: ss_times.append(sst) else: ss_times.append(random.randint(round(length/5), round(length/2))) return ss_times def optimize_images(self, image): - if self.config['DEFAULT'].get('optimize_images', True) == True: + if self.config['DEFAULT'].get('optimize_images', True) is True: if os.path.exists(image): try: pyver = platform.python_version_tuple() if int(pyver[0]) == 3 and int(pyver[1]) >= 7: - import oxipng + import oxipng if os.path.getsize(image) >= 16000000: oxipng.optimize(image, level=6) else: @@ -1200,6 +1197,7 @@ def optimize_images(self, image): except: pass return + """ Get type and category """ @@ -1216,7 +1214,7 @@ def get_type(self, video, scene, is_disc): # type = "ENCODE" elif "hdtv" in filename: type = "HDTV" - elif is_disc != None: + elif is_disc is not None: type = "DISC" elif "dvdrip" in filename: console.print("[bold red]DVDRip Detected, exiting") @@ -1229,15 +1227,15 @@ def get_cat(self, video): # if category is None: category = guessit(video.replace('1.0', ''))['type'] if category.lower() == "movie": - category = "MOVIE" #1 + category = "MOVIE" # 1 elif category.lower() in ("tv", "episode"): - category = "TV" #2 + category = "TV" # 2 else: category = "MOVIE" return category async def get_tmdb_from_imdb(self, meta, filename): - if meta.get('tmdb_manual') != None: + if meta.get('tmdb_manual') is not None: meta['tmdb'] = meta['tmdb_manual'] return meta imdb_id = meta['imdb'] @@ -1254,10 +1252,10 @@ async def get_tmdb_from_imdb(self, meta, filename): else: imdb_info = await self.get_imdb_info(imdb_id.replace('tt', ''), meta) title = imdb_info.get("title") - if title == None: + if title is None: title = filename year = imdb_info.get('year') - if year == None: + if year is None: year = meta['search_year'] console.print(f"[yellow]TMDb was unable to find anything with that IMDb, searching TMDb for {title}") meta = await self.get_tmdb_id(title, year, meta, meta['category'], imdb_info.get('original title', imdb_info.get('localized title', meta['uuid']))) @@ -1277,11 +1275,11 @@ async def get_tmdb_id(self, filename, search_year, meta, category, untouched_fil search.movie(query=filename, year=search_year) elif category == "TV": search.tv(query=filename, first_air_date_year=search_year) - if meta.get('tmdb_manual') != None: + if meta.get('tmdb_manual') is not None: meta['tmdb'] = meta['tmdb_manual'] else: meta['tmdb'] = search.results[0]['id'] - meta['category'] = category + meta['category'] = category except IndexError: try: if category == "MOVIE": @@ -1300,7 +1298,7 @@ async def get_tmdb_id(self, filename, search_year, meta, category, untouched_fil meta = await self.get_tmdb_id(filename, search_year, meta, category, untouched_filename, attempted) elif attempted == 2: attempted += 1 - meta = await self.get_tmdb_id(anitopy.parse(guessit(untouched_filename, {"excludes" : ["country", "language"]})['title'])['anime_title'], search_year, meta, meta['category'], untouched_filename, attempted) + meta = await self.get_tmdb_id(anitopy.parse(guessit(untouched_filename, {"excludes": ["country", "language"]})['title'])['anime_title'], search_year, meta, meta['category'], untouched_filename, attempted) if meta['tmdb'] in (None, ""): console.print(f"[red]Unable to find TMDb match for {filename}") if meta.get('mode', 'discord') == 'cli': @@ -1311,14 +1309,14 @@ async def get_tmdb_id(self, filename, search_year, meta, category, untouched_fil return meta return meta - + async def tmdb_other_meta(self, meta): - + if meta['tmdb'] == "0": try: - title = guessit(meta['path'], {"excludes" : ["country", "language"]})['title'].lower() + title = guessit(meta['path'], {"excludes": ["country", "language"]})['title'].lower() title = title.split('aka')[0] - meta = await self.get_tmdb_id(guessit(title, {"excludes" : ["country", "language"]})['title'], meta['search_year'], meta) + meta = await self.get_tmdb_id(guessit(title, {"excludes": ["country", "language"]})['title'], meta['search_year'], meta) if meta['tmdb'] == "0": meta = await self.get_tmdb_id(title, "", meta, meta['category']) except: @@ -1333,14 +1331,14 @@ async def tmdb_other_meta(self, meta): response = movie.info() meta['title'] = response['title'] if response['release_date']: - meta['year'] = datetime.strptime(response['release_date'],'%Y-%m-%d').year + meta['year'] = datetime.strptime(response['release_date'], '%Y-%m-%d').year else: console.print('[yellow]TMDB does not have a release date, using year from filename instead (if it exists)') meta['year'] = meta['search_year'] external = movie.external_ids() - if meta.get('imdb', None) == None: + if meta.get('imdb', None) is None: imdb_id = external.get('imdb_id', "0") - if imdb_id == "" or imdb_id == None: + if imdb_id == "" or imdb_id is None: meta['imdb_id'] = '0' else: meta['imdb_id'] = str(int(imdb_id.replace('tt', ''))).zfill(7) @@ -1358,9 +1356,9 @@ async def tmdb_other_meta(self, meta): break except Exception: console.print('[yellow]Unable to grab videos from TMDb.') - + meta['aka'], original_language = await self.get_imdb_aka(meta['imdb_id']) - if original_language != None: + if original_language is not None: meta['original_language'] = original_language else: meta['original_language'] = response['original_language'] @@ -1369,7 +1367,7 @@ async def tmdb_other_meta(self, meta): meta['keywords'] = self.get_keywords(movie) meta['genres'] = self.get_genres(response) meta['tmdb_directors'] = self.get_directors(movie) - if meta.get('anime', False) == False: + if meta.get('anime', False) is False: meta['mal_id'], meta['aka'], meta['anime'] = self.get_anime(response, meta) meta['poster'] = response.get('poster_path', "") meta['overview'] = response['overview'] @@ -1380,14 +1378,14 @@ async def tmdb_other_meta(self, meta): response = tv.info() meta['title'] = response['name'] if response['first_air_date']: - meta['year'] = datetime.strptime(response['first_air_date'],'%Y-%m-%d').year + meta['year'] = datetime.strptime(response['first_air_date'], '%Y-%m-%d').year else: console.print('[yellow]TMDB does not have a release date, using year from filename instead (if it exists)') meta['year'] = meta['search_year'] external = tv.external_ids() - if meta.get('imdb', None) == None: + if meta.get('imdb', None) is None: imdb_id = external.get('imdb_id', "0") - if imdb_id == "" or imdb_id == None: + if imdb_id == "" or imdb_id is None: meta['imdb_id'] = '0' else: meta['imdb_id'] = str(int(imdb_id.replace('tt', ''))).zfill(7) @@ -1408,7 +1406,7 @@ async def tmdb_other_meta(self, meta): # meta['aka'] = f" AKA {response['original_name']}" meta['aka'], original_language = await self.get_imdb_aka(meta['imdb_id']) - if original_language != None: + if original_language is not None: meta['original_language'] = original_language else: meta['original_language'] = response['original_language'] @@ -1433,19 +1431,16 @@ async def tmdb_other_meta(self, meta): meta['aka'] = "" if f"({meta['year']})" in meta['aka']: meta['aka'] = meta['aka'].replace(f"({meta['year']})", "").strip() - - - return meta - + return meta def get_keywords(self, tmdb_info): if tmdb_info is not None: tmdb_keywords = tmdb_info.keywords() if tmdb_keywords.get('keywords') is not None: - keywords=[f"{keyword['name'].replace(',',' ')}" for keyword in tmdb_keywords.get('keywords')] + keywords=[f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('keywords')] elif tmdb_keywords.get('results') is not None: - keywords=[f"{keyword['name'].replace(',',' ')}" for keyword in tmdb_keywords.get('results')] + keywords=[f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('results')] return(', '.join(keywords)) else: return '' @@ -1454,7 +1449,7 @@ def get_genres(self, tmdb_info): if tmdb_info is not None: tmdb_genres = tmdb_info.get('genres', []) if tmdb_genres is not []: - genres=[f"{genre['name'].replace(',',' ')}" for genre in tmdb_genres] + genres=[f"{genre['name'].replace(',', ' ')}" for genre in tmdb_genres] return(', '.join(genres)) else: return '' @@ -1482,10 +1477,10 @@ def get_anime(self, response, meta): for each in response['genres']: if each['id'] == 16: animation = True - if response['original_language'] == 'ja' and animation == True: + if response['original_language'] == 'ja' and animation is True: romaji, mal_id, eng_title, season_year, episodes = self.get_romaji(tmdb_name, meta.get('mal', None)) alt_name = f" AKA {romaji}" - + anime = True # mal = AnimeSearch(romaji) # mal_id = mal.results[0].mal_id @@ -1498,7 +1493,7 @@ def get_anime(self, response, meta): return mal_id, alt_name, anime def get_romaji(self, tmdb_name, mal): - if mal == None: + if mal is None: mal = 0 tmdb_name = tmdb_name.replace('-', "").replace("The Movie", "") tmdb_name = ' '.join(tmdb_name.split()) @@ -1562,12 +1557,12 @@ def get_romaji(self, tmdb_name, mal): console.print('[red]Failed to get anime specific info from anilist. Continuing without it...') media = [] if media not in (None, []): - result = {'title' : {}} + result = {'title': {}} difference = 0 for anime in media: search_name = re.sub(r"[^0-9a-zA-Z\[\\]]+", "", tmdb_name.lower().replace(' ', '')) for title in anime['title'].values(): - if title != None: + if title is not None: title = re.sub(u'[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]+ (?=[A-Za-z ]+–)', "", title.lower().replace(' ', ''), re.U) diff = SequenceMatcher(None, title, search_name).ratio() if diff >= difference: @@ -1618,7 +1613,7 @@ def get_audio_v2(self, mi, meta, bdinfo): track = tracks[track_num] if len(tracks) > track_num else {} format = track.get('Format', '') commercial = track.get('Format_Commercial', '') - + if track.get('Language', '') == "zxx": meta['silent'] = True @@ -1627,7 +1622,7 @@ def get_audio_v2(self, mi, meta, bdinfo): format_settings = track.get('Format_Settings', '') if format_settings in ['Explicit']: format_settings = "" - #Channels + # Channels channels = mi['media']['track'][track_num].get('Channels_Original', mi['media']['track'][track_num]['Channels']) if not str(channels).isnumeric(): channels = mi['media']['track'][track_num]['Channels'] @@ -1649,7 +1644,7 @@ def get_audio_v2(self, mi, meta, bdinfo): chan = f"{int(channels) - 1}.1" else: chan = f"{channels}.0" - + if meta.get('original_language', '') != 'en': eng, orig = False, False try: @@ -1693,10 +1688,10 @@ def get_audio_v2(self, mi, meta, bdinfo): if "commentary" in t.get('Title', '').lower(): has_commentary = True - - #Convert commercial name to naming conventions + + # Convert commercial name to naming conventions audio = { - #Format + # Format "DTS": "DTS", "AAC": "AAC", "AAC LC": "AAC", @@ -1708,16 +1703,16 @@ def get_audio_v2(self, mi, meta, bdinfo): "Vorbis": "VORBIS", "PCM": "LPCM", - #BDINFO AUDIOS - "LPCM Audio" : "LPCM", - "Dolby Digital Audio" : "DD", - "Dolby Digital Plus Audio" : "DD+", + # BDINFO AUDIOS + "LPCM Audio": "LPCM", + "Dolby Digital Audio": "DD", + "Dolby Digital Plus Audio": "DD+", # "Dolby TrueHD" : "TrueHD", - "Dolby TrueHD Audio" : "TrueHD", - "DTS Audio" : "DTS", - "DTS-HD Master Audio" : "DTS-HD MA", - "DTS-HD High-Res Audio" : "DTS-HD HRA", - "DTS:X Master Audio" : "DTS:X" + "Dolby TrueHD Audio": "TrueHD", + "DTS Audio": "DTS", + "DTS-HD Master Audio": "DTS-HD MA", + "DTS-HD High-Res Audio": "DTS-HD HRA", + "DTS:X Master Audio": "DTS:X" } audio_extra = { "XLL": "-HD MA", @@ -1730,20 +1725,19 @@ def get_audio_v2(self, mi, meta, bdinfo): "Atmos Audio": " Atmos", } format_settings_extra = { - "Dolby Surround EX" : "EX" + "Dolby Surround EX": "EX" } commercial_names = { - "Dolby Digital" : "DD", - "Dolby Digital Plus" : "DD+", - "Dolby TrueHD" : "TrueHD", - "DTS-ES" : "DTS-ES", - "DTS-HD High" : "DTS-HD HRA", - "Free Lossless Audio Codec" : "FLAC", - "DTS-HD Master Audio" : "DTS-HD MA" + "Dolby Digital": "DD", + "Dolby Digital Plus": "DD+", + "Dolby TrueHD": "TrueHD", + "DTS-ES": "DTS-ES", + "DTS-HD High": "DTS-HD HRA", + "Free Lossless Audio Codec": "FLAC", + "DTS-HD Master Audio": "DTS-HD MA" } - search_format = True # Ensure commercial and additional are not None before iterating if commercial: @@ -1782,9 +1776,8 @@ def get_audio_v2(self, mi, meta, bdinfo): audio = ' '.join(audio.split()) return audio, chan, has_commentary - def is_3d(self, mi, bdinfo): - if bdinfo != None: + if bdinfo is not None: if bdinfo['video'][0]['3d'] != "": return "3D" else: @@ -1804,7 +1797,6 @@ def get_tag(self, video, meta): tag = "" return tag - def get_source(self, type, video, path, is_disc, meta): try: try: @@ -1842,8 +1834,8 @@ def get_source(self, type, video, path, is_disc, meta): except: system = "" finally: - if system == None: - system = "" + if system is None: + system = "" if type == "REMUX": system = f"{system} DVD".strip() source = system @@ -1879,7 +1871,7 @@ def get_uhd(self, type, guess, resolution, path): uhd = "UHD" elif type in ("DISC", "REMUX", "ENCODE", "WEBRIP"): uhd = "" - + if type in ("DISC", "REMUX", "ENCODE") and resolution == "2160p": uhd = "UHD" @@ -1888,7 +1880,7 @@ def get_uhd(self, type, guess, resolution, path): def get_hdr(self, mi, bdinfo): hdr = "" dv = "" - if bdinfo != None: #Disks + if bdinfo is not None: # Disks hdr_mi = bdinfo['video'][0]['hdr_dv'] if "HDR10+" in hdr_mi: hdr = "HDR10+" @@ -1899,7 +1891,7 @@ def get_hdr(self, mi, bdinfo): dv = "DV" except: pass - else: + else: video_track = mi['media']['track'][1] try: hdr_mi = video_track['colour_primaries'] @@ -1931,51 +1923,51 @@ def get_hdr(self, mi, bdinfo): def get_region(self, bdinfo, region=None): label = bdinfo.get('label', bdinfo.get('title', bdinfo.get('path', ''))).replace('.', ' ') - if region != None: + if region is not None: region = region.upper() - else: + else: regions = { - 'AFG': 'AFG', 'AIA': 'AIA', 'ALA': 'ALA', 'ALG': 'ALG', 'AND': 'AND', 'ANG': 'ANG', 'ARG': 'ARG', - 'ARM': 'ARM', 'ARU': 'ARU', 'ASA': 'ASA', 'ATA': 'ATA', 'ATF': 'ATF', 'ATG': 'ATG', 'AUS': 'AUS', - 'AUT': 'AUT', 'AZE': 'AZE', 'BAH': 'BAH', 'BAN': 'BAN', 'BDI': 'BDI', 'BEL': 'BEL', 'BEN': 'BEN', - 'BER': 'BER', 'BES': 'BES', 'BFA': 'BFA', 'BHR': 'BHR', 'BHU': 'BHU', 'BIH': 'BIH', 'BLM': 'BLM', - 'BLR': 'BLR', 'BLZ': 'BLZ', 'BOL': 'BOL', 'BOT': 'BOT', 'BRA': 'BRA', 'BRB': 'BRB', 'BRU': 'BRU', - 'BVT': 'BVT', 'CAM': 'CAM', 'CAN': 'CAN', 'CAY': 'CAY', 'CCK': 'CCK', 'CEE': 'CEE', 'CGO': 'CGO', - 'CHA': 'CHA', 'CHI': 'CHI', 'CHN': 'CHN', 'CIV': 'CIV', 'CMR': 'CMR', 'COD': 'COD', 'COK': 'COK', - 'COL': 'COL', 'COM': 'COM', 'CPV': 'CPV', 'CRC': 'CRC', 'CRO': 'CRO', 'CTA': 'CTA', 'CUB': 'CUB', - 'CUW': 'CUW', 'CXR': 'CXR', 'CYP': 'CYP', 'DJI': 'DJI', 'DMA': 'DMA', 'DOM': 'DOM', 'ECU': 'ECU', - 'EGY': 'EGY', 'ENG': 'ENG', 'EQG': 'EQG', 'ERI': 'ERI', 'ESH': 'ESH', 'ESP': 'ESP', 'ETH': 'ETH', - 'FIJ': 'FIJ', 'FLK': 'FLK', 'FRA': 'FRA', 'FRO': 'FRO', 'FSM': 'FSM', 'GAB': 'GAB', 'GAM': 'GAM', - 'GBR': 'GBR', 'GEO': 'GEO', 'GER': 'GER', 'GGY': 'GGY', 'GHA': 'GHA', 'GIB': 'GIB', 'GLP': 'GLP', - 'GNB': 'GNB', 'GRE': 'GRE', 'GRL': 'GRL', 'GRN': 'GRN', 'GUA': 'GUA', 'GUF': 'GUF', 'GUI': 'GUI', - 'GUM': 'GUM', 'GUY': 'GUY', 'HAI': 'HAI', 'HKG': 'HKG', 'HMD': 'HMD', 'HON': 'HON', 'HUN': 'HUN', - 'IDN': 'IDN', 'IMN': 'IMN', 'IND': 'IND', 'IOT': 'IOT', 'IRL': 'IRL', 'IRN': 'IRN', 'IRQ': 'IRQ', - 'ISL': 'ISL', 'ISR': 'ISR', 'ITA': 'ITA', 'JAM': 'JAM', 'JEY': 'JEY', 'JOR': 'JOR', 'JPN': 'JPN', - 'KAZ': 'KAZ', 'KEN': 'KEN', 'KGZ': 'KGZ', 'KIR': 'KIR', 'KNA': 'KNA', 'KOR': 'KOR', 'KSA': 'KSA', - 'KUW': 'KUW', 'KVX': 'KVX', 'LAO': 'LAO', 'LBN': 'LBN', 'LBR': 'LBR', 'LBY': 'LBY', 'LCA': 'LCA', - 'LES': 'LES', 'LIE': 'LIE', 'LKA': 'LKA', 'LUX': 'LUX', 'MAC': 'MAC', 'MAD': 'MAD', 'MAF': 'MAF', - 'MAR': 'MAR', 'MAS': 'MAS', 'MDA': 'MDA', 'MDV': 'MDV', 'MEX': 'MEX', 'MHL': 'MHL', 'MKD': 'MKD', - 'MLI': 'MLI', 'MLT': 'MLT', 'MNG': 'MNG', 'MNP': 'MNP', 'MON': 'MON', 'MOZ': 'MOZ', 'MRI': 'MRI', - 'MSR': 'MSR', 'MTN': 'MTN', 'MTQ': 'MTQ', 'MWI': 'MWI', 'MYA': 'MYA', 'MYT': 'MYT', 'NAM': 'NAM', - 'NCA': 'NCA', 'NCL': 'NCL', 'NEP': 'NEP', 'NFK': 'NFK', 'NIG': 'NIG', 'NIR': 'NIR', 'NIU': 'NIU', - 'NLD': 'NLD', 'NOR': 'NOR', 'NRU': 'NRU', 'NZL': 'NZL', 'OMA': 'OMA', 'PAK': 'PAK', 'PAN': 'PAN', - 'PAR': 'PAR', 'PCN': 'PCN', 'PER': 'PER', 'PHI': 'PHI', 'PLE': 'PLE', 'PLW': 'PLW', 'PNG': 'PNG', - 'POL': 'POL', 'POR': 'POR', 'PRK': 'PRK', 'PUR': 'PUR', 'QAT': 'QAT', 'REU': 'REU', 'ROU': 'ROU', - 'RSA': 'RSA', 'RUS': 'RUS', 'RWA': 'RWA', 'SAM': 'SAM', 'SCO': 'SCO', 'SDN': 'SDN', 'SEN': 'SEN', - 'SEY': 'SEY', 'SGS': 'SGS', 'SHN': 'SHN', 'SIN': 'SIN', 'SJM': 'SJM', 'SLE': 'SLE', 'SLV': 'SLV', - 'SMR': 'SMR', 'SOL': 'SOL', 'SOM': 'SOM', 'SPM': 'SPM', 'SRB': 'SRB', 'SSD': 'SSD', 'STP': 'STP', - 'SUI': 'SUI', 'SUR': 'SUR', 'SWZ': 'SWZ', 'SXM': 'SXM', 'SYR': 'SYR', 'TAH': 'TAH', 'TAN': 'TAN', - 'TCA': 'TCA', 'TGA': 'TGA', 'THA': 'THA', 'TJK': 'TJK', 'TKL': 'TKL', 'TKM': 'TKM', 'TLS': 'TLS', - 'TOG': 'TOG', 'TRI': 'TRI', 'TUN': 'TUN', 'TUR': 'TUR', 'TUV': 'TUV', 'TWN': 'TWN', 'UAE': 'UAE', - 'UGA': 'UGA', 'UKR': 'UKR', 'UMI': 'UMI', 'URU': 'URU', 'USA': 'USA', 'UZB': 'UZB', 'VAN': 'VAN', - 'VAT': 'VAT', 'VEN': 'VEN', 'VGB': 'VGB', 'VIE': 'VIE', 'VIN': 'VIN', 'VIR': 'VIR', 'WAL': 'WAL', + 'AFG': 'AFG', 'AIA': 'AIA', 'ALA': 'ALA', 'ALG': 'ALG', 'AND': 'AND', 'ANG': 'ANG', 'ARG': 'ARG', + 'ARM': 'ARM', 'ARU': 'ARU', 'ASA': 'ASA', 'ATA': 'ATA', 'ATF': 'ATF', 'ATG': 'ATG', 'AUS': 'AUS', + 'AUT': 'AUT', 'AZE': 'AZE', 'BAH': 'BAH', 'BAN': 'BAN', 'BDI': 'BDI', 'BEL': 'BEL', 'BEN': 'BEN', + 'BER': 'BER', 'BES': 'BES', 'BFA': 'BFA', 'BHR': 'BHR', 'BHU': 'BHU', 'BIH': 'BIH', 'BLM': 'BLM', + 'BLR': 'BLR', 'BLZ': 'BLZ', 'BOL': 'BOL', 'BOT': 'BOT', 'BRA': 'BRA', 'BRB': 'BRB', 'BRU': 'BRU', + 'BVT': 'BVT', 'CAM': 'CAM', 'CAN': 'CAN', 'CAY': 'CAY', 'CCK': 'CCK', 'CEE': 'CEE', 'CGO': 'CGO', + 'CHA': 'CHA', 'CHI': 'CHI', 'CHN': 'CHN', 'CIV': 'CIV', 'CMR': 'CMR', 'COD': 'COD', 'COK': 'COK', + 'COL': 'COL', 'COM': 'COM', 'CPV': 'CPV', 'CRC': 'CRC', 'CRO': 'CRO', 'CTA': 'CTA', 'CUB': 'CUB', + 'CUW': 'CUW', 'CXR': 'CXR', 'CYP': 'CYP', 'DJI': 'DJI', 'DMA': 'DMA', 'DOM': 'DOM', 'ECU': 'ECU', + 'EGY': 'EGY', 'ENG': 'ENG', 'EQG': 'EQG', 'ERI': 'ERI', 'ESH': 'ESH', 'ESP': 'ESP', 'ETH': 'ETH', + 'FIJ': 'FIJ', 'FLK': 'FLK', 'FRA': 'FRA', 'FRO': 'FRO', 'FSM': 'FSM', 'GAB': 'GAB', 'GAM': 'GAM', + 'GBR': 'GBR', 'GEO': 'GEO', 'GER': 'GER', 'GGY': 'GGY', 'GHA': 'GHA', 'GIB': 'GIB', 'GLP': 'GLP', + 'GNB': 'GNB', 'GRE': 'GRE', 'GRL': 'GRL', 'GRN': 'GRN', 'GUA': 'GUA', 'GUF': 'GUF', 'GUI': 'GUI', + 'GUM': 'GUM', 'GUY': 'GUY', 'HAI': 'HAI', 'HKG': 'HKG', 'HMD': 'HMD', 'HON': 'HON', 'HUN': 'HUN', + 'IDN': 'IDN', 'IMN': 'IMN', 'IND': 'IND', 'IOT': 'IOT', 'IRL': 'IRL', 'IRN': 'IRN', 'IRQ': 'IRQ', + 'ISL': 'ISL', 'ISR': 'ISR', 'ITA': 'ITA', 'JAM': 'JAM', 'JEY': 'JEY', 'JOR': 'JOR', 'JPN': 'JPN', + 'KAZ': 'KAZ', 'KEN': 'KEN', 'KGZ': 'KGZ', 'KIR': 'KIR', 'KNA': 'KNA', 'KOR': 'KOR', 'KSA': 'KSA', + 'KUW': 'KUW', 'KVX': 'KVX', 'LAO': 'LAO', 'LBN': 'LBN', 'LBR': 'LBR', 'LBY': 'LBY', 'LCA': 'LCA', + 'LES': 'LES', 'LIE': 'LIE', 'LKA': 'LKA', 'LUX': 'LUX', 'MAC': 'MAC', 'MAD': 'MAD', 'MAF': 'MAF', + 'MAR': 'MAR', 'MAS': 'MAS', 'MDA': 'MDA', 'MDV': 'MDV', 'MEX': 'MEX', 'MHL': 'MHL', 'MKD': 'MKD', + 'MLI': 'MLI', 'MLT': 'MLT', 'MNG': 'MNG', 'MNP': 'MNP', 'MON': 'MON', 'MOZ': 'MOZ', 'MRI': 'MRI', + 'MSR': 'MSR', 'MTN': 'MTN', 'MTQ': 'MTQ', 'MWI': 'MWI', 'MYA': 'MYA', 'MYT': 'MYT', 'NAM': 'NAM', + 'NCA': 'NCA', 'NCL': 'NCL', 'NEP': 'NEP', 'NFK': 'NFK', 'NIG': 'NIG', 'NIR': 'NIR', 'NIU': 'NIU', + 'NLD': 'NLD', 'NOR': 'NOR', 'NRU': 'NRU', 'NZL': 'NZL', 'OMA': 'OMA', 'PAK': 'PAK', 'PAN': 'PAN', + 'PAR': 'PAR', 'PCN': 'PCN', 'PER': 'PER', 'PHI': 'PHI', 'PLE': 'PLE', 'PLW': 'PLW', 'PNG': 'PNG', + 'POL': 'POL', 'POR': 'POR', 'PRK': 'PRK', 'PUR': 'PUR', 'QAT': 'QAT', 'REU': 'REU', 'ROU': 'ROU', + 'RSA': 'RSA', 'RUS': 'RUS', 'RWA': 'RWA', 'SAM': 'SAM', 'SCO': 'SCO', 'SDN': 'SDN', 'SEN': 'SEN', + 'SEY': 'SEY', 'SGS': 'SGS', 'SHN': 'SHN', 'SIN': 'SIN', 'SJM': 'SJM', 'SLE': 'SLE', 'SLV': 'SLV', + 'SMR': 'SMR', 'SOL': 'SOL', 'SOM': 'SOM', 'SPM': 'SPM', 'SRB': 'SRB', 'SSD': 'SSD', 'STP': 'STP', + 'SUI': 'SUI', 'SUR': 'SUR', 'SWZ': 'SWZ', 'SXM': 'SXM', 'SYR': 'SYR', 'TAH': 'TAH', 'TAN': 'TAN', + 'TCA': 'TCA', 'TGA': 'TGA', 'THA': 'THA', 'TJK': 'TJK', 'TKL': 'TKL', 'TKM': 'TKM', 'TLS': 'TLS', + 'TOG': 'TOG', 'TRI': 'TRI', 'TUN': 'TUN', 'TUR': 'TUR', 'TUV': 'TUV', 'TWN': 'TWN', 'UAE': 'UAE', + 'UGA': 'UGA', 'UKR': 'UKR', 'UMI': 'UMI', 'URU': 'URU', 'USA': 'USA', 'UZB': 'UZB', 'VAN': 'VAN', + 'VAT': 'VAT', 'VEN': 'VEN', 'VGB': 'VGB', 'VIE': 'VIE', 'VIN': 'VIN', 'VIR': 'VIR', 'WAL': 'WAL', 'WLF': 'WLF', 'YEM': 'YEM', 'ZAM': 'ZAM', 'ZIM': 'ZIM', "EUR" : "EUR" } for key, value in regions.items(): if f" {key} " in label: region = value - if region == None: + if region is None: region = "" return region @@ -1983,16 +1975,16 @@ def get_distributor(self, distributor_in): distributor_list = [ '01 DISTRIBUTION', '100 DESTINATIONS TRAVEL FILM', '101 FILMS', '1FILMS', '2 ENTERTAIN VIDEO', '20TH CENTURY FOX', '2L', '3D CONTENT HUB', '3D MEDIA', '3L FILM', '4DIGITAL', '4DVD', '4K ULTRA HD MOVIES', '4K UHD', '8-FILMS', '84 ENTERTAINMENT', '88 FILMS', '@ANIME', 'ANIME', 'A CONTRACORRIENTE', 'A CONTRACORRIENTE FILMS', 'A&E HOME VIDEO', 'A&E', 'A&M RECORDS', 'A+E NETWORKS', 'A+R', 'A-FILM', 'AAA', 'AB VIDÉO', 'AB VIDEO', 'ABC - (AUSTRALIAN BROADCASTING CORPORATION)', 'ABC', 'ABKCO', 'ABSOLUT MEDIEN', 'ABSOLUTE', 'ACCENT FILM ENTERTAINMENT', 'ACCENTUS', 'ACORN MEDIA', 'AD VITAM', 'ADA', 'ADITYA VIDEOS', 'ADSO FILMS', 'AFM RECORDS', 'AGFA', 'AIX RECORDS', 'ALAMODE FILM', 'ALBA RECORDS', 'ALBANY RECORDS', 'ALBATROS', 'ALCHEMY', 'ALIVE', 'ALL ANIME', 'ALL INTERACTIVE ENTERTAINMENT', 'ALLEGRO', 'ALLIANCE', 'ALPHA MUSIC', 'ALTERDYSTRYBUCJA', 'ALTERED INNOCENCE', 'ALTITUDE FILM DISTRIBUTION', 'ALUCARD RECORDS', 'AMAZING D.C.', 'AMAZING DC', 'AMMO CONTENT', 'AMUSE SOFT ENTERTAINMENT', 'ANCONNECT', 'ANEC', 'ANIMATSU', 'ANIME HOUSE', 'ANIME LTD', 'ANIME WORKS', 'ANIMEIGO', 'ANIPLEX', 'ANOLIS ENTERTAINMENT', 'ANOTHER WORLD ENTERTAINMENT', 'AP INTERNATIONAL', 'APPLE', 'ARA MEDIA', 'ARBELOS', 'ARC ENTERTAINMENT', 'ARP SÉLECTION', 'ARP SELECTION', 'ARROW', 'ART SERVICE', 'ART VISION', 'ARTE ÉDITIONS', 'ARTE EDITIONS', 'ARTE VIDÉO', - 'ARTE VIDEO', 'ARTHAUS MUSIK', 'ARTIFICIAL EYE', 'ARTSPLOITATION FILMS', 'ARTUS FILMS', 'ASCOT ELITE HOME ENTERTAINMENT', 'ASIA VIDEO', 'ASMIK ACE', 'ASTRO RECORDS & FILMWORKS', 'ASYLUM', 'ATLANTIC FILM', 'ATLANTIC RECORDS', 'ATLAS FILM', 'AUDIO VISUAL ENTERTAINMENT', 'AURO-3D CREATIVE LABEL', 'AURUM', 'AV VISIONEN', 'AV-JET', 'AVALON', 'AVENTI', 'AVEX TRAX', 'AXIOM', 'AXIS RECORDS', 'AYNGARAN', 'BAC FILMS', 'BACH FILMS', 'BANDAI VISUAL', 'BARCLAY', 'BBC', 'BRITISH BROADCASTING CORPORATION', 'BBI FILMS', 'BBI', 'BCI HOME ENTERTAINMENT', 'BEGGARS BANQUET', 'BEL AIR CLASSIQUES', 'BELGA FILMS', 'BELVEDERE', 'BENELUX FILM DISTRIBUTORS', 'BENNETT-WATT MEDIA', 'BERLIN CLASSICS', 'BERLINER PHILHARMONIKER RECORDINGS', 'BEST ENTERTAINMENT', 'BEYOND HOME ENTERTAINMENT', 'BFI VIDEO', 'BFI', 'BRITISH FILM INSTITUTE', 'BFS ENTERTAINMENT', 'BFS', 'BHAVANI', 'BIBER RECORDS', 'BIG HOME VIDEO', 'BILDSTÖRUNG', - 'BILDSTORUNG', 'BILL ZEBUB', 'BIRNENBLATT', 'BIT WEL', 'BLACK BOX', 'BLACK HILL PICTURES', 'BLACK HILL', 'BLACK HOLE RECORDINGS', 'BLACK HOLE', 'BLAQOUT', 'BLAUFIELD MUSIC', 'BLAUFIELD', 'BLOCKBUSTER ENTERTAINMENT', 'BLOCKBUSTER', 'BLU PHASE MEDIA', 'BLU-RAY ONLY', 'BLU-RAY', 'BLURAY ONLY', 'BLURAY', 'BLUE GENTIAN RECORDS', 'BLUE KINO', 'BLUE UNDERGROUND', 'BMG/ARISTA', 'BMG', 'BMGARISTA', 'BMG ARISTA', 'ARISTA', 'ARISTA/BMG', 'ARISTABMG', 'ARISTA BMG', 'BONTON FILM', 'BONTON', 'BOOMERANG PICTURES', 'BOOMERANG', 'BQHL ÉDITIONS', 'BQHL EDITIONS', 'BQHL', 'BREAKING GLASS', 'BRIDGESTONE', 'BRINK', 'BROAD GREEN PICTURES', 'BROAD GREEN', 'BUSCH MEDIA GROUP', 'BUSCH', 'C MAJOR', 'C.B.S.', 'CAICHANG', 'CALIFÓRNIA FILMES', 'CALIFORNIA FILMES', 'CALIFORNIA', 'CAMEO', 'CAMERA OBSCURA', 'CAMERATA', 'CAMP MOTION PICTURES', 'CAMP MOTION', 'CAPELIGHT PICTURES', 'CAPELIGHT', 'CAPITOL', 'CAPITOL RECORDS', 'CAPRICCI', 'CARGO RECORDS', 'CARLOTTA FILMS', 'CARLOTTA', 'CARLOTA', 'CARMEN FILM', 'CASCADE', 'CATCHPLAY', 'CAULDRON FILMS', 'CAULDRON', 'CBS TELEVISION STUDIOS', 'CBS', 'CCTV', 'CCV ENTERTAINMENT', 'CCV', 'CD BABY', 'CD LAND', 'CECCHI GORI', 'CENTURY MEDIA', 'CHUAN XUN SHI DAI MULTIMEDIA', 'CINE-ASIA', 'CINÉART', 'CINEART', 'CINEDIGM', 'CINEFIL IMAGICA', 'CINEMA EPOCH', 'CINEMA GUILD', 'CINEMA LIBRE STUDIOS', 'CINEMA MONDO', 'CINEMATIC VISION', 'CINEPLOIT RECORDS', 'CINESTRANGE EXTREME', 'CITEL VIDEO', 'CITEL', 'CJ ENTERTAINMENT', 'CJ', 'CLASSIC MEDIA', 'CLASSICFLIX', 'CLASSICLINE', 'CLAUDIO RECORDS', 'CLEAR VISION', 'CLEOPATRA', 'CLOSE UP', 'CMS MEDIA LIMITED', 'CMV LASERVISION', 'CN ENTERTAINMENT', 'CODE RED', 'COHEN MEDIA GROUP', 'COHEN', 'COIN DE MIRE CINÉMA', 'COIN DE MIRE CINEMA', 'COLOSSEO FILM', 'COLUMBIA', 'COLUMBIA PICTURES', 'COLUMBIA/TRI-STAR', 'TRI-STAR', 'COMMERCIAL MARKETING', 'CONCORD MUSIC GROUP', 'CONCORDE VIDEO', 'CONDOR', 'CONSTANTIN FILM', 'CONSTANTIN', 'CONSTANTINO FILMES', 'CONSTANTINO', 'CONSTRUCTIVE MEDIA SERVICE', 'CONSTRUCTIVE', 'CONTENT ZONE', 'CONTENTS GATE', 'COQUEIRO VERDE', 'CORNERSTONE MEDIA', 'CORNERSTONE', 'CP DIGITAL', 'CREST MOVIES', 'CRITERION', 'CRITERION COLLECTION', 'CC', 'CRYSTAL CLASSICS', 'CULT EPICS', 'CULT FILMS', 'CULT VIDEO', 'CURZON FILM WORLD', 'D FILMS', "D'AILLY COMPANY", 'DAILLY COMPANY', 'D AILLY COMPANY', "D'AILLY", 'DAILLY', 'D AILLY', 'DA CAPO', 'DA MUSIC', "DALL'ANGELO PICTURES", 'DALLANGELO PICTURES', "DALL'ANGELO", 'DALL ANGELO PICTURES', 'DALL ANGELO', 'DAREDO', 'DARK FORCE ENTERTAINMENT', 'DARK FORCE', 'DARK SIDE RELEASING', 'DARK SIDE', 'DAZZLER MEDIA', 'DAZZLER', 'DCM PICTURES', 'DCM', 'DEAPLANETA', 'DECCA', 'DEEPJOY', 'DEFIANT SCREEN ENTERTAINMENT', 'DEFIANT SCREEN', 'DEFIANT', 'DELOS', 'DELPHIAN RECORDS', 'DELPHIAN', 'DELTA MUSIC & ENTERTAINMENT', 'DELTA MUSIC AND ENTERTAINMENT', 'DELTA MUSIC ENTERTAINMENT', 'DELTA MUSIC', 'DELTAMAC CO. LTD.', 'DELTAMAC CO LTD', 'DELTAMAC CO', 'DELTAMAC', 'DEMAND MEDIA', 'DEMAND', 'DEP', 'DEUTSCHE GRAMMOPHON', 'DFW', 'DGM', 'DIAPHANA', 'DIGIDREAMS STUDIOS', 'DIGIDREAMS', 'DIGITAL ENVIRONMENTS', 'DIGITAL', 'DISCOTEK MEDIA', 'DISCOVERY CHANNEL', 'DISCOVERY', 'DISK KINO', 'DISNEY / BUENA VISTA', 'DISNEY', 'BUENA VISTA', 'DISNEY BUENA VISTA', 'DISTRIBUTION SELECT', 'DIVISA', 'DNC ENTERTAINMENT', 'DNC', 'DOGWOOF', 'DOLMEN HOME VIDEO', 'DOLMEN', 'DONAU FILM', 'DONAU', 'DORADO FILMS', 'DORADO', 'DRAFTHOUSE FILMS', 'DRAFTHOUSE', 'DRAGON FILM ENTERTAINMENT', 'DRAGON ENTERTAINMENT', 'DRAGON FILM', 'DRAGON', 'DREAMWORKS', 'DRIVE ON RECORDS', 'DRIVE ON', 'DRIVE-ON', 'DRIVEON', 'DS MEDIA', 'DTP ENTERTAINMENT AG', 'DTP ENTERTAINMENT', 'DTP AG', 'DTP', 'DTS ENTERTAINMENT', 'DTS', 'DUKE MARKETING', 'DUKE VIDEO DISTRIBUTION', 'DUKE', 'DUTCH FILMWORKS', 'DUTCH', 'DVD INTERNATIONAL', 'DVD', 'DYBEX', 'DYNAMIC', 'DYNIT', 'E1 ENTERTAINMENT', 'E1', 'EAGLE ENTERTAINMENT', 'EAGLE HOME ENTERTAINMENT PVT.LTD.', 'EAGLE HOME ENTERTAINMENT PVTLTD', 'EAGLE HOME ENTERTAINMENT PVT LTD', 'EAGLE HOME ENTERTAINMENT', 'EAGLE PICTURES', 'EAGLE ROCK ENTERTAINMENT', 'EAGLE ROCK', 'EAGLE VISION MEDIA', 'EAGLE VISION', 'EARMUSIC', 'EARTH ENTERTAINMENT', 'EARTH', 'ECHO BRIDGE ENTERTAINMENT', 'ECHO BRIDGE', 'EDEL GERMANY GMBH', 'EDEL GERMANY', 'EDEL RECORDS', 'EDITION TONFILM', 'EDITIONS MONTPARNASSE', 'EDKO FILMS LTD.', 'EDKO FILMS LTD', 'EDKO FILMS', - 'EDKO', "EIN'S M&M CO", 'EINS M&M CO', "EIN'S M&M", 'EINS M&M', 'ELEA-MEDIA', 'ELEA MEDIA', 'ELEA', 'ELECTRIC PICTURE', 'ELECTRIC', 'ELEPHANT FILMS', 'ELEPHANT', 'ELEVATION', 'EMI', 'EMON', 'EMS', 'EMYLIA', 'ENE MEDIA', 'ENE', 'ENTERTAINMENT IN VIDEO', 'ENTERTAINMENT IN', 'ENTERTAINMENT ONE', 'ENTERTAINMENT ONE FILMS CANADA INC.', 'ENTERTAINMENT ONE FILMS CANADA INC', 'ENTERTAINMENT ONE FILMS CANADA', 'ENTERTAINMENT ONE CANADA INC', 'ENTERTAINMENT ONE CANADA', 'ENTERTAINMENTONE', 'EONE', 'EOS', 'EPIC PICTURES', 'EPIC', 'EPIC RECORDS', 'ERATO', 'EROS', 'ESC EDITIONS', 'ESCAPI MEDIA BV', 'ESOTERIC RECORDINGS', 'ESPN FILMS', 'EUREKA ENTERTAINMENT', 'EUREKA', 'EURO PICTURES', 'EURO VIDEO', 'EUROARTS', 'EUROPA FILMES', 'EUROPA', 'EUROPACORP', 'EUROZOOM', 'EXCEL', 'EXPLOSIVE MEDIA', 'EXPLOSIVE', 'EXTRALUCID FILMS', 'EXTRALUCID', 'EYE SEE MOVIES', 'EYE SEE', 'EYK MEDIA', 'EYK', 'FABULOUS FILMS', 'FABULOUS', 'FACTORIS FILMS', 'FACTORIS', 'FARAO RECORDS', 'FARBFILM HOME ENTERTAINMENT', 'FARBFILM ENTERTAINMENT', 'FARBFILM HOME', 'FARBFILM', 'FEELGOOD ENTERTAINMENT', 'FEELGOOD', 'FERNSEHJUWELEN', 'FILM CHEST', 'FILM MEDIA', 'FILM MOVEMENT', 'FILM4', 'FILMART', 'FILMAURO', 'FILMAX', 'FILMCONFECT HOME ENTERTAINMENT', 'FILMCONFECT ENTERTAINMENT', 'FILMCONFECT HOME', 'FILMCONFECT', 'FILMEDIA', 'FILMJUWELEN', 'FILMOTEKA NARODAWA', 'FILMRISE', 'FINAL CUT ENTERTAINMENT', 'FINAL CUT', 'FIREHOUSE 12 RECORDS', 'FIREHOUSE 12', 'FIRST INTERNATIONAL PRODUCTION', 'FIRST INTERNATIONAL', 'FIRST LOOK STUDIOS', 'FIRST LOOK', 'FLAGMAN TRADE', 'FLASHSTAR FILMES', 'FLASHSTAR', 'FLICKER ALLEY', 'FNC ADD CULTURE', 'FOCUS FILMES', 'FOCUS', 'FOKUS MEDIA', 'FOKUSA', 'FOX PATHE EUROPA', 'FOX PATHE', 'FOX EUROPA', 'FOX/MGM', 'FOX MGM', 'MGM', 'MGM/FOX', 'FOX', 'FPE', 'FRANCE TÉLÉVISIONS DISTRIBUTION', 'FRANCE TELEVISIONS DISTRIBUTION', 'FRANCE TELEVISIONS', 'FRANCE', 'FREE DOLPHIN ENTERTAINMENT', 'FREE DOLPHIN', 'FREESTYLE DIGITAL MEDIA', 'FREESTYLE DIGITAL', 'FREESTYLE', 'FREMANTLE HOME ENTERTAINMENT', 'FREMANTLE ENTERTAINMENT', 'FREMANTLE HOME', 'FREMANTL', 'FRENETIC FILMS', 'FRENETIC', 'FRONTIER WORKS', 'FRONTIER', 'FRONTIERS MUSIC', 'FRONTIERS RECORDS', 'FS FILM OY', 'FS FILM', 'FULL MOON FEATURES', 'FULL MOON', 'FUN CITY EDITIONS', 'FUN CITY', - 'FUNIMATION ENTERTAINMENT', 'FUNIMATION', 'FUSION', 'FUTUREFILM', 'G2 PICTURES', 'G2', 'GAGA COMMUNICATIONS', 'GAGA', 'GAIAM', 'GALAPAGOS', 'GAMMA HOME ENTERTAINMENT', 'GAMMA ENTERTAINMENT', 'GAMMA HOME', 'GAMMA', 'GARAGEHOUSE PICTURES', 'GARAGEHOUSE', 'GARAGEPLAY (車庫娛樂)', '車庫娛樂', 'GARAGEPLAY (Che Ku Yu Le )', 'GARAGEPLAY', 'Che Ku Yu Le', 'GAUMONT', 'GEFFEN', 'GENEON ENTERTAINMENT', 'GENEON', 'GENEON UNIVERSAL ENTERTAINMENT', 'GENERAL VIDEO RECORDING', 'GLASS DOLL FILMS', 'GLASS DOLL', 'GLOBE MUSIC MEDIA', 'GLOBE MUSIC', 'GLOBE MEDIA', 'GLOBE', 'GO ENTERTAIN', 'GO', 'GOLDEN HARVEST', 'GOOD!MOVIES', 'GOOD! MOVIES', 'GOOD MOVIES', 'GRAPEVINE VIDEO', 'GRAPEVINE', 'GRASSHOPPER FILM', 'GRASSHOPPER FILMS', 'GRASSHOPPER', 'GRAVITAS VENTURES', 'GRAVITAS', 'GREAT MOVIES', 'GREAT', 'GREEN APPLE ENTERTAINMENT', 'GREEN ENTERTAINMENT', 'GREEN APPLE', 'GREEN', 'GREENNARAE MEDIA', 'GREENNARAE', 'GRINDHOUSE RELEASING', 'GRINDHOUSE', 'GRIND HOUSE', 'GRYPHON ENTERTAINMENT', 'GRYPHON', 'GUNPOWDER & SKY', 'GUNPOWDER AND SKY', 'GUNPOWDER SKY', 'GUNPOWDER + SKY', 'GUNPOWDER', 'HANABEE ENTERTAINMENT', 'HANABEE', 'HANNOVER HOUSE', 'HANNOVER', 'HANSESOUND', 'HANSE SOUND', 'HANSE', 'HAPPINET', 'HARMONIA MUNDI', 'HARMONIA', 'HBO', 'HDC', 'HEC', 'HELL & BACK RECORDINGS', 'HELL AND BACK RECORDINGS', 'HELL & BACK', 'HELL AND BACK', "HEN'S TOOTH VIDEO", 'HENS TOOTH VIDEO', "HEN'S TOOTH", 'HENS TOOTH', 'HIGH FLIERS', 'HIGHLIGHT', 'HILLSONG', 'HISTORY CHANNEL', 'HISTORY', 'HK VIDÉO', 'HK VIDEO', 'HK', 'HMH HAMBURGER MEDIEN HAUS', 'HAMBURGER MEDIEN HAUS', 'HMH HAMBURGER MEDIEN', 'HMH HAMBURGER', 'HMH', 'HOLLYWOOD CLASSIC ENTERTAINMENT', 'HOLLYWOOD CLASSIC', 'HOLLYWOOD PICTURES', 'HOLLYWOOD', 'HOPSCOTCH ENTERTAINMENT', 'HOPSCOTCH', 'HPM', 'HÄNNSLER CLASSIC', 'HANNSLER CLASSIC', 'HANNSLER', 'I-CATCHER', 'I CATCHER', 'ICATCHER', 'I-ON NEW MEDIA', 'I ON NEW MEDIA', 'ION NEW MEDIA', 'ION MEDIA', 'I-ON', 'ION', 'IAN PRODUCTIONS', 'IAN', 'ICESTORM', 'ICON FILM DISTRIBUTION', 'ICON DISTRIBUTION', 'ICON FILM', 'ICON', 'IDEALE AUDIENCE', 'IDEALE', 'IFC FILMS', 'IFC', 'IFILM', 'ILLUSIONS UNLTD.', 'ILLUSIONS UNLTD', 'ILLUSIONS', 'IMAGE ENTERTAINMENT', 'IMAGE', 'IMAGEM FILMES', 'IMAGEM', 'IMOVISION', 'IMPERIAL CINEPIX', 'IMPRINT', 'IMPULS HOME ENTERTAINMENT', 'IMPULS ENTERTAINMENT', 'IMPULS HOME', 'IMPULS', 'IN-AKUSTIK', 'IN AKUSTIK', 'INAKUSTIK', 'INCEPTION MEDIA GROUP', 'INCEPTION MEDIA', 'INCEPTION GROUP', 'INCEPTION', 'INDEPENDENT', 'INDICAN', 'INDIE RIGHTS', 'INDIE', 'INDIGO', 'INFO', 'INJOINGAN', 'INKED PICTURES', 'INKED', 'INSIDE OUT MUSIC', 'INSIDE MUSIC', 'INSIDE OUT', 'INSIDE', 'INTERCOM', 'INTERCONTINENTAL VIDEO', 'INTERCONTINENTAL', 'INTERGROOVE', 'INTERSCOPE', 'INVINCIBLE PICTURES', 'INVINCIBLE', 'ISLAND/MERCURY', 'ISLAND MERCURY', 'ISLANDMERCURY', 'ISLAND & MERCURY', 'ISLAND AND MERCURY', 'ISLAND', 'ITN', 'ITV DVD', 'ITV', 'IVC', 'IVE ENTERTAINMENT', 'IVE', 'J&R ADVENTURES', 'J&R', 'JR', 'JAKOB', 'JONU MEDIA', 'JONU', 'JRB PRODUCTIONS', 'JRB', 'JUST BRIDGE ENTERTAINMENT', 'JUST BRIDGE', 'JUST ENTERTAINMENT', 'JUST', 'KABOOM ENTERTAINMENT', 'KABOOM', 'KADOKAWA ENTERTAINMENT', 'KADOKAWA', 'KAIROS', 'KALEIDOSCOPE ENTERTAINMENT', 'KALEIDOSCOPE', 'KAM & RONSON ENTERPRISES', 'KAM & RONSON', 'KAM&RONSON ENTERPRISES', 'KAM&RONSON', 'KAM AND RONSON ENTERPRISES', 'KAM AND RONSON', 'KANA HOME VIDEO', 'KARMA FILMS', 'KARMA', 'KATZENBERGER', 'KAZE', - 'KBS MEDIA', 'KBS', 'KD MEDIA', 'KD', 'KING MEDIA', 'KING', 'KING RECORDS', 'KINO LORBER', 'KINO', 'KINO SWIAT', 'KINOKUNIYA', 'KINOWELT HOME ENTERTAINMENT/DVD', 'KINOWELT HOME ENTERTAINMENT', 'KINOWELT ENTERTAINMENT', 'KINOWELT HOME DVD', 'KINOWELT ENTERTAINMENT/DVD', 'KINOWELT DVD', 'KINOWELT', 'KIT PARKER FILMS', 'KIT PARKER', 'KITTY MEDIA', 'KNM HOME ENTERTAINMENT', 'KNM ENTERTAINMENT', 'KNM HOME', 'KNM', 'KOBA FILMS', 'KOBA', 'KOCH ENTERTAINMENT', 'KOCH MEDIA', 'KOCH', 'KRAKEN RELEASING', 'KRAKEN', 'KSCOPE', 'KSM', 'KULTUR', "L'ATELIER D'IMAGES", "LATELIER D'IMAGES", "L'ATELIER DIMAGES", 'LATELIER DIMAGES', "L ATELIER D'IMAGES", "L'ATELIER D IMAGES", - 'L ATELIER D IMAGES', "L'ATELIER", 'L ATELIER', 'LATELIER', 'LA AVENTURA AUDIOVISUAL', 'LA AVENTURA', 'LACE GROUP', 'LACE', 'LASER PARADISE', 'LAYONS', 'LCJ EDITIONS', 'LCJ', 'LE CHAT QUI FUME', 'LE PACTE', 'LEDICK FILMHANDEL', 'LEGEND', 'LEOMARK STUDIOS', 'LEOMARK', 'LEONINE FILMS', 'LEONINE', 'LICHTUNG MEDIA LTD', 'LICHTUNG LTD', 'LICHTUNG MEDIA LTD.', 'LICHTUNG LTD.', 'LICHTUNG MEDIA', 'LICHTUNG', 'LIGHTHOUSE HOME ENTERTAINMENT', 'LIGHTHOUSE ENTERTAINMENT', 'LIGHTHOUSE HOME', 'LIGHTHOUSE', 'LIGHTYEAR', 'LIONSGATE FILMS', 'LIONSGATE', 'LIZARD CINEMA TRADE', 'LLAMENTOL', 'LOBSTER FILMS', 'LOBSTER', 'LOGON', 'LORBER FILMS', 'LORBER', 'LOS BANDITOS FILMS', 'LOS BANDITOS', 'LOUD & PROUD RECORDS', 'LOUD AND PROUD RECORDS', 'LOUD & PROUD', 'LOUD AND PROUD', 'LSO LIVE', 'LUCASFILM', 'LUCKY RED', 'LUMIÈRE HOME ENTERTAINMENT', 'LUMIERE HOME ENTERTAINMENT', 'LUMIERE ENTERTAINMENT', 'LUMIERE HOME', 'LUMIERE', 'M6 VIDEO', 'M6', 'MAD DIMENSION', 'MADMAN ENTERTAINMENT', 'MADMAN', 'MAGIC BOX', 'MAGIC PLAY', 'MAGNA HOME ENTERTAINMENT', 'MAGNA ENTERTAINMENT', 'MAGNA HOME', 'MAGNA', 'MAGNOLIA PICTURES', 'MAGNOLIA', 'MAIDEN JAPAN', 'MAIDEN', 'MAJENG MEDIA', 'MAJENG', 'MAJESTIC HOME ENTERTAINMENT', 'MAJESTIC ENTERTAINMENT', 'MAJESTIC HOME', 'MAJESTIC', 'MANGA HOME ENTERTAINMENT', 'MANGA ENTERTAINMENT', 'MANGA HOME', 'MANGA', 'MANTA LAB', 'MAPLE STUDIOS', 'MAPLE', 'MARCO POLO PRODUCTION', 'MARCO POLO', 'MARIINSKY', 'MARVEL STUDIOS', 'MARVEL', 'MASCOT RECORDS', 'MASCOT', 'MASSACRE VIDEO', 'MASSACRE', 'MATCHBOX', 'MATRIX D', 'MAXAM', 'MAYA HOME ENTERTAINMENT', 'MAYA ENTERTAINMENT', 'MAYA HOME', 'MAYAT', 'MDG', 'MEDIA BLASTERS', 'MEDIA FACTORY', 'MEDIA TARGET DISTRIBUTION', 'MEDIA TARGET', 'MEDIAINVISION', 'MEDIATOON', 'MEDIATRES ESTUDIO', 'MEDIATRES STUDIO', 'MEDIATRES', 'MEDICI ARTS', 'MEDICI CLASSICS', 'MEDIUMRARE ENTERTAINMENT', 'MEDIUMRARE', 'MEDUSA', 'MEGASTAR', 'MEI AH', 'MELI MÉDIAS', 'MELI MEDIAS', 'MEMENTO FILMS', 'MEMENTO', 'MENEMSHA FILMS', 'MENEMSHA', 'MERCURY', 'MERCURY STUDIOS', 'MERGE SOFT PRODUCTIONS', 'MERGE PRODUCTIONS', 'MERGE SOFT', 'MERGE', 'METAL BLADE RECORDS', 'METAL BLADE', 'METEOR', 'METRO-GOLDWYN-MAYER', 'METRO GOLDWYN MAYER', 'METROGOLDWYNMAYER', 'METRODOME VIDEO', 'METRODOME', 'METROPOLITAN', 'MFA+', 'MFA', 'MIG FILMGROUP', 'MIG', 'MILESTONE', 'MILL CREEK ENTERTAINMENT', 'MILL CREEK', 'MILLENNIUM MEDIA', 'MILLENNIUM', 'MIRAGE ENTERTAINMENT', 'MIRAGE', 'MIRAMAX', 'MISTERIYA ZVUKA', 'MK2', 'MODE RECORDS', 'MODE', 'MOMENTUM PICTURES', 'MONDO HOME ENTERTAINMENT', 'MONDO ENTERTAINMENT', 'MONDO HOME', 'MONDO MACABRO', 'MONGREL MEDIA', 'MONOLIT', 'MONOLITH VIDEO', 'MONOLITH', 'MONSTER PICTURES', 'MONSTER', 'MONTEREY VIDEO', 'MONTEREY', 'MONUMENT RELEASING', 'MONUMENT', 'MORNINGSTAR', 'MORNING STAR', 'MOSERBAER', 'MOVIEMAX', 'MOVINSIDE', 'MPI MEDIA GROUP', 'MPI MEDIA', 'MPI', 'MR. BONGO FILMS', 'MR BONGO FILMS', 'MR BONGO', 'MRG (MERIDIAN)', 'MRG MERIDIAN', 'MRG', 'MERIDIAN', 'MUBI', 'MUG SHOT PRODUCTIONS', 'MUG SHOT', 'MULTIMUSIC', 'MULTI-MUSIC', 'MULTI MUSIC', 'MUSE', 'MUSIC BOX FILMS', 'MUSIC BOX', 'MUSICBOX', 'MUSIC BROKERS', 'MUSIC THEORIES', 'MUSIC VIDEO DISTRIBUTORS', 'MUSIC VIDEO', 'MUSTANG ENTERTAINMENT', 'MUSTANG', 'MVD VISUAL', 'MVD', 'MVD/VSC', 'MVL', 'MVM ENTERTAINMENT', 'MVM', 'MYNDFORM', 'MYSTIC NIGHT PICTURES', 'MYSTIC NIGHT', 'NAMELESS MEDIA', 'NAMELESS', 'NAPALM RECORDS', 'NAPALM', 'NATIONAL ENTERTAINMENT MEDIA', 'NATIONAL ENTERTAINMENT', 'NATIONAL MEDIA', 'NATIONAL FILM ARCHIVE', 'NATIONAL ARCHIVE', 'NATIONAL FILM', 'NATIONAL GEOGRAPHIC', 'NAT GEO TV', 'NAT GEO', 'NGO', 'NAXOS', 'NBCUNIVERSAL ENTERTAINMENT JAPAN', 'NBC UNIVERSAL ENTERTAINMENT JAPAN', 'NBCUNIVERSAL JAPAN', 'NBC UNIVERSAL JAPAN', 'NBC JAPAN', 'NBO ENTERTAINMENT', 'NBO', 'NEOS', 'NETFLIX', 'NETWORK', 'NEW BLOOD', 'NEW DISC', 'NEW KSM', 'NEW LINE CINEMA', 'NEW LINE', 'NEW MOVIE TRADING CO. LTD', 'NEW MOVIE TRADING CO LTD', 'NEW MOVIE TRADING CO', 'NEW MOVIE TRADING', 'NEW WAVE FILMS', 'NEW WAVE', 'NFI', 'NHK', 'NIPPONART', 'NIS AMERICA', 'NJUTAFILMS', 'NOBLE ENTERTAINMENT', 'NOBLE', 'NORDISK FILM', 'NORDISK', 'NORSK FILM', 'NORSK', 'NORTH AMERICAN MOTION PICTURES', 'NOS AUDIOVISUAIS', 'NOTORIOUS PICTURES', 'NOTORIOUS', 'NOVA MEDIA', 'NOVA', 'NOVA SALES AND DISTRIBUTION', 'NOVA SALES & DISTRIBUTION', 'NSM', 'NSM RECORDS', 'NUCLEAR BLAST', 'NUCLEUS FILMS', 'NUCLEUS', 'OBERLIN MUSIC', 'OBERLIN', 'OBRAS-PRIMAS DO CINEMA', 'OBRAS PRIMAS DO CINEMA', 'OBRASPRIMAS DO CINEMA', 'OBRAS-PRIMAS CINEMA', 'OBRAS PRIMAS CINEMA', 'OBRASPRIMAS CINEMA', 'OBRAS-PRIMAS', 'OBRAS PRIMAS', 'OBRASPRIMAS', 'ODEON', 'OFDB FILMWORKS', 'OFDB', 'OLIVE FILMS', 'OLIVE', 'ONDINE', 'ONSCREEN FILMS', 'ONSCREEN', 'OPENING DISTRIBUTION', 'OPERA AUSTRALIA', 'OPTIMUM HOME ENTERTAINMENT', 'OPTIMUM ENTERTAINMENT', 'OPTIMUM HOME', 'OPTIMUM', 'OPUS ARTE', 'ORANGE STUDIO', 'ORANGE', 'ORLANDO EASTWOOD FILMS', 'ORLANDO FILMS', 'ORLANDO EASTWOOD', 'ORLANDO', 'ORUSTAK PICTURES', 'ORUSTAK', 'OSCILLOSCOPE PICTURES', 'OSCILLOSCOPE', 'OUTPLAY', 'PALISADES TARTAN', 'PAN VISION', 'PANVISION', 'PANAMINT CINEMA', 'PANAMINT', 'PANDASTORM ENTERTAINMENT', 'PANDA STORM ENTERTAINMENT', 'PANDASTORM', 'PANDA STORM', 'PANDORA FILM', 'PANDORA', 'PANEGYRIC', 'PANORAMA', 'PARADE DECK FILMS', 'PARADE DECK', 'PARADISE', 'PARADISO FILMS', 'PARADOX', 'PARAMOUNT PICTURES', 'PARAMOUNT', 'PARIS FILMES', 'PARIS FILMS', 'PARIS', 'PARK CIRCUS', 'PARLOPHONE', 'PASSION RIVER', 'PATHE DISTRIBUTION', 'PATHE', 'PBS', 'PEACE ARCH TRINITY', 'PECCADILLO PICTURES', 'PEPPERMINT', 'PHASE 4 FILMS', 'PHASE 4', 'PHILHARMONIA BAROQUE', 'PICTURE HOUSE ENTERTAINMENT', 'PICTURE ENTERTAINMENT', 'PICTURE HOUSE', 'PICTURE', 'PIDAX', - 'PINK FLOYD RECORDS', 'PINK FLOYD', 'PINNACLE FILMS', 'PINNACLE', 'PLAIN', 'PLATFORM ENTERTAINMENT LIMITED', 'PLATFORM ENTERTAINMENT LTD', 'PLATFORM ENTERTAINMENT LTD.', 'PLATFORM ENTERTAINMENT', 'PLATFORM', 'PLAYARTE', 'PLG UK CLASSICS', 'PLG UK', 'PLG', 'POLYBAND & TOPPIC VIDEO/WVG', 'POLYBAND AND TOPPIC VIDEO/WVG', 'POLYBAND & TOPPIC VIDEO WVG', 'POLYBAND & TOPPIC VIDEO AND WVG', 'POLYBAND & TOPPIC VIDEO & WVG', 'POLYBAND AND TOPPIC VIDEO WVG', 'POLYBAND AND TOPPIC VIDEO AND WVG', 'POLYBAND AND TOPPIC VIDEO & WVG', 'POLYBAND & TOPPIC VIDEO', 'POLYBAND AND TOPPIC VIDEO', 'POLYBAND & TOPPIC', 'POLYBAND AND TOPPIC', 'POLYBAND', 'WVG', 'POLYDOR', 'PONY', 'PONY CANYON', 'POTEMKINE', 'POWERHOUSE FILMS', 'POWERHOUSE', 'POWERSTATIOM', 'PRIDE & JOY', 'PRIDE AND JOY', 'PRINZ MEDIA', 'PRINZ', 'PRIS AUDIOVISUAIS', 'PRO VIDEO', 'PRO-VIDEO', 'PRO-MOTION', 'PRO MOTION', 'PROD. JRB', 'PROD JRB', 'PRODISC', 'PROKINO', 'PROVOGUE RECORDS', 'PROVOGUE', 'PROWARE', 'PULP VIDEO', 'PULP', 'PULSE VIDEO', 'PULSE', 'PURE AUDIO RECORDINGS', 'PURE AUDIO', 'PURE FLIX ENTERTAINMENT', 'PURE FLIX', 'PURE ENTERTAINMENT', 'PYRAMIDE VIDEO', 'PYRAMIDE', 'QUALITY FILMS', 'QUALITY', 'QUARTO VALLEY RECORDS', 'QUARTO VALLEY', 'QUESTAR', 'R SQUARED FILMS', 'R SQUARED', 'RAPID EYE MOVIES', 'RAPID EYE', 'RARO VIDEO', 'RARO', 'RAROVIDEO U.S.', 'RAROVIDEO US', 'RARO VIDEO US', 'RARO VIDEO U.S.', 'RARO U.S.', 'RARO US', 'RAVEN BANNER RELEASING', 'RAVEN BANNER', 'RAVEN', 'RAZOR DIGITAL ENTERTAINMENT', 'RAZOR DIGITAL', 'RCA', 'RCO LIVE', 'RCO', 'RCV', 'REAL GONE MUSIC', 'REAL GONE', 'REANIMEDIA', 'REANI MEDIA', 'REDEMPTION', 'REEL', 'RELIANCE HOME VIDEO & GAMES', 'RELIANCE HOME VIDEO AND GAMES', 'RELIANCE HOME VIDEO', 'RELIANCE VIDEO', 'RELIANCE HOME', 'RELIANCE', 'REM CULTURE', 'REMAIN IN LIGHT', 'REPRISE', 'RESEN', 'RETROMEDIA', 'REVELATION FILMS LTD.', 'REVELATION FILMS LTD', 'REVELATION FILMS', 'REVELATION LTD.', 'REVELATION LTD', 'REVELATION', 'REVOLVER ENTERTAINMENT', 'REVOLVER', 'RHINO MUSIC', 'RHINO', 'RHV', 'RIGHT STUF', 'RIMINI EDITIONS', 'RISING SUN MEDIA', 'RLJ ENTERTAINMENT', 'RLJ', 'ROADRUNNER RECORDS', 'ROADSHOW ENTERTAINMENT', 'ROADSHOW', 'RONE', 'RONIN FLIX', 'ROTANA HOME ENTERTAINMENT', 'ROTANA ENTERTAINMENT', 'ROTANA HOME', 'ROTANA', 'ROUGH TRADE', - 'ROUNDER', 'SAFFRON HILL FILMS', 'SAFFRON HILL', 'SAFFRON', 'SAMUEL GOLDWYN FILMS', 'SAMUEL GOLDWYN', 'SAN FRANCISCO SYMPHONY', 'SANDREW METRONOME', 'SAPHRANE', 'SAVOR', 'SCANBOX ENTERTAINMENT', 'SCANBOX', 'SCENIC LABS', 'SCHRÖDERMEDIA', 'SCHRODERMEDIA', 'SCHRODER MEDIA', 'SCORPION RELEASING', 'SCORPION', 'SCREAM TEAM RELEASING', 'SCREAM TEAM', 'SCREEN MEDIA', 'SCREEN', 'SCREENBOUND PICTURES', 'SCREENBOUND', 'SCREENWAVE MEDIA', 'SCREENWAVE', 'SECOND RUN', 'SECOND SIGHT', 'SEEDSMAN GROUP', 'SELECT VIDEO', 'SELECTA VISION', 'SENATOR', 'SENTAI FILMWORKS', 'SENTAI', 'SEVEN7', 'SEVERIN FILMS', 'SEVERIN', 'SEVILLE', 'SEYONS ENTERTAINMENT', 'SEYONS', 'SF STUDIOS', 'SGL ENTERTAINMENT', 'SGL', 'SHAMELESS', 'SHAMROCK MEDIA', 'SHAMROCK', 'SHANGHAI EPIC MUSIC ENTERTAINMENT', 'SHANGHAI EPIC ENTERTAINMENT', 'SHANGHAI EPIC MUSIC', 'SHANGHAI MUSIC ENTERTAINMENT', 'SHANGHAI ENTERTAINMENT', 'SHANGHAI MUSIC', 'SHANGHAI', 'SHEMAROO', 'SHOCHIKU', 'SHOCK', 'SHOGAKU KAN', 'SHOUT FACTORY', 'SHOUT! FACTORY', 'SHOUT', 'SHOUT!', 'SHOWBOX', 'SHOWTIME ENTERTAINMENT', 'SHOWTIME', 'SHRIEK SHOW', 'SHUDDER', 'SIDONIS', 'SIDONIS CALYSTA', 'SIGNAL ONE ENTERTAINMENT', 'SIGNAL ONE', 'SIGNATURE ENTERTAINMENT', 'SIGNATURE', 'SILVER VISION', 'SINISTER FILM', 'SINISTER', 'SIREN VISUAL ENTERTAINMENT', 'SIREN VISUAL', 'SIREN ENTERTAINMENT', 'SIREN', 'SKANI', 'SKY DIGI', - 'SLASHER // VIDEO', 'SLASHER / VIDEO', 'SLASHER VIDEO', 'SLASHER', 'SLOVAK FILM INSTITUTE', 'SLOVAK FILM', 'SFI', 'SM LIFE DESIGN GROUP', 'SMOOTH PICTURES', 'SMOOTH', 'SNAPPER MUSIC', 'SNAPPER', 'SODA PICTURES', 'SODA', 'SONO LUMINUS', 'SONY MUSIC', 'SONY PICTURES', 'SONY', 'SONY PICTURES CLASSICS', 'SONY CLASSICS', 'SOUL MEDIA', 'SOUL', 'SOULFOOD MUSIC DISTRIBUTION', 'SOULFOOD DISTRIBUTION', 'SOULFOOD MUSIC', 'SOULFOOD', 'SOYUZ', 'SPECTRUM', 'SPENTZOS FILM', 'SPENTZOS', 'SPIRIT ENTERTAINMENT', 'SPIRIT', 'SPIRIT MEDIA GMBH', 'SPIRIT MEDIA', 'SPLENDID ENTERTAINMENT', 'SPLENDID FILM', 'SPO', 'SQUARE ENIX', 'SRI BALAJI VIDEO', 'SRI BALAJI', 'SRI', 'SRI VIDEO', 'SRS CINEMA', 'SRS', 'SSO RECORDINGS', 'SSO', 'ST2 MUSIC', 'ST2', 'STAR MEDIA ENTERTAINMENT', 'STAR ENTERTAINMENT', 'STAR MEDIA', 'STAR', 'STARLIGHT', 'STARZ / ANCHOR BAY', 'STARZ ANCHOR BAY', 'STARZ', 'ANCHOR BAY', 'STER KINEKOR', 'STERLING ENTERTAINMENT', 'STERLING', 'STINGRAY', 'STOCKFISCH RECORDS', 'STOCKFISCH', 'STRAND RELEASING', 'STRAND', 'STUDIO 4K', 'STUDIO CANAL', 'STUDIO GHIBLI', 'GHIBLI', 'STUDIO HAMBURG ENTERPRISES', 'HAMBURG ENTERPRISES', 'STUDIO HAMBURG', 'HAMBURG', 'STUDIO S', 'SUBKULTUR ENTERTAINMENT', 'SUBKULTUR', 'SUEVIA FILMS', 'SUEVIA', 'SUMMIT ENTERTAINMENT', 'SUMMIT', 'SUNFILM ENTERTAINMENT', 'SUNFILM', 'SURROUND RECORDS', 'SURROUND', 'SVENSK FILMINDUSTRI', 'SVENSK', 'SWEN FILMES', 'SWEN FILMS', 'SWEN', 'SYNAPSE FILMS', 'SYNAPSE', 'SYNDICADO', 'SYNERGETIC', 'T- SERIES', 'T-SERIES', 'T SERIES', 'TSERIES', 'T.V.P.', 'TVP', 'TACET RECORDS', 'TACET', 'TAI SENG', 'TAI SHENG', 'TAKEONE', 'TAKESHOBO', 'TAMASA DIFFUSION', 'TC ENTERTAINMENT', 'TC', 'TDK', 'TEAM MARKETING', 'TEATRO REAL', 'TEMA DISTRIBUCIONES', 'TEMPE DIGITAL', 'TF1 VIDÉO', 'TF1 VIDEO', 'TF1', 'THE BLU', 'BLU', 'THE ECSTASY OF FILMS', 'THE FILM DETECTIVE', 'FILM DETECTIVE', 'THE JOKERS', 'JOKERS', 'THE ON', 'ON', 'THIMFILM', 'THIM FILM', 'THIM', 'THIRD WINDOW FILMS', 'THIRD WINDOW', '3RD WINDOW FILMS', '3RD WINDOW', 'THUNDERBEAN ANIMATION', 'THUNDERBEAN', 'THUNDERBIRD RELEASING', 'THUNDERBIRD', 'TIBERIUS FILM', 'TIME LIFE', 'TIMELESS MEDIA GROUP', 'TIMELESS MEDIA', 'TIMELESS GROUP', 'TIMELESS', 'TLA RELEASING', 'TLA', 'TOBIS FILM', 'TOBIS', 'TOEI', 'TOHO', 'TOKYO SHOCK', 'TOKYO', 'TONPOOL MEDIEN GMBH', 'TONPOOL MEDIEN', 'TOPICS ENTERTAINMENT', 'TOPICS', 'TOUCHSTONE PICTURES', 'TOUCHSTONE', 'TRANSMISSION FILMS', 'TRANSMISSION', 'TRAVEL VIDEO STORE', 'TRIART', 'TRIGON FILM', 'TRIGON', 'TRINITY HOME ENTERTAINMENT', 'TRINITY ENTERTAINMENT', 'TRINITY HOME', 'TRINITY', 'TRIPICTURES', 'TRI-PICTURES', 'TRI PICTURES', 'TROMA', 'TURBINE MEDIEN', 'TURTLE RECORDS', 'TURTLE', 'TVA FILMS', 'TVA', 'TWILIGHT TIME', 'TWILIGHT', 'TT', 'TWIN CO., LTD.', 'TWIN CO, LTD.', 'TWIN CO., LTD', 'TWIN CO, LTD', 'TWIN CO LTD', 'TWIN LTD', 'TWIN CO.', 'TWIN CO', 'TWIN', 'UCA', 'UDR', 'UEK', 'UFA/DVD', 'UFA DVD', 'UFADVD', 'UGC PH', 'ULTIMATE3DHEAVEN', 'ULTRA', 'UMBRELLA ENTERTAINMENT', 'UMBRELLA', 'UMC', "UNCORK'D ENTERTAINMENT", 'UNCORKD ENTERTAINMENT', 'UNCORK D ENTERTAINMENT', "UNCORK'D", 'UNCORK D', 'UNCORKD', 'UNEARTHED FILMS', 'UNEARTHED', 'UNI DISC', 'UNIMUNDOS', 'UNITEL', 'UNIVERSAL MUSIC', 'UNIVERSAL SONY PICTURES HOME ENTERTAINMENT', 'UNIVERSAL SONY PICTURES ENTERTAINMENT', 'UNIVERSAL SONY PICTURES HOME', 'UNIVERSAL SONY PICTURES', 'UNIVERSAL HOME ENTERTAINMENT', 'UNIVERSAL ENTERTAINMENT', - 'UNIVERSAL HOME', 'UNIVERSAL STUDIOS', 'UNIVERSAL', 'UNIVERSE LASER & VIDEO CO.', 'UNIVERSE LASER AND VIDEO CO.', 'UNIVERSE LASER & VIDEO CO', 'UNIVERSE LASER AND VIDEO CO', 'UNIVERSE LASER CO.', 'UNIVERSE LASER CO', 'UNIVERSE LASER', 'UNIVERSUM FILM', 'UNIVERSUM', 'UTV', 'VAP', 'VCI', 'VENDETTA FILMS', 'VENDETTA', 'VERSÁTIL HOME VIDEO', 'VERSÁTIL VIDEO', 'VERSÁTIL HOME', 'VERSÁTIL', 'VERSATIL HOME VIDEO', 'VERSATIL VIDEO', 'VERSATIL HOME', 'VERSATIL', 'VERTICAL ENTERTAINMENT', 'VERTICAL', 'VÉRTICE 360º', 'VÉRTICE 360', 'VERTICE 360o', 'VERTICE 360', 'VERTIGO BERLIN', 'VÉRTIGO FILMS', 'VÉRTIGO', 'VERTIGO FILMS', 'VERTIGO', 'VERVE PICTURES', 'VIA VISION ENTERTAINMENT', 'VIA VISION', 'VICOL ENTERTAINMENT', 'VICOL', 'VICOM', 'VICTOR ENTERTAINMENT', 'VICTOR', 'VIDEA CDE', 'VIDEO FILM EXPRESS', 'VIDEO FILM', 'VIDEO EXPRESS', 'VIDEO MUSIC, INC.', 'VIDEO MUSIC, INC', 'VIDEO MUSIC INC.', 'VIDEO MUSIC INC', 'VIDEO MUSIC', 'VIDEO SERVICE CORP.', 'VIDEO SERVICE CORP', 'VIDEO SERVICE', 'VIDEO TRAVEL', 'VIDEOMAX', 'VIDEO MAX', 'VII PILLARS ENTERTAINMENT', 'VII PILLARS', 'VILLAGE FILMS', 'VINEGAR SYNDROME', 'VINEGAR', 'VS', 'VINNY MOVIES', 'VINNY', 'VIRGIL FILMS & ENTERTAINMENT', 'VIRGIL FILMS AND ENTERTAINMENT', 'VIRGIL ENTERTAINMENT', 'VIRGIL FILMS', 'VIRGIL', 'VIRGIN RECORDS', 'VIRGIN', 'VISION FILMS', 'VISION', 'VISUAL ENTERTAINMENT GROUP', + 'ARTE VIDEO', 'ARTHAUS MUSIK', 'ARTIFICIAL EYE', 'ARTSPLOITATION FILMS', 'ARTUS FILMS', 'ASCOT ELITE HOME ENTERTAINMENT', 'ASIA VIDEO', 'ASMIK ACE', 'ASTRO RECORDS & FILMWORKS', 'ASYLUM', 'ATLANTIC FILM', 'ATLANTIC RECORDS', 'ATLAS FILM', 'AUDIO VISUAL ENTERTAINMENT', 'AURO-3D CREATIVE LABEL', 'AURUM', 'AV VISIONEN', 'AV-JET', 'AVALON', 'AVENTI', 'AVEX TRAX', 'AXIOM', 'AXIS RECORDS', 'AYNGARAN', 'BAC FILMS', 'BACH FILMS', 'BANDAI VISUAL', 'BARCLAY', 'BBC', 'BRITISH BROADCASTING CORPORATION', 'BBI FILMS', 'BBI', 'BCI HOME ENTERTAINMENT', 'BEGGARS BANQUET', 'BEL AIR CLASSIQUES', 'BELGA FILMS', 'BELVEDERE', 'BENELUX FILM DISTRIBUTORS', 'BENNETT-WATT MEDIA', 'BERLIN CLASSICS', 'BERLINER PHILHARMONIKER RECORDINGS', 'BEST ENTERTAINMENT', 'BEYOND HOME ENTERTAINMENT', 'BFI VIDEO', 'BFI', 'BRITISH FILM INSTITUTE', 'BFS ENTERTAINMENT', 'BFS', 'BHAVANI', 'BIBER RECORDS', 'BIG HOME VIDEO', 'BILDSTÖRUNG', + 'BILDSTORUNG', 'BILL ZEBUB', 'BIRNENBLATT', 'BIT WEL', 'BLACK BOX', 'BLACK HILL PICTURES', 'BLACK HILL', 'BLACK HOLE RECORDINGS', 'BLACK HOLE', 'BLAQOUT', 'BLAUFIELD MUSIC', 'BLAUFIELD', 'BLOCKBUSTER ENTERTAINMENT', 'BLOCKBUSTER', 'BLU PHASE MEDIA', 'BLU-RAY ONLY', 'BLU-RAY', 'BLURAY ONLY', 'BLURAY', 'BLUE GENTIAN RECORDS', 'BLUE KINO', 'BLUE UNDERGROUND', 'BMG/ARISTA', 'BMG', 'BMGARISTA', 'BMG ARISTA', 'ARISTA', 'ARISTA/BMG', 'ARISTABMG', 'ARISTA BMG', 'BONTON FILM', 'BONTON', 'BOOMERANG PICTURES', 'BOOMERANG', 'BQHL ÉDITIONS', 'BQHL EDITIONS', 'BQHL', 'BREAKING GLASS', 'BRIDGESTONE', 'BRINK', 'BROAD GREEN PICTURES', 'BROAD GREEN', 'BUSCH MEDIA GROUP', 'BUSCH', 'C MAJOR', 'C.B.S.', 'CAICHANG', 'CALIFÓRNIA FILMES', 'CALIFORNIA FILMES', 'CALIFORNIA', 'CAMEO', 'CAMERA OBSCURA', 'CAMERATA', 'CAMP MOTION PICTURES', 'CAMP MOTION', 'CAPELIGHT PICTURES', 'CAPELIGHT', 'CAPITOL', 'CAPITOL RECORDS', 'CAPRICCI', 'CARGO RECORDS', 'CARLOTTA FILMS', 'CARLOTTA', 'CARLOTA', 'CARMEN FILM', 'CASCADE', 'CATCHPLAY', 'CAULDRON FILMS', 'CAULDRON', 'CBS TELEVISION STUDIOS', 'CBS', 'CCTV', 'CCV ENTERTAINMENT', 'CCV', 'CD BABY', 'CD LAND', 'CECCHI GORI', 'CENTURY MEDIA', 'CHUAN XUN SHI DAI MULTIMEDIA', 'CINE-ASIA', 'CINÉART', 'CINEART', 'CINEDIGM', 'CINEFIL IMAGICA', 'CINEMA EPOCH', 'CINEMA GUILD', 'CINEMA LIBRE STUDIOS', 'CINEMA MONDO', 'CINEMATIC VISION', 'CINEPLOIT RECORDS', 'CINESTRANGE EXTREME', 'CITEL VIDEO', 'CITEL', 'CJ ENTERTAINMENT', 'CJ', 'CLASSIC MEDIA', 'CLASSICFLIX', 'CLASSICLINE', 'CLAUDIO RECORDS', 'CLEAR VISION', 'CLEOPATRA', 'CLOSE UP', 'CMS MEDIA LIMITED', 'CMV LASERVISION', 'CN ENTERTAINMENT', 'CODE RED', 'COHEN MEDIA GROUP', 'COHEN', 'COIN DE MIRE CINÉMA', 'COIN DE MIRE CINEMA', 'COLOSSEO FILM', 'COLUMBIA', 'COLUMBIA PICTURES', 'COLUMBIA/TRI-STAR', 'TRI-STAR', 'COMMERCIAL MARKETING', 'CONCORD MUSIC GROUP', 'CONCORDE VIDEO', 'CONDOR', 'CONSTANTIN FILM', 'CONSTANTIN', 'CONSTANTINO FILMES', 'CONSTANTINO', 'CONSTRUCTIVE MEDIA SERVICE', 'CONSTRUCTIVE', 'CONTENT ZONE', 'CONTENTS GATE', 'COQUEIRO VERDE', 'CORNERSTONE MEDIA', 'CORNERSTONE', 'CP DIGITAL', 'CREST MOVIES', 'CRITERION', 'CRITERION COLLECTION', 'CC', 'CRYSTAL CLASSICS', 'CULT EPICS', 'CULT FILMS', 'CULT VIDEO', 'CURZON FILM WORLD', 'D FILMS', "D'AILLY COMPANY", 'DAILLY COMPANY', 'D AILLY COMPANY', "D'AILLY", 'DAILLY', 'D AILLY', 'DA CAPO', 'DA MUSIC', "DALL'ANGELO PICTURES", 'DALLANGELO PICTURES', "DALL'ANGELO", 'DALL ANGELO PICTURES', 'DALL ANGELO', 'DAREDO', 'DARK FORCE ENTERTAINMENT', 'DARK FORCE', 'DARK SIDE RELEASING', 'DARK SIDE', 'DAZZLER MEDIA', 'DAZZLER', 'DCM PICTURES', 'DCM', 'DEAPLANETA', 'DECCA', 'DEEPJOY', 'DEFIANT SCREEN ENTERTAINMENT', 'DEFIANT SCREEN', 'DEFIANT', 'DELOS', 'DELPHIAN RECORDS', 'DELPHIAN', 'DELTA MUSIC & ENTERTAINMENT', 'DELTA MUSIC AND ENTERTAINMENT', 'DELTA MUSIC ENTERTAINMENT', 'DELTA MUSIC', 'DELTAMAC CO. LTD.', 'DELTAMAC CO LTD', 'DELTAMAC CO', 'DELTAMAC', 'DEMAND MEDIA', 'DEMAND', 'DEP', 'DEUTSCHE GRAMMOPHON', 'DFW', 'DGM', 'DIAPHANA', 'DIGIDREAMS STUDIOS', 'DIGIDREAMS', 'DIGITAL ENVIRONMENTS', 'DIGITAL', 'DISCOTEK MEDIA', 'DISCOVERY CHANNEL', 'DISCOVERY', 'DISK KINO', 'DISNEY / BUENA VISTA', 'DISNEY', 'BUENA VISTA', 'DISNEY BUENA VISTA', 'DISTRIBUTION SELECT', 'DIVISA', 'DNC ENTERTAINMENT', 'DNC', 'DOGWOOF', 'DOLMEN HOME VIDEO', 'DOLMEN', 'DONAU FILM', 'DONAU', 'DORADO FILMS', 'DORADO', 'DRAFTHOUSE FILMS', 'DRAFTHOUSE', 'DRAGON FILM ENTERTAINMENT', 'DRAGON ENTERTAINMENT', 'DRAGON FILM', 'DRAGON', 'DREAMWORKS', 'DRIVE ON RECORDS', 'DRIVE ON', 'DRIVE-ON', 'DRIVEON', 'DS MEDIA', 'DTP ENTERTAINMENT AG', 'DTP ENTERTAINMENT', 'DTP AG', 'DTP', 'DTS ENTERTAINMENT', 'DTS', 'DUKE MARKETING', 'DUKE VIDEO DISTRIBUTION', 'DUKE', 'DUTCH FILMWORKS', 'DUTCH', 'DVD INTERNATIONAL', 'DVD', 'DYBEX', 'DYNAMIC', 'DYNIT', 'E1 ENTERTAINMENT', 'E1', 'EAGLE ENTERTAINMENT', 'EAGLE HOME ENTERTAINMENT PVT.LTD.', 'EAGLE HOME ENTERTAINMENT PVTLTD', 'EAGLE HOME ENTERTAINMENT PVT LTD', 'EAGLE HOME ENTERTAINMENT', 'EAGLE PICTURES', 'EAGLE ROCK ENTERTAINMENT', 'EAGLE ROCK', 'EAGLE VISION MEDIA', 'EAGLE VISION', 'EARMUSIC', 'EARTH ENTERTAINMENT', 'EARTH', 'ECHO BRIDGE ENTERTAINMENT', 'ECHO BRIDGE', 'EDEL GERMANY GMBH', 'EDEL GERMANY', 'EDEL RECORDS', 'EDITION TONFILM', 'EDITIONS MONTPARNASSE', 'EDKO FILMS LTD.', 'EDKO FILMS LTD', 'EDKO FILMS', + 'EDKO', "EIN'S M&M CO", 'EINS M&M CO', "EIN'S M&M", 'EINS M&M', 'ELEA-MEDIA', 'ELEA MEDIA', 'ELEA', 'ELECTRIC PICTURE', 'ELECTRIC', 'ELEPHANT FILMS', 'ELEPHANT', 'ELEVATION', 'EMI', 'EMON', 'EMS', 'EMYLIA', 'ENE MEDIA', 'ENE', 'ENTERTAINMENT IN VIDEO', 'ENTERTAINMENT IN', 'ENTERTAINMENT ONE', 'ENTERTAINMENT ONE FILMS CANADA INC.', 'ENTERTAINMENT ONE FILMS CANADA INC', 'ENTERTAINMENT ONE FILMS CANADA', 'ENTERTAINMENT ONE CANADA INC', 'ENTERTAINMENT ONE CANADA', 'ENTERTAINMENTONE', 'EONE', 'EOS', 'EPIC PICTURES', 'EPIC', 'EPIC RECORDS', 'ERATO', 'EROS', 'ESC EDITIONS', 'ESCAPI MEDIA BV', 'ESOTERIC RECORDINGS', 'ESPN FILMS', 'EUREKA ENTERTAINMENT', 'EUREKA', 'EURO PICTURES', 'EURO VIDEO', 'EUROARTS', 'EUROPA FILMES', 'EUROPA', 'EUROPACORP', 'EUROZOOM', 'EXCEL', 'EXPLOSIVE MEDIA', 'EXPLOSIVE', 'EXTRALUCID FILMS', 'EXTRALUCID', 'EYE SEE MOVIES', 'EYE SEE', 'EYK MEDIA', 'EYK', 'FABULOUS FILMS', 'FABULOUS', 'FACTORIS FILMS', 'FACTORIS', 'FARAO RECORDS', 'FARBFILM HOME ENTERTAINMENT', 'FARBFILM ENTERTAINMENT', 'FARBFILM HOME', 'FARBFILM', 'FEELGOOD ENTERTAINMENT', 'FEELGOOD', 'FERNSEHJUWELEN', 'FILM CHEST', 'FILM MEDIA', 'FILM MOVEMENT', 'FILM4', 'FILMART', 'FILMAURO', 'FILMAX', 'FILMCONFECT HOME ENTERTAINMENT', 'FILMCONFECT ENTERTAINMENT', 'FILMCONFECT HOME', 'FILMCONFECT', 'FILMEDIA', 'FILMJUWELEN', 'FILMOTEKA NARODAWA', 'FILMRISE', 'FINAL CUT ENTERTAINMENT', 'FINAL CUT', 'FIREHOUSE 12 RECORDS', 'FIREHOUSE 12', 'FIRST INTERNATIONAL PRODUCTION', 'FIRST INTERNATIONAL', 'FIRST LOOK STUDIOS', 'FIRST LOOK', 'FLAGMAN TRADE', 'FLASHSTAR FILMES', 'FLASHSTAR', 'FLICKER ALLEY', 'FNC ADD CULTURE', 'FOCUS FILMES', 'FOCUS', 'FOKUS MEDIA', 'FOKUSA', 'FOX PATHE EUROPA', 'FOX PATHE', 'FOX EUROPA', 'FOX/MGM', 'FOX MGM', 'MGM', 'MGM/FOX', 'FOX', 'FPE', 'FRANCE TÉLÉVISIONS DISTRIBUTION', 'FRANCE TELEVISIONS DISTRIBUTION', 'FRANCE TELEVISIONS', 'FRANCE', 'FREE DOLPHIN ENTERTAINMENT', 'FREE DOLPHIN', 'FREESTYLE DIGITAL MEDIA', 'FREESTYLE DIGITAL', 'FREESTYLE', 'FREMANTLE HOME ENTERTAINMENT', 'FREMANTLE ENTERTAINMENT', 'FREMANTLE HOME', 'FREMANTL', 'FRENETIC FILMS', 'FRENETIC', 'FRONTIER WORKS', 'FRONTIER', 'FRONTIERS MUSIC', 'FRONTIERS RECORDS', 'FS FILM OY', 'FS FILM', 'FULL MOON FEATURES', 'FULL MOON', 'FUN CITY EDITIONS', 'FUN CITY', + 'FUNIMATION ENTERTAINMENT', 'FUNIMATION', 'FUSION', 'FUTUREFILM', 'G2 PICTURES', 'G2', 'GAGA COMMUNICATIONS', 'GAGA', 'GAIAM', 'GALAPAGOS', 'GAMMA HOME ENTERTAINMENT', 'GAMMA ENTERTAINMENT', 'GAMMA HOME', 'GAMMA', 'GARAGEHOUSE PICTURES', 'GARAGEHOUSE', 'GARAGEPLAY (車庫娛樂)', '車庫娛樂', 'GARAGEPLAY (Che Ku Yu Le )', 'GARAGEPLAY', 'Che Ku Yu Le', 'GAUMONT', 'GEFFEN', 'GENEON ENTERTAINMENT', 'GENEON', 'GENEON UNIVERSAL ENTERTAINMENT', 'GENERAL VIDEO RECORDING', 'GLASS DOLL FILMS', 'GLASS DOLL', 'GLOBE MUSIC MEDIA', 'GLOBE MUSIC', 'GLOBE MEDIA', 'GLOBE', 'GO ENTERTAIN', 'GO', 'GOLDEN HARVEST', 'GOOD!MOVIES', 'GOOD! MOVIES', 'GOOD MOVIES', 'GRAPEVINE VIDEO', 'GRAPEVINE', 'GRASSHOPPER FILM', 'GRASSHOPPER FILMS', 'GRASSHOPPER', 'GRAVITAS VENTURES', 'GRAVITAS', 'GREAT MOVIES', 'GREAT', 'GREEN APPLE ENTERTAINMENT', 'GREEN ENTERTAINMENT', 'GREEN APPLE', 'GREEN', 'GREENNARAE MEDIA', 'GREENNARAE', 'GRINDHOUSE RELEASING', 'GRINDHOUSE', 'GRIND HOUSE', 'GRYPHON ENTERTAINMENT', 'GRYPHON', 'GUNPOWDER & SKY', 'GUNPOWDER AND SKY', 'GUNPOWDER SKY', 'GUNPOWDER + SKY', 'GUNPOWDER', 'HANABEE ENTERTAINMENT', 'HANABEE', 'HANNOVER HOUSE', 'HANNOVER', 'HANSESOUND', 'HANSE SOUND', 'HANSE', 'HAPPINET', 'HARMONIA MUNDI', 'HARMONIA', 'HBO', 'HDC', 'HEC', 'HELL & BACK RECORDINGS', 'HELL AND BACK RECORDINGS', 'HELL & BACK', 'HELL AND BACK', "HEN'S TOOTH VIDEO", 'HENS TOOTH VIDEO', "HEN'S TOOTH", 'HENS TOOTH', 'HIGH FLIERS', 'HIGHLIGHT', 'HILLSONG', 'HISTORY CHANNEL', 'HISTORY', 'HK VIDÉO', 'HK VIDEO', 'HK', 'HMH HAMBURGER MEDIEN HAUS', 'HAMBURGER MEDIEN HAUS', 'HMH HAMBURGER MEDIEN', 'HMH HAMBURGER', 'HMH', 'HOLLYWOOD CLASSIC ENTERTAINMENT', 'HOLLYWOOD CLASSIC', 'HOLLYWOOD PICTURES', 'HOLLYWOOD', 'HOPSCOTCH ENTERTAINMENT', 'HOPSCOTCH', 'HPM', 'HÄNNSLER CLASSIC', 'HANNSLER CLASSIC', 'HANNSLER', 'I-CATCHER', 'I CATCHER', 'ICATCHER', 'I-ON NEW MEDIA', 'I ON NEW MEDIA', 'ION NEW MEDIA', 'ION MEDIA', 'I-ON', 'ION', 'IAN PRODUCTIONS', 'IAN', 'ICESTORM', 'ICON FILM DISTRIBUTION', 'ICON DISTRIBUTION', 'ICON FILM', 'ICON', 'IDEALE AUDIENCE', 'IDEALE', 'IFC FILMS', 'IFC', 'IFILM', 'ILLUSIONS UNLTD.', 'ILLUSIONS UNLTD', 'ILLUSIONS', 'IMAGE ENTERTAINMENT', 'IMAGE', 'IMAGEM FILMES', 'IMAGEM', 'IMOVISION', 'IMPERIAL CINEPIX', 'IMPRINT', 'IMPULS HOME ENTERTAINMENT', 'IMPULS ENTERTAINMENT', 'IMPULS HOME', 'IMPULS', 'IN-AKUSTIK', 'IN AKUSTIK', 'INAKUSTIK', 'INCEPTION MEDIA GROUP', 'INCEPTION MEDIA', 'INCEPTION GROUP', 'INCEPTION', 'INDEPENDENT', 'INDICAN', 'INDIE RIGHTS', 'INDIE', 'INDIGO', 'INFO', 'INJOINGAN', 'INKED PICTURES', 'INKED', 'INSIDE OUT MUSIC', 'INSIDE MUSIC', 'INSIDE OUT', 'INSIDE', 'INTERCOM', 'INTERCONTINENTAL VIDEO', 'INTERCONTINENTAL', 'INTERGROOVE', 'INTERSCOPE', 'INVINCIBLE PICTURES', 'INVINCIBLE', 'ISLAND/MERCURY', 'ISLAND MERCURY', 'ISLANDMERCURY', 'ISLAND & MERCURY', 'ISLAND AND MERCURY', 'ISLAND', 'ITN', 'ITV DVD', 'ITV', 'IVC', 'IVE ENTERTAINMENT', 'IVE', 'J&R ADVENTURES', 'J&R', 'JR', 'JAKOB', 'JONU MEDIA', 'JONU', 'JRB PRODUCTIONS', 'JRB', 'JUST BRIDGE ENTERTAINMENT', 'JUST BRIDGE', 'JUST ENTERTAINMENT', 'JUST', 'KABOOM ENTERTAINMENT', 'KABOOM', 'KADOKAWA ENTERTAINMENT', 'KADOKAWA', 'KAIROS', 'KALEIDOSCOPE ENTERTAINMENT', 'KALEIDOSCOPE', 'KAM & RONSON ENTERPRISES', 'KAM & RONSON', 'KAM&RONSON ENTERPRISES', 'KAM&RONSON', 'KAM AND RONSON ENTERPRISES', 'KAM AND RONSON', 'KANA HOME VIDEO', 'KARMA FILMS', 'KARMA', 'KATZENBERGER', 'KAZE', + 'KBS MEDIA', 'KBS', 'KD MEDIA', 'KD', 'KING MEDIA', 'KING', 'KING RECORDS', 'KINO LORBER', 'KINO', 'KINO SWIAT', 'KINOKUNIYA', 'KINOWELT HOME ENTERTAINMENT/DVD', 'KINOWELT HOME ENTERTAINMENT', 'KINOWELT ENTERTAINMENT', 'KINOWELT HOME DVD', 'KINOWELT ENTERTAINMENT/DVD', 'KINOWELT DVD', 'KINOWELT', 'KIT PARKER FILMS', 'KIT PARKER', 'KITTY MEDIA', 'KNM HOME ENTERTAINMENT', 'KNM ENTERTAINMENT', 'KNM HOME', 'KNM', 'KOBA FILMS', 'KOBA', 'KOCH ENTERTAINMENT', 'KOCH MEDIA', 'KOCH', 'KRAKEN RELEASING', 'KRAKEN', 'KSCOPE', 'KSM', 'KULTUR', "L'ATELIER D'IMAGES", "LATELIER D'IMAGES", "L'ATELIER DIMAGES", 'LATELIER DIMAGES', "L ATELIER D'IMAGES", "L'ATELIER D IMAGES", + 'L ATELIER D IMAGES', "L'ATELIER", 'L ATELIER', 'LATELIER', 'LA AVENTURA AUDIOVISUAL', 'LA AVENTURA', 'LACE GROUP', 'LACE', 'LASER PARADISE', 'LAYONS', 'LCJ EDITIONS', 'LCJ', 'LE CHAT QUI FUME', 'LE PACTE', 'LEDICK FILMHANDEL', 'LEGEND', 'LEOMARK STUDIOS', 'LEOMARK', 'LEONINE FILMS', 'LEONINE', 'LICHTUNG MEDIA LTD', 'LICHTUNG LTD', 'LICHTUNG MEDIA LTD.', 'LICHTUNG LTD.', 'LICHTUNG MEDIA', 'LICHTUNG', 'LIGHTHOUSE HOME ENTERTAINMENT', 'LIGHTHOUSE ENTERTAINMENT', 'LIGHTHOUSE HOME', 'LIGHTHOUSE', 'LIGHTYEAR', 'LIONSGATE FILMS', 'LIONSGATE', 'LIZARD CINEMA TRADE', 'LLAMENTOL', 'LOBSTER FILMS', 'LOBSTER', 'LOGON', 'LORBER FILMS', 'LORBER', 'LOS BANDITOS FILMS', 'LOS BANDITOS', 'LOUD & PROUD RECORDS', 'LOUD AND PROUD RECORDS', 'LOUD & PROUD', 'LOUD AND PROUD', 'LSO LIVE', 'LUCASFILM', 'LUCKY RED', 'LUMIÈRE HOME ENTERTAINMENT', 'LUMIERE HOME ENTERTAINMENT', 'LUMIERE ENTERTAINMENT', 'LUMIERE HOME', 'LUMIERE', 'M6 VIDEO', 'M6', 'MAD DIMENSION', 'MADMAN ENTERTAINMENT', 'MADMAN', 'MAGIC BOX', 'MAGIC PLAY', 'MAGNA HOME ENTERTAINMENT', 'MAGNA ENTERTAINMENT', 'MAGNA HOME', 'MAGNA', 'MAGNOLIA PICTURES', 'MAGNOLIA', 'MAIDEN JAPAN', 'MAIDEN', 'MAJENG MEDIA', 'MAJENG', 'MAJESTIC HOME ENTERTAINMENT', 'MAJESTIC ENTERTAINMENT', 'MAJESTIC HOME', 'MAJESTIC', 'MANGA HOME ENTERTAINMENT', 'MANGA ENTERTAINMENT', 'MANGA HOME', 'MANGA', 'MANTA LAB', 'MAPLE STUDIOS', 'MAPLE', 'MARCO POLO PRODUCTION', 'MARCO POLO', 'MARIINSKY', 'MARVEL STUDIOS', 'MARVEL', 'MASCOT RECORDS', 'MASCOT', 'MASSACRE VIDEO', 'MASSACRE', 'MATCHBOX', 'MATRIX D', 'MAXAM', 'MAYA HOME ENTERTAINMENT', 'MAYA ENTERTAINMENT', 'MAYA HOME', 'MAYAT', 'MDG', 'MEDIA BLASTERS', 'MEDIA FACTORY', 'MEDIA TARGET DISTRIBUTION', 'MEDIA TARGET', 'MEDIAINVISION', 'MEDIATOON', 'MEDIATRES ESTUDIO', 'MEDIATRES STUDIO', 'MEDIATRES', 'MEDICI ARTS', 'MEDICI CLASSICS', 'MEDIUMRARE ENTERTAINMENT', 'MEDIUMRARE', 'MEDUSA', 'MEGASTAR', 'MEI AH', 'MELI MÉDIAS', 'MELI MEDIAS', 'MEMENTO FILMS', 'MEMENTO', 'MENEMSHA FILMS', 'MENEMSHA', 'MERCURY', 'MERCURY STUDIOS', 'MERGE SOFT PRODUCTIONS', 'MERGE PRODUCTIONS', 'MERGE SOFT', 'MERGE', 'METAL BLADE RECORDS', 'METAL BLADE', 'METEOR', 'METRO-GOLDWYN-MAYER', 'METRO GOLDWYN MAYER', 'METROGOLDWYNMAYER', 'METRODOME VIDEO', 'METRODOME', 'METROPOLITAN', 'MFA+', 'MFA', 'MIG FILMGROUP', 'MIG', 'MILESTONE', 'MILL CREEK ENTERTAINMENT', 'MILL CREEK', 'MILLENNIUM MEDIA', 'MILLENNIUM', 'MIRAGE ENTERTAINMENT', 'MIRAGE', 'MIRAMAX', 'MISTERIYA ZVUKA', 'MK2', 'MODE RECORDS', 'MODE', 'MOMENTUM PICTURES', 'MONDO HOME ENTERTAINMENT', 'MONDO ENTERTAINMENT', 'MONDO HOME', 'MONDO MACABRO', 'MONGREL MEDIA', 'MONOLIT', 'MONOLITH VIDEO', 'MONOLITH', 'MONSTER PICTURES', 'MONSTER', 'MONTEREY VIDEO', 'MONTEREY', 'MONUMENT RELEASING', 'MONUMENT', 'MORNINGSTAR', 'MORNING STAR', 'MOSERBAER', 'MOVIEMAX', 'MOVINSIDE', 'MPI MEDIA GROUP', 'MPI MEDIA', 'MPI', 'MR. BONGO FILMS', 'MR BONGO FILMS', 'MR BONGO', 'MRG (MERIDIAN)', 'MRG MERIDIAN', 'MRG', 'MERIDIAN', 'MUBI', 'MUG SHOT PRODUCTIONS', 'MUG SHOT', 'MULTIMUSIC', 'MULTI-MUSIC', 'MULTI MUSIC', 'MUSE', 'MUSIC BOX FILMS', 'MUSIC BOX', 'MUSICBOX', 'MUSIC BROKERS', 'MUSIC THEORIES', 'MUSIC VIDEO DISTRIBUTORS', 'MUSIC VIDEO', 'MUSTANG ENTERTAINMENT', 'MUSTANG', 'MVD VISUAL', 'MVD', 'MVD/VSC', 'MVL', 'MVM ENTERTAINMENT', 'MVM', 'MYNDFORM', 'MYSTIC NIGHT PICTURES', 'MYSTIC NIGHT', 'NAMELESS MEDIA', 'NAMELESS', 'NAPALM RECORDS', 'NAPALM', 'NATIONAL ENTERTAINMENT MEDIA', 'NATIONAL ENTERTAINMENT', 'NATIONAL MEDIA', 'NATIONAL FILM ARCHIVE', 'NATIONAL ARCHIVE', 'NATIONAL FILM', 'NATIONAL GEOGRAPHIC', 'NAT GEO TV', 'NAT GEO', 'NGO', 'NAXOS', 'NBCUNIVERSAL ENTERTAINMENT JAPAN', 'NBC UNIVERSAL ENTERTAINMENT JAPAN', 'NBCUNIVERSAL JAPAN', 'NBC UNIVERSAL JAPAN', 'NBC JAPAN', 'NBO ENTERTAINMENT', 'NBO', 'NEOS', 'NETFLIX', 'NETWORK', 'NEW BLOOD', 'NEW DISC', 'NEW KSM', 'NEW LINE CINEMA', 'NEW LINE', 'NEW MOVIE TRADING CO. LTD', 'NEW MOVIE TRADING CO LTD', 'NEW MOVIE TRADING CO', 'NEW MOVIE TRADING', 'NEW WAVE FILMS', 'NEW WAVE', 'NFI', 'NHK', 'NIPPONART', 'NIS AMERICA', 'NJUTAFILMS', 'NOBLE ENTERTAINMENT', 'NOBLE', 'NORDISK FILM', 'NORDISK', 'NORSK FILM', 'NORSK', 'NORTH AMERICAN MOTION PICTURES', 'NOS AUDIOVISUAIS', 'NOTORIOUS PICTURES', 'NOTORIOUS', 'NOVA MEDIA', 'NOVA', 'NOVA SALES AND DISTRIBUTION', 'NOVA SALES & DISTRIBUTION', 'NSM', 'NSM RECORDS', 'NUCLEAR BLAST', 'NUCLEUS FILMS', 'NUCLEUS', 'OBERLIN MUSIC', 'OBERLIN', 'OBRAS-PRIMAS DO CINEMA', 'OBRAS PRIMAS DO CINEMA', 'OBRASPRIMAS DO CINEMA', 'OBRAS-PRIMAS CINEMA', 'OBRAS PRIMAS CINEMA', 'OBRASPRIMAS CINEMA', 'OBRAS-PRIMAS', 'OBRAS PRIMAS', 'OBRASPRIMAS', 'ODEON', 'OFDB FILMWORKS', 'OFDB', 'OLIVE FILMS', 'OLIVE', 'ONDINE', 'ONSCREEN FILMS', 'ONSCREEN', 'OPENING DISTRIBUTION', 'OPERA AUSTRALIA', 'OPTIMUM HOME ENTERTAINMENT', 'OPTIMUM ENTERTAINMENT', 'OPTIMUM HOME', 'OPTIMUM', 'OPUS ARTE', 'ORANGE STUDIO', 'ORANGE', 'ORLANDO EASTWOOD FILMS', 'ORLANDO FILMS', 'ORLANDO EASTWOOD', 'ORLANDO', 'ORUSTAK PICTURES', 'ORUSTAK', 'OSCILLOSCOPE PICTURES', 'OSCILLOSCOPE', 'OUTPLAY', 'PALISADES TARTAN', 'PAN VISION', 'PANVISION', 'PANAMINT CINEMA', 'PANAMINT', 'PANDASTORM ENTERTAINMENT', 'PANDA STORM ENTERTAINMENT', 'PANDASTORM', 'PANDA STORM', 'PANDORA FILM', 'PANDORA', 'PANEGYRIC', 'PANORAMA', 'PARADE DECK FILMS', 'PARADE DECK', 'PARADISE', 'PARADISO FILMS', 'PARADOX', 'PARAMOUNT PICTURES', 'PARAMOUNT', 'PARIS FILMES', 'PARIS FILMS', 'PARIS', 'PARK CIRCUS', 'PARLOPHONE', 'PASSION RIVER', 'PATHE DISTRIBUTION', 'PATHE', 'PBS', 'PEACE ARCH TRINITY', 'PECCADILLO PICTURES', 'PEPPERMINT', 'PHASE 4 FILMS', 'PHASE 4', 'PHILHARMONIA BAROQUE', 'PICTURE HOUSE ENTERTAINMENT', 'PICTURE ENTERTAINMENT', 'PICTURE HOUSE', 'PICTURE', 'PIDAX', + 'PINK FLOYD RECORDS', 'PINK FLOYD', 'PINNACLE FILMS', 'PINNACLE', 'PLAIN', 'PLATFORM ENTERTAINMENT LIMITED', 'PLATFORM ENTERTAINMENT LTD', 'PLATFORM ENTERTAINMENT LTD.', 'PLATFORM ENTERTAINMENT', 'PLATFORM', 'PLAYARTE', 'PLG UK CLASSICS', 'PLG UK', 'PLG', 'POLYBAND & TOPPIC VIDEO/WVG', 'POLYBAND AND TOPPIC VIDEO/WVG', 'POLYBAND & TOPPIC VIDEO WVG', 'POLYBAND & TOPPIC VIDEO AND WVG', 'POLYBAND & TOPPIC VIDEO & WVG', 'POLYBAND AND TOPPIC VIDEO WVG', 'POLYBAND AND TOPPIC VIDEO AND WVG', 'POLYBAND AND TOPPIC VIDEO & WVG', 'POLYBAND & TOPPIC VIDEO', 'POLYBAND AND TOPPIC VIDEO', 'POLYBAND & TOPPIC', 'POLYBAND AND TOPPIC', 'POLYBAND', 'WVG', 'POLYDOR', 'PONY', 'PONY CANYON', 'POTEMKINE', 'POWERHOUSE FILMS', 'POWERHOUSE', 'POWERSTATIOM', 'PRIDE & JOY', 'PRIDE AND JOY', 'PRINZ MEDIA', 'PRINZ', 'PRIS AUDIOVISUAIS', 'PRO VIDEO', 'PRO-VIDEO', 'PRO-MOTION', 'PRO MOTION', 'PROD. JRB', 'PROD JRB', 'PRODISC', 'PROKINO', 'PROVOGUE RECORDS', 'PROVOGUE', 'PROWARE', 'PULP VIDEO', 'PULP', 'PULSE VIDEO', 'PULSE', 'PURE AUDIO RECORDINGS', 'PURE AUDIO', 'PURE FLIX ENTERTAINMENT', 'PURE FLIX', 'PURE ENTERTAINMENT', 'PYRAMIDE VIDEO', 'PYRAMIDE', 'QUALITY FILMS', 'QUALITY', 'QUARTO VALLEY RECORDS', 'QUARTO VALLEY', 'QUESTAR', 'R SQUARED FILMS', 'R SQUARED', 'RAPID EYE MOVIES', 'RAPID EYE', 'RARO VIDEO', 'RARO', 'RAROVIDEO U.S.', 'RAROVIDEO US', 'RARO VIDEO US', 'RARO VIDEO U.S.', 'RARO U.S.', 'RARO US', 'RAVEN BANNER RELEASING', 'RAVEN BANNER', 'RAVEN', 'RAZOR DIGITAL ENTERTAINMENT', 'RAZOR DIGITAL', 'RCA', 'RCO LIVE', 'RCO', 'RCV', 'REAL GONE MUSIC', 'REAL GONE', 'REANIMEDIA', 'REANI MEDIA', 'REDEMPTION', 'REEL', 'RELIANCE HOME VIDEO & GAMES', 'RELIANCE HOME VIDEO AND GAMES', 'RELIANCE HOME VIDEO', 'RELIANCE VIDEO', 'RELIANCE HOME', 'RELIANCE', 'REM CULTURE', 'REMAIN IN LIGHT', 'REPRISE', 'RESEN', 'RETROMEDIA', 'REVELATION FILMS LTD.', 'REVELATION FILMS LTD', 'REVELATION FILMS', 'REVELATION LTD.', 'REVELATION LTD', 'REVELATION', 'REVOLVER ENTERTAINMENT', 'REVOLVER', 'RHINO MUSIC', 'RHINO', 'RHV', 'RIGHT STUF', 'RIMINI EDITIONS', 'RISING SUN MEDIA', 'RLJ ENTERTAINMENT', 'RLJ', 'ROADRUNNER RECORDS', 'ROADSHOW ENTERTAINMENT', 'ROADSHOW', 'RONE', 'RONIN FLIX', 'ROTANA HOME ENTERTAINMENT', 'ROTANA ENTERTAINMENT', 'ROTANA HOME', 'ROTANA', 'ROUGH TRADE', + 'ROUNDER', 'SAFFRON HILL FILMS', 'SAFFRON HILL', 'SAFFRON', 'SAMUEL GOLDWYN FILMS', 'SAMUEL GOLDWYN', 'SAN FRANCISCO SYMPHONY', 'SANDREW METRONOME', 'SAPHRANE', 'SAVOR', 'SCANBOX ENTERTAINMENT', 'SCANBOX', 'SCENIC LABS', 'SCHRÖDERMEDIA', 'SCHRODERMEDIA', 'SCHRODER MEDIA', 'SCORPION RELEASING', 'SCORPION', 'SCREAM TEAM RELEASING', 'SCREAM TEAM', 'SCREEN MEDIA', 'SCREEN', 'SCREENBOUND PICTURES', 'SCREENBOUND', 'SCREENWAVE MEDIA', 'SCREENWAVE', 'SECOND RUN', 'SECOND SIGHT', 'SEEDSMAN GROUP', 'SELECT VIDEO', 'SELECTA VISION', 'SENATOR', 'SENTAI FILMWORKS', 'SENTAI', 'SEVEN7', 'SEVERIN FILMS', 'SEVERIN', 'SEVILLE', 'SEYONS ENTERTAINMENT', 'SEYONS', 'SF STUDIOS', 'SGL ENTERTAINMENT', 'SGL', 'SHAMELESS', 'SHAMROCK MEDIA', 'SHAMROCK', 'SHANGHAI EPIC MUSIC ENTERTAINMENT', 'SHANGHAI EPIC ENTERTAINMENT', 'SHANGHAI EPIC MUSIC', 'SHANGHAI MUSIC ENTERTAINMENT', 'SHANGHAI ENTERTAINMENT', 'SHANGHAI MUSIC', 'SHANGHAI', 'SHEMAROO', 'SHOCHIKU', 'SHOCK', 'SHOGAKU KAN', 'SHOUT FACTORY', 'SHOUT! FACTORY', 'SHOUT', 'SHOUT!', 'SHOWBOX', 'SHOWTIME ENTERTAINMENT', 'SHOWTIME', 'SHRIEK SHOW', 'SHUDDER', 'SIDONIS', 'SIDONIS CALYSTA', 'SIGNAL ONE ENTERTAINMENT', 'SIGNAL ONE', 'SIGNATURE ENTERTAINMENT', 'SIGNATURE', 'SILVER VISION', 'SINISTER FILM', 'SINISTER', 'SIREN VISUAL ENTERTAINMENT', 'SIREN VISUAL', 'SIREN ENTERTAINMENT', 'SIREN', 'SKANI', 'SKY DIGI', + 'SLASHER // VIDEO', 'SLASHER / VIDEO', 'SLASHER VIDEO', 'SLASHER', 'SLOVAK FILM INSTITUTE', 'SLOVAK FILM', 'SFI', 'SM LIFE DESIGN GROUP', 'SMOOTH PICTURES', 'SMOOTH', 'SNAPPER MUSIC', 'SNAPPER', 'SODA PICTURES', 'SODA', 'SONO LUMINUS', 'SONY MUSIC', 'SONY PICTURES', 'SONY', 'SONY PICTURES CLASSICS', 'SONY CLASSICS', 'SOUL MEDIA', 'SOUL', 'SOULFOOD MUSIC DISTRIBUTION', 'SOULFOOD DISTRIBUTION', 'SOULFOOD MUSIC', 'SOULFOOD', 'SOYUZ', 'SPECTRUM', 'SPENTZOS FILM', 'SPENTZOS', 'SPIRIT ENTERTAINMENT', 'SPIRIT', 'SPIRIT MEDIA GMBH', 'SPIRIT MEDIA', 'SPLENDID ENTERTAINMENT', 'SPLENDID FILM', 'SPO', 'SQUARE ENIX', 'SRI BALAJI VIDEO', 'SRI BALAJI', 'SRI', 'SRI VIDEO', 'SRS CINEMA', 'SRS', 'SSO RECORDINGS', 'SSO', 'ST2 MUSIC', 'ST2', 'STAR MEDIA ENTERTAINMENT', 'STAR ENTERTAINMENT', 'STAR MEDIA', 'STAR', 'STARLIGHT', 'STARZ / ANCHOR BAY', 'STARZ ANCHOR BAY', 'STARZ', 'ANCHOR BAY', 'STER KINEKOR', 'STERLING ENTERTAINMENT', 'STERLING', 'STINGRAY', 'STOCKFISCH RECORDS', 'STOCKFISCH', 'STRAND RELEASING', 'STRAND', 'STUDIO 4K', 'STUDIO CANAL', 'STUDIO GHIBLI', 'GHIBLI', 'STUDIO HAMBURG ENTERPRISES', 'HAMBURG ENTERPRISES', 'STUDIO HAMBURG', 'HAMBURG', 'STUDIO S', 'SUBKULTUR ENTERTAINMENT', 'SUBKULTUR', 'SUEVIA FILMS', 'SUEVIA', 'SUMMIT ENTERTAINMENT', 'SUMMIT', 'SUNFILM ENTERTAINMENT', 'SUNFILM', 'SURROUND RECORDS', 'SURROUND', 'SVENSK FILMINDUSTRI', 'SVENSK', 'SWEN FILMES', 'SWEN FILMS', 'SWEN', 'SYNAPSE FILMS', 'SYNAPSE', 'SYNDICADO', 'SYNERGETIC', 'T- SERIES', 'T-SERIES', 'T SERIES', 'TSERIES', 'T.V.P.', 'TVP', 'TACET RECORDS', 'TACET', 'TAI SENG', 'TAI SHENG', 'TAKEONE', 'TAKESHOBO', 'TAMASA DIFFUSION', 'TC ENTERTAINMENT', 'TC', 'TDK', 'TEAM MARKETING', 'TEATRO REAL', 'TEMA DISTRIBUCIONES', 'TEMPE DIGITAL', 'TF1 VIDÉO', 'TF1 VIDEO', 'TF1', 'THE BLU', 'BLU', 'THE ECSTASY OF FILMS', 'THE FILM DETECTIVE', 'FILM DETECTIVE', 'THE JOKERS', 'JOKERS', 'THE ON', 'ON', 'THIMFILM', 'THIM FILM', 'THIM', 'THIRD WINDOW FILMS', 'THIRD WINDOW', '3RD WINDOW FILMS', '3RD WINDOW', 'THUNDERBEAN ANIMATION', 'THUNDERBEAN', 'THUNDERBIRD RELEASING', 'THUNDERBIRD', 'TIBERIUS FILM', 'TIME LIFE', 'TIMELESS MEDIA GROUP', 'TIMELESS MEDIA', 'TIMELESS GROUP', 'TIMELESS', 'TLA RELEASING', 'TLA', 'TOBIS FILM', 'TOBIS', 'TOEI', 'TOHO', 'TOKYO SHOCK', 'TOKYO', 'TONPOOL MEDIEN GMBH', 'TONPOOL MEDIEN', 'TOPICS ENTERTAINMENT', 'TOPICS', 'TOUCHSTONE PICTURES', 'TOUCHSTONE', 'TRANSMISSION FILMS', 'TRANSMISSION', 'TRAVEL VIDEO STORE', 'TRIART', 'TRIGON FILM', 'TRIGON', 'TRINITY HOME ENTERTAINMENT', 'TRINITY ENTERTAINMENT', 'TRINITY HOME', 'TRINITY', 'TRIPICTURES', 'TRI-PICTURES', 'TRI PICTURES', 'TROMA', 'TURBINE MEDIEN', 'TURTLE RECORDS', 'TURTLE', 'TVA FILMS', 'TVA', 'TWILIGHT TIME', 'TWILIGHT', 'TT', 'TWIN CO., LTD.', 'TWIN CO, LTD.', 'TWIN CO., LTD', 'TWIN CO, LTD', 'TWIN CO LTD', 'TWIN LTD', 'TWIN CO.', 'TWIN CO', 'TWIN', 'UCA', 'UDR', 'UEK', 'UFA/DVD', 'UFA DVD', 'UFADVD', 'UGC PH', 'ULTIMATE3DHEAVEN', 'ULTRA', 'UMBRELLA ENTERTAINMENT', 'UMBRELLA', 'UMC', "UNCORK'D ENTERTAINMENT", 'UNCORKD ENTERTAINMENT', 'UNCORK D ENTERTAINMENT', "UNCORK'D", 'UNCORK D', 'UNCORKD', 'UNEARTHED FILMS', 'UNEARTHED', 'UNI DISC', 'UNIMUNDOS', 'UNITEL', 'UNIVERSAL MUSIC', 'UNIVERSAL SONY PICTURES HOME ENTERTAINMENT', 'UNIVERSAL SONY PICTURES ENTERTAINMENT', 'UNIVERSAL SONY PICTURES HOME', 'UNIVERSAL SONY PICTURES', 'UNIVERSAL HOME ENTERTAINMENT', 'UNIVERSAL ENTERTAINMENT', + 'UNIVERSAL HOME', 'UNIVERSAL STUDIOS', 'UNIVERSAL', 'UNIVERSE LASER & VIDEO CO.', 'UNIVERSE LASER AND VIDEO CO.', 'UNIVERSE LASER & VIDEO CO', 'UNIVERSE LASER AND VIDEO CO', 'UNIVERSE LASER CO.', 'UNIVERSE LASER CO', 'UNIVERSE LASER', 'UNIVERSUM FILM', 'UNIVERSUM', 'UTV', 'VAP', 'VCI', 'VENDETTA FILMS', 'VENDETTA', 'VERSÁTIL HOME VIDEO', 'VERSÁTIL VIDEO', 'VERSÁTIL HOME', 'VERSÁTIL', 'VERSATIL HOME VIDEO', 'VERSATIL VIDEO', 'VERSATIL HOME', 'VERSATIL', 'VERTICAL ENTERTAINMENT', 'VERTICAL', 'VÉRTICE 360º', 'VÉRTICE 360', 'VERTICE 360o', 'VERTICE 360', 'VERTIGO BERLIN', 'VÉRTIGO FILMS', 'VÉRTIGO', 'VERTIGO FILMS', 'VERTIGO', 'VERVE PICTURES', 'VIA VISION ENTERTAINMENT', 'VIA VISION', 'VICOL ENTERTAINMENT', 'VICOL', 'VICOM', 'VICTOR ENTERTAINMENT', 'VICTOR', 'VIDEA CDE', 'VIDEO FILM EXPRESS', 'VIDEO FILM', 'VIDEO EXPRESS', 'VIDEO MUSIC, INC.', 'VIDEO MUSIC, INC', 'VIDEO MUSIC INC.', 'VIDEO MUSIC INC', 'VIDEO MUSIC', 'VIDEO SERVICE CORP.', 'VIDEO SERVICE CORP', 'VIDEO SERVICE', 'VIDEO TRAVEL', 'VIDEOMAX', 'VIDEO MAX', 'VII PILLARS ENTERTAINMENT', 'VII PILLARS', 'VILLAGE FILMS', 'VINEGAR SYNDROME', 'VINEGAR', 'VS', 'VINNY MOVIES', 'VINNY', 'VIRGIL FILMS & ENTERTAINMENT', 'VIRGIL FILMS AND ENTERTAINMENT', 'VIRGIL ENTERTAINMENT', 'VIRGIL FILMS', 'VIRGIL', 'VIRGIN RECORDS', 'VIRGIN', 'VISION FILMS', 'VISION', 'VISUAL ENTERTAINMENT GROUP', 'VISUAL GROUP', 'VISUAL ENTERTAINMENT', 'VISUAL', 'VIVENDI VISUAL ENTERTAINMENT', 'VIVENDI VISUAL', 'VIVENDI', 'VIZ PICTURES', 'VIZ', 'VLMEDIA', 'VL MEDIA', 'VL', 'VOLGA', 'VVS FILMS', 'VVS', 'VZ HANDELS GMBH', 'VZ HANDELS', 'WARD RECORDS', 'WARD', 'WARNER BROS.', 'WARNER BROS', 'WARNER ARCHIVE', 'WARNER ARCHIVE COLLECTION', 'WAC', 'WARNER', 'WARNER MUSIC', 'WEA', 'WEINSTEIN COMPANY', 'WEINSTEIN', 'WELL GO USA', 'WELL GO', 'WELTKINO FILMVERLEIH', 'WEST VIDEO', 'WEST', 'WHITE PEARL MOVIES', 'WHITE PEARL', 'WICKED-VISION MEDIA', 'WICKED VISION MEDIA', 'WICKEDVISION MEDIA', 'WICKED-VISION', 'WICKED VISION', 'WICKEDVISION', 'WIENERWORLD', 'WILD BUNCH', 'WILD EYE RELEASING', 'WILD EYE', 'WILD SIDE VIDEO', 'WILD SIDE', 'WME', 'WOLFE VIDEO', 'WOLFE', 'WORD ON FIRE', 'WORKS FILM GROUP', 'WORLD WRESTLING', 'WVG MEDIEN', 'WWE STUDIOS', 'WWE', 'X RATED KULT', 'X-RATED KULT', 'X RATED CULT', 'X-RATED CULT', 'X RATED', 'X-RATED', 'XCESS', 'XLRATOR', 'XT VIDEO', 'XT', 'YAMATO VIDEO', 'YAMATO', 'YASH RAJ FILMS', 'YASH RAJS', 'ZEITGEIST FILMS', 'ZEITGEIST', 'ZENITH PICTURES', 'ZENITH', 'ZIMA', 'ZYLO', 'ZYX MUSIC', 'ZYX', 'MASTERS OF CINEMA', 'MOC' ] @@ -2005,10 +1997,10 @@ def get_distributor(self, distributor_in): def get_video_codec(self, bdinfo): codecs = { - "MPEG-2 Video" : "MPEG-2", - "MPEG-4 AVC Video" : "AVC", - "MPEG-H HEVC Video" : "HEVC", - "VC-1 Video" : "VC-1" + "MPEG-2 Video": "MPEG-2", + "MPEG-4 AVC Video": "AVC", + "MPEG-H HEVC Video": "HEVC", + "VC-1 Video": "VC-1" } codec = codecs.get(bdinfo['video'][0]['codec'], "") return codec @@ -2027,18 +2019,18 @@ def get_video_encode(self, mi, type, bdinfo): except: format = bdinfo['video'][0]['codec'] format_profile = bdinfo['video'][0]['profile'] - if type in ("ENCODE", "WEBRIP"): #ENCODE or WEBRIP + if type in ("ENCODE", "WEBRIP"): # ENCODE or WEBRIP if format == 'AVC': codec = 'x264' elif format == 'HEVC': codec = 'x265' - elif type in ('WEBDL', 'HDTV'): #WEB-DL + elif type in ('WEBDL', 'HDTV'): # WEB-DL if format == 'AVC': codec = 'H.264' elif format == 'HEVC': codec = 'H.265' - - if type == 'HDTV' and has_encode_settings == True: + + if type == 'HDTV' and has_encode_settings is True: codec = codec.replace('H.', 'x') elif format == "VP9": codec = "VP9" @@ -2057,12 +2049,12 @@ def get_video_encode(self, mi, type, bdinfo): def get_edition(self, video, bdinfo, filelist, manual_edition): if video.lower().startswith('dc'): video = video.replace('dc', '', 1) - + guess = guessit(video) tag = guess.get('release_group', 'NOGROUP') repack = "" edition = "" - + if bdinfo is not None: try: edition = guessit(bdinfo['label'])['edition'] @@ -2075,10 +2067,10 @@ def get_edition(self, video, bdinfo, filelist, manual_edition): except Exception as e: print(f"Video Edition Guess Error: {e}") edition = "" - + if isinstance(edition, list): edition = " ".join(edition) - + if len(filelist) == 1: video = os.path.basename(video) @@ -2089,9 +2081,9 @@ def get_edition(self, video, bdinfo, filelist, manual_edition): if manual_edition: edition = str(manual_edition) - + print(f"Edition After Manual Edition: {edition}") - + if "REPACK" in edition.upper() or "V2" in video: repack = "REPACK" if "REPACK2" in edition.upper() or "V3" in video: @@ -2102,16 +2094,16 @@ def get_edition(self, video, bdinfo, filelist, manual_edition): repack = "PROPER" if "RERIP" in edition.upper(): repack = "RERIP" - + print(f"Repack after Checks: {repack}") - + # Only remove REPACK, RERIP, or PROPER from edition if they're not part of manual_edition edition = re.sub(r"(\bREPACK\d?\b|\bRERIP\b|\bPROPER\b)", "", edition, flags=re.IGNORECASE).strip() bad = ['internal', 'limited', 'retail'] if edition.lower() in bad: edition = "" - + return edition, repack """ @@ -2205,7 +2197,7 @@ def create_torrent(self, meta, path, output_filename): if meta['is_disc']: include, exclude = "", "" else: - exclude = ["*.*", "*sample.mkv", "!sample*.*"] + exclude = ["*.*", "*sample.mkv", "!sample*.*"] include = ["*.mkv", "*.mp4", "*.ts"] # Create and write the new torrent using the CustomTorrent class @@ -2231,7 +2223,7 @@ def create_torrent(self, meta, path, output_filename): console.print("[bold green].torrent created", end="\r") return torrent - + def torf_cb(self, torrent, filepath, pieces_done, pieces_total): # print(f'{pieces_done/pieces_total*100:3.0f} % done') cli_ui.info_progress("Hashing...", pieces_done, pieces_total) @@ -2250,7 +2242,7 @@ def create_base_from_existing_torrent(self, torrentpath, base_dir, uuid): base_torrent.trackers = ['https://fake.tracker'] base_torrent.comment = "Created by L4G's Upload Assistant" base_torrent.created_by = "Created by L4G's Upload Assistant" - #Remove Un-whitelisted info from torrent + # Remove Un-whitelisted info from torrent for each in list(base_torrent.metainfo['info']): if each not in ('files', 'length', 'name', 'piece length', 'pieces', 'private', 'source'): base_torrent.metainfo['info'].pop(each, None) @@ -2400,7 +2392,7 @@ def upload_screens(self, meta, screens, img_host_num, i, total_screens, custom_i # If we broke out of the loop due to a failure, switch to the next host and retry img_host_num += 1 if img_host_num > len(self.config['DEFAULT']) - 1: - console.print(f"[red]All image hosts failed. Unable to complete uploads.") + console.print("[red]All image hosts failed. Unable to complete uploads.") return image_list, i # Or you could raise an exception if preferred img_host = self.config['DEFAULT'][f'img_host_{img_host_num}'] @@ -2744,49 +2736,49 @@ async def get_season_episode(self, video, meta): def get_service(self, video, tag, audio, guess_title): service = guessit(video).get('streaming_service', "") services = { - '9NOW': '9NOW', '9Now': '9NOW', 'AE': 'AE', 'A&E': 'AE', 'AJAZ': 'AJAZ', 'Al Jazeera English': 'AJAZ', - 'ALL4': 'ALL4', 'Channel 4': 'ALL4', 'AMBC': 'AMBC', 'ABC': 'AMBC', 'AMC': 'AMC', 'AMZN': 'AMZN', - 'Amazon Prime': 'AMZN', 'ANLB': 'ANLB', 'AnimeLab': 'ANLB', 'ANPL': 'ANPL', 'Animal Planet': 'ANPL', - 'AOL': 'AOL', 'ARD': 'ARD', 'AS': 'AS', 'Adult Swim': 'AS', 'ATK': 'ATK', "America's Test Kitchen": 'ATK', - 'ATVP': 'ATVP', 'AppleTV': 'ATVP', 'AUBC': 'AUBC', 'ABC Australia': 'AUBC', 'BCORE': 'BCORE', 'BKPL': 'BKPL', - 'Blackpills': 'BKPL', 'BluTV': 'BLU', 'Binge': 'BNGE', 'BOOM': 'BOOM', 'Boomerang': 'BOOM', 'BRAV': 'BRAV', - 'BravoTV': 'BRAV', 'CBC': 'CBC', 'CBS': 'CBS', 'CC': 'CC', 'Comedy Central': 'CC', 'CCGC': 'CCGC', - 'Comedians in Cars Getting Coffee': 'CCGC', 'CHGD': 'CHGD', 'CHRGD': 'CHGD', 'CMAX': 'CMAX', 'Cinemax': 'CMAX', - 'CMOR': 'CMOR', 'CMT': 'CMT', 'Country Music Television': 'CMT', 'CN': 'CN', 'Cartoon Network': 'CN', 'CNBC': 'CNBC', - 'CNLP': 'CNLP', 'Canal+': 'CNLP', 'COOK': 'COOK', 'CORE': 'CORE', 'CR': 'CR', 'Crunchy Roll': 'CR', 'Crave': 'CRAV', - 'CRIT': 'CRIT', 'Criterion' : 'CRIT', 'CRKL': 'CRKL', 'Crackle': 'CRKL', 'CSPN': 'CSPN', 'CSpan': 'CSPN', 'CTV': 'CTV', 'CUR': 'CUR', - 'CuriosityStream': 'CUR', 'CW': 'CW', 'The CW': 'CW', 'CWS': 'CWS', 'CWSeed': 'CWS', 'DAZN': 'DAZN', 'DCU': 'DCU', - 'DC Universe': 'DCU', 'DDY': 'DDY', 'Digiturk Diledigin Yerde': 'DDY', 'DEST': 'DEST', 'DramaFever': 'DF', 'DHF': 'DHF', - 'Deadhouse Films': 'DHF', 'DISC': 'DISC', 'Discovery': 'DISC', 'DIY': 'DIY', 'DIY Network': 'DIY', 'DOCC': 'DOCC', - 'Doc Club': 'DOCC', 'DPLY': 'DPLY', 'DPlay': 'DPLY', 'DRPO': 'DRPO', 'Discovery Plus': 'DSCP', 'DSKI': 'DSKI', - 'Daisuki': 'DSKI', 'DSNP': 'DSNP', 'Disney+': 'DSNP', 'DSNY': 'DSNY', 'Disney': 'DSNY', 'DTV': 'DTV', - 'EPIX': 'EPIX', 'ePix': 'EPIX', 'ESPN': 'ESPN', 'ESQ': 'ESQ', 'Esquire': 'ESQ', 'ETTV': 'ETTV', 'El Trece': 'ETTV', - 'ETV': 'ETV', 'E!': 'ETV', 'FAM': 'FAM', 'Fandor': 'FANDOR', 'Facebook Watch': 'FBWatch', 'FJR': 'FJR', - 'Family Jr': 'FJR', 'FOOD': 'FOOD', 'Food Network': 'FOOD', 'FOX': 'FOX', 'Fox': 'FOX', 'Fox Premium': 'FOXP', + '9NOW': '9NOW', '9Now': '9NOW', 'AE': 'AE', 'A&E': 'AE', 'AJAZ': 'AJAZ', 'Al Jazeera English': 'AJAZ', + 'ALL4': 'ALL4', 'Channel 4': 'ALL4', 'AMBC': 'AMBC', 'ABC': 'AMBC', 'AMC': 'AMC', 'AMZN': 'AMZN', + 'Amazon Prime': 'AMZN', 'ANLB': 'ANLB', 'AnimeLab': 'ANLB', 'ANPL': 'ANPL', 'Animal Planet': 'ANPL', + 'AOL': 'AOL', 'ARD': 'ARD', 'AS': 'AS', 'Adult Swim': 'AS', 'ATK': 'ATK', "America's Test Kitchen": 'ATK', + 'ATVP': 'ATVP', 'AppleTV': 'ATVP', 'AUBC': 'AUBC', 'ABC Australia': 'AUBC', 'BCORE': 'BCORE', 'BKPL': 'BKPL', + 'Blackpills': 'BKPL', 'BluTV': 'BLU', 'Binge': 'BNGE', 'BOOM': 'BOOM', 'Boomerang': 'BOOM', 'BRAV': 'BRAV', + 'BravoTV': 'BRAV', 'CBC': 'CBC', 'CBS': 'CBS', 'CC': 'CC', 'Comedy Central': 'CC', 'CCGC': 'CCGC', + 'Comedians in Cars Getting Coffee': 'CCGC', 'CHGD': 'CHGD', 'CHRGD': 'CHGD', 'CMAX': 'CMAX', 'Cinemax': 'CMAX', + 'CMOR': 'CMOR', 'CMT': 'CMT', 'Country Music Television': 'CMT', 'CN': 'CN', 'Cartoon Network': 'CN', 'CNBC': 'CNBC', + 'CNLP': 'CNLP', 'Canal+': 'CNLP', 'COOK': 'COOK', 'CORE': 'CORE', 'CR': 'CR', 'Crunchy Roll': 'CR', 'Crave': 'CRAV', + 'CRIT': 'CRIT', 'Criterion' : 'CRIT', 'CRKL': 'CRKL', 'Crackle': 'CRKL', 'CSPN': 'CSPN', 'CSpan': 'CSPN', 'CTV': 'CTV', 'CUR': 'CUR', + 'CuriosityStream': 'CUR', 'CW': 'CW', 'The CW': 'CW', 'CWS': 'CWS', 'CWSeed': 'CWS', 'DAZN': 'DAZN', 'DCU': 'DCU', + 'DC Universe': 'DCU', 'DDY': 'DDY', 'Digiturk Diledigin Yerde': 'DDY', 'DEST': 'DEST', 'DramaFever': 'DF', 'DHF': 'DHF', + 'Deadhouse Films': 'DHF', 'DISC': 'DISC', 'Discovery': 'DISC', 'DIY': 'DIY', 'DIY Network': 'DIY', 'DOCC': 'DOCC', + 'Doc Club': 'DOCC', 'DPLY': 'DPLY', 'DPlay': 'DPLY', 'DRPO': 'DRPO', 'Discovery Plus': 'DSCP', 'DSKI': 'DSKI', + 'Daisuki': 'DSKI', 'DSNP': 'DSNP', 'Disney+': 'DSNP', 'DSNY': 'DSNY', 'Disney': 'DSNY', 'DTV': 'DTV', + 'EPIX': 'EPIX', 'ePix': 'EPIX', 'ESPN': 'ESPN', 'ESQ': 'ESQ', 'Esquire': 'ESQ', 'ETTV': 'ETTV', 'El Trece': 'ETTV', + 'ETV': 'ETV', 'E!': 'ETV', 'FAM': 'FAM', 'Fandor': 'FANDOR', 'Facebook Watch': 'FBWatch', 'FJR': 'FJR', + 'Family Jr': 'FJR', 'FOOD': 'FOOD', 'Food Network': 'FOOD', 'FOX': 'FOX', 'Fox': 'FOX', 'Fox Premium': 'FOXP', 'UFC Fight Pass': 'FP', 'FPT': 'FPT', 'FREE': 'FREE', 'Freeform': 'FREE', 'FTV': 'FTV', 'FUNI': 'FUNI', 'FUNi' : 'FUNI', - 'Foxtel': 'FXTL', 'FYI': 'FYI', 'FYI Network': 'FYI', 'GC': 'GC', 'NHL GameCenter': 'GC', 'GLBL': 'GLBL', - 'Global': 'GLBL', 'GLOB': 'GLOB', 'GloboSat Play': 'GLOB', 'GO90': 'GO90', 'GagaOOLala': 'Gaga', 'HBO': 'HBO', - 'HBO Go': 'HBO', 'HGTV': 'HGTV', 'HIDI': 'HIDI', 'HIST': 'HIST', 'History': 'HIST', 'HLMK': 'HLMK', 'Hallmark': 'HLMK', - 'HMAX': 'HMAX', 'HBO Max': 'HMAX', 'HS': 'HTSR', 'HTSR' : 'HTSR', 'HSTR': 'Hotstar', 'HULU': 'HULU', 'Hulu': 'HULU', 'hoichoi': 'HoiChoi', 'ID': 'ID', - 'Investigation Discovery': 'ID', 'IFC': 'IFC', 'iflix': 'IFX', 'National Audiovisual Institute': 'INA', 'ITV': 'ITV', - 'KAYO': 'KAYO', 'KNOW': 'KNOW', 'Knowledge Network': 'KNOW', 'KNPY': 'KNPY', 'Kanopy' : 'KNPY', 'LIFE': 'LIFE', 'Lifetime': 'LIFE', 'LN': 'LN', - 'MA' : 'MA', 'Movies Anywhere' : 'MA', 'MAX' : 'MAX', 'MBC': 'MBC', 'MNBC': 'MNBC', 'MSNBC': 'MNBC', 'MTOD': 'MTOD', 'Motor Trend OnDemand': 'MTOD', 'MTV': 'MTV', 'MUBI': 'MUBI', - 'NATG': 'NATG', 'National Geographic': 'NATG', 'NBA': 'NBA', 'NBA TV': 'NBA', 'NBC': 'NBC', 'NF': 'NF', 'Netflix': 'NF', - 'National Film Board': 'NFB', 'NFL': 'NFL', 'NFLN': 'NFLN', 'NFL Now': 'NFLN', 'NICK': 'NICK', 'Nickelodeon': 'NICK', 'NRK': 'NRK', - 'Norsk Rikskringkasting': 'NRK', 'OnDemandKorea': 'ODK', 'Opto': 'OPTO', 'Oprah Winfrey Network': 'OWN', 'PA': 'PA', 'PBS': 'PBS', - 'PBSK': 'PBSK', 'PBS Kids': 'PBSK', 'PCOK': 'PCOK', 'Peacock': 'PCOK', 'PLAY': 'PLAY', 'PLUZ': 'PLUZ', 'Pluzz': 'PLUZ', 'PMNP': 'PMNP', - 'PMNT': 'PMNT', 'PMTP' : 'PMTP', 'POGO': 'POGO', 'PokerGO': 'POGO', 'PSN': 'PSN', 'Playstation Network': 'PSN', 'PUHU': 'PUHU', 'QIBI': 'QIBI', - 'RED': 'RED', 'YouTube Red': 'RED', 'RKTN': 'RKTN', 'Rakuten TV': 'RKTN', 'The Roku Channel': 'ROKU', 'RSTR': 'RSTR', 'RTE': 'RTE', + 'Foxtel': 'FXTL', 'FYI': 'FYI', 'FYI Network': 'FYI', 'GC': 'GC', 'NHL GameCenter': 'GC', 'GLBL': 'GLBL', + 'Global': 'GLBL', 'GLOB': 'GLOB', 'GloboSat Play': 'GLOB', 'GO90': 'GO90', 'GagaOOLala': 'Gaga', 'HBO': 'HBO', + 'HBO Go': 'HBO', 'HGTV': 'HGTV', 'HIDI': 'HIDI', 'HIST': 'HIST', 'History': 'HIST', 'HLMK': 'HLMK', 'Hallmark': 'HLMK', + 'HMAX': 'HMAX', 'HBO Max': 'HMAX', 'HS': 'HTSR', 'HTSR' : 'HTSR', 'HSTR': 'Hotstar', 'HULU': 'HULU', 'Hulu': 'HULU', 'hoichoi': 'HoiChoi', 'ID': 'ID', + 'Investigation Discovery': 'ID', 'IFC': 'IFC', 'iflix': 'IFX', 'National Audiovisual Institute': 'INA', 'ITV': 'ITV', + 'KAYO': 'KAYO', 'KNOW': 'KNOW', 'Knowledge Network': 'KNOW', 'KNPY': 'KNPY', 'Kanopy' : 'KNPY', 'LIFE': 'LIFE', 'Lifetime': 'LIFE', 'LN': 'LN', + 'MA' : 'MA', 'Movies Anywhere' : 'MA', 'MAX' : 'MAX', 'MBC': 'MBC', 'MNBC': 'MNBC', 'MSNBC': 'MNBC', 'MTOD': 'MTOD', 'Motor Trend OnDemand': 'MTOD', 'MTV': 'MTV', 'MUBI': 'MUBI', + 'NATG': 'NATG', 'National Geographic': 'NATG', 'NBA': 'NBA', 'NBA TV': 'NBA', 'NBC': 'NBC', 'NF': 'NF', 'Netflix': 'NF', + 'National Film Board': 'NFB', 'NFL': 'NFL', 'NFLN': 'NFLN', 'NFL Now': 'NFLN', 'NICK': 'NICK', 'Nickelodeon': 'NICK', 'NRK': 'NRK', + 'Norsk Rikskringkasting': 'NRK', 'OnDemandKorea': 'ODK', 'Opto': 'OPTO', 'Oprah Winfrey Network': 'OWN', 'PA': 'PA', 'PBS': 'PBS', + 'PBSK': 'PBSK', 'PBS Kids': 'PBSK', 'PCOK': 'PCOK', 'Peacock': 'PCOK', 'PLAY': 'PLAY', 'PLUZ': 'PLUZ', 'Pluzz': 'PLUZ', 'PMNP': 'PMNP', + 'PMNT': 'PMNT', 'PMTP' : 'PMTP', 'POGO': 'POGO', 'PokerGO': 'POGO', 'PSN': 'PSN', 'Playstation Network': 'PSN', 'PUHU': 'PUHU', 'QIBI': 'QIBI', + 'RED': 'RED', 'YouTube Red': 'RED', 'RKTN': 'RKTN', 'Rakuten TV': 'RKTN', 'The Roku Channel': 'ROKU', 'RSTR': 'RSTR', 'RTE': 'RTE', 'RTE One': 'RTE', 'RUUTU': 'RUUTU', 'SBS': 'SBS', 'Science Channel': 'SCI', 'SESO': 'SESO', 'SeeSo': 'SESO', 'SHMI': 'SHMI', 'Shomi': 'SHMI', 'SKST' : 'SKST', 'SkyShowtime': 'SKST', - 'SHO': 'SHO', 'Showtime': 'SHO', 'SNET': 'SNET', 'Sportsnet': 'SNET', 'Sony': 'SONY', 'SPIK': 'SPIK', 'Spike': 'SPIK', 'Spike TV': 'SPKE', - 'SPRT': 'SPRT', 'Sprout': 'SPRT', 'STAN': 'STAN', 'Stan': 'STAN', 'STARZ': 'STARZ', 'STRP': 'STRP', 'Star+' : 'STRP', 'STZ': 'STZ', 'Starz': 'STZ', 'SVT': 'SVT', - 'Sveriges Television': 'SVT', 'SWER': 'SWER', 'SwearNet': 'SWER', 'SYFY': 'SYFY', 'Syfy': 'SYFY', 'TBS': 'TBS', 'TEN': 'TEN', - 'TFOU': 'TFOU', 'TFou': 'TFOU', 'TIMV': 'TIMV', 'TLC': 'TLC', 'TOU': 'TOU', 'TRVL': 'TRVL', 'TUBI': 'TUBI', 'TubiTV': 'TUBI', - 'TV3': 'TV3', 'TV3 Ireland': 'TV3', 'TV4': 'TV4', 'TV4 Sweeden': 'TV4', 'TVING': 'TVING', 'TVL': 'TVL', 'TV Land': 'TVL', - 'TVNZ': 'TVNZ', 'UFC': 'UFC', 'UKTV': 'UKTV', 'UNIV': 'UNIV', 'Univision': 'UNIV', 'USAN': 'USAN', 'USA Network': 'USAN', - 'VH1': 'VH1', 'VIAP': 'VIAP', 'VICE': 'VICE', 'Viceland': 'VICE', 'Viki': 'VIKI', 'VIMEO': 'VIMEO', 'VLCT': 'VLCT', - 'Velocity': 'VLCT', 'VMEO': 'VMEO', 'Vimeo': 'VMEO', 'VRV': 'VRV', 'VUDU': 'VUDU', 'WME': 'WME', 'WatchMe': 'WME', 'WNET': 'WNET', - 'W Network': 'WNET', 'WWEN': 'WWEN', 'WWE Network': 'WWEN', 'XBOX': 'XBOX', 'Xbox Video': 'XBOX', 'YHOO': 'YHOO', 'Yahoo': 'YHOO', + 'SHO': 'SHO', 'Showtime': 'SHO', 'SNET': 'SNET', 'Sportsnet': 'SNET', 'Sony': 'SONY', 'SPIK': 'SPIK', 'Spike': 'SPIK', 'Spike TV': 'SPKE', + 'SPRT': 'SPRT', 'Sprout': 'SPRT', 'STAN': 'STAN', 'Stan': 'STAN', 'STARZ': 'STARZ', 'STRP': 'STRP', 'Star+' : 'STRP', 'STZ': 'STZ', 'Starz': 'STZ', 'SVT': 'SVT', + 'Sveriges Television': 'SVT', 'SWER': 'SWER', 'SwearNet': 'SWER', 'SYFY': 'SYFY', 'Syfy': 'SYFY', 'TBS': 'TBS', 'TEN': 'TEN', + 'TFOU': 'TFOU', 'TFou': 'TFOU', 'TIMV': 'TIMV', 'TLC': 'TLC', 'TOU': 'TOU', 'TRVL': 'TRVL', 'TUBI': 'TUBI', 'TubiTV': 'TUBI', + 'TV3': 'TV3', 'TV3 Ireland': 'TV3', 'TV4': 'TV4', 'TV4 Sweeden': 'TV4', 'TVING': 'TVING', 'TVL': 'TVL', 'TV Land': 'TVL', + 'TVNZ': 'TVNZ', 'UFC': 'UFC', 'UKTV': 'UKTV', 'UNIV': 'UNIV', 'Univision': 'UNIV', 'USAN': 'USAN', 'USA Network': 'USAN', + 'VH1': 'VH1', 'VIAP': 'VIAP', 'VICE': 'VICE', 'Viceland': 'VICE', 'Viki': 'VIKI', 'VIMEO': 'VIMEO', 'VLCT': 'VLCT', + 'Velocity': 'VLCT', 'VMEO': 'VMEO', 'Vimeo': 'VMEO', 'VRV': 'VRV', 'VUDU': 'VUDU', 'WME': 'WME', 'WatchMe': 'WME', 'WNET': 'WNET', + 'W Network': 'WNET', 'WWEN': 'WWEN', 'WWE Network': 'WWEN', 'XBOX': 'XBOX', 'Xbox Video': 'XBOX', 'YHOO': 'YHOO', 'Yahoo': 'YHOO', 'YT': 'YT', 'ZDF': 'ZDF', 'iP': 'iP', 'BBC iPlayer': 'iP', 'iQIYI': 'iQIYI', 'iT': 'iT', 'iTunes': 'iT' } From 6457df7ab60749e8f339baf25002e6bb5cdbc917 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 14:07:31 +1000 Subject: [PATCH 20/33] More linting --- src/prep.py | 297 +++++++++++++++++++++++++--------------------------- upload.py | 10 +- 2 files changed, 148 insertions(+), 159 deletions(-) diff --git a/src/prep.py b/src/prep.py index 5408b2a53..cb56bcf57 100644 --- a/src/prep.py +++ b/src/prep.py @@ -345,7 +345,7 @@ async def gather_prep(self, meta, mode): while ds.is_alive() is True: await asyncio.sleep(1) except KeyboardInterrupt: - ds.terminate() + ds.terminate() elif meta['is_disc'] == "DVD": if meta.get('edit', False) is False: try: @@ -373,7 +373,7 @@ async def gather_prep(self, meta, mode): else: meta['category'] = meta['category'].upper() if meta.get('tmdb', None) is None and meta.get('imdb', None) is None: - meta['category'], meta['tmdb'], meta['imdb'] = self.get_tmdb_imdb_from_mediainfo(mi, meta['category'], meta['is_disc'], meta['tmdb'], meta['imdb']) + meta['category'], meta['tmdb'], meta['imdb'] = self.get_tmdb_imdb_from_mediainfo(mi, meta['category'], meta['is_disc'], meta['tmdb'], meta['imdb']) if meta.get('tmdb', None) is None and meta.get('imdb', None) is None: meta = await self.get_tmdb_id(filename, meta['search_year'], meta, meta['category'], untouched_filename) elif meta.get('imdb', None) is not None and meta.get('tmdb_manual', None) is None: @@ -775,7 +775,7 @@ def mi_resolution(self, res, guess, width, scan, height, actual_height): if actual_height == 540: resolution = "OTHER" if resolution is None: - try: + try: resolution = guess['screen_size'] except: width_map = { @@ -841,7 +841,7 @@ def disc_screenshots(self, filename, bdinfo, folder_id, base_dir, use_vs, image_ if num_screens == 0 or len(image_list) >= num_screens: return # Get longest m2ts - length = 0 + length = 0 for each in bdinfo['files']: int_length = sum(int(float(x)) * 60 ** i for i, x in enumerate(reversed(each['length'].split(':')))) if int_length > length: @@ -1068,7 +1068,7 @@ def _is_vob_good(n, loops, num_screens): def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=None): if num_screens is None: num_screens = self.screens - len(meta.get('image_list', [])) - if num_screens == 0: + if num_screens == 0: # or len(meta.get('image_list', [])) >= num_screens: return with open(f"{base_dir}/tmp/{folder_id}/MediaInfo.json", encoding='utf-8') as f: @@ -1245,10 +1245,10 @@ async def get_tmdb_from_imdb(self, meta, filename): info = find.info(external_source="imdb_id") if len(info['movie_results']) >= 1: meta['category'] = "MOVIE" - meta['tmdb'] = info['movie_results'][0]['id'] + meta['tmdb'] = info['movie_results'][0]['id'] elif len(info['tv_results']) >= 1: meta['category'] = "TV" - meta['tmdb'] = info['tv_results'][0]['id'] + meta['tmdb'] = info['tv_results'][0]['id'] else: imdb_info = await self.get_imdb_info(imdb_id.replace('tt', ''), meta) title = imdb_info.get("title") @@ -1438,9 +1438,9 @@ def get_keywords(self, tmdb_info): if tmdb_info is not None: tmdb_keywords = tmdb_info.keywords() if tmdb_keywords.get('keywords') is not None: - keywords=[f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('keywords')] + keywords = [f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('keywords')] elif tmdb_keywords.get('results') is not None: - keywords=[f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('results')] + keywords = [f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('results')] return(', '.join(keywords)) else: return '' @@ -1449,7 +1449,7 @@ def get_genres(self, tmdb_info): if tmdb_info is not None: tmdb_genres = tmdb_info.get('genres', []) if tmdb_genres is not []: - genres=[f"{genre['name'].replace(',', ' ')}" for genre in tmdb_genres] + genres = [f"{genre['name'].replace(',', ' ')}" for genre in tmdb_genres] return(', '.join(genres)) else: return '' @@ -1575,7 +1575,7 @@ def get_romaji(self, tmdb_name, mal): season_year = result.get('season_year', "") episodes = result.get('episodes', 0) else: - romaji = eng_title = season_year = "" + romaji = eng_title = season_year = "" episodes = mal_id = 0 if mal_id in [None, 0]: mal_id = mal @@ -1927,54 +1927,54 @@ def get_region(self, bdinfo, region=None): region = region.upper() else: regions = { - 'AFG': 'AFG', 'AIA': 'AIA', 'ALA': 'ALA', 'ALG': 'ALG', 'AND': 'AND', 'ANG': 'ANG', 'ARG': 'ARG', - 'ARM': 'ARM', 'ARU': 'ARU', 'ASA': 'ASA', 'ATA': 'ATA', 'ATF': 'ATF', 'ATG': 'ATG', 'AUS': 'AUS', - 'AUT': 'AUT', 'AZE': 'AZE', 'BAH': 'BAH', 'BAN': 'BAN', 'BDI': 'BDI', 'BEL': 'BEL', 'BEN': 'BEN', - 'BER': 'BER', 'BES': 'BES', 'BFA': 'BFA', 'BHR': 'BHR', 'BHU': 'BHU', 'BIH': 'BIH', 'BLM': 'BLM', - 'BLR': 'BLR', 'BLZ': 'BLZ', 'BOL': 'BOL', 'BOT': 'BOT', 'BRA': 'BRA', 'BRB': 'BRB', 'BRU': 'BRU', - 'BVT': 'BVT', 'CAM': 'CAM', 'CAN': 'CAN', 'CAY': 'CAY', 'CCK': 'CCK', 'CEE': 'CEE', 'CGO': 'CGO', - 'CHA': 'CHA', 'CHI': 'CHI', 'CHN': 'CHN', 'CIV': 'CIV', 'CMR': 'CMR', 'COD': 'COD', 'COK': 'COK', - 'COL': 'COL', 'COM': 'COM', 'CPV': 'CPV', 'CRC': 'CRC', 'CRO': 'CRO', 'CTA': 'CTA', 'CUB': 'CUB', - 'CUW': 'CUW', 'CXR': 'CXR', 'CYP': 'CYP', 'DJI': 'DJI', 'DMA': 'DMA', 'DOM': 'DOM', 'ECU': 'ECU', - 'EGY': 'EGY', 'ENG': 'ENG', 'EQG': 'EQG', 'ERI': 'ERI', 'ESH': 'ESH', 'ESP': 'ESP', 'ETH': 'ETH', - 'FIJ': 'FIJ', 'FLK': 'FLK', 'FRA': 'FRA', 'FRO': 'FRO', 'FSM': 'FSM', 'GAB': 'GAB', 'GAM': 'GAM', - 'GBR': 'GBR', 'GEO': 'GEO', 'GER': 'GER', 'GGY': 'GGY', 'GHA': 'GHA', 'GIB': 'GIB', 'GLP': 'GLP', - 'GNB': 'GNB', 'GRE': 'GRE', 'GRL': 'GRL', 'GRN': 'GRN', 'GUA': 'GUA', 'GUF': 'GUF', 'GUI': 'GUI', - 'GUM': 'GUM', 'GUY': 'GUY', 'HAI': 'HAI', 'HKG': 'HKG', 'HMD': 'HMD', 'HON': 'HON', 'HUN': 'HUN', - 'IDN': 'IDN', 'IMN': 'IMN', 'IND': 'IND', 'IOT': 'IOT', 'IRL': 'IRL', 'IRN': 'IRN', 'IRQ': 'IRQ', - 'ISL': 'ISL', 'ISR': 'ISR', 'ITA': 'ITA', 'JAM': 'JAM', 'JEY': 'JEY', 'JOR': 'JOR', 'JPN': 'JPN', - 'KAZ': 'KAZ', 'KEN': 'KEN', 'KGZ': 'KGZ', 'KIR': 'KIR', 'KNA': 'KNA', 'KOR': 'KOR', 'KSA': 'KSA', - 'KUW': 'KUW', 'KVX': 'KVX', 'LAO': 'LAO', 'LBN': 'LBN', 'LBR': 'LBR', 'LBY': 'LBY', 'LCA': 'LCA', - 'LES': 'LES', 'LIE': 'LIE', 'LKA': 'LKA', 'LUX': 'LUX', 'MAC': 'MAC', 'MAD': 'MAD', 'MAF': 'MAF', - 'MAR': 'MAR', 'MAS': 'MAS', 'MDA': 'MDA', 'MDV': 'MDV', 'MEX': 'MEX', 'MHL': 'MHL', 'MKD': 'MKD', - 'MLI': 'MLI', 'MLT': 'MLT', 'MNG': 'MNG', 'MNP': 'MNP', 'MON': 'MON', 'MOZ': 'MOZ', 'MRI': 'MRI', - 'MSR': 'MSR', 'MTN': 'MTN', 'MTQ': 'MTQ', 'MWI': 'MWI', 'MYA': 'MYA', 'MYT': 'MYT', 'NAM': 'NAM', - 'NCA': 'NCA', 'NCL': 'NCL', 'NEP': 'NEP', 'NFK': 'NFK', 'NIG': 'NIG', 'NIR': 'NIR', 'NIU': 'NIU', - 'NLD': 'NLD', 'NOR': 'NOR', 'NRU': 'NRU', 'NZL': 'NZL', 'OMA': 'OMA', 'PAK': 'PAK', 'PAN': 'PAN', - 'PAR': 'PAR', 'PCN': 'PCN', 'PER': 'PER', 'PHI': 'PHI', 'PLE': 'PLE', 'PLW': 'PLW', 'PNG': 'PNG', - 'POL': 'POL', 'POR': 'POR', 'PRK': 'PRK', 'PUR': 'PUR', 'QAT': 'QAT', 'REU': 'REU', 'ROU': 'ROU', - 'RSA': 'RSA', 'RUS': 'RUS', 'RWA': 'RWA', 'SAM': 'SAM', 'SCO': 'SCO', 'SDN': 'SDN', 'SEN': 'SEN', - 'SEY': 'SEY', 'SGS': 'SGS', 'SHN': 'SHN', 'SIN': 'SIN', 'SJM': 'SJM', 'SLE': 'SLE', 'SLV': 'SLV', - 'SMR': 'SMR', 'SOL': 'SOL', 'SOM': 'SOM', 'SPM': 'SPM', 'SRB': 'SRB', 'SSD': 'SSD', 'STP': 'STP', - 'SUI': 'SUI', 'SUR': 'SUR', 'SWZ': 'SWZ', 'SXM': 'SXM', 'SYR': 'SYR', 'TAH': 'TAH', 'TAN': 'TAN', - 'TCA': 'TCA', 'TGA': 'TGA', 'THA': 'THA', 'TJK': 'TJK', 'TKL': 'TKL', 'TKM': 'TKM', 'TLS': 'TLS', - 'TOG': 'TOG', 'TRI': 'TRI', 'TUN': 'TUN', 'TUR': 'TUR', 'TUV': 'TUV', 'TWN': 'TWN', 'UAE': 'UAE', - 'UGA': 'UGA', 'UKR': 'UKR', 'UMI': 'UMI', 'URU': 'URU', 'USA': 'USA', 'UZB': 'UZB', 'VAN': 'VAN', - 'VAT': 'VAT', 'VEN': 'VEN', 'VGB': 'VGB', 'VIE': 'VIE', 'VIN': 'VIN', 'VIR': 'VIR', 'WAL': 'WAL', - 'WLF': 'WLF', 'YEM': 'YEM', 'ZAM': 'ZAM', 'ZIM': 'ZIM', "EUR" : "EUR" + 'AFG': 'AFG', 'AIA': 'AIA', 'ALA': 'ALA', 'ALG': 'ALG', 'AND': 'AND', 'ANG': 'ANG', 'ARG': 'ARG', + 'ARM': 'ARM', 'ARU': 'ARU', 'ASA': 'ASA', 'ATA': 'ATA', 'ATF': 'ATF', 'ATG': 'ATG', 'AUS': 'AUS', + 'AUT': 'AUT', 'AZE': 'AZE', 'BAH': 'BAH', 'BAN': 'BAN', 'BDI': 'BDI', 'BEL': 'BEL', 'BEN': 'BEN', + 'BER': 'BER', 'BES': 'BES', 'BFA': 'BFA', 'BHR': 'BHR', 'BHU': 'BHU', 'BIH': 'BIH', 'BLM': 'BLM', + 'BLR': 'BLR', 'BLZ': 'BLZ', 'BOL': 'BOL', 'BOT': 'BOT', 'BRA': 'BRA', 'BRB': 'BRB', 'BRU': 'BRU', + 'BVT': 'BVT', 'CAM': 'CAM', 'CAN': 'CAN', 'CAY': 'CAY', 'CCK': 'CCK', 'CEE': 'CEE', 'CGO': 'CGO', + 'CHA': 'CHA', 'CHI': 'CHI', 'CHN': 'CHN', 'CIV': 'CIV', 'CMR': 'CMR', 'COD': 'COD', 'COK': 'COK', + 'COL': 'COL', 'COM': 'COM', 'CPV': 'CPV', 'CRC': 'CRC', 'CRO': 'CRO', 'CTA': 'CTA', 'CUB': 'CUB', + 'CUW': 'CUW', 'CXR': 'CXR', 'CYP': 'CYP', 'DJI': 'DJI', 'DMA': 'DMA', 'DOM': 'DOM', 'ECU': 'ECU', + 'EGY': 'EGY', 'ENG': 'ENG', 'EQG': 'EQG', 'ERI': 'ERI', 'ESH': 'ESH', 'ESP': 'ESP', 'ETH': 'ETH', + 'FIJ': 'FIJ', 'FLK': 'FLK', 'FRA': 'FRA', 'FRO': 'FRO', 'FSM': 'FSM', 'GAB': 'GAB', 'GAM': 'GAM', + 'GBR': 'GBR', 'GEO': 'GEO', 'GER': 'GER', 'GGY': 'GGY', 'GHA': 'GHA', 'GIB': 'GIB', 'GLP': 'GLP', + 'GNB': 'GNB', 'GRE': 'GRE', 'GRL': 'GRL', 'GRN': 'GRN', 'GUA': 'GUA', 'GUF': 'GUF', 'GUI': 'GUI', + 'GUM': 'GUM', 'GUY': 'GUY', 'HAI': 'HAI', 'HKG': 'HKG', 'HMD': 'HMD', 'HON': 'HON', 'HUN': 'HUN', + 'IDN': 'IDN', 'IMN': 'IMN', 'IND': 'IND', 'IOT': 'IOT', 'IRL': 'IRL', 'IRN': 'IRN', 'IRQ': 'IRQ', + 'ISL': 'ISL', 'ISR': 'ISR', 'ITA': 'ITA', 'JAM': 'JAM', 'JEY': 'JEY', 'JOR': 'JOR', 'JPN': 'JPN', + 'KAZ': 'KAZ', 'KEN': 'KEN', 'KGZ': 'KGZ', 'KIR': 'KIR', 'KNA': 'KNA', 'KOR': 'KOR', 'KSA': 'KSA', + 'KUW': 'KUW', 'KVX': 'KVX', 'LAO': 'LAO', 'LBN': 'LBN', 'LBR': 'LBR', 'LBY': 'LBY', 'LCA': 'LCA', + 'LES': 'LES', 'LIE': 'LIE', 'LKA': 'LKA', 'LUX': 'LUX', 'MAC': 'MAC', 'MAD': 'MAD', 'MAF': 'MAF', + 'MAR': 'MAR', 'MAS': 'MAS', 'MDA': 'MDA', 'MDV': 'MDV', 'MEX': 'MEX', 'MHL': 'MHL', 'MKD': 'MKD', + 'MLI': 'MLI', 'MLT': 'MLT', 'MNG': 'MNG', 'MNP': 'MNP', 'MON': 'MON', 'MOZ': 'MOZ', 'MRI': 'MRI', + 'MSR': 'MSR', 'MTN': 'MTN', 'MTQ': 'MTQ', 'MWI': 'MWI', 'MYA': 'MYA', 'MYT': 'MYT', 'NAM': 'NAM', + 'NCA': 'NCA', 'NCL': 'NCL', 'NEP': 'NEP', 'NFK': 'NFK', 'NIG': 'NIG', 'NIR': 'NIR', 'NIU': 'NIU', + 'NLD': 'NLD', 'NOR': 'NOR', 'NRU': 'NRU', 'NZL': 'NZL', 'OMA': 'OMA', 'PAK': 'PAK', 'PAN': 'PAN', + 'PAR': 'PAR', 'PCN': 'PCN', 'PER': 'PER', 'PHI': 'PHI', 'PLE': 'PLE', 'PLW': 'PLW', 'PNG': 'PNG', + 'POL': 'POL', 'POR': 'POR', 'PRK': 'PRK', 'PUR': 'PUR', 'QAT': 'QAT', 'REU': 'REU', 'ROU': 'ROU', + 'RSA': 'RSA', 'RUS': 'RUS', 'RWA': 'RWA', 'SAM': 'SAM', 'SCO': 'SCO', 'SDN': 'SDN', 'SEN': 'SEN', + 'SEY': 'SEY', 'SGS': 'SGS', 'SHN': 'SHN', 'SIN': 'SIN', 'SJM': 'SJM', 'SLE': 'SLE', 'SLV': 'SLV', + 'SMR': 'SMR', 'SOL': 'SOL', 'SOM': 'SOM', 'SPM': 'SPM', 'SRB': 'SRB', 'SSD': 'SSD', 'STP': 'STP', + 'SUI': 'SUI', 'SUR': 'SUR', 'SWZ': 'SWZ', 'SXM': 'SXM', 'SYR': 'SYR', 'TAH': 'TAH', 'TAN': 'TAN', + 'TCA': 'TCA', 'TGA': 'TGA', 'THA': 'THA', 'TJK': 'TJK', 'TKL': 'TKL', 'TKM': 'TKM', 'TLS': 'TLS', + 'TOG': 'TOG', 'TRI': 'TRI', 'TUN': 'TUN', 'TUR': 'TUR', 'TUV': 'TUV', 'TWN': 'TWN', 'UAE': 'UAE', + 'UGA': 'UGA', 'UKR': 'UKR', 'UMI': 'UMI', 'URU': 'URU', 'USA': 'USA', 'UZB': 'UZB', 'VAN': 'VAN', + 'VAT': 'VAT', 'VEN': 'VEN', 'VGB': 'VGB', 'VIE': 'VIE', 'VIN': 'VIN', 'VIR': 'VIR', 'WAL': 'WAL', + 'WLF': 'WLF', 'YEM': 'YEM', 'ZAM': 'ZAM', 'ZIM': 'ZIM', "EUR": "EUR" } for key, value in regions.items(): if f" {key} " in label: region = value - + if region is None: region = "" return region def get_distributor(self, distributor_in): distributor_list = [ - '01 DISTRIBUTION', '100 DESTINATIONS TRAVEL FILM', '101 FILMS', '1FILMS', '2 ENTERTAIN VIDEO', '20TH CENTURY FOX', '2L', '3D CONTENT HUB', '3D MEDIA', '3L FILM', '4DIGITAL', '4DVD', '4K ULTRA HD MOVIES', '4K UHD', '8-FILMS', '84 ENTERTAINMENT', '88 FILMS', '@ANIME', 'ANIME', 'A CONTRACORRIENTE', 'A CONTRACORRIENTE FILMS', 'A&E HOME VIDEO', 'A&E', 'A&M RECORDS', 'A+E NETWORKS', 'A+R', 'A-FILM', 'AAA', 'AB VIDÉO', 'AB VIDEO', 'ABC - (AUSTRALIAN BROADCASTING CORPORATION)', 'ABC', 'ABKCO', 'ABSOLUT MEDIEN', 'ABSOLUTE', 'ACCENT FILM ENTERTAINMENT', 'ACCENTUS', 'ACORN MEDIA', 'AD VITAM', 'ADA', 'ADITYA VIDEOS', 'ADSO FILMS', 'AFM RECORDS', 'AGFA', 'AIX RECORDS', - 'ALAMODE FILM', 'ALBA RECORDS', 'ALBANY RECORDS', 'ALBATROS', 'ALCHEMY', 'ALIVE', 'ALL ANIME', 'ALL INTERACTIVE ENTERTAINMENT', 'ALLEGRO', 'ALLIANCE', 'ALPHA MUSIC', 'ALTERDYSTRYBUCJA', 'ALTERED INNOCENCE', 'ALTITUDE FILM DISTRIBUTION', 'ALUCARD RECORDS', 'AMAZING D.C.', 'AMAZING DC', 'AMMO CONTENT', 'AMUSE SOFT ENTERTAINMENT', 'ANCONNECT', 'ANEC', 'ANIMATSU', 'ANIME HOUSE', 'ANIME LTD', 'ANIME WORKS', 'ANIMEIGO', 'ANIPLEX', 'ANOLIS ENTERTAINMENT', 'ANOTHER WORLD ENTERTAINMENT', 'AP INTERNATIONAL', 'APPLE', 'ARA MEDIA', 'ARBELOS', 'ARC ENTERTAINMENT', 'ARP SÉLECTION', 'ARP SELECTION', 'ARROW', 'ART SERVICE', 'ART VISION', 'ARTE ÉDITIONS', 'ARTE EDITIONS', 'ARTE VIDÉO', + '01 DISTRIBUTION', '100 DESTINATIONS TRAVEL FILM', '101 FILMS', '1FILMS', '2 ENTERTAIN VIDEO', '20TH CENTURY FOX', '2L', '3D CONTENT HUB', '3D MEDIA', '3L FILM', '4DIGITAL', '4DVD', '4K ULTRA HD MOVIES', '4K UHD', '8-FILMS', '84 ENTERTAINMENT', '88 FILMS', '@ANIME', 'ANIME', 'A CONTRACORRIENTE', 'A CONTRACORRIENTE FILMS', 'A&E HOME VIDEO', 'A&E', 'A&M RECORDS', 'A+E NETWORKS', 'A+R', 'A-FILM', 'AAA', 'AB VIDÉO', 'AB VIDEO', 'ABC - (AUSTRALIAN BROADCASTING CORPORATION)', 'ABC', 'ABKCO', 'ABSOLUT MEDIEN', 'ABSOLUTE', 'ACCENT FILM ENTERTAINMENT', 'ACCENTUS', 'ACORN MEDIA', 'AD VITAM', 'ADA', 'ADITYA VIDEOS', 'ADSO FILMS', 'AFM RECORDS', 'AGFA', 'AIX RECORDS', + 'ALAMODE FILM', 'ALBA RECORDS', 'ALBANY RECORDS', 'ALBATROS', 'ALCHEMY', 'ALIVE', 'ALL ANIME', 'ALL INTERACTIVE ENTERTAINMENT', 'ALLEGRO', 'ALLIANCE', 'ALPHA MUSIC', 'ALTERDYSTRYBUCJA', 'ALTERED INNOCENCE', 'ALTITUDE FILM DISTRIBUTION', 'ALUCARD RECORDS', 'AMAZING D.C.', 'AMAZING DC', 'AMMO CONTENT', 'AMUSE SOFT ENTERTAINMENT', 'ANCONNECT', 'ANEC', 'ANIMATSU', 'ANIME HOUSE', 'ANIME LTD', 'ANIME WORKS', 'ANIMEIGO', 'ANIPLEX', 'ANOLIS ENTERTAINMENT', 'ANOTHER WORLD ENTERTAINMENT', 'AP INTERNATIONAL', 'APPLE', 'ARA MEDIA', 'ARBELOS', 'ARC ENTERTAINMENT', 'ARP SÉLECTION', 'ARP SELECTION', 'ARROW', 'ART SERVICE', 'ART VISION', 'ARTE ÉDITIONS', 'ARTE EDITIONS', 'ARTE VIDÉO', 'ARTE VIDEO', 'ARTHAUS MUSIK', 'ARTIFICIAL EYE', 'ARTSPLOITATION FILMS', 'ARTUS FILMS', 'ASCOT ELITE HOME ENTERTAINMENT', 'ASIA VIDEO', 'ASMIK ACE', 'ASTRO RECORDS & FILMWORKS', 'ASYLUM', 'ATLANTIC FILM', 'ATLANTIC RECORDS', 'ATLAS FILM', 'AUDIO VISUAL ENTERTAINMENT', 'AURO-3D CREATIVE LABEL', 'AURUM', 'AV VISIONEN', 'AV-JET', 'AVALON', 'AVENTI', 'AVEX TRAX', 'AXIOM', 'AXIS RECORDS', 'AYNGARAN', 'BAC FILMS', 'BACH FILMS', 'BANDAI VISUAL', 'BARCLAY', 'BBC', 'BRITISH BROADCASTING CORPORATION', 'BBI FILMS', 'BBI', 'BCI HOME ENTERTAINMENT', 'BEGGARS BANQUET', 'BEL AIR CLASSIQUES', 'BELGA FILMS', 'BELVEDERE', 'BENELUX FILM DISTRIBUTORS', 'BENNETT-WATT MEDIA', 'BERLIN CLASSICS', 'BERLINER PHILHARMONIKER RECORDINGS', 'BEST ENTERTAINMENT', 'BEYOND HOME ENTERTAINMENT', 'BFI VIDEO', 'BFI', 'BRITISH FILM INSTITUTE', 'BFS ENTERTAINMENT', 'BFS', 'BHAVANI', 'BIBER RECORDS', 'BIG HOME VIDEO', 'BILDSTÖRUNG', 'BILDSTORUNG', 'BILL ZEBUB', 'BIRNENBLATT', 'BIT WEL', 'BLACK BOX', 'BLACK HILL PICTURES', 'BLACK HILL', 'BLACK HOLE RECORDINGS', 'BLACK HOLE', 'BLAQOUT', 'BLAUFIELD MUSIC', 'BLAUFIELD', 'BLOCKBUSTER ENTERTAINMENT', 'BLOCKBUSTER', 'BLU PHASE MEDIA', 'BLU-RAY ONLY', 'BLU-RAY', 'BLURAY ONLY', 'BLURAY', 'BLUE GENTIAN RECORDS', 'BLUE KINO', 'BLUE UNDERGROUND', 'BMG/ARISTA', 'BMG', 'BMGARISTA', 'BMG ARISTA', 'ARISTA', 'ARISTA/BMG', 'ARISTABMG', 'ARISTA BMG', 'BONTON FILM', 'BONTON', 'BOOMERANG PICTURES', 'BOOMERANG', 'BQHL ÉDITIONS', 'BQHL EDITIONS', 'BQHL', 'BREAKING GLASS', 'BRIDGESTONE', 'BRINK', 'BROAD GREEN PICTURES', 'BROAD GREEN', 'BUSCH MEDIA GROUP', 'BUSCH', 'C MAJOR', 'C.B.S.', 'CAICHANG', 'CALIFÓRNIA FILMES', 'CALIFORNIA FILMES', 'CALIFORNIA', 'CAMEO', 'CAMERA OBSCURA', 'CAMERATA', 'CAMP MOTION PICTURES', 'CAMP MOTION', 'CAPELIGHT PICTURES', 'CAPELIGHT', 'CAPITOL', 'CAPITOL RECORDS', 'CAPRICCI', 'CARGO RECORDS', 'CARLOTTA FILMS', 'CARLOTTA', 'CARLOTA', 'CARMEN FILM', 'CASCADE', 'CATCHPLAY', 'CAULDRON FILMS', 'CAULDRON', 'CBS TELEVISION STUDIOS', 'CBS', 'CCTV', 'CCV ENTERTAINMENT', 'CCV', 'CD BABY', 'CD LAND', 'CECCHI GORI', 'CENTURY MEDIA', 'CHUAN XUN SHI DAI MULTIMEDIA', 'CINE-ASIA', 'CINÉART', 'CINEART', 'CINEDIGM', 'CINEFIL IMAGICA', 'CINEMA EPOCH', 'CINEMA GUILD', 'CINEMA LIBRE STUDIOS', 'CINEMA MONDO', 'CINEMATIC VISION', 'CINEPLOIT RECORDS', 'CINESTRANGE EXTREME', 'CITEL VIDEO', 'CITEL', 'CJ ENTERTAINMENT', 'CJ', 'CLASSIC MEDIA', 'CLASSICFLIX', 'CLASSICLINE', 'CLAUDIO RECORDS', 'CLEAR VISION', 'CLEOPATRA', 'CLOSE UP', 'CMS MEDIA LIMITED', 'CMV LASERVISION', 'CN ENTERTAINMENT', 'CODE RED', 'COHEN MEDIA GROUP', 'COHEN', 'COIN DE MIRE CINÉMA', 'COIN DE MIRE CINEMA', 'COLOSSEO FILM', 'COLUMBIA', 'COLUMBIA PICTURES', 'COLUMBIA/TRI-STAR', 'TRI-STAR', 'COMMERCIAL MARKETING', 'CONCORD MUSIC GROUP', 'CONCORDE VIDEO', 'CONDOR', 'CONSTANTIN FILM', 'CONSTANTIN', 'CONSTANTINO FILMES', 'CONSTANTINO', 'CONSTRUCTIVE MEDIA SERVICE', 'CONSTRUCTIVE', 'CONTENT ZONE', 'CONTENTS GATE', 'COQUEIRO VERDE', 'CORNERSTONE MEDIA', 'CORNERSTONE', 'CP DIGITAL', 'CREST MOVIES', 'CRITERION', 'CRITERION COLLECTION', 'CC', 'CRYSTAL CLASSICS', 'CULT EPICS', 'CULT FILMS', 'CULT VIDEO', 'CURZON FILM WORLD', 'D FILMS', "D'AILLY COMPANY", 'DAILLY COMPANY', 'D AILLY COMPANY', "D'AILLY", 'DAILLY', 'D AILLY', 'DA CAPO', 'DA MUSIC', "DALL'ANGELO PICTURES", 'DALLANGELO PICTURES', "DALL'ANGELO", 'DALL ANGELO PICTURES', 'DALL ANGELO', 'DAREDO', 'DARK FORCE ENTERTAINMENT', 'DARK FORCE', 'DARK SIDE RELEASING', 'DARK SIDE', 'DAZZLER MEDIA', 'DAZZLER', 'DCM PICTURES', 'DCM', 'DEAPLANETA', 'DECCA', 'DEEPJOY', 'DEFIANT SCREEN ENTERTAINMENT', 'DEFIANT SCREEN', 'DEFIANT', 'DELOS', 'DELPHIAN RECORDS', 'DELPHIAN', 'DELTA MUSIC & ENTERTAINMENT', 'DELTA MUSIC AND ENTERTAINMENT', 'DELTA MUSIC ENTERTAINMENT', 'DELTA MUSIC', 'DELTAMAC CO. LTD.', 'DELTAMAC CO LTD', 'DELTAMAC CO', 'DELTAMAC', 'DEMAND MEDIA', 'DEMAND', 'DEP', 'DEUTSCHE GRAMMOPHON', 'DFW', 'DGM', 'DIAPHANA', 'DIGIDREAMS STUDIOS', 'DIGIDREAMS', 'DIGITAL ENVIRONMENTS', 'DIGITAL', 'DISCOTEK MEDIA', 'DISCOVERY CHANNEL', 'DISCOVERY', 'DISK KINO', 'DISNEY / BUENA VISTA', 'DISNEY', 'BUENA VISTA', 'DISNEY BUENA VISTA', 'DISTRIBUTION SELECT', 'DIVISA', 'DNC ENTERTAINMENT', 'DNC', 'DOGWOOF', 'DOLMEN HOME VIDEO', 'DOLMEN', 'DONAU FILM', 'DONAU', 'DORADO FILMS', 'DORADO', 'DRAFTHOUSE FILMS', 'DRAFTHOUSE', 'DRAGON FILM ENTERTAINMENT', 'DRAGON ENTERTAINMENT', 'DRAGON FILM', 'DRAGON', 'DREAMWORKS', 'DRIVE ON RECORDS', 'DRIVE ON', 'DRIVE-ON', 'DRIVEON', 'DS MEDIA', 'DTP ENTERTAINMENT AG', 'DTP ENTERTAINMENT', 'DTP AG', 'DTP', 'DTS ENTERTAINMENT', 'DTS', 'DUKE MARKETING', 'DUKE VIDEO DISTRIBUTION', 'DUKE', 'DUTCH FILMWORKS', 'DUTCH', 'DVD INTERNATIONAL', 'DVD', 'DYBEX', 'DYNAMIC', 'DYNIT', 'E1 ENTERTAINMENT', 'E1', 'EAGLE ENTERTAINMENT', 'EAGLE HOME ENTERTAINMENT PVT.LTD.', 'EAGLE HOME ENTERTAINMENT PVTLTD', 'EAGLE HOME ENTERTAINMENT PVT LTD', 'EAGLE HOME ENTERTAINMENT', 'EAGLE PICTURES', 'EAGLE ROCK ENTERTAINMENT', 'EAGLE ROCK', 'EAGLE VISION MEDIA', 'EAGLE VISION', 'EARMUSIC', 'EARTH ENTERTAINMENT', 'EARTH', 'ECHO BRIDGE ENTERTAINMENT', 'ECHO BRIDGE', 'EDEL GERMANY GMBH', 'EDEL GERMANY', 'EDEL RECORDS', 'EDITION TONFILM', 'EDITIONS MONTPARNASSE', 'EDKO FILMS LTD.', 'EDKO FILMS LTD', 'EDKO FILMS', 'EDKO', "EIN'S M&M CO", 'EINS M&M CO', "EIN'S M&M", 'EINS M&M', 'ELEA-MEDIA', 'ELEA MEDIA', 'ELEA', 'ELECTRIC PICTURE', 'ELECTRIC', 'ELEPHANT FILMS', 'ELEPHANT', 'ELEVATION', 'EMI', 'EMON', 'EMS', 'EMYLIA', 'ENE MEDIA', 'ENE', 'ENTERTAINMENT IN VIDEO', 'ENTERTAINMENT IN', 'ENTERTAINMENT ONE', 'ENTERTAINMENT ONE FILMS CANADA INC.', 'ENTERTAINMENT ONE FILMS CANADA INC', 'ENTERTAINMENT ONE FILMS CANADA', 'ENTERTAINMENT ONE CANADA INC', 'ENTERTAINMENT ONE CANADA', 'ENTERTAINMENTONE', 'EONE', 'EOS', 'EPIC PICTURES', 'EPIC', 'EPIC RECORDS', 'ERATO', 'EROS', 'ESC EDITIONS', 'ESCAPI MEDIA BV', 'ESOTERIC RECORDINGS', 'ESPN FILMS', 'EUREKA ENTERTAINMENT', 'EUREKA', 'EURO PICTURES', 'EURO VIDEO', 'EUROARTS', 'EUROPA FILMES', 'EUROPA', 'EUROPACORP', 'EUROZOOM', 'EXCEL', 'EXPLOSIVE MEDIA', 'EXPLOSIVE', 'EXTRALUCID FILMS', 'EXTRALUCID', 'EYE SEE MOVIES', 'EYE SEE', 'EYK MEDIA', 'EYK', 'FABULOUS FILMS', 'FABULOUS', 'FACTORIS FILMS', 'FACTORIS', 'FARAO RECORDS', 'FARBFILM HOME ENTERTAINMENT', 'FARBFILM ENTERTAINMENT', 'FARBFILM HOME', 'FARBFILM', 'FEELGOOD ENTERTAINMENT', 'FEELGOOD', 'FERNSEHJUWELEN', 'FILM CHEST', 'FILM MEDIA', 'FILM MOVEMENT', 'FILM4', 'FILMART', 'FILMAURO', 'FILMAX', 'FILMCONFECT HOME ENTERTAINMENT', 'FILMCONFECT ENTERTAINMENT', 'FILMCONFECT HOME', 'FILMCONFECT', 'FILMEDIA', 'FILMJUWELEN', 'FILMOTEKA NARODAWA', 'FILMRISE', 'FINAL CUT ENTERTAINMENT', 'FINAL CUT', 'FIREHOUSE 12 RECORDS', 'FIREHOUSE 12', 'FIRST INTERNATIONAL PRODUCTION', 'FIRST INTERNATIONAL', 'FIRST LOOK STUDIOS', 'FIRST LOOK', 'FLAGMAN TRADE', 'FLASHSTAR FILMES', 'FLASHSTAR', 'FLICKER ALLEY', 'FNC ADD CULTURE', 'FOCUS FILMES', 'FOCUS', 'FOKUS MEDIA', 'FOKUSA', 'FOX PATHE EUROPA', 'FOX PATHE', 'FOX EUROPA', 'FOX/MGM', 'FOX MGM', 'MGM', 'MGM/FOX', 'FOX', 'FPE', 'FRANCE TÉLÉVISIONS DISTRIBUTION', 'FRANCE TELEVISIONS DISTRIBUTION', 'FRANCE TELEVISIONS', 'FRANCE', 'FREE DOLPHIN ENTERTAINMENT', 'FREE DOLPHIN', 'FREESTYLE DIGITAL MEDIA', 'FREESTYLE DIGITAL', 'FREESTYLE', 'FREMANTLE HOME ENTERTAINMENT', 'FREMANTLE ENTERTAINMENT', 'FREMANTLE HOME', 'FREMANTL', 'FRENETIC FILMS', 'FRENETIC', 'FRONTIER WORKS', 'FRONTIER', 'FRONTIERS MUSIC', 'FRONTIERS RECORDS', 'FS FILM OY', 'FS FILM', 'FULL MOON FEATURES', 'FULL MOON', 'FUN CITY EDITIONS', 'FUN CITY', @@ -2416,7 +2416,7 @@ async def imgbox_upload(self, chdir, image_glob): async def get_name(self, meta): type = meta.get('type', "") - title = meta.get('title',"") + title = meta.get('title', "") alt_title = meta.get('aka', "") year = meta.get('year', "") resolution = meta.get('resolution', "") @@ -2434,7 +2434,7 @@ async def get_name(self, meta): uhd = meta.get('uhd', "") hdr = meta.get('hdr', "") episode_title = meta.get('episode_title', '') - if meta.get('is_disc', "") == "BDMV": #Disk + if meta.get('is_disc', "") == "BDMV": # Disk video_codec = meta.get('video_codec', "") region = meta.get('region', "") elif meta.get('is_disc', "") == "DVD": @@ -2450,11 +2450,11 @@ async def get_name(self, meta): year = meta['year'] else: year = "" - if meta.get('no_season', False) == True: + if meta.get('no_season', False) is True: season = '' - if meta.get('no_year', False) == True: + if meta.get('no_year', False) is True: year = '' - if meta.get('no_aka', False) == True: + if meta.get('no_aka', False) is True: alt_title = '' if meta['debug']: console.log("[cyan]get_name cat/type") @@ -2463,38 +2463,38 @@ async def get_name(self, meta): console.log("[cyan]get_name meta:") console.log(meta) - #YAY NAMING FUN - if meta['category'] == "MOVIE": #MOVIE SPECIFIC - if type == "DISC": #Disk + # YAY NAMING FUN + if meta['category'] == "MOVIE": # MOVIE SPECIFIC + if type == "DISC": # Disk if meta['is_disc'] == 'BDMV': name = f"{title} {alt_title} {year} {three_d} {edition} {repack} {resolution} {region} {uhd} {source} {hdr} {video_codec} {audio}" potential_missing = ['edition', 'region', 'distributor'] - elif meta['is_disc'] == 'DVD': + elif meta['is_disc'] == 'DVD': name = f"{title} {alt_title} {year} {edition} {repack} {source} {dvd_size} {audio}" potential_missing = ['edition', 'distributor'] elif meta['is_disc'] == 'HDDVD': name = f"{title} {alt_title} {year} {edition} {repack} {resolution} {source} {video_codec} {audio}" potential_missing = ['edition', 'region', 'distributor'] - elif type == "REMUX" and source in ("BluRay", "HDDVD"): #BluRay/HDDVD Remux - name = f"{title} {alt_title} {year} {three_d} {edition} {repack} {resolution} {uhd} {source} REMUX {hdr} {video_codec} {audio}" + elif type == "REMUX" and source in ("BluRay", "HDDVD"): # BluRay/HDDVD Remux + name = f"{title} {alt_title} {year} {three_d} {edition} {repack} {resolution} {uhd} {source} REMUX {hdr} {video_codec} {audio}" potential_missing = ['edition', 'description'] - elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD", "DVD"): #DVD Remux - name = f"{title} {alt_title} {year} {edition} {repack} {source} REMUX {audio}" + elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD", "DVD"): # DVD Remux + name = f"{title} {alt_title} {year} {edition} {repack} {source} REMUX {audio}" potential_missing = ['edition', 'description'] - elif type == "ENCODE": #Encode - name = f"{title} {alt_title} {year} {edition} {repack} {resolution} {uhd} {source} {audio} {hdr} {video_encode}" + elif type == "ENCODE": # Encode + name = f"{title} {alt_title} {year} {edition} {repack} {resolution} {uhd} {source} {audio} {hdr} {video_encode}" potential_missing = ['edition', 'description'] - elif type == "WEBDL": #WEB-DL + elif type == "WEBDL": # WEB-DL name = f"{title} {alt_title} {year} {edition} {repack} {resolution} {uhd} {service} WEB-DL {audio} {hdr} {video_encode}" potential_missing = ['edition', 'service'] - elif type == "WEBRIP": #WEBRip + elif type == "WEBRIP": # WEBRip name = f"{title} {alt_title} {year} {edition} {repack} {resolution} {uhd} {service} WEBRip {audio} {hdr} {video_encode}" potential_missing = ['edition', 'service'] - elif type == "HDTV": #HDTV + elif type == "HDTV": # HDTV name = f"{title} {alt_title} {year} {edition} {repack} {resolution} {source} {audio} {video_encode}" potential_missing = [] - elif meta['category'] == "TV": #TV SPECIFIC - if type == "DISC": #Disk + elif meta['category'] == "TV": #T V SPECIFIC + if type == "DISC": # Disk if meta['is_disc'] == 'BDMV': name = f"{title} {year} {alt_title} {season}{episode} {three_d} {edition} {repack} {resolution} {region} {uhd} {source} {hdr} {video_codec} {audio}" potential_missing = ['edition', 'region', 'distributor'] @@ -2504,26 +2504,26 @@ async def get_name(self, meta): elif meta['is_disc'] == 'HDDVD': name = f"{title} {alt_title} {year} {edition} {repack} {resolution} {source} {video_codec} {audio}" potential_missing = ['edition', 'region', 'distributor'] - elif type == "REMUX" and source in ("BluRay", "HDDVD"): #BluRay Remux - name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {three_d} {edition} {repack} {resolution} {uhd} {source} REMUX {hdr} {video_codec} {audio}" #SOURCE + elif type == "REMUX" and source in ("BluRay", "HDDVD"): # BluRay Remux + name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {three_d} {edition} {repack} {resolution} {uhd} {source} REMUX {hdr} {video_codec} {audio}" # SOURCE potential_missing = ['edition', 'description'] - elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD"): #DVD Remux - name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {edition} {repack} {source} REMUX {audio}" #SOURCE + elif type == "REMUX" and source in ("PAL DVD", "NTSC DVD"): # DVD Remux + name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {edition} {repack} {source} REMUX {audio}" # SOURCE potential_missing = ['edition', 'description'] - elif type == "ENCODE": #Encode - name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {edition} {repack} {resolution} {uhd} {source} {audio} {hdr} {video_encode}" #SOURCE + elif type == "ENCODE": # Encode + name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {edition} {repack} {resolution} {uhd} {source} {audio} {hdr} {video_encode}" # SOURCE potential_missing = ['edition', 'description'] - elif type == "WEBDL": #WEB-DL + elif type == "WEBDL": # WEB-DL name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {edition} {repack} {resolution} {uhd} {service} WEB-DL {audio} {hdr} {video_encode}" potential_missing = ['edition', 'service'] - elif type == "WEBRIP": #WEBRip + elif type == "WEBRIP": # WEBRip name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {edition} {repack} {resolution} {uhd} {service} WEBRip {audio} {hdr} {video_encode}" potential_missing = ['edition', 'service'] - elif type == "HDTV": #HDTV + elif type == "HDTV": # HDTV name = f"{title} {year} {alt_title} {season}{episode} {episode_title} {part} {edition} {repack} {resolution} {source} {audio} {video_encode}" potential_missing = [] - try: + try: name = ' '.join(name.split()) except: console.print("[bold red]Unable to generate name. Please re-run and correct any of the following args if needed.") @@ -2542,7 +2542,7 @@ async def get_season_episode(self, video, meta): filelist = meta['filelist'] meta['tv_pack'] = 0 is_daily = False - if meta['anime'] == False: + if meta['anime'] is False: try: if meta.get('manual_date'): raise ManualDateException @@ -2552,13 +2552,13 @@ async def get_season_episode(self, video, meta): guess_year = "" if guessit(video)["season"] == guess_year: if f"s{guessit(video)['season']}" in video.lower(): - season_int = str(guessit(video)["season"]) + season_int = str(guessit(video)["season"]) season = "S" + season_int.zfill(2) else: season_int = "1" season = "S01" else: - season_int = str(guessit(video)["season"]) + season_int = str(guessit(video)["season"]) season = "S" + season_int.zfill(2) except Exception: @@ -2575,11 +2575,11 @@ async def get_season_episode(self, video, meta): season_int = "1" season = "S01" try: - if is_daily != True: + if is_daily is not True: episodes = "" if len(filelist) == 1: episodes = guessit(video)['episode'] - if type(episodes) == list: + if isinstance(episodes, list): episode = "" for item in guessit(video)["episode"]: ep = (str(item).zfill(2)) @@ -2597,7 +2597,7 @@ async def get_season_episode(self, video, meta): episode_int = "0" meta['tv_pack'] = 1 else: - #If Anime + # If Anime parsed = anitopy.parse(Path(video).name) romaji, mal_id, eng_title, seasonYear, anilist_episodes = self.get_romaji(parsed['anime_title'], meta.get('mal', None)) if mal_id: @@ -2702,29 +2702,28 @@ async def get_season_episode(self, video, meta): console.print(f"[bold yellow]{meta['title']} does not exist on thexem, guessing {season}") console.print(f"[bold yellow]If [green]{season}[/green] is incorrect, use --season to correct") await asyncio.sleep(3) - - if meta.get('manual_season', None) == None: + + if meta.get('manual_season', None) is None: meta['season'] = season else: season_int = meta['manual_season'].lower().replace('s', '') meta['season'] = f"S{meta['manual_season'].lower().replace('s', '').zfill(2)}" - if meta.get('manual_episode', None) == None: + if meta.get('manual_episode', None) is None: meta['episode'] = episode else: episode_int = meta['manual_episode'].lower().replace('e', '') meta['episode'] = f"E{meta['manual_episode'].lower().replace('e', '').zfill(2)}" meta['tv_pack'] = 0 - + # if " COMPLETE " in Path(video).name.replace('.', ' '): # meta['season'] = "COMPLETE" meta['season_int'] = season_int meta['episode_int'] = episode_int - - meta['episode_title_storage'] = guessit(video,{"excludes" : "part"}).get('episode_title', '') + meta['episode_title_storage'] = guessit(video, {"excludes": "part"}).get('episode_title', '') if meta['season'] == "S00" or meta['episode'] == "E00": meta['episode_title'] = meta['episode_title_storage'] - + # Guess the part of the episode (if available) meta['part'] = "" if meta['tv_pack'] == 1: @@ -2746,7 +2745,7 @@ def get_service(self, video, tag, audio, guess_title): 'Comedians in Cars Getting Coffee': 'CCGC', 'CHGD': 'CHGD', 'CHRGD': 'CHGD', 'CMAX': 'CMAX', 'Cinemax': 'CMAX', 'CMOR': 'CMOR', 'CMT': 'CMT', 'Country Music Television': 'CMT', 'CN': 'CN', 'Cartoon Network': 'CN', 'CNBC': 'CNBC', 'CNLP': 'CNLP', 'Canal+': 'CNLP', 'COOK': 'COOK', 'CORE': 'CORE', 'CR': 'CR', 'Crunchy Roll': 'CR', 'Crave': 'CRAV', - 'CRIT': 'CRIT', 'Criterion' : 'CRIT', 'CRKL': 'CRKL', 'Crackle': 'CRKL', 'CSPN': 'CSPN', 'CSpan': 'CSPN', 'CTV': 'CTV', 'CUR': 'CUR', + 'CRIT': 'CRIT', 'Criterion': 'CRIT', 'CRKL': 'CRKL', 'Crackle': 'CRKL', 'CSPN': 'CSPN', 'CSpan': 'CSPN', 'CTV': 'CTV', 'CUR': 'CUR', 'CuriosityStream': 'CUR', 'CW': 'CW', 'The CW': 'CW', 'CWS': 'CWS', 'CWSeed': 'CWS', 'DAZN': 'DAZN', 'DCU': 'DCU', 'DC Universe': 'DCU', 'DDY': 'DDY', 'Digiturk Diledigin Yerde': 'DDY', 'DEST': 'DEST', 'DramaFever': 'DF', 'DHF': 'DHF', 'Deadhouse Films': 'DHF', 'DISC': 'DISC', 'Discovery': 'DISC', 'DIY': 'DIY', 'DIY Network': 'DIY', 'DOCC': 'DOCC', @@ -2755,23 +2754,23 @@ def get_service(self, video, tag, audio, guess_title): 'EPIX': 'EPIX', 'ePix': 'EPIX', 'ESPN': 'ESPN', 'ESQ': 'ESQ', 'Esquire': 'ESQ', 'ETTV': 'ETTV', 'El Trece': 'ETTV', 'ETV': 'ETV', 'E!': 'ETV', 'FAM': 'FAM', 'Fandor': 'FANDOR', 'Facebook Watch': 'FBWatch', 'FJR': 'FJR', 'Family Jr': 'FJR', 'FOOD': 'FOOD', 'Food Network': 'FOOD', 'FOX': 'FOX', 'Fox': 'FOX', 'Fox Premium': 'FOXP', - 'UFC Fight Pass': 'FP', 'FPT': 'FPT', 'FREE': 'FREE', 'Freeform': 'FREE', 'FTV': 'FTV', 'FUNI': 'FUNI', 'FUNi' : 'FUNI', + 'UFC Fight Pass': 'FP', 'FPT': 'FPT', 'FREE': 'FREE', 'Freeform': 'FREE', 'FTV': 'FTV', 'FUNI': 'FUNI', 'FUNi': 'FUNI', 'Foxtel': 'FXTL', 'FYI': 'FYI', 'FYI Network': 'FYI', 'GC': 'GC', 'NHL GameCenter': 'GC', 'GLBL': 'GLBL', 'Global': 'GLBL', 'GLOB': 'GLOB', 'GloboSat Play': 'GLOB', 'GO90': 'GO90', 'GagaOOLala': 'Gaga', 'HBO': 'HBO', 'HBO Go': 'HBO', 'HGTV': 'HGTV', 'HIDI': 'HIDI', 'HIST': 'HIST', 'History': 'HIST', 'HLMK': 'HLMK', 'Hallmark': 'HLMK', - 'HMAX': 'HMAX', 'HBO Max': 'HMAX', 'HS': 'HTSR', 'HTSR' : 'HTSR', 'HSTR': 'Hotstar', 'HULU': 'HULU', 'Hulu': 'HULU', 'hoichoi': 'HoiChoi', 'ID': 'ID', + 'HMAX': 'HMAX', 'HBO Max': 'HMAX', 'HS': 'HTSR', 'HTSR': 'HTSR', 'HSTR': 'Hotstar', 'HULU': 'HULU', 'Hulu': 'HULU', 'hoichoi': 'HoiChoi', 'ID': 'ID', 'Investigation Discovery': 'ID', 'IFC': 'IFC', 'iflix': 'IFX', 'National Audiovisual Institute': 'INA', 'ITV': 'ITV', - 'KAYO': 'KAYO', 'KNOW': 'KNOW', 'Knowledge Network': 'KNOW', 'KNPY': 'KNPY', 'Kanopy' : 'KNPY', 'LIFE': 'LIFE', 'Lifetime': 'LIFE', 'LN': 'LN', - 'MA' : 'MA', 'Movies Anywhere' : 'MA', 'MAX' : 'MAX', 'MBC': 'MBC', 'MNBC': 'MNBC', 'MSNBC': 'MNBC', 'MTOD': 'MTOD', 'Motor Trend OnDemand': 'MTOD', 'MTV': 'MTV', 'MUBI': 'MUBI', + 'KAYO': 'KAYO', 'KNOW': 'KNOW', 'Knowledge Network': 'KNOW', 'KNPY': 'KNPY', 'Kanopy': 'KNPY', 'LIFE': 'LIFE', 'Lifetime': 'LIFE', 'LN': 'LN', + 'MA': 'MA', 'Movies Anywhere': 'MA', 'MAX': 'MAX', 'MBC': 'MBC', 'MNBC': 'MNBC', 'MSNBC': 'MNBC', 'MTOD': 'MTOD', 'Motor Trend OnDemand': 'MTOD', 'MTV': 'MTV', 'MUBI': 'MUBI', 'NATG': 'NATG', 'National Geographic': 'NATG', 'NBA': 'NBA', 'NBA TV': 'NBA', 'NBC': 'NBC', 'NF': 'NF', 'Netflix': 'NF', 'National Film Board': 'NFB', 'NFL': 'NFL', 'NFLN': 'NFLN', 'NFL Now': 'NFLN', 'NICK': 'NICK', 'Nickelodeon': 'NICK', 'NRK': 'NRK', 'Norsk Rikskringkasting': 'NRK', 'OnDemandKorea': 'ODK', 'Opto': 'OPTO', 'Oprah Winfrey Network': 'OWN', 'PA': 'PA', 'PBS': 'PBS', 'PBSK': 'PBSK', 'PBS Kids': 'PBSK', 'PCOK': 'PCOK', 'Peacock': 'PCOK', 'PLAY': 'PLAY', 'PLUZ': 'PLUZ', 'Pluzz': 'PLUZ', 'PMNP': 'PMNP', - 'PMNT': 'PMNT', 'PMTP' : 'PMTP', 'POGO': 'POGO', 'PokerGO': 'POGO', 'PSN': 'PSN', 'Playstation Network': 'PSN', 'PUHU': 'PUHU', 'QIBI': 'QIBI', + 'PMNT': 'PMNT', 'PMTP': 'PMTP', 'POGO': 'POGO', 'PokerGO': 'POGO', 'PSN': 'PSN', 'Playstation Network': 'PSN', 'PUHU': 'PUHU', 'QIBI': 'QIBI', 'RED': 'RED', 'YouTube Red': 'RED', 'RKTN': 'RKTN', 'Rakuten TV': 'RKTN', 'The Roku Channel': 'ROKU', 'RSTR': 'RSTR', 'RTE': 'RTE', - 'RTE One': 'RTE', 'RUUTU': 'RUUTU', 'SBS': 'SBS', 'Science Channel': 'SCI', 'SESO': 'SESO', 'SeeSo': 'SESO', 'SHMI': 'SHMI', 'Shomi': 'SHMI', 'SKST' : 'SKST', 'SkyShowtime': 'SKST', + 'RTE One': 'RTE', 'RUUTU': 'RUUTU', 'SBS': 'SBS', 'Science Channel': 'SCI', 'SESO': 'SESO', 'SeeSo': 'SESO', 'SHMI': 'SHMI', 'Shomi': 'SHMI', 'SKST': 'SKST', 'SkyShowtime': 'SKST', 'SHO': 'SHO', 'Showtime': 'SHO', 'SNET': 'SNET', 'Sportsnet': 'SNET', 'Sony': 'SONY', 'SPIK': 'SPIK', 'Spike': 'SPIK', 'Spike TV': 'SPKE', - 'SPRT': 'SPRT', 'Sprout': 'SPRT', 'STAN': 'STAN', 'Stan': 'STAN', 'STARZ': 'STARZ', 'STRP': 'STRP', 'Star+' : 'STRP', 'STZ': 'STZ', 'Starz': 'STZ', 'SVT': 'SVT', + 'SPRT': 'SPRT', 'Sprout': 'SPRT', 'STAN': 'STAN', 'Stan': 'STAN', 'STARZ': 'STARZ', 'STRP': 'STRP', 'Star+': 'STRP', 'STZ': 'STZ', 'Starz': 'STZ', 'SVT': 'SVT', 'Sveriges Television': 'SVT', 'SWER': 'SWER', 'SwearNet': 'SWER', 'SYFY': 'SYFY', 'Syfy': 'SYFY', 'TBS': 'TBS', 'TEN': 'TEN', 'TFOU': 'TFOU', 'TFou': 'TFOU', 'TIMV': 'TIMV', 'TLC': 'TLC', 'TOU': 'TOU', 'TRVL': 'TRVL', 'TUBI': 'TUBI', 'TubiTV': 'TUBI', 'TV3': 'TV3', 'TV3 Ireland': 'TV3', 'TV4': 'TV4', 'TV4 Sweeden': 'TV4', 'TVING': 'TVING', 'TVL': 'TVL', 'TV Land': 'TVL', @@ -2786,7 +2785,7 @@ def get_service(self, video, tag, audio, guess_title): if "DTS-HD MA" in audio: video_name = video_name.replace("DTS-HD.MA.", "").replace("DTS-HD MA ", "") for key, value in services.items(): - if (' ' + key + ' ') in video_name and key not in guessit(video, {"excludes" : ["country", "language"]}).get('title', ''): + if (' ' + key + ' ') in video_name and key not in guessit(video, {"excludes": ["country", "language"]}).get('title', ''): service = value elif key == service: service = value @@ -2799,7 +2798,7 @@ def get_service(self, video, tag, audio, guess_title): return service, service_longname def stream_optimized(self, stream_opt): - if stream_opt == True: + if stream_opt is True: stream = 1 else: stream = 0 @@ -2810,22 +2809,22 @@ def is_anon(self, anon_in): if anon.lower() == "true": console.print("[bold red]Global ANON has been removed in favor of per-tracker settings. Please update your config accordingly.") time.sleep(10) - if anon_in == True: + if anon_in is True: anon_out = 1 else: anon_out = 0 return anon_out async def upload_image(self, session, url, data, headers, files): - if headers == None and files == None: + if headers is None and files is None: async with session.post(url=url, data=data) as resp: response = await resp.json() return response - elif headers == None and files != None: + elif headers is None and files is not None: async with session.post(url=url, data=data, files=files) as resp: response = await resp.json() return response - elif headers != None and files == None: + elif headers is not None and files is None: async with session.post(url=url, data=data, headers=headers) as resp: response = await resp.json() return response @@ -2835,7 +2834,7 @@ async def upload_image(self, session, url, data, headers, files): return response def clean_filename(self, name): - invalid = '<>:"/\|?*' + invalid = '<>:"/\\|?*' for char in invalid: name = name.replace(char, '-') return name @@ -2848,16 +2847,16 @@ async def gen_desc(self, meta): with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'w', newline="", encoding='utf8') as description: description.seek(0) if (desclink, descfile, meta['desc']) == (None, None, None): - if meta.get('ptp_manual') != None: + if meta.get('ptp_manual') is not None: desc_source.append('PTP') - if meta.get('blu_manual') != None: + if meta.get('blu_manual') is not None: desc_source.append('BLU') if len(desc_source) != 1: desc_source = None else: desc_source = desc_source[0] - if meta.get('ptp', None) != None and str(self.config['TRACKERS'].get('PTP', {}).get('useAPI')).lower() == "true" and desc_source in ['PTP', None]: + if meta.get('ptp', None) is not None and str(self.config['TRACKERS'].get('PTP', {}).get('useAPI')).lower() == "true" and desc_source in ['PTP', None]: ptp = PTP(config=self.config) ptp_desc = await ptp.get_ptp_description(meta['ptp'], meta['is_disc']) if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": @@ -2870,7 +2869,7 @@ async def gen_desc(self, meta): description.write(meta['blu_desc']) meta['description'] = 'BLU' - if meta.get('desc_template', None) != None: + if meta.get('desc_template', None) is not None: from jinja2 import Template with open(f"{meta['base_dir']}/data/templates/{meta['desc_template']}.txt", 'r') as f: desc_templater = Template(f.read()) @@ -2879,14 +2878,14 @@ async def gen_desc(self, meta): description.write(template_desc) description.write("\n") - if meta['nfo'] != False: + if meta['nfo'] is not False: description.write("[code]") nfo = glob.glob("*.nfo")[0] description.write(open(nfo, 'r', encoding="utf-8").read()) description.write("[/code]") description.write("\n") meta['description'] = "CUSTOM" - if desclink != None: + if desclink is not None: parsed = urllib.parse.urlparse(desclink.replace('/raw/', '/')) split = os.path.split(parsed.path) if split[0] != '/': @@ -2898,12 +2897,12 @@ async def gen_desc(self, meta): description.write("\n") meta['description'] = "CUSTOM" - if descfile != None: - if os.path.isfile(descfile) == True: + if descfile is not None: + if os.path.isfile(descfile) is True: text = open(descfile, 'r').read() description.write(text) meta['description'] = "CUSTOM" - if meta['desc'] != None: + if meta['desc'] is not None: description.write(meta['desc']) description.write("\n") meta['description'] = "CUSTOM" @@ -2914,7 +2913,7 @@ async def tag_override(self, meta): with open(f"{meta['base_dir']}/data/tags.json", 'r', encoding="utf-8") as f: tags = json.load(f) f.close() - + for tag in tags: value = tags.get(tag) if value.get('in_name', "") == tag and tag in meta['path']: @@ -2933,7 +2932,6 @@ async def tag_override(self, meta): else: meta[key] = value.get(key) return meta - async def package(self, meta): if meta['tag'] == "": @@ -2957,7 +2955,7 @@ async def package(self, meta): generic.write(f"TVDB: https://www.thetvdb.com/?id={meta['tvdb_id']}&tab=series\n") poster_img = f"{meta['base_dir']}/tmp/{meta['uuid']}/POSTER.png" if meta.get('poster', None) not in ['', None] and not os.path.exists(poster_img): - if meta.get('rehosted_poster', None) == None: + if meta.get('rehosted_poster', None) is None: r = requests.get(meta['poster'], stream=True) if r.status_code == 200: console.print("[bold yellow]Rehosting Poster") @@ -2973,18 +2971,18 @@ async def package(self, meta): metafile.close() else: console.print("[bold yellow]Poster could not be retrieved") - elif os.path.exists(poster_img) and meta.get('rehosted_poster') != None: + elif os.path.exists(poster_img) and meta.get('rehosted_poster') is not None: generic.write(f"TMDB Poster: {meta.get('rehosted_poster')}\n") if len(meta['image_list']) > 0: - generic.write(f"\nImage Webpage:\n") + generic.write("\nImage Webpage:\n") for each in meta['image_list']: generic.write(f"{each['web_url']}\n") - generic.write(f"\nThumbnail Image:\n") + generic.write("\nThumbnail Image:\n") for each in meta['image_list']: generic.write(f"{each['img_url']}\n") title = re.sub(r"[^0-9a-zA-Z\[\\]]+", "", meta['title']) archive = f"{meta['base_dir']}/tmp/{meta['uuid']}/{title}" - torrent_files = glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}","*.torrent") + torrent_files = glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}", "*.torrent") if isinstance(torrent_files, list) and len(torrent_files) > 1: for each in torrent_files: if not each.startswith(('BASE', '[RAND')): @@ -2997,12 +2995,12 @@ async def package(self, meta): # shutil.copy(os.path.abspath(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent"), os.path.abspath(f"{meta['base_dir']}/tmp/{meta['uuid']}/{meta['name'].replace(' ', '.')}.torrent").replace(' ', '.')) filebrowser = self.config['TRACKERS'].get('MANUAL', {}).get('filebrowser', None) shutil.make_archive(archive, 'tar', f"{meta['base_dir']}/tmp/{meta['uuid']}") - if filebrowser != None: + if filebrowser is not None: url = '/'.join(s.strip('/') for s in (filebrowser, f"/tmp/{meta['uuid']}")) url = urllib.parse.quote(url, safe="https://") else: files = { - "files[]" : (f"{meta['title']}.tar", open(f"{archive}.tar", 'rb')) + "files[]": (f"{meta['title']}.tar", open(f"{archive}.tar", 'rb')) } response = requests.post("https://uguu.se/upload.php", files=files).json() if meta['debug']: @@ -3011,14 +3009,14 @@ async def package(self, meta): return url except Exception: return False - return + return async def get_imdb_aka(self, imdb_id): if imdb_id == "0": return "", None ia = Cinemagoer() result = ia.get_movie(imdb_id.replace('tt', '')) - + original_language = result.get('language codes') if isinstance(original_language, list): if len(original_language) > 1: @@ -3044,7 +3042,6 @@ async def get_dvd_size(self, discs): dvd_sizes.sort() compact = " ".join(dvd_sizes) return compact - def get_tmdb_imdb_from_mediainfo(self, mediainfo, category, is_disc, tmdbid, imdbid): if not is_disc: @@ -3053,7 +3050,7 @@ def get_tmdb_imdb_from_mediainfo(self, mediainfo, category, is_disc, tmdbid, imd for each in extra: if each.lower().startswith('tmdb'): parser = Args(config=self.config) - category, tmdbid = parser.parse_tmdb_id(id = extra[each], category=category) + category, tmdbid = parser.parse_tmdb_id(id=extra[each], category=category) if each.lower().startswith('imdb'): try: imdbid = str(int(extra[each].replace('tt', ''))).zfill(7) @@ -3061,7 +3058,6 @@ def get_tmdb_imdb_from_mediainfo(self, mediainfo, category, is_disc, tmdbid, imd pass return category, tmdbid, imdbid - def daily_to_tmdb_season_episode(self, tmdbid, date): show = tmdb.TV(tmdbid) seasons = show.info().get('seasons') @@ -3081,9 +3077,6 @@ def daily_to_tmdb_season_episode(self, tmdbid, date): console.print(f"[yellow]Unable to map the date ([bold yellow]{str(date)}[/bold yellow]) to a Season/Episode number") return season, episode - - - async def get_imdb_info(self, imdbID, meta): imdb_info = {} if int(str(imdbID).replace('tt', '')) != 0: @@ -3112,18 +3105,17 @@ async def get_imdb_info(self, imdbID, meta): imdb_info['directors'].append(f"nm{director.getID()}") else: imdb_info = { - 'title' : meta['title'], - 'year' : meta['year'], - 'aka' : '', - 'type' : None, - 'runtime' : meta.get('runtime', '60'), - 'cover' : meta.get('poster'), + 'title': meta['title'], + 'year': meta['year'], + 'aka': '', + 'type': None, + 'runtime': meta.get('runtime', '60'), + 'cover': meta.get('poster'), } if len(meta.get('tmdb_directors', [])) >= 1: imdb_info['directors'] = meta['tmdb_directors'] return imdb_info - async def search_imdb(self, filename, search_year): imdbID = '0' @@ -3135,7 +3127,6 @@ async def search_imdb(self, filename, search_year): imdbID = str(movie.movieID).replace('tt', '') return imdbID - async def imdb_other_meta(self, meta): imdb_info = meta['imdb_info'] = await self.get_imdb_info(meta['imdb_id'], meta) meta['title'] = imdb_info['title'] @@ -3157,49 +3148,49 @@ async def search_tvmaze(self, filename, year, imdbID, tvdbID): tvmazeID = 0 lookup = False show = None - if imdbID == None: + if imdbID is None: imdbID = '0' - if tvdbID == None: + if tvdbID is None: tvdbID = 0 if int(tvdbID) != 0: params = { - "thetvdb" : tvdbID + "thetvdb": tvdbID } url = "https://api.tvmaze.com/lookup/shows" lookup = True elif int(imdbID) != 0: params = { - "imdb" : f"tt{imdbID}" + "imdb": f"tt{imdbID}" } url = "https://api.tvmaze.com/lookup/shows" lookup = True else: params = { - "q" : filename + "q": filename } - url = f"https://api.tvmaze.com/search/shows" + url = "https://api.tvmaze.com/search/shows" resp = requests.get(url=url, params=params) if resp.ok: resp = resp.json() - if resp == None: + if resp is None: return tvmazeID, imdbID, tvdbID - if lookup == True: + if lookup is True: show = resp else: if year not in (None, ''): for each in resp: premier_date = each['show'].get('premiered', '') - if premier_date != None: + if premier_date is not None: if premier_date.startswith(str(year)): show = each['show'] elif len(resp) >= 1: show = resp[0]['show'] - if show != None: + if show is not None: tvmazeID = show.get('id') if int(imdbID) == 0: - if show.get('externals', {}).get('imdb', '0') != None: + if show.get('externals', {}).get('imdb', '0') is not None: imdbID = str(show.get('externals', {}).get('imdb', '0')).replace('tt', '') if int(tvdbID) == 0: - if show.get('externals', {}).get('tvdb', '0') != None: + if show.get('externals', {}).get('tvdb', '0') is not None: tvdbID = show.get('externals', {}).get('tvdb', '0') return tvmazeID, imdbID, tvdbID diff --git a/upload.py b/upload.py index d003baff1..1bbb4fb7f 100644 --- a/upload.py +++ b/upload.py @@ -195,7 +195,7 @@ async def do_the_thing(base_dir): if meta['debug']: console.print(meta['image_list']) # meta['uploaded_screens'] = True - elif meta.get('skip_imghost_upload', False) == True and meta.get('image_list', False) is False: + elif meta.get('skip_imghost_upload', False) is True and meta.get('image_list', False) is False: meta['image_list'] = [] if not os.path.exists(os.path.abspath(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent")): @@ -208,11 +208,11 @@ async def do_the_thing(base_dir): prep.create_torrent(meta, Path(meta['path']), "BASE") if meta['nohash']: meta['client'] = "none" - elif os.path.exists(os.path.abspath(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent")) and meta.get('rehash', False) == True and meta['nohash'] is False: + elif os.path.exists(os.path.abspath(f"{meta['base_dir']}/tmp/{meta['uuid']}/BASE.torrent")) and meta.get('rehash', False) is True and meta['nohash'] is False: prep.create_torrent(meta, Path(meta['path']), "BASE") if int(meta.get('randomized', 0)) >= 1: prep.create_random_torrents(meta['base_dir'], meta['uuid'], meta['randomized'], meta['path']) - + if meta.get('trackers', None) is not None: trackers = meta['trackers'] else: @@ -242,9 +242,7 @@ async def do_the_thing(base_dir): if meta.get('manual', False): trackers.insert(0, "MANUAL") - #################################### - ####### Upload to Trackers ####### - #################################### + # Upload to Trackers common = COMMON(config=config) api_trackers = ['BLU', 'AITHER', 'STC', 'R4E', 'STT', 'RF', 'ACM', 'LCD', 'LST', 'HUNO', 'SN', 'LT', 'NBL', 'ANT', 'JPTV', 'TDC', 'OE', 'BHDTV', 'RTF', 'OTW', 'FNP', 'CBR', 'UTP', 'AL', 'HDB'] http_trackers = ['TTG', 'FL', 'PTER', 'HDT', 'MTV'] From 7c462350e1f53767a134b253e2be7a6c55b34f59 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 17:04:19 +1000 Subject: [PATCH 21/33] More linter --- .flake8 | 2 +- .github/workflows/.flake8 | 2 +- cogs/commands.py | 7 +- discordbot.py | 11 +- src/bbcode.py | 2 +- src/discparse.py | 8 +- src/prep.py | 112 ++++++++++----------- src/trackers/ACM.py | 206 +++++++++++++++++++------------------- src/trackers/AITHER.py | 4 +- src/trackers/ANT.py | 12 +-- src/trackers/BHD.py | 6 +- src/trackers/BHDTV.py | 56 ++++------- src/trackers/BLU.py | 4 +- src/trackers/CBR.py | 116 ++++++++++----------- src/trackers/COMMON.py | 6 +- src/trackers/LST.py | 132 +++++++++++------------- 16 files changed, 320 insertions(+), 366 deletions(-) diff --git a/.flake8 b/.flake8 index 0cb611f43..ac7343518 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,2 @@ [flake8] -max-line-length = 220 \ No newline at end of file +max-line-length = 1000 \ No newline at end of file diff --git a/.github/workflows/.flake8 b/.github/workflows/.flake8 index 0cb611f43..b603cf126 100644 --- a/.github/workflows/.flake8 +++ b/.github/workflows/.flake8 @@ -1,2 +1,2 @@ [flake8] -max-line-length = 220 \ No newline at end of file +max-line-length = 500 \ No newline at end of file diff --git a/cogs/commands.py b/cogs/commands.py index 230de6b73..647c4787b 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -317,7 +317,7 @@ async def send_embed_and_upload(self, ctx, meta): embed.add_field(name="Links", value=f"[TMDB](https://www.themoviedb.org/{meta['category'].lower()}/{meta['tmdb']}){imdb}{tvdb}") embed.add_field(name=f"{res} / {meta['type']}{tag}", value=f"```{meta['name']}```", inline=False) if missing != []: - embed.add_field(name=f"POTENTIALLY MISSING INFORMATION:", value="\n".join(missing), inline=False) + embed.add_field(name="POTENTIALLY MISSING INFORMATION:", value="\n".join(missing), inline=False) embed.set_thumbnail(url=f"https://image.tmdb.org/t/p/original{meta['poster']}") embed.set_footer(text=meta['uuid']) embed.set_author(name="L4G's Upload Assistant", url="https://github.com/Audionut/Upload-Assistant", icon_url="https://images2.imgbox.com/6e/da/dXfdgNYs_o.png") @@ -380,7 +380,7 @@ def check(reaction, user): await msg.clear_reactions() await msg.edit(embed=timeout_embed) return - except: + except Exception: print("timeout after edit") pass except CancelException: @@ -533,7 +533,7 @@ def check(reaction, user): try: await channel.send(f"{meta['uuid']} timed out") meta['upload'] = False - except: + except Exception: return except CancelException: await channel.send(f"{meta['title']} cancelled") @@ -557,6 +557,7 @@ async def get_missing(self, meta): missing.append(f"--{each}") return missing + def setup(bot): bot.add_cog(Commands(bot)) diff --git a/discordbot.py b/discordbot.py index 4e6e6d3ae..6fadac007 100644 --- a/discordbot.py +++ b/discordbot.py @@ -1,21 +1,18 @@ import asyncio import datetime -import json import logging -import configparser from pathlib import Path import discord from discord.ext import commands - - def config_load(): # Python Config from data.config import config return config + async def run(): """ Where the bot gets started. If you wanted to create an database connection pool or other session for the bot to use, @@ -75,7 +72,6 @@ async def load_all_extensions(self): error = f'{extension}\n {type(e).__name__} : {e}' print(f'failed to load extension {error}') print('-' * 10) - async def on_ready(self): """ @@ -101,11 +97,6 @@ async def on_message(self, message): return # ignore all bots await self.process_commands(message) - - - - - if __name__ == '__main__': logging.basicConfig(level=logging.INFO) diff --git a/src/bbcode.py b/src/bbcode.py index 51bdacbab..2821a7a03 100644 --- a/src/bbcode.py +++ b/src/bbcode.py @@ -126,7 +126,7 @@ def clean_ptp_description(self, desc, is_disc): # Re-place comparisons if comp_placeholders != []: for i, comp in enumerate(comp_placeholders): - comp = re.sub(r"\[\/?img[\s\S]*?\]", "",comp, flags=re.IGNORECASE) + comp = re.sub(r"\[\/?img[\s\S]*?\]", "", comp, flags=re.IGNORECASE) desc = desc.replace(f"COMPARISON_PLACEHOLDER-{i} ", comp) # Convert hides with multiple images to comparison diff --git a/src/discparse.py b/src/discparse.py index c4db1499a..0a1cb28c0 100644 --- a/src/discparse.py +++ b/src/discparse.py @@ -39,7 +39,7 @@ async def get_bdinfo(self, discs, folder_id, base_dir, meta_discs): console.print(f"[bold green]Scanning {path}") proc = await asyncio.create_subprocess_exec('mono', f"{base_dir}/bin/BDInfo/BDInfo.exe", '-w', path, save_dir) await proc.wait() - except: + except Exception: console.print('[bold red]mono not found, please install mono') elif sys.platform.startswith('win32'): @@ -135,7 +135,7 @@ def parse_bdinfo(self, bdinfo_input, files, path): bit_depth = split2[n+6].strip() hdr_dv = split2[n+7].strip() color = split2[n+8].strip() - except: + except Exception: bit_depth = "" hdr_dv = "" color = "" @@ -165,7 +165,7 @@ def parse_bdinfo(self, bdinfo_input, files, path): fuckatmos = "" try: bit_depth = split2[n+5].strip() - except: + except Exception: bit_depth = "" bdinfo['audio'].append({ 'language': split2[0].strip(), @@ -200,7 +200,7 @@ def parse_bdinfo(self, bdinfo_input, files, path): m2ts['file'] = bd_file m2ts['length'] = bd_length bdinfo['files'].append(m2ts) - except: + except Exception: pass return bdinfo diff --git a/src/prep.py b/src/prep.py index cb56bcf57..390ef334a 100644 --- a/src/prep.py +++ b/src/prep.py @@ -714,13 +714,13 @@ def get_resolution(self, guess, folder_id, base_dir): try: width = mi['media']['track'][1]['Width'] height = mi['media']['track'][1]['Height'] - except: + except Exception: width = 0 height = 0 framerate = mi['media']['track'][1].get('FrameRate', '') try: scan = mi['media']['track'][1]['ScanType'] - except: + except Exception: scan = "Progressive" if scan == "Progressive": scan = "p" @@ -777,7 +777,7 @@ def mi_resolution(self, res, guess, width, scan, height, actual_height): if resolution is None: try: resolution = guess['screen_size'] - except: + except Exception: width_map = { '3840p': '2160p', '2560p': '1550p', @@ -879,39 +879,39 @@ def disc_screenshots(self, filename, bdinfo, folder_id, base_dir, use_vs, image_ "[cyan]{task.completed}/{task.total}", TimeRemainingColumn() ) as progress: - screen_task = progress.add_task("[green]Saving Screens...", total=num_screens + 1) - ss_times = [] - for i in range(num_screens + 1): - image = f"{base_dir}/tmp/{folder_id}/{filename}-{i}.png" - try: - ss_times = self.valid_ss_time(ss_times, num_screens+1, length) - ( - ffmpeg - .input(file, ss=ss_times[-1], skip_frame=keyframe) - .output(image, vframes=1, pix_fmt="rgb24") - .overwrite_output() - .global_args('-loglevel', loglevel) - .run(quiet=debug) - ) - except Exception: - console.print(traceback.format_exc()) - - self.optimize_images(image) - if os.path.getsize(Path(image)) <= 31000000 and self.img_host == "imgbb": - i += 1 - elif os.path.getsize(Path(image)) <= 10000000 and self.img_host in ["imgbox", 'pixhost']: - i += 1 - elif os.path.getsize(Path(image)) <= 75000: - console.print("[bold yellow]Image is incredibly small, retaking") - time.sleep(1) - elif self.img_host == "ptpimg": - i += 1 - elif self.img_host == "lensdump": - i += 1 - else: - console.print("[red]Image too large for your image host, retaking") - time.sleep(1) - progress.advance(screen_task) + screen_task = progress.add_task("[green]Saving Screens...", total=num_screens + 1) + ss_times = [] + for i in range(num_screens + 1): + image = f"{base_dir}/tmp/{folder_id}/{filename}-{i}.png" + try: + ss_times = self.valid_ss_time(ss_times, num_screens+1, length) + ( + ffmpeg + .input(file, ss=ss_times[-1], skip_frame=keyframe) + .output(image, vframes=1, pix_fmt="rgb24") + .overwrite_output() + .global_args('-loglevel', loglevel) + .run(quiet=debug) + ) + except Exception: + console.print(traceback.format_exc()) + + self.optimize_images(image) + if os.path.getsize(Path(image)) <= 31000000 and self.img_host == "imgbb": + i += 1 + elif os.path.getsize(Path(image)) <= 10000000 and self.img_host in ["imgbox", 'pixhost']: + i += 1 + elif os.path.getsize(Path(image)) <= 75000: + console.print("[bold yellow]Image is incredibly small, retaking") + time.sleep(1) + elif self.img_host == "ptpimg": + i += 1 + elif self.img_host == "lensdump": + i += 1 + else: + console.print("[red]Image too large for your image host, retaking") + time.sleep(1) + progress.advance(screen_task) # remove smallest image smallest = "" smallestsize = 99 ** 99 @@ -1194,7 +1194,7 @@ def optimize_images(self, image): oxipng.optimize(image, level=6) else: oxipng.optimize(image, level=3) - except: + except Exception: pass return @@ -1319,7 +1319,7 @@ async def tmdb_other_meta(self, meta): meta = await self.get_tmdb_id(guessit(title, {"excludes": ["country", "language"]})['title'], meta['search_year'], meta) if meta['tmdb'] == "0": meta = await self.get_tmdb_id(title, "", meta, meta['category']) - except: + except Exception: if meta.get('mode', 'discord') == 'cli': console.print("[bold red]Unable to find tmdb entry. Exiting.") exit() @@ -1441,7 +1441,7 @@ def get_keywords(self, tmdb_info): keywords = [f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('keywords')] elif tmdb_keywords.get('results') is not None: keywords = [f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('results')] - return(', '.join(keywords)) + return(', '.join (keywords)) else: return '' @@ -1450,7 +1450,7 @@ def get_genres(self, tmdb_info): tmdb_genres = tmdb_info.get('genres', []) if tmdb_genres is not []: genres = [f"{genre['name'].replace(',', ' ')}" for genre in tmdb_genres] - return(', '.join(genres)) + return(', '.join (genres)) else: return '' @@ -1553,7 +1553,7 @@ def get_romaji(self, tmdb_name, mal): response = requests.post(url, json={'query': query, 'variables': variables}) json = response.json() media = json['data']['Page']['media'] - except: + except Exception: console.print('[red]Failed to get anime specific info from anilist. Continuing without it...') media = [] if media not in (None, []): @@ -1628,10 +1628,10 @@ def get_audio_v2(self, mi, meta, bdinfo): channels = mi['media']['track'][track_num]['Channels'] try: channel_layout = mi['media']['track'][track_num]['ChannelLayout'] - except: + except Exception: try: channel_layout = mi['media']['track'][track_num]['ChannelLayout_Original'] - except: + except Exception: channel_layout = "" # Ensure channel_layout is not None or an empty string before iterating @@ -1789,7 +1789,7 @@ def get_tag(self, video, meta): try: tag = guessit(video)['release_group'] tag = f"-{tag}" - except: + except Exception: tag = "" if tag == "-": tag = "" @@ -1801,10 +1801,10 @@ def get_source(self, type, video, path, is_disc, meta): try: try: source = guessit(video)['source'] - except: + except Exception: try: source = guessit(path)['source'] - except: + except Exception: source = "BluRay" if meta.get('manual_source', None): source = meta['manual_source'] @@ -1824,14 +1824,14 @@ def get_source(self, type, video, path, is_disc, meta): system = track.standard if system not in ("PAL", "NTSC"): raise WeirdSystem - except: + except Exception: try: other = guessit(video)['other'] if "PAL" in other: system = "PAL" elif "NTSC" in other: system = "NTSC" - except: + except Exception: system = "" finally: if system is None: @@ -1861,7 +1861,7 @@ def get_uhd(self, type, guess, resolution, path): try: source = guess['Source'] other = guess['Other'] - except: + except Exception: source = "" other = "" uhd = "" @@ -1889,7 +1889,7 @@ def get_hdr(self, mi, bdinfo): try: if bdinfo['video'][1]['hdr_dv'] == "Dolby Vision": dv = "DV" - except: + except Exception: pass else: video_track = mi['media']['track'][1] @@ -1909,13 +1909,13 @@ def get_hdr(self, mi, bdinfo): hdr = "HLG" if hdr != "HLG" and "BT.2020 (10-bit)" in transfer_characteristics: hdr = "WCG" - except: + except Exception: pass try: if "Dolby Vision" in video_track.get('HDR_Format', '') or "Dolby Vision" in video_track.get('HDR_Format_String', ''): dv = "DV" - except: + except Exception: pass hdr = f"{dv} {hdr}".strip() @@ -2016,7 +2016,7 @@ def get_video_encode(self, mi, type, bdinfo): if mi['media']['track'][1].get('Encoded_Library_Settings', None): has_encode_settings = True bit_depth = mi['media']['track'][1].get('BitDepth', '0') - except: + except Exception: format = bdinfo['video'][0]['codec'] format_profile = bdinfo['video'][0]['profile'] if type in ("ENCODE", "WEBRIP"): # ENCODE or WEBRIP @@ -2493,7 +2493,7 @@ async def get_name(self, meta): elif type == "HDTV": # HDTV name = f"{title} {alt_title} {year} {edition} {repack} {resolution} {source} {audio} {video_encode}" potential_missing = [] - elif meta['category'] == "TV": #T V SPECIFIC + elif meta['category'] == "TV": # TV SPECIFIC if type == "DISC": # Disk if meta['is_disc'] == 'BDMV': name = f"{title} {year} {alt_title} {season}{episode} {three_d} {edition} {repack} {resolution} {region} {uhd} {source} {hdr} {video_codec} {audio}" @@ -2525,7 +2525,7 @@ async def get_name(self, meta): try: name = ' '.join(name.split()) - except: + except Exception: console.print("[bold red]Unable to generate name. Please re-run and correct any of the following args if needed.") console.print(f"--category [yellow]{meta['category']}") console.print(f"--type [yellow]{meta['type']}") @@ -2966,7 +2966,7 @@ async def package(self, meta): poster = poster[0] generic.write(f"TMDB Poster: {poster.get('raw_url', poster.get('img_url'))}\n") meta['rehosted_poster'] = poster.get('raw_url', poster.get('img_url')) - with open (f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as metafile: + with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as metafile: json.dump(meta, metafile, indent=4) metafile.close() else: diff --git a/src/trackers/ACM.py b/src/trackers/ACM.py index ec4276cd9..99fecbe80 100644 --- a/src/trackers/ACM.py +++ b/src/trackers/ACM.py @@ -30,12 +30,12 @@ def __init__(self, config): async def get_cat_id(self, category_name): category_id = { - 'MOVIE': '1', - 'TV': '2', + 'MOVIE': '1', + 'TV': '2', }.get(category_name, '0') return category_id - async def get_type (self, meta): + async def get_type(self, meta): if meta['is_disc'] == "BDMV": bdinfo = meta['bdinfo'] bd_sizes = [25, 50, 66, 100] @@ -53,7 +53,7 @@ async def get_type (self, meta): if "DVD5" in meta['dvd_size']: type_string = "DVD 5" elif "DVD9" in meta['dvd_size']: - type_string = "DVD 9" + type_string = "DVD 9" else: if meta['type'] == "REMUX": if meta['source'] == "BluRay": @@ -66,18 +66,18 @@ async def get_type (self, meta): # acceptable_res = ["2160p", "1080p", "1080i", "720p", "576p", "576i", "540p", "480p", "Other"] # if meta['resolution'] in acceptable_res: # type_id = meta['resolution'] - # else: + # else: # type_id = "Other" return type_string async def get_type_id(self, type): type_id = { - 'UHD 100': '1', + 'UHD 100': '1', 'UHD 66': '2', 'UHD 50': '3', 'UHD REMUX': '12', 'BD 50': '4', - 'BD 25': '5', + 'BD 25': '5', 'DVD 5': '14', 'REMUX': '7', 'WEBDL': '9', @@ -89,68 +89,68 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { - '2160p': '1', + '2160p': '1', '1080p': '2', - '1080i':'2', - '720p': '3', - '576p': '4', + '1080i': '2', + '720p': '3', + '576p': '4', '576i': '4', - '480p': '5', + '480p': '5', '480i': '5' }.get(resolution, '10') - return resolution_id + return resolution_id - #ACM rejects uploads with more that 4 keywords + # ACM rejects uploads with more that 4 keywords async def get_keywords(self, keywords): - if keywords !='': - keywords_list = keywords.split(',') + if keywords != '': + keywords_list = keywords.split(',') keywords_list = [keyword for keyword in keywords_list if " " not in keyword][:4] - keywords = ', '.join( keywords_list) + keywords = ', '.join(keywords_list) return keywords def get_subtitles(self, meta): sub_lang_map = { - ("Arabic", "ara", "ar") : 'Ara', - ("Brazilian Portuguese", "Brazilian", "Portuguese-BR", 'pt-br') : 'Por-BR', - ("Bulgarian", "bul", "bg") : 'Bul', - ("Chinese", "chi", "zh", "Chinese (Simplified)", "Chinese (Traditional)") : 'Chi', - ("Croatian", "hrv", "hr", "scr") : 'Cro', - ("Czech", "cze", "cz", "cs") : 'Cze', - ("Danish", "dan", "da") : 'Dan', - ("Dutch", "dut", "nl") : 'Dut', - ("English", "eng", "en", "English (CC)", "English - SDH") : 'Eng', - ("English - Forced", "English (Forced)", "en (Forced)") : 'Eng', - ("English Intertitles", "English (Intertitles)", "English - Intertitles", "en (Intertitles)") : 'Eng', - ("Estonian", "est", "et") : 'Est', - ("Finnish", "fin", "fi") : 'Fin', - ("French", "fre", "fr") : 'Fre', - ("German", "ger", "de") : 'Ger', - ("Greek", "gre", "el") : 'Gre', - ("Hebrew", "heb", "he") : 'Heb', - ("Hindi" "hin", "hi") : 'Hin', - ("Hungarian", "hun", "hu") : 'Hun', - ("Icelandic", "ice", "is") : 'Ice', - ("Indonesian", "ind", "id") : 'Ind', - ("Italian", "ita", "it") : 'Ita', - ("Japanese", "jpn", "ja") : 'Jpn', - ("Korean", "kor", "ko") : 'Kor', - ("Latvian", "lav", "lv") : 'Lav', - ("Lithuanian", "lit", "lt") : 'Lit', - ("Norwegian", "nor", "no") : 'Nor', - ("Persian", "fa", "far") : 'Per', - ("Polish", "pol", "pl") : 'Pol', - ("Portuguese", "por", "pt") : 'Por', - ("Romanian", "rum", "ro") : 'Rom', - ("Russian", "rus", "ru") : 'Rus', - ("Serbian", "srp", "sr", "scc") : 'Ser', - ("Slovak", "slo", "sk") : 'Slo', - ("Slovenian", "slv", "sl") : 'Slv', - ("Spanish", "spa", "es") : 'Spa', - ("Swedish", "swe", "sv") : 'Swe', - ("Thai", "tha", "th") : 'Tha', - ("Turkish", "tur", "tr") : 'Tur', - ("Ukrainian", "ukr", "uk") : 'Ukr', - ("Vietnamese", "vie", "vi") : 'Vie', + ("Arabic", "ara", "ar"): 'Ara', + ("Brazilian Portuguese", "Brazilian", "Portuguese-BR", 'pt-br'): 'Por-BR', + ("Bulgarian", "bul", "bg"): 'Bul', + ("Chinese", "chi", "zh", "Chinese (Simplified)", "Chinese (Traditional)"): 'Chi', + ("Croatian", "hrv", "hr", "scr"): 'Cro', + ("Czech", "cze", "cz", "cs"): 'Cze', + ("Danish", "dan", "da"): 'Dan', + ("Dutch", "dut", "nl"): 'Dut', + ("English", "eng", "en", "English (CC)", "English - SDH"): 'Eng', + ("English - Forced", "English (Forced)", "en (Forced)"): 'Eng', + ("English Intertitles", "English (Intertitles)", "English - Intertitles", "en (Intertitles)"): 'Eng', + ("Estonian", "est", "et"): 'Est', + ("Finnish", "fin", "fi"): 'Fin', + ("French", "fre", "fr"): 'Fre', + ("German", "ger", "de"): 'Ger', + ("Greek", "gre", "el"): 'Gre', + ("Hebrew", "heb", "he"): 'Heb', + ("Hindi" "hin", "hi"): 'Hin', + ("Hungarian", "hun", "hu"): 'Hun', + ("Icelandic", "ice", "is"): 'Ice', + ("Indonesian", "ind", "id"): 'Ind', + ("Italian", "ita", "it"): 'Ita', + ("Japanese", "jpn", "ja"): 'Jpn', + ("Korean", "kor", "ko"): 'Kor', + ("Latvian", "lav", "lv"): 'Lav', + ("Lithuanian", "lit", "lt"): 'Lit', + ("Norwegian", "nor", "no"): 'Nor', + ("Persian", "fa", "far"): 'Per', + ("Polish", "pol", "pl"): 'Pol', + ("Portuguese", "por", "pt"): 'Por', + ("Romanian", "rum", "ro"): 'Rom', + ("Russian", "rus", "ru"): 'Rus', + ("Serbian", "srp", "sr", "scc"): 'Ser', + ("Slovak", "slo", "sk"): 'Slo', + ("Slovenian", "slv", "sl"): 'Slv', + ("Spanish", "spa", "es"): 'Spa', + ("Swedish", "swe", "sv"): 'Swe', + ("Thai", "tha", "th"): 'Tha', + ("Turkish", "tur", "tr"): 'Tur', + ("Ukrainian", "ukr", "uk"): 'Ukr', + ("Vietnamese", "vie", "vi"): 'Vie', } sub_langs = [] @@ -173,11 +173,11 @@ def get_subtitles(self, meta): if language in lang and subID not in sub_langs: sub_langs.append(subID) - # if sub_langs == []: + # if sub_langs == []: # sub_langs = [44] # No Subtitle return sub_langs - def get_subs_tag(self, subs): + def get_subs_tag(self, subs): if subs == []: return ' [No subs]' elif 'Eng' in subs: @@ -201,43 +201,43 @@ async def upload(self, meta): else: anon = 1 - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: # bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() mi_dump = None bd_dump = "" for each in meta['discs']: bd_dump = bd_dump + each['summary'].strip() + "\n\n" - else: + else: mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt", 'r', encoding='utf-8').read() bd_dump = None desc = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'r').read() open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent", 'rb') files = {'torrent': open_torrent} data = { - 'name' : acm_name, - 'description' : desc, - 'mediainfo' : mi_dump, - 'bdinfo' : bd_dump, - 'category_id' : cat_id, - 'type_id' : type_id, - 'resolution_id' : resolution_id, - 'tmdb' : meta['tmdb'], - 'imdb' : meta['imdb_id'].replace('tt', ''), - 'tvdb' : meta['tvdb_id'], - 'mal' : meta['mal_id'], - 'igdb' : 0, - 'anonymous' : anon, - 'stream' : meta['stream'], - 'sd' : meta['sd'], - 'keywords' : await self.get_keywords(meta['keywords']), - 'personal_release' : int(meta.get('personalrelease', False)), - 'internal' : 0, - 'featured' : 0, - 'free' : 0, - 'doubleup' : 0, - 'sticky' : 0, + 'name': acm_name, + 'description': desc, + 'mediainfo': mi_dump, + 'bdinfo': bd_dump, + 'category_id': cat_id, + 'type_id': type_id, + 'resolution_id': resolution_id, + 'tmdb': meta['tmdb'], + 'imdb': meta['imdb_id'].replace('tt', ''), + 'tvdb': meta['tvdb_id'], + 'mal': meta['mal_id'], + 'igdb': 0, + 'anonymous': anon, + 'stream': meta['stream'], + 'sd': meta['sd'], + 'keywords': await self.get_keywords(meta['keywords']), + 'personal_release': int(meta.get('personalrelease', False)), + 'internal': 0, + 'featured': 0, + 'free': 0, + 'doubleup': 0, + 'sticky': 0, } - if self.config['TRACKERS'][self.tracker].get('internal', False) == True: + if self.config['TRACKERS'][self.tracker].get('internal', False) is True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 if region_id != 0: @@ -251,16 +251,16 @@ async def upload(self, meta): 'User-Agent': f'Upload Assistant/2.1 ({platform.system()} {platform.release()})' } params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip() + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip() } - if meta['debug'] == False: + if meta['debug'] is False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) - except: + except Exception: console.print("It may have uploaded, go check") - return + return else: console.print(f"[cyan]Request Data:") console.print(data) @@ -270,10 +270,10 @@ async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'tmdb' : meta['tmdb'], - 'categories[]' : await self.get_cat_id(meta['category']), - 'types[]' : await self.get_type_id(await self.get_type(meta)), + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'tmdb': meta['tmdb'], + 'categories[]': await self.get_cat_id(meta['category']), + 'types[]': await self.get_type_id(await self.get_type(meta)), # A majority of the ACM library doesn't contain resolution information # 'resolutions[]' : await self.get_res_id(meta['resolution']), # 'name' : "" @@ -287,7 +287,7 @@ async def search_existing(self, meta): # difference = SequenceMatcher(None, meta['clean_name'], result).ratio() # if difference >= 0.05: dupes.append(result) - except: + except Exception: console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) @@ -313,7 +313,7 @@ async def edit_name(self, meta): if aka != '': # ugly fix to remove the extra space in the title aka = aka + ' ' - name = name.replace (aka, f' / {original_title} {chr(int("202A", 16))}') + name = name.replace(aka, f' / {original_title} {chr(int("202A", 16))}') elif aka == '': if meta.get('title') != original_title: # name = f'{name[:name.find(year)]}/ {original_title} {chr(int("202A", 16))}{name[name.find(year):]}' @@ -321,14 +321,14 @@ async def edit_name(self, meta): if 'AAC' in audio: name = name.replace(audio.strip().replace(" ", " "), audio.replace("AAC ", "AAC")) name = name.replace("DD+ ", "DD+") - name = name.replace ("UHD BluRay REMUX", "Remux") - name = name.replace ("BluRay REMUX", "Remux") - name = name.replace ("H.265", "HEVC") + name = name.replace("UHD BluRay REMUX", "Remux") + name = name.replace("BluRay REMUX", "Remux") + name = name.replace("H.265", "HEVC") if is_disc == 'DVD': - name = name.replace (f'{source} DVD5', f'{resolution} DVD {source}') - name = name.replace (f'{source} DVD9', f'{resolution} DVD {source}') + name = name.replace(f'{source} DVD5', f'{resolution} DVD {source}') + name = name.replace(f'{source} DVD9', f'{resolution} DVD {source}') if audio == meta.get('channels'): - name = name.replace (f'{audio}', f'MPEG {audio}') + name = name.replace(f'{audio}', f'MPEG {audio}') name = name + self.get_subs_tag(subs) return name @@ -363,14 +363,14 @@ async def edit_desc(self, meta): desc = desc.replace('[img]', '[img=300]') descfile.write(desc) images = meta['image_list'] - if len(images) > 0: + if len(images) > 0: descfile.write("[center]") for each in range(len(images[:int(meta['screens'])])): web_url = images[each]['web_url'] img_url = images[each]['img_url'] descfile.write(f"[url={web_url}][img=350]{img_url}[/img][/url]") descfile.write("[/center]") - if self.signature != None: + if self.signature is not None: descfile.write(self.signature) descfile.close() return diff --git a/src/trackers/AITHER.py b/src/trackers/AITHER.py index 6a9b709de..a9d485699 100644 --- a/src/trackers/AITHER.py +++ b/src/trackers/AITHER.py @@ -94,7 +94,7 @@ async def upload(self, meta): response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) - except: + except Exception: console.print("It may have uploaded, go check") return else: @@ -187,7 +187,7 @@ async def search_existing(self, meta): # difference = SequenceMatcher(None, meta['clean_name'], result).ratio() # if difference >= 0.05: dupes.append(result) - except: + except Exception: console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) diff --git a/src/trackers/ANT.py b/src/trackers/ANT.py index 941639824..de1e5ac5b 100644 --- a/src/trackers/ANT.py +++ b/src/trackers/ANT.py @@ -29,12 +29,12 @@ def __init__(self, config): self.search_url = 'https://anthelion.me/api.php' self.upload_url = 'https://anthelion.me/api.php' self.banned_groups = ['3LTON', '4yEo', 'ADE', 'AFG', 'AniHLS', 'AnimeRG', 'AniURL', 'AROMA', 'aXXo', 'Brrip', 'CHD', 'CM8', - 'CrEwSaDe', 'd3g', 'DDR', 'DNL', 'DeadFish', 'ELiTE', 'eSc', 'FaNGDiNG0', 'FGT', 'Flights', 'FRDS', - 'FUM', 'HAiKU', 'HD2DVD', 'HDS', 'HDTime', 'Hi10', 'ION10', 'iPlanet', 'JIVE', 'KiNGDOM', 'Leffe', - 'LiGaS', 'LOAD', 'MeGusta', 'MkvCage', 'mHD', 'mSD', 'NhaNc3', 'nHD', 'NOIVTC', 'nSD', 'Oj', 'Ozlem', - 'PiRaTeS', 'PRoDJi', 'RAPiDCOWS', 'RARBG', 'RetroPeeps', 'RDN', 'REsuRRecTioN', 'RMTeam', 'SANTi', - 'SicFoI', 'SPASM', 'SPDVD', 'STUTTERSHIT', 'TBS', 'Telly', 'TM', 'UPiNSMOKE', 'URANiME', 'WAF', 'xRed', - 'XS', 'YIFY', 'YTS', 'Zeus', 'ZKBL', 'ZmN', 'ZMNT'] + 'CrEwSaDe', 'd3g', 'DDR', 'DNL', 'DeadFish', 'ELiTE', 'eSc', 'FaNGDiNG0', 'FGT', 'Flights', 'FRDS', + 'FUM', 'HAiKU', 'HD2DVD', 'HDS', 'HDTime', 'Hi10', 'ION10', 'iPlanet', 'JIVE', 'KiNGDOM', 'Leffe', + 'LiGaS', 'LOAD', 'MeGusta', 'MkvCage', 'mHD', 'mSD', 'NhaNc3', 'nHD', 'NOIVTC', 'nSD', 'Oj', 'Ozlem', + 'PiRaTeS', 'PRoDJi', 'RAPiDCOWS', 'RARBG', 'RetroPeeps', 'RDN', 'REsuRRecTioN', 'RMTeam', 'SANTi', + 'SicFoI', 'SPASM', 'SPDVD', 'STUTTERSHIT', 'TBS', 'Telly', 'TM', 'UPiNSMOKE', 'URANiME', 'WAF', 'xRed', + 'XS', 'YIFY', 'YTS', 'Zeus', 'ZKBL', 'ZmN', 'ZMNT'] self.signature = None pass diff --git a/src/trackers/BHD.py b/src/trackers/BHD.py index 46cb0129a..66dfbaa61 100644 --- a/src/trackers/BHD.py +++ b/src/trackers/BHD.py @@ -112,7 +112,7 @@ async def upload(self, meta): elif response['satus_message'].startswith('Invalid name value'): console.print(f"[bold yellow]Submitted Name: {bhd_name}") console.print(response) - except: + except Exception: console.print("It may have uploaded, go check") return else: @@ -135,7 +135,7 @@ async def get_source(self, source): "Web": "WEB", "HDTV": "HDTV", "UHDTV": "HDTV", - "NTSC": "DVD", "NTSC DVD" : "DVD", + "NTSC": "DVD", "NTSC DVD": "DVD", "PAL": "DVD", "PAL DVD": "DVD", } @@ -245,7 +245,7 @@ async def search_existing(self, meta): else: console.print(f"[yellow]{response.get('status_message')}") await asyncio.sleep(5) - except: + except Exception: console.print('[bold red]Unable to search for existing torrents on site. Most likely the site is down.') await asyncio.sleep(5) diff --git a/src/trackers/BHDTV.py b/src/trackers/BHDTV.py index ea6f911c1..7d7969067 100644 --- a/src/trackers/BHDTV.py +++ b/src/trackers/BHDTV.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # import discord -import asyncio -from torf import Torrent import requests from src.console import console from str2bool import str2bool @@ -12,8 +10,6 @@ from pymediainfo import MediaInfo -# from pprint import pprint - class BHDTV(): """ Edit for Tracker: @@ -27,10 +23,10 @@ def __init__(self, config): self.config = config self.tracker = 'BHDTV' self.source_flag = 'BIT-HDTV' - #search not implemented - #self.search_url = 'https://api.bit-hdtv.com/torrent/search/advanced' + # search not implemented + # self.search_url = 'https://api.bit-hdtv.com/torrent/search/advanced' self.upload_url = 'https://www.bit-hdtv.com/takeupload.php' - #self.forum_link = 'https://www.bit-hdtv.com/rules.php' + # self.forum_link = 'https://www.bit-hdtv.com/rules.php' self.banned_groups = [] pass @@ -48,18 +44,16 @@ async def upload(self, meta): # must be TV pack sub_cat_id = await self.get_type_tv_pack_id(meta['type']) - - resolution_id = await self.get_res_id(meta['resolution']) # region_id = await common.unit3d_region_ids(meta.get('region')) # distributor_id = await common.unit3d_distributor_ids(meta.get('distributor')) if meta['anon'] == 0 and bool( - str2bool(self.config['TRACKERS'][self.tracker].get('anon', "False"))) == False: + str2bool(self.config['TRACKERS'][self.tracker].get('anon', "False"))) is False: anon = 0 else: anon = 1 - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: @@ -80,32 +74,31 @@ async def upload(self, meta): data = { 'api_key': self.config['TRACKERS'][self.tracker]['api_key'].strip(), 'name': meta['name'].replace(' ', '.').replace(':.', '.').replace(':', '.').replace('DD+', 'DDP'), - 'mediainfo': mi_dump if bd_dump == None else bd_dump, + 'mediainfo': mi_dump if bd_dump is None else bd_dump, 'cat': cat_id, 'subcat': sub_cat_id, 'resolution': resolution_id, - #'anon': anon, + # 'anon': anon, # admins asked to remove short description. 'sdescr': " ", - 'descr': media_info if bd_dump == None else "Disc so Check Mediainfo dump ", + 'descr': media_info if bd_dump is None else "Disc so Check Mediainfo dump ", 'screen': desc, 'url': f"https://www.tvmaze.com/shows/{meta['tvmaze_id']}" if meta['category'] == 'TV' else f"https://www.imdb.com/title/tt{meta['imdb_id']}", 'format': 'json' } - - if meta['debug'] == False: + if meta['debug'] is False: response = requests.post(url=self.upload_url, data=data, files=files) try: # pprint(data) console.print(response.json()) - except: - console.print(f"[cyan]It may have uploaded, go check") + except Exception: + console.print("[cyan]It may have uploaded, go check") # cprint(f"Request Data:", 'cyan') pprint(data) console.print(traceback.print_exc()) else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") pprint(data) # # adding my anounce url to torrent. if 'view' in response.json()['data']: @@ -116,7 +109,6 @@ async def upload(self, meta): "Torrent Did not upload") open_torrent.close() - async def get_cat_id(self, meta): category_id = '0' if meta['category'] == 'MOVIE': @@ -128,7 +120,6 @@ async def get_cat_id(self, meta): category_id = '10' return category_id - async def get_type_movie_id(self, meta): type_id = '0' test = meta['type'] @@ -138,7 +129,7 @@ async def get_type_movie_id(self, meta): else: type_id = '2' elif meta['type'] == 'REMUX': - if str(meta['name']).__contains__('265') : + if str(meta['name']).__contains__('265'): type_id = '48' elif meta['3D']: type_id = '45' @@ -147,51 +138,48 @@ async def get_type_movie_id(self, meta): elif meta['type'] == 'HDTV': type_id = '6' elif meta['type'] == 'ENCODE': - if str(meta['name']).__contains__('265') : + if str(meta['name']).__contains__('265'): type_id = '43' elif meta['3D']: type_id = '44' else: type_id = '1' elif meta['type'] == 'WEBDL' or meta['type'] == 'WEBRIP': - type_id = '5' + type_id = '5' return type_id - async def get_type_tv_id(self, type): type_id = { 'HDTV': '7', 'WEBDL': '8', 'WEBRIP': '8', - #'WEBRIP': '55', - #'SD': '59', + # 'WEBRIP': '55', + # 'SD': '59', 'ENCODE': '10', 'REMUX': '11', 'DISC': '12', }.get(type, '0') return type_id - async def get_type_tv_pack_id(self, type): type_id = { 'HDTV': '13', 'WEBDL': '14', 'WEBRIP': '8', - #'WEBRIP': '55', - #'SD': '59', + # 'WEBRIP': '55', + # 'SD': '59', 'ENCODE': '16', 'REMUX': '17', 'DISC': '18', }.get(type, '0') return type_id - async def get_res_id(self, resolution): resolution_id = { '2160p': '4', '1080p': '3', - '1080i':'2', + '1080i': '2', '720p': '1' }.get(resolution, '10') return resolution_id @@ -211,6 +199,6 @@ async def edit_desc(self, meta): return async def search_existing(self, meta): - console.print(f"[red]Dupes must be checked Manually") + console.print("[red]Dupes must be checked Manually") return ['Dupes must be checked Manually'] - ### hopefully someone else has the time to implement this. + # hopefully someone else has the time to implement this. diff --git a/src/trackers/BLU.py b/src/trackers/BLU.py index a0d0040b0..571e8451d 100644 --- a/src/trackers/BLU.py +++ b/src/trackers/BLU.py @@ -109,7 +109,7 @@ async def upload(self, meta): response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) - except: + except Exception: console.print("It may have uploaded, go check") return @@ -204,7 +204,7 @@ async def search_existing(self, meta): # difference = SequenceMatcher(None, meta['clean_name'], result).ratio() # if difference >= 0.05: dupes.append(result) - except: + except Exception: console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) diff --git a/src/trackers/CBR.py b/src/trackers/CBR.py index 445f4e9d5..255636538 100644 --- a/src/trackers/CBR.py +++ b/src/trackers/CBR.py @@ -3,14 +3,12 @@ import asyncio import requests from str2bool import str2bool -import os import platform from src.trackers.COMMON import COMMON from src.console import console - class CBR(): """ Edit for Tracker: @@ -25,11 +23,11 @@ def __init__(self, config): self.source_flag = 'CapybaraBR' self.search_url = 'https://capybarabr.com/api/torrents/filter' self.torrent_url = 'https://capybarabr.com/api/torrents/' - self.upload_url = 'https://capybarabr.com/api/torrents/upload' - self.signature = f"\n[center][img]https://i.ibb.co/tYNzwgd/thanks-cbr.png[/img][/center]" + self.upload_url = 'https://capybarabr.com/api/torrents/upload' + self.signature = "\n[center][img]https://i.ibb.co/tYNzwgd/thanks-cbr.png[/img][/center]" self.banned_groups = [""] pass - + async def upload(self, meta): common = COMMON(config=self.config) await common.edit_torrent(meta, self.tracker, self.source_flag) @@ -40,12 +38,12 @@ async def upload(self, meta): region_id = await common.unit3d_region_ids(meta.get('region')) distributor_id = await common.unit3d_distributor_ids(meta.get('distributor')) name = await self.edit_name(meta) - if meta['anon'] == 0 and bool(str2obool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) == False: + if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) is False: anon = 0 else: anon = 1 - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: @@ -55,31 +53,31 @@ async def upload(self, meta): open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[CBR]{meta['clean_name']}.torrent", 'rb') files = {'torrent': ("placeholder.torrent", open_torrent, "application/x-bittorrent")} data = { - 'name' : name, - 'description' : desc, - 'mediainfo' : mi_dump, - 'bdinfo' : bd_dump, - 'category_id' : cat_id, - 'type_id' : type_id, - 'resolution_id' : resolution_id, - 'tmdb' : meta['tmdb'], - 'imdb' : meta['imdb_id'].replace('tt', ''), - 'tvdb' : meta['tvdb_id'], - 'mal' : meta['mal_id'], - 'igdb' : 0, - 'anonymous' : anon, - 'stream' : meta['stream'], - 'sd' : meta['sd'], - 'keywords' : meta['keywords'], - 'personal_release' : int(meta.get('personalrelease', False)), - 'internal' : 0, - 'featured' : 0, - 'free' : 0, - 'doubleup' : 0, - 'sticky' : 0, + 'name': name, + 'description': desc, + 'mediainfo': mi_dump, + 'bdinfo': bd_dump, + 'category_id': cat_id, + 'type_id': type_id, + 'resolution_id': resolution_id, + 'tmdb': meta['tmdb'], + 'imdb': meta['imdb_id'].replace('tt', ''), + 'tvdb': meta['tvdb_id'], + 'mal': meta['mal_id'], + 'igdb': 0, + 'anonymous': anon, + 'stream': meta['stream'], + 'sd': meta['sd'], + 'keywords': meta['keywords'], + 'personal_release': int(meta.get('personalrelease', False)), + 'internal': 0, + 'featured': 0, + 'free': 0, + 'doubleup': 0, + 'sticky': 0, } # Internal - if self.config['TRACKERS'][self.tracker].get('internal', False) == True: + if self.config['TRACKERS'][self.tracker].get('internal', False) is True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 @@ -96,40 +94,36 @@ async def upload(self, meta): params = { 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip() } - - if meta['debug'] == False: + + if meta['debug'] is False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) - except: + except Exception: console.print("It may have uploaded, go check") - return + return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) open_torrent.close() - - - - async def get_cat_id(self, category_name, edition, meta): category_id = { - 'MOVIE': '1', + 'MOVIE': '1', 'TV': '2', 'ANIMES': '4' }.get(category_name, '0') - if meta['anime'] == True and category_id == '2': + if meta['anime'] is True and category_id == '2': category_id = '4' return category_id async def get_type_id(self, type): type_id = { - 'DISC': '1', + 'DISC': '1', 'REMUX': '2', 'ENCODE': '3', - 'WEBDL': '4', - 'WEBRIP': '5', + 'WEBDL': '4', + 'WEBRIP': '5', 'HDTV': '6' }.get(type, '0') return type_id @@ -137,31 +131,28 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { '4320p': '1', - '2160p': '2', + '2160p': '2', '1080p': '3', - '1080i':'4', - '720p': '5', - '576p': '6', + '1080i': '4', + '720p': '5', + '576p': '6', '576i': '7', - '480p': '8', + '480p': '8', '480i': '9', 'Other': '10', }.get(resolution, '10') return resolution_id - - - async def search_existing(self, meta): dupes = [] console.print("[yellow]Buscando por duplicatas no tracker...") params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'tmdbId' : meta['tmdb'], - 'categories[]' : await self.get_cat_id(meta['category'], meta.get('edition', ''), meta), - 'types[]' : await self.get_type_id(meta['type']), - 'resolutions[]' : await self.get_res_id(meta['resolution']), - 'name' : "" + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'tmdbId': meta['tmdb'], + 'categories[]': await self.get_cat_id(meta['category'], meta.get('edition', ''), meta), + 'types[]': await self.get_type_id(meta['type']), + 'resolutions[]': await self.get_res_id(meta['resolution']), + 'name': "" } if meta['category'] == 'TV': params['name'] = params['name'] + f" {meta.get('season', '')}{meta.get('episode', '')}" @@ -175,15 +166,14 @@ async def search_existing(self, meta): # difference = SequenceMatcher(None, meta['clean_name'], result).ratio() # if difference >= 0.05: dupes.append(result) - except: + except Exception: console.print('[bold red]Não foi possivel buscar no tracker torrents duplicados. O tracker está offline ou sua api está incorreta') await asyncio.sleep(5) return dupes async def edit_name(self, meta): - - - name = meta['uuid'].replace('.mkv','').replace('.mp4','').replace(".", " ").replace("DDP2 0","DDP2.0").replace("DDP5 1","DDP5.1").replace("H 264","H.264").replace("H 265","H.265").replace("DD+7 1","DDP7.1").replace("AAC2 0","AAC2.0").replace('DD5 1','DD5.1').replace('DD2 0','DD2.0').replace('TrueHD 7 1','TrueHD 7.1').replace('DTS-HD MA 7 1','DTS-HD MA 7.1').replace('DTS-HD MA 5 1','DTS-HD MA 5.1').replace("TrueHD 5 1","TrueHD 5.1").replace("DTS-X 7 1","DTS-X 7.1").replace("DTS-X 5 1","DTS-X 5.1").replace("FLAC 2 0","FLAC 2.0").replace("FLAC 2 0","FLAC 2.0").replace("FLAC 5 1","FLAC 5.1").replace("DD1 0","DD1.0").replace("DTS ES 5 1","DTS ES 5.1").replace("DTS5 1","DTS 5.1").replace("AAC1 0","AAC1.0").replace("DD+5 1","DDP5.1").replace("DD+2 0","DDP2.0").replace("DD+1 0","DDP1.0") - + + name = meta['uuid'].replace('.mkv', '').replace('.mp4', '').replace(".", " ").replace("DDP2 0", "DDP2.0").replace("DDP5 1", "DDP5.1").replace("H 264", "H.264").replace("H 265", "H.265").replace("DD+7 1", "DDP7.1").replace("AAC2 0", "AAC2.0").replace('DD5 1', 'DD5.1').replace('DD2 0', 'DD2.0').replace('TrueHD 7 1', 'TrueHD 7.1').replace('DTS-HD MA 7 1', 'DTS-HD MA 7.1').replace('DTS-HD MA 5 1', 'DTS-HD MA 5.1').replace("TrueHD 5 1", "TrueHD 5.1").replace("DTS-X 7 1", "DTS-X 7.1").replace("DTS-X 5 1", "DTS-X 5.1").replace("FLAC 2 0", "FLAC 2.0").replace("FLAC 5 1", "FLAC 5.1").replace("DD1 0", "DD1.0").replace("DTS ES 5 1", "DTS ES 5.1").replace("DTS5 1", "DTS 5.1").replace("AAC1 0", "AAC1.0").replace("DD+5 1", "DDP5.1").replace("DD+2 0", "DDP2.0").replace("DD+1 0", "DDP1.0") + return name diff --git a/src/trackers/COMMON.py b/src/trackers/COMMON.py index 2a2f317b9..02b059fe5 100644 --- a/src/trackers/COMMON.py +++ b/src/trackers/COMMON.py @@ -200,7 +200,7 @@ async def parseCookieFile(self, cookiefile): compatible with requests.""" cookies = {} - with open (cookiefile, 'r') as fp: + with open(cookiefile, 'r') as fp: for line in fp: if not line.startswith(("# ", "\n", "#\n")): lineFields = re.split(' |\t', line.strip()) @@ -215,7 +215,7 @@ async def ptgen(self, meta, ptgen_site="", ptgen_retry=3): url = ptgen_site params = {} data = {} - # get douban url + # get douban url if int(meta.get('imdb_id', '0')) != 0: data['search'] = f"tt{meta['imdb_id']}" ptgen = requests.get(url, params=data) @@ -244,7 +244,7 @@ async def ptgen(self, meta, ptgen_site="", ptgen_retry=3): break ptgen = ptgen.json() meta['ptgen'] = ptgen - with open (f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as f: + with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/meta.json", 'w') as f: json.dump(meta, f, indent=4) f.close() ptgen = ptgen['format'] diff --git a/src/trackers/LST.py b/src/trackers/LST.py index 4fe525b09..bf25df687 100644 --- a/src/trackers/LST.py +++ b/src/trackers/LST.py @@ -2,7 +2,6 @@ # import discord import asyncio import requests -import os import platform from str2bool import str2bool @@ -19,29 +18,23 @@ class LST(): Upload """ - ############################################################### - ######## EDIT ME ######## - ############################################################### - - # ALSO EDIT CLASS NAME ABOVE - def __init__(self, config): self.config = config self.tracker = 'LST' self.source_flag = 'LST.GG' self.upload_url = 'https://lst.gg/api/torrents/upload' self.search_url = 'https://lst.gg/api/torrents/filter' - self.signature = f"\n[center][url=https://github.com/Audionut/Upload-Assistant]Created by L4G's Upload Assistant[/url][/center]" + self.signature = "\n[center][url=https://github.com/Audionut/Upload-Assistant]Created by L4G's Upload Assistant[/url][/center]" self.banned_groups = ['aXXo', 'BRrip', 'CM8', 'CrEwSaDe', 'CTFOH', 'DNL', 'FaNGDiNG0', 'HD2DVD', 'HDTime', 'ION10', 'iPlanet', 'KiNGDOM', 'mHD', 'mSD', 'nHD', 'nikt0', 'nSD', 'NhaNc3', 'OFT', 'PRODJi', 'SANTi', 'STUTTERSHIT', 'ViSION', 'VXT', 'WAF', 'x0r', 'YIFY', 'Sicario', 'RARBG', 'MeGusta', 'TSP', 'TSPxL', 'GalaxyTV', 'TGALAXY', 'TORRENTGALAXY'] pass - + async def get_cat_id(self, category_name, keywords, service): category_id = { - 'MOVIE': '1', + 'MOVIE': '1', 'TV': '2', - 'Anime': '6', + 'Anime': '6', }.get(category_name, '0') if category_name == 'TV' and 'anime' in keywords: category_id = '6' @@ -51,10 +44,10 @@ async def get_cat_id(self, category_name, keywords, service): async def get_type_id(self, type): type_id = { - 'DISC': '1', + 'DISC': '1', 'REMUX': '2', - 'WEBDL': '4', - 'WEBRIP': '5', + 'WEBDL': '4', + 'WEBRIP': '5', 'HDTV': '6', 'ENCODE': '3' }.get(type, '0') @@ -62,24 +55,20 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { - '8640p':'10', - '4320p': '1', - '2160p': '2', - '1440p' : '3', + '8640p': '10', + '4320p': '1', + '2160p': '2', + '1440p': '3', '1080p': '3', - '1080i':'4', - '720p': '5', - '576p': '6', + '1080i': '4', + '720p': '5', + '576p': '6', '576i': '7', - '480p': '8', + '480p': '8', '480i': '9' }.get(resolution, '10') return resolution_id - ############################################################### - ###### STOP HERE UNLESS EXTRA MODIFICATION IS NEEDED ###### - ############################################################### - async def upload(self, meta): common = COMMON(config=self.config) await common.edit_torrent(meta, self.tracker, self.source_flag) @@ -89,55 +78,54 @@ async def upload(self, meta): await common.unit3d_edit_desc(meta, self.tracker, self.signature) region_id = await common.unit3d_region_ids(meta.get('region')) distributor_id = await common.unit3d_distributor_ids(meta.get('distributor')) - if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) == False: + if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) is False: anon = 0 else: anon = 1 - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt", 'r', encoding='utf-8').read() bd_dump = None - + desc = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'r').read() if meta.get('service') == "hentai": - desc = "[center]" + "[img]" + str(meta['poster']) + "[/img][/center]" + f"\n[center]" + "https://www.themoviedb.org/tv/" + str(meta['tmdb']) + f"\nhttps://myanimelist.net/anime/" + str(meta['mal']) + "[/center]" + desc - + desc = "[center]" + "[img]" + str(meta['poster']) + "[/img][/center]" + "\n[center]" + "https://www.themoviedb.org/tv/" + str(meta['tmdb']) + "\nhttps://myanimelist.net/anime/" + str(meta['mal']) + "[/center]" + desc + open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent", 'rb') files = {'torrent': open_torrent} data = { - 'name' : meta['name'], - 'description' : desc, - 'mediainfo' : mi_dump, - 'bdinfo' : bd_dump, - 'category_id' : cat_id, - 'type_id' : type_id, - 'resolution_id' : resolution_id, - 'tmdb' : meta['tmdb'], - 'imdb' : meta['imdb_id'].replace('tt', ''), - 'tvdb' : meta['tvdb_id'], - 'mal' : meta['mal_id'], - 'igdb' : 0, - 'anonymous' : anon, - 'stream' : meta['stream'], - 'sd' : meta['sd'], - 'keywords' : meta['keywords'], - 'personal_release' : int(meta.get('personalrelease', False)), - 'internal' : 0, - 'featured' : 0, - 'free' : 0, - 'doubleup' : 0, - 'sticky' : 0, + 'name': meta['name'], + 'description': desc, + 'mediainfo': mi_dump, + 'bdinfo': bd_dump, + 'category_id': cat_id, + 'type_id': type_id, + 'resolution_id': resolution_id, + 'tmdb': meta['tmdb'], + 'imdb': meta['imdb_id'].replace('tt', ''), + 'tvdb': meta['tvdb_id'], + 'mal': meta['mal_id'], + 'igdb': 0, + 'anonymous': anon, + 'stream': meta['stream'], + 'sd': meta['sd'], + 'keywords': meta['keywords'], + 'personal_release': int(meta.get('personalrelease', False)), + 'internal': 0, + 'featured': 0, + 'free': 0, + 'doubleup': 0, + 'sticky': 0, } - - + # Internal - if self.config['TRACKERS'][self.tracker].get('internal', False) == True: + if self.config['TRACKERS'][self.tracker].get('internal', False) is True: if meta['tag'] != "" and (meta['tag'][1:] in self.config['TRACKERS'][self.tracker].get('internal_groups', [])): data['internal'] = 1 - + if region_id != 0: data['region_id'] = region_id if distributor_id != 0: @@ -149,35 +137,31 @@ async def upload(self, meta): 'User-Agent': f'Upload Assistant/2.1 ({platform.system()} {platform.release()})' } params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip() + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip() } - - if meta['debug'] == False: + + if meta['debug'] is False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) - except: + except Exception: console.print("It may have uploaded, go check") - return + return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) open_torrent.close() - - - - async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") params = { - 'api_token' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'tmdbId' : meta['tmdb'], - 'categories[]' : await self.get_cat_id(meta['category'], meta.get('keywords', ''), meta.get('service', '')), - 'types[]' : await self.get_type_id(meta['type']), - 'resolutions[]' : await self.get_res_id(meta['resolution']), - 'name' : "" + 'api_token': self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'tmdbId': meta['tmdb'], + 'categories[]': await self.get_cat_id(meta['category'], meta.get('keywords', ''), meta.get('service', '')), + 'types[]': await self.get_type_id(meta['type']), + 'resolutions[]': await self.get_res_id(meta['resolution']), + 'name': "" } if meta['category'] == 'TV': params['name'] = params['name'] + f" {meta.get('season', '')}{meta.get('episode', '')}" @@ -191,7 +175,7 @@ async def search_existing(self, meta): # difference = SequenceMatcher(None, meta['clean_name'], result).ratio() # if difference >= 0.05: dupes.append(result) - except: + except Exception: console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) From b7ed255f0815c154e684aafc978b7e160fe9756d Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 19:24:48 +1000 Subject: [PATCH 22/33] More linting --- cogs/commands.py | 2 +- discordbot.py | 1 + src/args.py | 6 +- src/prep.py | 4 +- src/trackers/ACM.py | 4 +- src/trackers/CBR.py | 2 +- src/trackers/HDB.py | 12 +- src/trackers/MTV.py | 89 +++++----- src/trackers/NBL.py | 59 +++---- src/trackers/PTP.py | 302 ++++++++++++++++---------------- src/trackers/RTF.py | 33 ++-- src/trackers/SN.py | 24 ++- src/trackers/TL.py | 24 +-- src/trackers/UNIT3D_TEMPLATE.py | 10 +- upload.py | 10 +- 15 files changed, 277 insertions(+), 305 deletions(-) diff --git a/cogs/commands.py b/cogs/commands.py index 647c4787b..7bcd8e7ef 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -313,7 +313,7 @@ async def send_embed_and_upload(self, ctx, meta): res = meta['resolution'] missing = await self.get_missing(meta) - embed=discord.Embed(title=f"Upload: {meta['title']}", url=f"https://www.themoviedb.org/{meta['category'].lower()}/{meta['tmdb']}", description=meta['overview'], color=0x0080ff, timestamp=datetime.utcnow()) + embed=discord.Embed(title = f"Upload: {meta['title']}", url=f"https://www.themoviedb.org/{meta['category'].lower()}/{meta['tmdb']}", description=meta['overview'], color=0x0080ff, timestamp=datetime.utcnow()) embed.add_field(name="Links", value=f"[TMDB](https://www.themoviedb.org/{meta['category'].lower()}/{meta['tmdb']}){imdb}{tvdb}") embed.add_field(name=f"{res} / {meta['type']}{tag}", value=f"```{meta['name']}```", inline=False) if missing != []: diff --git a/discordbot.py b/discordbot.py index 6fadac007..297c29713 100644 --- a/discordbot.py +++ b/discordbot.py @@ -97,6 +97,7 @@ async def on_message(self, message): return # ignore all bots await self.process_commands(message) + if __name__ == '__main__': logging.basicConfig(level=logging.INFO) diff --git a/src/args.py b/src/args.py index e3bf8651b..0db2f521e 100644 --- a/src/args.py +++ b/src/args.py @@ -118,7 +118,7 @@ def parse(self, args, meta): parsed = urllib.parse.urlparse(value2) try: meta['ptp'] = urllib.parse.parse_qs(parsed.query)['torrentid'][0] - except: + except Exception: console.print('[red]Your terminal ate part of the url, please surround in quotes next time, or pass only the torrentid') console.print('[red]Continuing without -ptp') else: @@ -131,7 +131,7 @@ def parse(self, args, meta): if blupath.endswith('/'): blupath = blupath[:-1] meta['blu'] = blupath.split('/')[-1] - except: + except Exception: console.print('[red]Unable to parse id from url') console.print('[red]Continuing without --blu') else: @@ -141,7 +141,7 @@ def parse(self, args, meta): parsed = urllib.parse.urlparse(value2) try: meta['hdb'] = urllib.parse.parse_qs(parsed.query)['id'][0] - except: + except Exception: console.print('[red]Your terminal ate part of the url, please surround in quotes next time, or pass only the torrentid') console.print('[red]Continuing without -hdb') else: diff --git a/src/prep.py b/src/prep.py index 390ef334a..912410e5a 100644 --- a/src/prep.py +++ b/src/prep.py @@ -1441,7 +1441,7 @@ def get_keywords(self, tmdb_info): keywords = [f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('keywords')] elif tmdb_keywords.get('results') is not None: keywords = [f"{keyword['name'].replace(',', ' ')}" for keyword in tmdb_keywords.get('results')] - return(', '.join (keywords)) + return (', '.join(keywords)) else: return '' @@ -1450,7 +1450,7 @@ def get_genres(self, tmdb_info): tmdb_genres = tmdb_info.get('genres', []) if tmdb_genres is not []: genres = [f"{genre['name'].replace(',', ' ')}" for genre in tmdb_genres] - return(', '.join (genres)) + return (', '.join(genres)) else: return '' diff --git a/src/trackers/ACM.py b/src/trackers/ACM.py index 99fecbe80..18a5fc7df 100644 --- a/src/trackers/ACM.py +++ b/src/trackers/ACM.py @@ -196,7 +196,7 @@ async def upload(self, meta): region_id = await common.unit3d_region_ids(meta.get('region')) distributor_id = await common.unit3d_distributor_ids(meta.get('distributor')) acm_name = await self.edit_name(meta) - if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) == False: + if meta['anon'] == 0 and bool(str2bool(str(self.config['TRACKERS'][self.tracker].get('anon', "False")))) is False: anon = 0 else: anon = 1 @@ -262,7 +262,7 @@ async def upload(self, meta): console.print("It may have uploaded, go check") return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) open_torrent.close() diff --git a/src/trackers/CBR.py b/src/trackers/CBR.py index 255636538..2c26c0897 100644 --- a/src/trackers/CBR.py +++ b/src/trackers/CBR.py @@ -130,7 +130,7 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { - '4320p': '1', + '4320p': '1', '2160p': '2', '1080p': '3', '1080i': '4', diff --git a/src/trackers/HDB.py b/src/trackers/HDB.py index 1d76ef09a..14a6ca5a1 100644 --- a/src/trackers/HDB.py +++ b/src/trackers/HDB.py @@ -132,7 +132,7 @@ async def get_tags(self, meta): "MASTERS OF CINEMA": 19, "MOC": 19, "KINO LORBER": 55, "KINO": 55, "BFI VIDEO": 63, "BFI": 63, "BRITISH FILM INSTITUTE": 63, - "STUDIO CANAL":65, + "STUDIO CANAL": 65, "ARROW": 64 } if meta.get('distributor') in distributor_dict.keys(): @@ -330,16 +330,16 @@ async def search_existing(self, meta): 'search': meta['resolution'] } if int(meta.get('imdb_id', '0').replace('tt', '0')) != 0: - data['imdb'] = {'id' : meta['imdb_id']} + data['imdb'] = {'id': meta['imdb_id']} if int(meta.get('tvdb_id', '0')) != 0: - data['tvdb'] = {'id' : meta['tvdb_id']} + data['tvdb'] = {'id': meta['tvdb_id']} try: response = requests.get(url=url, data=json.dumps(data)) response = response.json() for each in response['data']: result = each['name'] dupes.append(result) - except: + except Exception: console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your passkey is incorrect') await asyncio.sleep(5) @@ -367,7 +367,7 @@ async def validate_api(self): if r.get('status', 5) == 0: return True return False - except: + except Exception: return False async def validate_cookies(self, meta): @@ -503,7 +503,7 @@ async def get_info_from_torrent_id(self, hdb_id): hdb_name = response['data'][0]['name'] hdb_torrenthash = response['data'][0]['hash'] - except: + except Exception: console.print_exception() else: console.print("Failed to get info from HDB ID. Either the site is down or your credentials are invalid") diff --git a/src/trackers/MTV.py b/src/trackers/MTV.py index 56c071f11..fb5b3f601 100644 --- a/src/trackers/MTV.py +++ b/src/trackers/MTV.py @@ -8,11 +8,11 @@ import cli_ui import pickle import re -import traceback from pathlib import Path from str2bool import str2bool from src.trackers.COMMON import COMMON -from datetime import datetime, date +from datetime import datetime + class MTV(): """ @@ -43,7 +43,7 @@ async def upload(self, meta): # Initiate the upload with retry logic await self.upload_with_retry(meta, cookiefile, common) - + async def upload_with_retry(self, meta, cookiefile, common, img_host_index=1): approved_image_hosts = ['ptpimg', 'imgbox'] @@ -102,16 +102,16 @@ async def upload_with_retry(self, meta, cookiefile, common, img_host_index=1): comment="Created by L4G's Upload Assistant", created_by="L4G's Upload Assistant" ) - + # Explicitly set the piece size and update metainfo new_torrent.piece_size = 8388608 # 8 MiB in bytes new_torrent.metainfo['info']['piece length'] = 8388608 # Ensure 'piece length' is set - + # Validate and write the new torrent new_torrent.validate_piece_size() new_torrent.generate(callback=prep.torf_cb, interval=5) new_torrent.write(f"{meta['base_dir']}/tmp/{meta['uuid']}/MTV.torrent", overwrite=True) - + torrent_filename = "MTV" await common.edit_torrent(meta, self.tracker, self.source_flag, torrent_filename=torrent_filename) @@ -170,17 +170,17 @@ async def upload_with_retry(self, meta, cookiefile, common, img_host_index=1): console.print(response.url) else: if "authkey.php" in response.url: - console.print(f"[red]No DL link in response, It may have uploaded, check manually.") + console.print("[red]No DL link in response, It may have uploaded, check manually.") else: - console.print(f"[red]Upload Failed. It doesn't look like you are logged in.") - except: - console.print(f"[red]It may have uploaded, check manually.") + console.print("[red]Upload Failed. It doesn't look like you are logged in.") + except Exception: + console.print("[red]It may have uploaded, check manually.") print(traceback.print_exc()) else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) return - + async def handle_image_upload(self, meta, img_host_index=1, approved_image_hosts=None): if approved_image_hosts is None: approved_image_hosts = ['ptpimg', 'imgbox'] @@ -229,7 +229,7 @@ async def edit_desc(self, meta): base = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'r').read() with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w') as desc: # adding bd_dump to description if it exits and adding empty string to mediainfo - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: @@ -241,19 +241,19 @@ async def edit_desc(self, meta): desc.write("[mediainfo]" + mi_dump + "[/mediainfo]\n\n") images = meta['image_list'] if len(images) > 0: - desc.write(f"[spoiler=Screenshots]") + desc.write("[spoiler=Screenshots]") for each in range(len(images)): raw_url = images[each]['raw_url'] img_url = images[each]['img_url'] desc.write(f"[url={raw_url}][img=250]{img_url}[/img][/url]") - desc.write(f"[/spoiler]") + desc.write("[/spoiler]") desc.write(f"\n\n{base}") desc.close() return async def edit_group_desc(self, meta): description = "" - if meta['imdb_id'] not in ("0", "", None): + if meta['imdb_id'] not in ("0", "", None): description += f"https://www.imdb.com/title/tt{meta['imdb_id']}" if meta['tmdb'] != 0: description += f"\nhttps://www.themoviedb.org/{str(meta['category'].lower())}/{str(meta['tmdb'])}" @@ -289,15 +289,15 @@ async def edit_name(self, meta): mtv_name = re.sub(r"[^0-9a-zA-ZÀ-ÿ. &+'\-\[\]]+", "", mtv_name) mtv_name = mtv_name.replace(' ', '.').replace('..', '.') return mtv_name - + async def get_res_id(self, resolution): resolution_id = { - '8640p':'0', + '8640p': '0', '4320p': '4000', '2160p': '2160', - '1440p' : '1440', + '1440p': '1440', '1080p': '1080', - '1080i':'1080', + '1080i': '1080', '720p': '720', '576p': '0', '576i': '0', @@ -355,6 +355,7 @@ async def get_origin_id(self, meta): # returning P2P else: return '3' + async def get_tags(self, meta): tags = [] # Genres @@ -369,7 +370,7 @@ async def get_tags(self, meta): tags.append('hd') # Streaming Service if str(meta['service_longname']) != "": - tags.append(f"{meta['service_longname'].lower().replace(' ', '.')}.source") + tags.append(f"{meta['service_longname'].lower().replace(' ', '.')}.source") # Release Type/Source for each in ['remux', 'WEB.DL', 'WEBRip', 'HDTV', 'BluRay', 'DVD', 'HDDVD']: if (each.lower().replace('.', '') in meta['type'].lower()) or (each.lower().replace('-', '') in meta['source']): @@ -388,14 +389,14 @@ async def get_tags(self, meta): tags.append('sd.season') else: tags.append('hd.season') - + # movie tags if meta['category'] == 'MOVIE': if meta['sd'] == 1: tags.append('sd.movie') else: tags.append('hd.movie') - + # Audio tags audio_tag = "" for each in ['dd', 'ddp', 'aac', 'truehd', 'mp3', 'mp2', 'dts', 'dts.hd', 'dts.x']: @@ -436,10 +437,10 @@ async def validate_credentials(self, meta): if not os.path.exists(cookiefile): await self.login(cookiefile) vcookie = await self.validate_cookies(meta, cookiefile) - if vcookie != True: + if vcookie is not True: console.print('[red]Failed to validate cookies. Please confirm that the site is up and your username and password is valid.') recreate = cli_ui.ask_yes_no("Log in again and create new session?") - if recreate == True: + if recreate is True: if os.path.exists(cookiefile): os.remove(cookiefile) await self.login(cookiefile) @@ -448,14 +449,14 @@ async def validate_credentials(self, meta): else: return False vapi = await self.validate_api() - if vapi != True: + if vapi is not True: console.print('[red]Failed to validate API. Please confirm that the site is up and your API key is valid.') return True async def validate_api(self): url = self.search_url params = { - 'apikey' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), } try: r = requests.get(url, params=params) @@ -464,7 +465,7 @@ async def validate_api(self): console.print("[red]Invalid API Key") return False return True - except: + except Exception: return False async def validate_cookies(self, meta, cookiefile): @@ -499,12 +500,12 @@ async def login(self, cookiefile): with requests.Session() as session: url = 'https://www.morethantv.me/login' payload = { - 'username' : self.config['TRACKERS'][self.tracker].get('username'), - 'password' : self.config['TRACKERS'][self.tracker].get('password'), - 'keeploggedin' : 1, - 'cinfo' : '1920|1080|24|0', - 'submit' : 'login', - 'iplocked' : 1, + 'username': self.config['TRACKERS'][self.tracker].get('username'), + 'password': self.config['TRACKERS'][self.tracker].get('password'), + 'keeploggedin': 1, + 'cinfo': '1920|1080|24|0', + 'submit': 'login', + 'iplocked': 1, # 'ssl' : 'yes' } res = session.get(url="https://www.morethantv.me/login") @@ -521,11 +522,11 @@ async def login(self, cookiefile): mfa_code = pyotp.parse_uri(otp_uri).now() else: mfa_code = console.input('[yellow]MTV 2FA Code: ') - + two_factor_payload = { - 'token' : resp.text.rsplit('name="token" value="', 1)[1][:48], - 'code' : mfa_code, - 'submit' : 'login' + 'token': resp.text.rsplit('name="token" value="', 1)[1][:48], + 'code': mfa_code, + 'submit': 'login' } resp = session.post(url="https://www.morethantv.me/twofactor/login", data=two_factor_payload) # checking if logged in @@ -543,9 +544,9 @@ async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") params = { - 't' : 'search', + 't': 'search', 'apikey' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), - 'q' : "" + 'q': "" } if meta['imdb_id'] not in ("0", "", None): params['imdbid'] = "tt" + meta['imdb_id'] @@ -569,11 +570,11 @@ async def search_existing(self, meta): console.print(f"[yellow]{rr.get('status_message')}") await asyncio.sleep(5) else: - console.print(f"[red]Site Seems to be down or not responding to API") - except: - console.print(f"[red]Unable to search for existing torrents on site. Most likely the site is down.") + console.print("[red]Site Seems to be down or not responding to API") + except Exception: + console.print("[red]Unable to search for existing torrents on site. Most likely the site is down.") dupes.append("FAILED SEARCH") print(traceback.print_exc()) await asyncio.sleep(5) - return dupes \ No newline at end of file + return dupes diff --git a/src/trackers/NBL.py b/src/trackers/NBL.py index 56e01a671..a870918d3 100644 --- a/src/trackers/NBL.py +++ b/src/trackers/NBL.py @@ -2,9 +2,7 @@ # import discord import asyncio import requests -import os -from guessit import guessit -from str2bool import str2bool +from guessit import guessit from src.trackers.COMMON import COMMON from src.console import console @@ -18,13 +16,6 @@ class NBL(): Set type/category IDs Upload """ - - ############################################################### - ######## EDIT ME ######## - ############################################################### - - # ALSO EDIT CLASS NAME ABOVE - def __init__(self, config): self.config = config self.tracker = 'NBL' @@ -38,9 +29,8 @@ def __init__(self, config): 'PlaySD', 'playXD', 'project-gxs', 'PSA', 'QaS', 'Ranger', 'RAPiDCOWS', 'Raze', 'Reaktor', 'REsuRRecTioN', 'RMTeam', 'ROBOTS', 'SpaceFish', 'SPASM', 'SSA', 'Telly', 'Tenrai-Sensei', 'TM', 'Trix', 'URANiME', 'VipapkStudios', 'ViSiON', 'Wardevil', 'xRed', 'XS', 'YakuboEncodes', 'YuiSubs', 'ZKBL', 'ZmN', 'ZMNT'] - + pass - async def get_cat_id(self, meta): if meta.get('tv_pack', 0) == 1: @@ -49,9 +39,6 @@ async def get_cat_id(self, meta): cat_id = 1 return cat_id - ############################################################### - ###### STOP HERE UNLESS EXTRA MODIFICATION IS NEEDED ###### - ############################################################### async def edit_desc(self, meta): # Leave this in so manual works return @@ -63,21 +50,21 @@ async def upload(self, meta): common = COMMON(config=self.config) await common.edit_torrent(meta, self.tracker, self.source_flag) - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt", 'r', encoding='utf-8').read()[:-65].strip() open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent", 'rb') files = {'file_input': open_torrent} data = { - 'api_key' : self.api_key, - 'tvmazeid' : int(meta.get('tvmaze_id', 0)), - 'mediainfo' : mi_dump, - 'category' : await self.get_cat_id(meta), - 'ignoredupes' : 'on' + 'api_key': self.api_key, + 'tvmazeid': int(meta.get('tvmaze_id', 0)), + 'mediainfo': mi_dump, + 'category': await self.get_cat_id(meta), + 'ignoredupes': 'on' } - - if meta['debug'] == False: + + if meta['debug'] is False: response = requests.post(url=self.upload_url, files=files, data=data) try: if response.ok: @@ -86,34 +73,30 @@ async def upload(self, meta): else: console.print(response) console.print(response.text) - except: + except Exception: console.print_exception() console.print("[bold yellow]It may have uploaded, go check") - return + return else: console.print(f"[cyan]Request Data:") console.print(data) open_torrent.close() - - - - async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") if int(meta.get('tvmaze_id', 0)) != 0: - search_term = {'tvmaze' : int(meta['tvmaze_id'])} + search_term = {'tvmaze': int(meta['tvmaze_id'])} elif int(meta.get('imdb_id', '0').replace('tt', '')) == 0: - search_term = {'imdb' : meta.get('imdb_id', '0').replace('tt', '')} + search_term = {'imdb': meta.get('imdb_id', '0').replace('tt', '')} else: - search_term = {'series' : meta['title']} + search_term = {'series': meta['title']} json = { - 'jsonrpc' : '2.0', - 'id' : 1, - 'method' : 'getTorrents', - 'params' : [ - self.api_key, + 'jsonrpc': '2.0', + 'id': 1, + 'method': 'getTorrents', + 'params': [ + self.api_key, search_term ] } @@ -143,4 +126,4 @@ async def search_existing(self, meta): except Exception: console.print_exception() - return dupes \ No newline at end of file + return dupes diff --git a/src/trackers/PTP.py b/src/trackers/PTP.py index 758b87d6c..9bd25fa2d 100644 --- a/src/trackers/PTP.py +++ b/src/trackers/PTP.py @@ -5,8 +5,6 @@ import os from pathlib import Path from str2bool import str2bool -import time -import traceback import json import glob import multiprocessing @@ -18,7 +16,8 @@ from src.exceptions import * from src.console import console from torf import Torrent -from datetime import datetime, date +from datetime import datetime + class PTP(): @@ -28,69 +27,69 @@ def __init__(self, config): self.source_flag = 'PTP' self.api_user = config['TRACKERS']['PTP'].get('ApiUser', '').strip() self.api_key = config['TRACKERS']['PTP'].get('ApiKey', '').strip() - self.announce_url = config['TRACKERS']['PTP'].get('announce_url', '').strip() - self.username = config['TRACKERS']['PTP'].get('username', '').strip() + self.announce_url = config['TRACKERS']['PTP'].get('announce_url', '').strip() + self.username = config['TRACKERS']['PTP'].get('username', '').strip() self.password = config['TRACKERS']['PTP'].get('password', '').strip() self.web_source = str2bool(str(config['TRACKERS']['PTP'].get('add_web_source_to_desc', True))) self.user_agent = f'Upload Assistant/2.1 ({platform.system()} {platform.release()})' self.banned_groups = ['aXXo', 'BMDru', 'BRrip', 'CM8', 'CrEwSaDe', 'CTFOH', 'd3g', 'DNL', 'FaNGDiNG0', 'HD2DVD', 'HDTime', 'ION10', 'iPlanet', 'KiNGDOM', 'mHD', 'mSD', 'nHD', 'nikt0', 'nSD', 'NhaNc3', 'OFT', 'PRODJi', 'SANTi', 'SPiRiT', 'STUTTERSHIT', 'ViSION', 'VXT', 'WAF', 'x0r', 'YIFY',] - + self.sub_lang_map = { - ("Arabic", "ara", "ar") : 22, - ("Brazilian Portuguese", "Brazilian", "Portuguese-BR", 'pt-br') : 49, - ("Bulgarian", "bul", "bg") : 29, - ("Chinese", "chi", "zh", "Chinese (Simplified)", "Chinese (Traditional)") : 14, - ("Croatian", "hrv", "hr", "scr") : 23, - ("Czech", "cze", "cz", "cs") : 30, - ("Danish", "dan", "da") : 10, - ("Dutch", "dut", "nl") : 9, - ("English", "eng", "en", "English (CC)", "English - SDH") : 3, - ("English - Forced", "English (Forced)", "en (Forced)") : 50, - ("English Intertitles", "English (Intertitles)", "English - Intertitles", "en (Intertitles)") : 51, - ("Estonian", "est", "et") : 38, - ("Finnish", "fin", "fi") : 15, - ("French", "fre", "fr") : 5, - ("German", "ger", "de") : 6, - ("Greek", "gre", "el") : 26, - ("Hebrew", "heb", "he") : 40, - ("Hindi" "hin", "hi") : 41, - ("Hungarian", "hun", "hu") : 24, - ("Icelandic", "ice", "is") : 28, - ("Indonesian", "ind", "id") : 47, - ("Italian", "ita", "it") : 16, - ("Japanese", "jpn", "ja") : 8, - ("Korean", "kor", "ko") : 19, - ("Latvian", "lav", "lv") : 37, - ("Lithuanian", "lit", "lt") : 39, - ("Norwegian", "nor", "no") : 12, - ("Persian", "fa", "far") : 52, - ("Polish", "pol", "pl") : 17, - ("Portuguese", "por", "pt") : 21, - ("Romanian", "rum", "ro") : 13, - ("Russian", "rus", "ru") : 7, - ("Serbian", "srp", "sr", "scc") : 31, - ("Slovak", "slo", "sk") : 42, - ("Slovenian", "slv", "sl") : 43, - ("Spanish", "spa", "es") : 4, - ("Swedish", "swe", "sv") : 11, - ("Thai", "tha", "th") : 20, - ("Turkish", "tur", "tr") : 18, - ("Ukrainian", "ukr", "uk") : 34, - ("Vietnamese", "vie", "vi") : 25, + ("Arabic", "ara", "ar"): 22, + ("Brazilian Portuguese", "Brazilian", "Portuguese-BR", 'pt-br'): 49, + ("Bulgarian", "bul", "bg"): 29, + ("Chinese", "chi", "zh", "Chinese (Simplified)", "Chinese (Traditional)"): 14, + ("Croatian", "hrv", "hr", "scr"): 23, + ("Czech", "cze", "cz", "cs"): 30, + ("Danish", "dan", "da"): 10, + ("Dutch", "dut", "nl"): 9, + ("English", "eng", "en", "English (CC)", "English - SDH"): 3, + ("English - Forced", "English (Forced)", "en (Forced)"): 50, + ("English Intertitles", "English (Intertitles)", "English - Intertitles", "en (Intertitles)"): 51, + ("Estonian", "est", "et"): 38, + ("Finnish", "fin", "fi"): 15, + ("French", "fre", "fr"): 5, + ("German", "ger", "de"): 6, + ("Greek", "gre", "el"): 26, + ("Hebrew", "heb", "he"): 40, + ("Hindi" "hin", "hi"): 41, + ("Hungarian", "hun", "hu"): 24, + ("Icelandic", "ice", "is"): 28, + ("Indonesian", "ind", "id"): 47, + ("Italian", "ita", "it"): 16, + ("Japanese", "jpn", "ja"): 8, + ("Korean", "kor", "ko"): 19, + ("Latvian", "lav", "lv"): 37, + ("Lithuanian", "lit", "lt"): 39, + ("Norwegian", "nor", "no"): 12, + ("Persian", "fa", "far"): 52, + ("Polish", "pol", "pl"): 17, + ("Portuguese", "por", "pt"): 21, + ("Romanian", "rum", "ro"): 13, + ("Russian", "rus", "ru"): 7, + ("Serbian", "srp", "sr", "scc"): 31, + ("Slovak", "slo", "sk"): 42, + ("Slovenian", "slv", "sl"): 43, + ("Spanish", "spa", "es"): 4, + ("Swedish", "swe", "sv"): 11, + ("Thai", "tha", "th"): 20, + ("Turkish", "tur", "tr"): 18, + ("Ukrainian", "ukr", "uk"): 34, + ("Vietnamese", "vie", "vi"): 25, } async def get_ptp_id_imdb(self, search_term, search_file_folder): imdb_id = ptp_torrent_id = None filename = str(os.path.basename(search_term)) params = { - 'filelist' : filename + 'filelist': filename } headers = { - 'ApiUser' : self.api_user, - 'ApiKey' : self.api_key, - 'User-Agent' : self.user_agent + 'ApiUser': self.api_user, + 'ApiKey': self.api_key, + 'User-Agent': self.user_agent } url = 'https://passthepopcorn.me/torrents.php' response = requests.get(url, params=params, headers=headers) @@ -133,15 +132,15 @@ async def get_ptp_id_imdb(self, search_term, search_file_folder): pass console.print(f'[yellow]Could not find any release matching [bold yellow]{filename}[/bold yellow] on PTP') return None, None, None - + async def get_imdb_from_torrent_id(self, ptp_torrent_id): params = { - 'torrentid' : ptp_torrent_id + 'torrentid': ptp_torrent_id } headers = { - 'ApiUser' : self.api_user, - 'ApiKey' : self.api_key, - 'User-Agent' : self.user_agent + 'ApiUser': self.api_user, + 'ApiKey': self.api_key, + 'User-Agent': self.user_agent } url = 'https://passthepopcorn.me/torrents.php' response = requests.get(url, params=params, headers=headers) @@ -164,16 +163,16 @@ async def get_imdb_from_torrent_id(self, ptp_torrent_id): return None, None except Exception: return None, None - + async def get_ptp_description(self, ptp_torrent_id, is_disc): params = { - 'id' : ptp_torrent_id, - 'action' : 'get_description' + 'id': ptp_torrent_id, + 'action': 'get_description' } headers = { - 'ApiUser' : self.api_user, - 'ApiKey' : self.api_key, - 'User-Agent' : self.user_agent + 'ApiUser': self.api_user, + 'ApiKey': self.api_key, + 'User-Agent': self.user_agent } url = 'https://passthepopcorn.me/torrents.php' response = requests.get(url, params=params, headers=headers) @@ -181,26 +180,26 @@ async def get_ptp_description(self, ptp_torrent_id, is_disc): ptp_desc = response.text bbcode = BBCODE() desc = bbcode.clean_ptp_description(ptp_desc, is_disc) - console.print(f"[bold green]Successfully grabbed description from PTP") + console.print("[bold green]Successfully grabbed description from PTP") return desc - + async def get_group_by_imdb(self, imdb): params = { - 'imdb' : imdb, + 'imdb': imdb, } headers = { - 'ApiUser' : self.api_user, - 'ApiKey' : self.api_key, - 'User-Agent' : self.user_agent + 'ApiUser': self.api_user, + 'ApiKey': self.api_key, + 'User-Agent': self.user_agent } url = 'https://passthepopcorn.me/torrents.php' response = requests.get(url=url, headers=headers, params=params) await asyncio.sleep(1) try: response = response.json() - if response.get("Page") == "Browse": # No Releases on Site with ID + if response.get("Page") == "Browse": # No Releases on Site with ID return None - elif response.get('Page') == "Details": # Group Found + elif response.get('Page') == "Details": # Group Found groupID = response.get('GroupId') console.print(f"[green]Matched IMDb: [yellow]tt{imdb}[/yellow] to Group ID: [yellow]{groupID}[/yellow][/green]") console.print(f"[green]Title: [yellow]{response.get('Name')}[/yellow] ([yellow]{response.get('Year')}[/yellow])") @@ -212,14 +211,14 @@ async def get_group_by_imdb(self, imdb): async def get_torrent_info(self, imdb, meta): params = { - 'imdb' : imdb, - 'action' : 'torrent_info', - 'fast' : 1 + 'imdb': imdb, + 'action': 'torrent_info', + 'fast': 1 } headers = { - 'ApiUser' : self.api_user, - 'ApiKey' : self.api_key, - 'User-Agent' : self.user_agent + 'ApiUser': self.api_user, + 'ApiKey': self.api_key, + 'User-Agent': self.user_agent } url = "https://passthepopcorn.me/ajax.php" response = requests.get(url=url, params=params, headers=headers) @@ -240,9 +239,9 @@ async def get_torrent_info(self, imdb, meta): async def get_torrent_info_tmdb(self, meta): tinfo = { - "title" : meta.get("title", ""), - "year" : meta.get("year", ""), - "album_desc" : meta.get("overview", ""), + "title": meta.get("title", ""), + "year": meta.get("year", ""), + "album_desc": meta.get("overview", ""), } tags = await self.get_tags([meta.get("genres", ""), meta.get("keywords", "")]) tinfo['tags'] = ", ".join(tags) @@ -266,21 +265,20 @@ async def get_tags(self, check_against): async def search_existing(self, groupID, meta): # Map resolutions to SD / HD / UHD quality = None - if meta.get('sd', 0) == 1: # 1 is SD + if meta.get('sd', 0) == 1: # 1 is SD quality = "Standard Definition" elif meta['resolution'] in ["1440p", "1080p", "1080i", "720p"]: quality = "High Definition" elif meta['resolution'] in ["2160p", "4320p", "8640p"]: quality = "Ultra High Definition" - params = { - 'id' : groupID, + 'id': groupID, } headers = { - 'ApiUser' : self.api_user, - 'ApiKey' : self.api_key, - 'User-Agent' : self.user_agent + 'ApiUser': self.api_user, + 'ApiKey': self.api_key, + 'User-Agent': self.user_agent } url = 'https://passthepopcorn.me/torrents.php' response = requests.get(url=url, headers=headers, params=params) @@ -291,7 +289,7 @@ async def search_existing(self, groupID, meta): torrents = response.get('Torrents', []) if len(torrents) != 0: for torrent in torrents: - if torrent.get('Quality') == quality and quality != None: + if torrent.get('Quality') == quality and quality is not None: existing.append(f"[{torrent.get('Resolution')}] {torrent.get('ReleaseName', 'RELEASE NAME NOT FOUND')}") except Exception: console.print("[red]An error has occured trying to find existing releases") @@ -299,9 +297,9 @@ async def search_existing(self, groupID, meta): async def ptpimg_url_rehost(self, image_url): payload = { - 'format' : 'json', - 'api_key' : self.config["DEFAULT"]["ptpimg_api"], - 'link-upload' : image_url + 'format': 'json', + 'api_key': self.config["DEFAULT"]["ptpimg_api"], + 'link-upload': image_url } headers = { 'referer': 'https://ptpimg.me/index.php'} url = "https://ptpimg.me/upload.php" @@ -312,7 +310,7 @@ async def ptpimg_url_rehost(self, image_url): ptpimg_code = response[0]['code'] ptpimg_ext = response[0]['ext'] img_url = f"https://ptpimg.me/{ptpimg_code}.{ptpimg_ext}" - except: + except Exception: console.print("[red]PTPIMG image rehost failed") img_url = image_url # img_url = ptpimg_upload(image_url, ptpimg_api) @@ -351,7 +349,7 @@ def get_type(self, imdb_info, meta): ptpType = "Stand-up Comedy" elif "concert" in keywords: ptpType = "Concert" - if ptpType == None: + if ptpType is None: if meta.get('mode', 'discord') == 'cli': ptpTypeList = ["Feature Film", "Short Film", "Miniseries", "Stand-up Comedy", "Concert", "Movie Collection"] ptpType = cli_ui.ask_choice("Select the proper type", choices=ptpTypeList) @@ -372,14 +370,14 @@ def get_codec(self, meta): codec = "DVD9" else: codecmap = { - "AVC" : "H.264", - "H.264" : "H.264", - "HEVC" : "H.265", - "H.265" : "H.265", + "AVC": "H.264", + "H.264": "H.264", + "HEVC": "H.265", + "H.265": "H.265", } searchcodec = meta.get('video_codec', meta.get('video_encode')) codec = codecmap.get(searchcodec, searchcodec) - if meta.get('has_encode_settings') == True: + if meta.get('has_encode_settings') is True: codec = codec.replace("H.", "x") return codec @@ -403,23 +401,23 @@ def get_container(self, meta): else: ext = os.path.splitext(meta['filelist'][0])[1] containermap = { - '.mkv' : "MKV", - '.mp4' : 'MP4' + '.mkv': "MKV", + '.mp4': 'MP4' } container = containermap.get(ext, 'Other') return container def get_source(self, source): sources = { - "Blu-ray" : "Blu-ray", - "BluRay" : "Blu-ray", - "HD DVD" : "HD-DVD", - "HDDVD" : "HD-DVD", - "Web" : "WEB", - "HDTV" : "HDTV", - 'UHDTV' : 'HDTV', - "NTSC" : "DVD", - "PAL" : "DVD" + "Blu-ray": "Blu-ray", + "BluRay": "Blu-ray", + "HD DVD": "HD-DVD", + "HDDVD": "HD-DVD", + "Web": "WEB", + "HDTV": "HDTV", + 'UHDTV': 'HDTV', + "NTSC": "DVD", + "PAL": "DVD" } source_id = sources.get(source, "OtherR") return source_id @@ -451,27 +449,27 @@ def get_subtitles(self, meta): sub_langs.append(subID) if sub_langs == []: - sub_langs = [44] # No Subtitle + sub_langs = [44] # No Subtitle return sub_langs def get_trumpable(self, sub_langs): trumpable_values = { - "English Hardcoded Subs (Full)" : 4, - "English Hardcoded Subs (Forced)" : 50, - "No English Subs" : 14, - "English Softsubs Exist (Mislabeled)" : None, - "Hardcoded Subs (Non-English)" : "OTHER" + "English Hardcoded Subs (Full)": 4, + "English Hardcoded Subs (Forced)": 50, + "No English Subs": 14, + "English Softsubs Exist (Mislabeled)": None, + "Hardcoded Subs (Non-English)": "OTHER" } opts = cli_ui.select_choices("English subtitles not found. Please select any/all applicable options:", choices=list(trumpable_values.keys())) trumpable = [] if opts: for t, v in trumpable_values.items(): if t in ''.join(opts): - if v == None: + if v is None: break - elif v != 50: # Hardcoded, Forced + elif v != 50: # Hardcoded, Forced trumpable.append(v) - elif v == "OTHER": #Hardcoded, Non-English + elif v == "OTHER": # Hardcoded, Non-English trumpable.append(14) hc_sub_langs = cli_ui.ask_string("Enter language code for HC Subtitle languages") for lang, subID in self.sub_lang_map.items(): @@ -480,7 +478,7 @@ def get_trumpable(self, sub_langs): else: sub_langs.append(v) trumpable.append(4) - + sub_langs = list(set(sub_langs)) trumpable = list(set(trumpable)) if trumpable == []: @@ -497,7 +495,7 @@ def get_remaster_title(self, meta): remaster_title.append('The Criterion Collection') elif meta.get('distributor') in ('MASTERS OF CINEMA', 'MOC'): remaster_title.append('Masters of Cinema') - + # Editions # Director's Cut, Extended Edition, Rifftrax, Theatrical Cut, Uncut, Unrated if "director's cut" in meta.get('edition', '').lower(): @@ -518,7 +516,7 @@ def get_remaster_title(self, meta): # Features # 2-Disc Set, 2in1, 2D/3D Edition, 3D Anaglyph, 3D Full SBS, 3D Half OU, 3D Half SBS, - # 4K Restoration, 4K Remaster, + # 4K Restoration, 4K Remaster, # Extras, Remux, if meta.get('type') == "REMUX": remaster_title.append("Remux") @@ -532,10 +530,10 @@ def get_remaster_title(self, meta): remaster_title.append('Dual Audio') if "Dubbed" in meta['audio']: remaster_title.append('English Dub') - if meta.get('has_commentary', False) == True: + if meta.get('has_commentary', False) is True: remaster_title.append('With Commentary') - # HDR10, HDR10+, Dolby Vision, 10-bit, + # HDR10, HDR10+, Dolby Vision, 10-bit, # if "Hi10P" in meta.get('video_encode', ''): # remaster_title.append('10-bit') if meta.get('hdr', '').strip() == '' and meta.get('bit_depth') == '10': @@ -585,16 +583,16 @@ async def edit_desc(self, meta): mi_dump = each['summary'] else: mi_dump = each['summary'] - if meta.get('vapoursynth', False) == True: + if meta.get('vapoursynth', False) is True: use_vs = True else: use_vs = False ds = multiprocessing.Process(target=prep.disc_screenshots, args=(f"FILE_{i}", each['bdinfo'], meta['uuid'], meta['base_dir'], use_vs, [], meta.get('ffdebug', False), 2)) ds.start() - while ds.is_alive() == True: + while ds.is_alive() is True: await asyncio.sleep(1) - new_screens = glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}",f"FILE_{i}-*.png") - images, dummy = prep.upload_screens(meta, 2, 1, 0, 2, new_screens, {}) + new_screens = glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}", f"FILE_{i}-*.png") + images, dummy = prep.upload_screens(meta, 2, 1, 0, 2, new_screens, {}) if each['type'] == "DVD": desc.write(f"[b][size=3]{each['name']}:[/size][/b]\n") @@ -609,12 +607,12 @@ async def edit_desc(self, meta): else: ds = multiprocessing.Process(target=prep.dvd_screenshots, args=(meta, i, 2)) ds.start() - while ds.is_alive() == True: + while ds.is_alive() is True: await asyncio.sleep(1) new_screens = glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}", f"{meta['discs'][i]['name']}-*.png") - images, dummy = prep.upload_screens(meta, 2, 1, 0, 2, new_screens, {}) - - if len(images) > 0: + images, dummy = prep.upload_screens(meta, 2, 1, 0, 2, new_screens, {}) + + if len(images) > 0: for each in range(len(images[:int(meta['screens'])])): raw_url = images[each]['raw_url'] desc.write(f"[img]{raw_url}[/img]\n") @@ -625,12 +623,12 @@ async def edit_desc(self, meta): file = meta['filelist'][i] if i == 0: # Add This line for all web-dls - if meta['type'] == 'WEBDL' and meta.get('service_longname', '') != '' and meta.get('description', None) == None and self.web_source == True: + if meta['type'] == 'WEBDL' and meta.get('service_longname', '') is not '' and meta.get('description', None) == None and self.web_source is True: desc.write(f"[quote][align=center]This release is sourced from {meta['service_longname']}[/align][/quote]") mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt", 'r', encoding='utf-8').read() else: # Export Mediainfo - mi_dump = MediaInfo.parse(file, output="STRING", full=False, mediainfo_options={'inform_version' : '1'}) + mi_dump = MediaInfo.parse(file, output="STRING", full=False, mediainfo_options={'inform_version': '1'}) # mi_dump = mi_dump.replace(file, os.path.basename(file)) with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/TEMP_PTP_MEDIAINFO.txt", "w", newline="", encoding="utf-8") as f: f.write(mi_dump) @@ -638,9 +636,9 @@ async def edit_desc(self, meta): # Generate and upload screens for other files s = multiprocessing.Process(target=prep.screenshots, args=(file, f"FILE_{i}", meta['uuid'], meta['base_dir'], meta, 2)) s.start() - while s.is_alive() == True: + while s.is_alive() is True: await asyncio.sleep(3) - new_screens = glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}",f"FILE_{i}-*.png") + new_screens = glob.glob1(f"{meta['base_dir']}/tmp/{meta['uuid']}", f"FILE_{i}-*.png") images, dummy = prep.upload_screens(meta, 2, 1, 0, 2, new_screens, {}) desc.write(f"[mediainfo]{mi_dump}[/mediainfo]\n") @@ -648,8 +646,8 @@ async def edit_desc(self, meta): base2ptp = self.convert_bbcode(base) if base2ptp.strip() != "": desc.write(base2ptp) - desc.write("\n\n") - if len(images) > 0: + desc.write("\n\n") + if len(images) > 0: for each in range(len(images[:int(meta['screens'])])): raw_url = images[each]['raw_url'] desc.write(f"[img]{raw_url}[/img]\n") @@ -668,7 +666,7 @@ async def get_AntiCsrfToken(self, meta): loggedIn = await self.validate_login(uploadresponse) else: console.print("[yellow]PTP Cookies not found. Creating new session.") - if loggedIn == True: + if loggedIn is True: AntiCsrfToken = re.search(r'data-AntiCsrfToken="(.*)"', uploadresponse.text).group(1) else: passKey = re.match(r"https?://please\.passthepopcorn\.me:?\d*/(.+)/announce",self.announce_url).group(1) @@ -678,7 +676,7 @@ async def get_AntiCsrfToken(self, meta): "passkey": passKey, "keeplogged": "1", } - headers = {"User-Agent" : self.user_agent} + headers = {"User-Agent": self.user_agent} loginresponse = session.post("https://passthepopcorn.me/ajax.php?action=login", data=data, headers=headers) await asyncio.sleep(2) try: @@ -724,26 +722,26 @@ async def fill_upload_form(self, groupID, meta): data = { "submit": "true", "remaster_year": "", - "remaster_title": self.get_remaster_title(meta), #Eg.: Hardcoded English + "remaster_title": self.get_remaster_title(meta), # Eg.: Hardcoded English "type": self.get_type(meta['imdb_info'], meta), - "codec": "Other", # Sending the codec as custom. + "codec": "Other", # Sending the codec as custom. "other_codec": self.get_codec(meta), "container": "Other", "other_container": self.get_container(meta), "resolution": resolution, - "source": "Other", # Sending the source as custom. + "source": "Other", # Sending the source as custom. "other_source": self.get_source(meta['source']), "release_desc": desc, "nfo_text": "", - "subtitles[]" : ptp_subtitles, - "trumpable[]" : ptp_trumpable, - "AntiCsrfToken" : await self.get_AntiCsrfToken(meta) + "subtitles[]": ptp_subtitles, + "trumpable[]": ptp_trumpable, + "AntiCsrfToken": await self.get_AntiCsrfToken(meta) } if data["remaster_year"] != "" or data["remaster_title"] != "": data["remaster"] = "on" if resolution == "Other": data["other_resolution"] = other_resolution - if meta.get('personalrelease', False) == True: + if meta.get('personalrelease', False) is True: data["internalrip"] = "on" # IF SPECIAL (idk how to check for this automatically) # data["special"] = "on" @@ -752,18 +750,18 @@ async def fill_upload_form(self, groupID, meta): else: data["imdb"] = meta["imdb_id"] - if groupID == None: # If need to make new group + if groupID is None: # If need to make new group url = "https://passthepopcorn.me/upload.php" if data["imdb"] == "0": tinfo = await self.get_torrent_info_tmdb(meta) else: tinfo = await self.get_torrent_info(meta.get("imdb_id", "0"), meta) cover = meta["imdb_info"].get("cover") - if cover == None: + if cover is None: cover = meta.get('poster') - if cover != None and "ptpimg" not in cover: + if cover is not None and "ptpimg" not in cover: cover = await self.ptpimg_url_rehost(cover) - while cover == None: + while cover is None: cover = cli_ui.ask_string("No Poster was found. Please input a link to a poster: \n", default="") if "ptpimg" not in str(cover) and str(cover).endswith(('.jpg', '.png')): cover = await self.ptpimg_url_rehost(cover) @@ -778,15 +776,15 @@ async def fill_upload_form(self, groupID, meta): if new_data['year'] in ['', '0', 0, None] and meta.get('manual_year') not in [0, '', None]: new_data['year'] = meta['manual_year'] while new_data["tags"] == "": - if meta.get('mode', 'discord') == 'cli': + if meta.get('mode', 'discord') == 'cli': console.print('[yellow]Unable to match any tags') console.print("Valid tags can be found on the PTP upload form") new_data["tags"] = console.input("Please enter at least one tag. Comma seperated (action, animation, short):") data.update(new_data) - if meta["imdb_info"].get("directors", None) != None: + if meta["imdb_info"].get("directors", None) is not None: data["artist[]"] = tuple(meta['imdb_info'].get('directors')) data["importance[]"] = "1" - else: # Upload on existing group + else: # Upload on existing group url = f"https://passthepopcorn.me/upload.php?groupid={groupID}" data["groupid"] = groupID @@ -826,7 +824,7 @@ async def upload(self, meta, url, data): comment="Created by L4G's Upload Assistant", created_by="L4G's Upload Assistant" ) - + # Explicitly set the piece size and update metainfo new_torrent.piece_size = 16777216 # 16 MiB in bytes new_torrent.metainfo['info']['piece length'] = 16777216 # Ensure 'piece length' is set diff --git a/src/trackers/RTF.py b/src/trackers/RTF.py index 28ce55924..232cbda2c 100644 --- a/src/trackers/RTF.py +++ b/src/trackers/RTF.py @@ -5,11 +5,11 @@ import base64 import re import datetime -import json from src.trackers.COMMON import COMMON from src.console import console + class RTF(): """ Edit for Tracker: @@ -18,10 +18,6 @@ class RTF(): Set type/category IDs Upload """ - - ############################################################### - ######## EDIT ME ######## - ############################################################### def __init__(self, config): self.config = config self.tracker = 'RTF' @@ -36,7 +32,7 @@ async def upload(self, meta): common = COMMON(config=self.config) await common.edit_torrent(meta, self.tracker, self.source_flag) await common.unit3d_edit_desc(meta, self.tracker, self.forum_link) - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: @@ -45,21 +41,21 @@ async def upload(self, meta): screenshots = [] for image in meta['image_list']: - if image['raw_url'] != None: + if image['raw_url'] is not None: screenshots.append(image['raw_url']) json_data = { - 'name' : meta['name'], + 'name': meta['name'], # description does not work for some reason # 'description' : meta['overview'] + "\n\n" + desc + "\n\n" + "Uploaded by L4G Upload Assistant", 'description': "this is a description", # editing mediainfo so that instead of 1 080p its 1,080p as site mediainfo parser wont work other wise. - 'mediaInfo': re.sub(r"(\d+)\s+(\d+)", r"\1,\2", mi_dump) if bd_dump == None else f"{bd_dump}", + 'mediaInfo': re.sub(r"(\d+)\s+(\d+)", r"\1,\2", mi_dump) if bd_dump is None else f"{bd_dump}", "nfo": "", "url": "https://www.imdb.com/title/" + (meta['imdb_id'] if str(meta['imdb_id']).startswith("tt") else "tt" + meta['imdb_id']) + "/", # auto pulled from IMDB "descr": "This is short description", - "poster": meta["poster"] if meta["poster"] != None else "", + "poster": meta["poster"] if meta["poster"] is not None else "", "type": "401" if meta['category'] == 'MOVIE'else "402", "screenshots": screenshots, 'isAnonymous': self.config['TRACKERS'][self.tracker]["anon"], @@ -77,13 +73,11 @@ async def upload(self, meta): 'Authorization': self.config['TRACKERS'][self.tracker]['api_key'].strip(), } - if datetime.date.today().year - meta['year'] <= 9: - console.print(f"[red]ERROR: Not uploading!\nMust be older than 10 Years as per rules") + console.print("[red]ERROR: Not uploading!\nMust be older than 10 Years as per rules") return - - if meta['debug'] == False: + if meta['debug'] is False: response = requests.post(url=self.upload_url, json=json_data, headers=headers) try: console.print(response.json()) @@ -91,14 +85,13 @@ async def upload(self, meta): t_id = response.json()['torrent']['id'] await common.add_tracker_torrent(meta, self.tracker, self.source_flag, self.config['TRACKERS'][self.tracker].get('announce_url'), "https://retroflix.club/browse/t/" + str(t_id)) - except: + except Exception: console.print("It may have uploaded, go check") return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(json_data) - async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") @@ -108,7 +101,7 @@ async def search_existing(self, meta): } params = { - 'includingDead' : '1' + 'includingDead': '1' } if meta['imdb_id'] != "0": @@ -122,7 +115,7 @@ async def search_existing(self, meta): for each in response: result = [each][0]['name'] dupes.append(result) - except: + except Exception: console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) @@ -157,7 +150,7 @@ async def generate_new_api(self, meta): if response.status_code == 201: console.print('[bold green]Using New API key generated for this upload') - console.print(f'[bold green]Please update your L4G config with the below RTF API Key for future uploads') + console.print('[bold green]Please update your L4G config with the below RTF API Key for future uploads') console.print(f'[bold yellow]{response.json()["token"]}') self.config['TRACKERS'][self.tracker]['api_key'] = response.json()["token"] else: diff --git a/src/trackers/SN.py b/src/trackers/SN.py index 54f13d64d..b987d3f37 100644 --- a/src/trackers/SN.py +++ b/src/trackers/SN.py @@ -15,7 +15,6 @@ class SN(): Set type/category IDs Upload """ - def __init__(self, config): self.config = config self.tracker = 'SN' @@ -31,7 +30,7 @@ async def get_type_id(self, type): 'BluRay': '3', 'Web': '1', # boxset is 4 - #'NA': '4', + # 'NA': '4', 'DVD': '2' }.get(type, '0') return type_id @@ -39,11 +38,11 @@ async def get_type_id(self, type): async def upload(self, meta): common = COMMON(config=self.config) await common.edit_torrent(meta, self.tracker, self.source_flag) - #await common.unit3d_edit_desc(meta, self.tracker, self.forum_link) + # await common.unit3d_edit_desc(meta, self.tracker, self.forum_link) await self.edit_desc(meta) cat_id = "" sub_cat_id = "" - #cat_id = await self.get_cat_id(meta) + # cat_id = await self.get_cat_id(meta) if meta['category'] == 'MOVIE': cat_id = 1 # sub cat is source so using source to get @@ -56,8 +55,7 @@ async def upload(self, meta): sub_cat_id = 5 # todo need to do a check for docs and add as subcat - - if meta['bdinfo'] != None: + if meta['bdinfo'] is not None: mi_dump = None bd_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/BD_SUMMARY_00.txt", 'r', encoding='utf-8').read() else: @@ -90,7 +88,7 @@ async def upload(self, meta): } - if meta['debug'] == False: + if meta['debug'] is False: response = requests.request("POST", url=self.upload_url, data=data, files=files) try: @@ -99,16 +97,15 @@ async def upload(self, meta): else: console.print("[red]Did not upload successfully") console.print(response.json()) - except: + except Exception: console.print("[red]Error! It may have uploaded, go check") console.print(data) console.print_exception() return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) - async def edit_desc(self, meta): base = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'r').read() with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'w') as desc: @@ -125,13 +122,12 @@ async def edit_desc(self, meta): desc.close() return - async def search_existing(self, meta): dupes = [] console.print("[yellow]Searching for existing torrents on site...") params = { - 'api_key' : self.config['TRACKERS'][self.tracker]['api_key'].strip() + 'api_key': self.config['TRACKERS'][self.tracker]['api_key'].strip() } # using title if IMDB id does not exist to search @@ -141,7 +137,7 @@ async def search_existing(self, meta): else: params['filter'] = meta['title'] else: - #using IMDB_id to search if it exists. + # using IMDB_id to search if it exists. if meta['category'] == 'TV': params['media_ref'] = f"tt{meta['imdb_id']}" params['filter'] = f"{meta.get('season', '')}{meta.get('episode', '')}" + " " + meta['resolution'] @@ -155,7 +151,7 @@ async def search_existing(self, meta): for i in response['data']: result = i['name'] dupes.append(result) - except: + except Exception: console.print('[red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) diff --git a/src/trackers/TL.py b/src/trackers/TL.py index 9b98f602f..15d6935b3 100644 --- a/src/trackers/TL.py +++ b/src/trackers/TL.py @@ -35,13 +35,13 @@ def __init__(self, config): self.upload_url = 'https://www.torrentleech.org/torrents/upload/apiupload' self.signature = None self.banned_groups = [""] - + self.announce_key = self.config['TRACKERS'][self.tracker]['announce_key'] self.config['TRACKERS'][self.tracker]['announce_url'] = f"https://tracker.torrentleech.org/a/{self.announce_key}/announce" pass - + async def get_cat_id(self, common, meta): - if meta.get('anime', 0): + if meta.get('anime', 0): return self.CATEGORIES['Anime'] if meta['category'] == 'MOVIE': @@ -64,7 +64,7 @@ async def get_cat_id(self, common, meta): elif meta['type'] == 'HDTV': return self.CATEGORIES['MovieHdRip'] elif meta['category'] == 'TV': - if meta['original_language'] != 'en': + if meta['original_language'] != 'en': return self.CATEGORIES['TvForeign'] elif meta.get('tv_pack', 0): return self.CATEGORIES['TvBoxsets'] @@ -82,13 +82,13 @@ async def upload(self, meta): await common.unit3d_edit_desc(meta, self.tracker, self.signature) open_desc = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]DESCRIPTION.txt", 'a+') - - info_filename = 'BD_SUMMARY_00' if meta['bdinfo'] != None else 'MEDIAINFO_CLEANPATH' + + info_filename = 'BD_SUMMARY_00' if meta['bdinfo'] is not None else 'MEDIAINFO_CLEANPATH' open_info = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/{info_filename}.txt", 'r', encoding='utf-8') open_desc.write('\n\n') open_desc.write(open_info.read()) open_info.close() - + open_desc.seek(0) open_torrent = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/[{self.tracker}]{meta['clean_name']}.torrent", 'rb') files = { @@ -96,19 +96,19 @@ async def upload(self, meta): 'torrent': (self.get_name(meta) + '.torrent', open_torrent) } data = { - 'announcekey' : self.announce_key, - 'category' : cat_id + 'announcekey': self.announce_key, + 'category': cat_id } headers = { 'User-Agent': f'Upload Assistant/2.1 ({platform.system()} {platform.release()})' } - - if meta['debug'] == False: + + if meta['debug'] is False: response = requests.post(url=self.upload_url, files=files, data=data, headers=headers) if not response.text.isnumeric(): console.print(f'[red]{response.text}') else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) open_torrent.close() open_desc.close() diff --git a/src/trackers/UNIT3D_TEMPLATE.py b/src/trackers/UNIT3D_TEMPLATE.py index 9d84b6dae..996bab254 100644 --- a/src/trackers/UNIT3D_TEMPLATE.py +++ b/src/trackers/UNIT3D_TEMPLATE.py @@ -54,7 +54,7 @@ async def get_type_id(self, type): async def get_res_id(self, resolution): resolution_id = { - '8640p':'10', + '8640p': '10', '4320p': '1', '2160p': '2', '1440p': '3', @@ -99,7 +99,7 @@ async def upload(self, meta): 'name': meta['name'], 'description': desc, 'mediainfo': mi_dump, - 'bdinfo': bd_dump, + 'bdinfo': bd_dump, 'category_id': cat_id, 'type_id': type_id, 'resolution_id': resolution_id, @@ -142,11 +142,11 @@ async def upload(self, meta): response = requests.post(url=self.upload_url, files=files, data=data, headers=headers, params=params) try: console.print(response.json()) - except: + except Exception: console.print("It may have uploaded, go check") return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) open_torrent.close() @@ -171,7 +171,7 @@ async def search_existing(self, meta): # difference = SequenceMatcher(None, meta['clean_name'], result).ratio() # if difference >= 0.05: dupes.append(result) - except: + except Exception: console.print('[bold red]Unable to search for existing torrents on site. Either the site is down or your API key is incorrect') await asyncio.sleep(5) diff --git a/upload.py b/upload.py index 1bbb4fb7f..33c83499c 100644 --- a/upload.py +++ b/upload.py @@ -47,6 +47,7 @@ import shutil import glob import cli_ui +import traceback from src.console import console from rich.markdown import Markdown @@ -54,13 +55,12 @@ cli_ui.setup(color='always', title="L4G's Upload Assistant") -import traceback base_dir = os.path.abspath(os.path.dirname(__file__)) try: from data.config import config -except: +except Exception: if not os.path.exists(os.path.abspath(f"{base_dir}/data/config.py")): try: if os.path.exists(os.path.abspath(f"{base_dir}/data/config.json")): @@ -75,7 +75,7 @@ from data.config import config else: raise NotImplementedError - except: + except Exception: cli_ui.info(cli_ui.red, "We have switched from .json to .py for config to have a much more lenient experience") cli_ui.info(cli_ui.red, "Looks like the auto updater didnt work though") cli_ui.info(cli_ui.red, "Updating is just 2 easy steps:") @@ -369,7 +369,7 @@ async def do_the_thing(base_dir): if meta['upload'] is True: await thr.upload(session, meta) await client.add_to_client(meta, "THR") - except: + except Exception: console.print(traceback.print_exc()) if tracker == "PTP": @@ -406,7 +406,7 @@ async def do_the_thing(base_dir): await ptp.upload(meta, ptpUrl, ptpData) await asyncio.sleep(5) await client.add_to_client(meta, "PTP") - except: + except Exception: console.print(traceback.print_exc()) if tracker == "TL": From 74501c2a25fd62121187b455f0da807b43583f11 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 20:02:58 +1000 Subject: [PATCH 23/33] More linting --- cogs/commands.py | 8 ++++- src/args.py | 2 +- src/prep.py | 84 ++++++++++++++++++++++----------------------- src/trackers/ANT.py | 16 +++++---- src/trackers/MTV.py | 2 +- src/trackers/NBL.py | 2 +- src/trackers/PTP.py | 8 ++--- src/vs.py | 5 +-- 8 files changed, 68 insertions(+), 59 deletions(-) diff --git a/cogs/commands.py b/cogs/commands.py index 7bcd8e7ef..a02b7362c 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -313,7 +313,13 @@ async def send_embed_and_upload(self, ctx, meta): res = meta['resolution'] missing = await self.get_missing(meta) - embed=discord.Embed(title = f"Upload: {meta['title']}", url=f"https://www.themoviedb.org/{meta['category'].lower()}/{meta['tmdb']}", description=meta['overview'], color=0x0080ff, timestamp=datetime.utcnow()) + embed = discord.Embed( + title=f"Upload: {meta['title']}", + url=f"https://www.themoviedb.org/{meta['category'].lower()}/{meta['tmdb']}", + description=meta['overview'], + color=0x0080ff, + timestamp=datetime.utcnow() + ) embed.add_field(name="Links", value=f"[TMDB](https://www.themoviedb.org/{meta['category'].lower()}/{meta['tmdb']}){imdb}{tvdb}") embed.add_field(name=f"{res} / {meta['type']}{tag}", value=f"```{meta['name']}```", inline=False) if missing != []: diff --git a/src/args.py b/src/args.py index 0db2f521e..485e76235 100644 --- a/src/args.py +++ b/src/args.py @@ -170,7 +170,7 @@ def list_to_string(self, list): return str(list[0]) try: result = " ".join(list) - except: + except Exception: result = "None" return result diff --git a/src/prep.py b/src/prep.py index 912410e5a..95ba752d0 100644 --- a/src/prep.py +++ b/src/prep.py @@ -873,7 +873,7 @@ def disc_screenshots(self, filename, bdinfo, folder_id, base_dir, use_vs, image_ else: loglevel = 'quiet' debug = True - with Progress( + with Progress( TextColumn("[bold green]Saving Screens..."), BarColumn(), "[cyan]{task.completed}/{task.total}", @@ -884,7 +884,7 @@ def disc_screenshots(self, filename, bdinfo, folder_id, base_dir, use_vs, image_ for i in range(num_screens + 1): image = f"{base_dir}/tmp/{folder_id}/{filename}-{i}.png" try: - ss_times = self.valid_ss_time(ss_times, num_screens+1, length) + ss_times = self.valid_ss_time(ss_times, num_screens + 1, length) ( ffmpeg .input(file, ss=ss_times[-1], skip_frame=keyframe) @@ -967,50 +967,50 @@ def dvd_screenshots(self, meta, disc_num, num_screens=None): looped = 0 retake = False with Progress( - TextColumn("[bold green]Saving Screens..."), - BarColumn(), - "[cyan]{task.completed}/{task.total}", - TimeRemainingColumn() - ) as progress: - screen_task = progress.add_task("[green]Saving Screens...", total=num_screens + 1) - ss_times = [] - for i in range(num_screens + 1): - if n >= len(main_set): - n = 0 - if n >= num_screens: - n -= num_screens - image = f"{meta['base_dir']}/tmp/{meta['uuid']}/{meta['discs'][disc_num]['name']}-{i}.png" - if not os.path.exists(image) or retake is not False: - retake = False - loglevel = 'quiet' - debug = True - if bool(meta.get('debug', False)): - loglevel = 'error' - debug = False - - def _is_vob_good(n, loops, num_screens): - voblength = 300 - vob_mi = MediaInfo.parse(f"{meta['discs'][disc_num]['path']}/VTS_{main_set[n]}", output='JSON') - vob_mi = json.loads(vob_mi) + TextColumn("[bold green]Saving Screens..."), + BarColumn(), + "[cyan]{task.completed}/{task.total}", + TimeRemainingColumn() + ) as progress: + screen_task = progress.add_task("[green]Saving Screens...", total=num_screens + 1) + ss_times = [] + for i in range(num_screens + 1): + if n >= len(main_set): + n = 0 + if n >= num_screens: + n -= num_screens + image = f"{meta['base_dir']}/tmp/{meta['uuid']}/{meta['discs'][disc_num]['name']}-{i}.png" + if not os.path.exists(image) or retake is not False: + retake = False + loglevel = 'quiet' + debug = True + if bool(meta.get('debug', False)): + loglevel = 'error' + debug = False + + def _is_vob_good(n, loops, num_screens): + voblength = 300 + vob_mi = MediaInfo.parse(f"{meta['discs'][disc_num]['path']}/VTS_{main_set[n]}", output='JSON') + vob_mi = json.loads(vob_mi) + try: + voblength = float(vob_mi['media']['track'][1]['Duration']) + return voblength, n + except Exception: try: - voblength = float(vob_mi['media']['track'][1]['Duration']) + voblength = float(vob_mi['media']['track'][2]['Duration']) return voblength, n except Exception: - try: - voblength = float(vob_mi['media']['track'][2]['Duration']) + n += 1 + if n >= len(main_set): + n = 0 + if n >= num_screens: + n -= num_screens + if loops < 6: + loops = loops + 1 + voblength, n = _is_vob_good(n, loops, num_screens) return voblength, n - except Exception: - n += 1 - if n >= len(main_set): - n = 0 - if n >= num_screens: - n -= num_screens - if loops < 6: - loops = loops + 1 - voblength, n = _is_vob_good(n, loops, num_screens) - return voblength, n - else: - return 300, n + else: + return 300, n try: voblength, n = _is_vob_good(n, 0, num_screens) img_time = random.randint(round(voblength/5), round(voblength - voblength/5)) diff --git a/src/trackers/ANT.py b/src/trackers/ANT.py index de1e5ac5b..2fcbdd603 100644 --- a/src/trackers/ANT.py +++ b/src/trackers/ANT.py @@ -28,13 +28,15 @@ def __init__(self, config): self.source_flag = 'ANT' self.search_url = 'https://anthelion.me/api.php' self.upload_url = 'https://anthelion.me/api.php' - self.banned_groups = ['3LTON', '4yEo', 'ADE', 'AFG', 'AniHLS', 'AnimeRG', 'AniURL', 'AROMA', 'aXXo', 'Brrip', 'CHD', 'CM8', - 'CrEwSaDe', 'd3g', 'DDR', 'DNL', 'DeadFish', 'ELiTE', 'eSc', 'FaNGDiNG0', 'FGT', 'Flights', 'FRDS', - 'FUM', 'HAiKU', 'HD2DVD', 'HDS', 'HDTime', 'Hi10', 'ION10', 'iPlanet', 'JIVE', 'KiNGDOM', 'Leffe', - 'LiGaS', 'LOAD', 'MeGusta', 'MkvCage', 'mHD', 'mSD', 'NhaNc3', 'nHD', 'NOIVTC', 'nSD', 'Oj', 'Ozlem', - 'PiRaTeS', 'PRoDJi', 'RAPiDCOWS', 'RARBG', 'RetroPeeps', 'RDN', 'REsuRRecTioN', 'RMTeam', 'SANTi', - 'SicFoI', 'SPASM', 'SPDVD', 'STUTTERSHIT', 'TBS', 'Telly', 'TM', 'UPiNSMOKE', 'URANiME', 'WAF', 'xRed', - 'XS', 'YIFY', 'YTS', 'Zeus', 'ZKBL', 'ZmN', 'ZMNT'] + self.banned_groups = [ + '3LTON', '4yEo', 'ADE', 'AFG', 'AniHLS', 'AnimeRG', 'AniURL', 'AROMA', 'aXXo', 'Brrip', 'CHD', 'CM8', + 'CrEwSaDe', 'd3g', 'DDR', 'DNL', 'DeadFish', 'ELiTE', 'eSc', 'FaNGDiNG0', 'FGT', 'Flights', 'FRDS', + 'FUM', 'HAiKU', 'HD2DVD', 'HDS', 'HDTime', 'Hi10', 'ION10', 'iPlanet', 'JIVE', 'KiNGDOM', 'Leffe', + 'LiGaS', 'LOAD', 'MeGusta', 'MkvCage', 'mHD', 'mSD', 'NhaNc3', 'nHD', 'NOIVTC', 'nSD', 'Oj', 'Ozlem', + 'PiRaTeS', 'PRoDJi', 'RAPiDCOWS', 'RARBG', 'RetroPeeps', 'RDN', 'REsuRRecTioN', 'RMTeam', 'SANTi', + 'SicFoI', 'SPASM', 'SPDVD', 'STUTTERSHIT', 'TBS', 'Telly', 'TM', 'UPiNSMOKE', 'URANiME', 'WAF', 'xRed', + 'XS', 'YIFY', 'YTS', 'Zeus', 'ZKBL', 'ZmN', 'ZMNT' + ] self.signature = None pass diff --git a/src/trackers/MTV.py b/src/trackers/MTV.py index fb5b3f601..33f9ee89f 100644 --- a/src/trackers/MTV.py +++ b/src/trackers/MTV.py @@ -545,7 +545,7 @@ async def search_existing(self, meta): console.print("[yellow]Searching for existing torrents on site...") params = { 't': 'search', - 'apikey' : self.config['TRACKERS'][self.tracker]['api_key'].strip(), + 'apikey': self.config['TRACKERS'][self.tracker]['api_key'].strip(), 'q': "" } if meta['imdb_id'] not in ("0", "", None): diff --git a/src/trackers/NBL.py b/src/trackers/NBL.py index a870918d3..35dd0fc50 100644 --- a/src/trackers/NBL.py +++ b/src/trackers/NBL.py @@ -78,7 +78,7 @@ async def upload(self, meta): console.print("[bold yellow]It may have uploaded, go check") return else: - console.print(f"[cyan]Request Data:") + console.print("[cyan]Request Data:") console.print(data) open_torrent.close() diff --git a/src/trackers/PTP.py b/src/trackers/PTP.py index 9bd25fa2d..4e924cdaa 100644 --- a/src/trackers/PTP.py +++ b/src/trackers/PTP.py @@ -301,7 +301,7 @@ async def ptpimg_url_rehost(self, image_url): 'api_key': self.config["DEFAULT"]["ptpimg_api"], 'link-upload': image_url } - headers = { 'referer': 'https://ptpimg.me/index.php'} + headers = {'referer': 'https://ptpimg.me/index.php'} url = "https://ptpimg.me/upload.php" response = requests.post(url, headers=headers, data=payload) @@ -623,7 +623,7 @@ async def edit_desc(self, meta): file = meta['filelist'][i] if i == 0: # Add This line for all web-dls - if meta['type'] == 'WEBDL' and meta.get('service_longname', '') is not '' and meta.get('description', None) == None and self.web_source is True: + if meta['type'] == 'WEBDL' and meta.get('service_longname', '') != '' and meta.get('description', None) is None and self.web_source is True: desc.write(f"[quote][align=center]This release is sourced from {meta['service_longname']}[/align][/quote]") mi_dump = open(f"{meta['base_dir']}/tmp/{meta['uuid']}/MEDIAINFO.txt", 'r', encoding='utf-8').read() else: @@ -669,7 +669,7 @@ async def get_AntiCsrfToken(self, meta): if loggedIn is True: AntiCsrfToken = re.search(r'data-AntiCsrfToken="(.*)"', uploadresponse.text).group(1) else: - passKey = re.match(r"https?://please\.passthepopcorn\.me:?\d*/(.+)/announce",self.announce_url).group(1) + passKey = re.match(r"https?://please\.passthepopcorn\.me:?\d*/(.+)/announce", self.announce_url).group(1) data = { "username": self.username, "password": self.password, @@ -750,7 +750,7 @@ async def fill_upload_form(self, groupID, meta): else: data["imdb"] = meta["imdb_id"] - if groupID is None: # If need to make new group + if groupID is None: # If need to make new group url = "https://passthepopcorn.me/upload.php" if data["imdb"] == "0": tinfo = await self.get_torrent_info_tmdb(meta) diff --git a/src/vs.py b/src/vs.py index f9d00dbd4..7fb918cfe 100644 --- a/src/vs.py +++ b/src/vs.py @@ -1,10 +1,11 @@ import vapoursynth as vs -core = vs.core -from awsmfunc import ScreenGen, DynamicTonemap, FrameInfo, zresize +from awsmfunc import ScreenGen, DynamicTonemap, zresize import random import os from functools import partial +core = vs.core + def CustomFrameInfo(clip, text): def FrameProps(n, f, clip): From 722dbceeb8fc78fa47741732e377ffebffa92bf0 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 20:08:02 +1000 Subject: [PATCH 24/33] Pull updated bdinfo binary --- bin/BDInfo/BDInfo.exe | Bin 676864 -> 685568 bytes bin/BDInfo/System.Resources.Extensions.dll | Bin 0 -> 137376 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 bin/BDInfo/System.Resources.Extensions.dll diff --git a/bin/BDInfo/BDInfo.exe b/bin/BDInfo/BDInfo.exe index e2462867eb63eba301d627c835e8b0e11c7eb69d..82e0a6f8615daa2a9f2e64672173e730673caf2a 100644 GIT binary patch delta 155954 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
o;Ku-GofnYmL>V?i zQf|gjrfnJ3E9rg^nV{W|R=B0UW1y2%Rw&L#G5T(?LRAia8i6tY5DJamRlFxAI_$#?T3GSNoAVaF%vYkWOvb6t$*h+|^lFzL5!EB5UKS+Kj zKu+0>DDsn~Y)SMjZR|@FdwF+pUL-{$3t@mFKPA{ov_B{W%l#~3aSn!$_E@3uU#q|$ zqwznfz#pseRU4vwPSE&F1sEf60=+R!V8j{X0Q0m$KdiTd0Jak0#E~VYlMh&}X80d*5980ez zj~5YS2c1gZMELpHci3o_&9IA7K12iqplH@JL(kpNWv+Aubh$b?_UY>-IFvaR19lx+j*Znw?A~ zW~JRpn{+yW`PWop&L@k%1$jyj&-!Dv3u6sRh^p9asDH8dF)Q_a2ohE09Et>|OKV_0 zz*~KKQukTmdrWXItjYr^zmrjZDTh-6&hHRiU)R2T>p%j|mw|KNhdBw3gYI?yB)AIY zTnM7RI(HG0GO`tvJM~bkCAQ@qYMNN?M8KT!uBh;;ba0?Ln;!W+b4gb(is$?Rh^vbz z!(eKIm$uX(vYM>(GJI=rLps*vt&vyYDAwXmsv3SZ%Nke&+Xag?)-C*ejJghGDp1)> zO*T_I@Eme6g-s@}B9yZY>@w+rJAuPu^0{fnKi&CD%MJbJA`<)qYt*=*@n}Mkx zTMCSqE}jpj3s(X=B?tg4TGvl7n9|6lfp*%bT{V2Pd zcXxNlrsdt;ow6l)w@h41qs%po{2AOcN$!=p71@nNe37L)TujbiLb-kts7$V78Bpc= z5)5-OaZ?NTe}c9Od;O&9!p$VvcG*U@_a<_B-Ci@zbI;m8G$MS%N;5Zs} z{|gp;Y;(s8*&RQgG+R=nUMB1QCR8N2_<<=znb<8T5ad7;asWkQx1v1kNhl0E8!8FZ zjs|3_)S=t_oe`lrr1wwh>T?Q6tlqS{WV2R9UR5ua!d0dx?KbDXSY*t5_wOLF+d`L* zP#fp)fhpedX?17Xva`P(Rtn8-^BJ0Hw)j`D!Mf6BG-))fr|{z}9cFg@r6l69bnWr} z!`S{69%xhY(Jth1B)~L;+5+5rVg8vlsZOj(y#jr6HTBKg8Ue`NN-}hK67?^*=|N3O z!Cu28$A93N=C^e?>H0UMChNS;tod&|#2ybc6~02d?@3rHyoRt|Ne<+Y2rLMpT=DT2 z&#qP<4Z^3ph8pC06Mh@u!^rF6NAQr_Ub<_g804au42BK}*^QZ4m@n$`A~@9N%Ag_B z&NXi=*6~Ui?Wc-W%xu+_{|q6q;9b4%TV=Wx6S-Awa9p|6(d!43!I2;b=9BZHe@N+*Ht7U*6ySrv$mae;^o z1VO*)l_Sg>SIpFf7#Wg)Pp7_O#!j&*TWrd;Y{h8V z9d@0$Xr>Z`_Y9RY&{5vT-V-v@I0JU_R67gKF+8M2#L1)4>h zvkJ7B(&r9fuQ9mQ-}yV5Orp$jo=jR`zlo|44e}X~g_D$i$9`7~Hnfdk)|<={{vFVq zJy0;1i1IZgK)LA(^YO7~p(z8+nT&a`Dq`1&>J*JAlPAA~^bG?r;9|AGtUT9uCe(!f z0ufC)v=#74_pS<=cfis7^Z6Gydooi_tsP#uJ!po5nutU}uol+Ha+_Csx^Y ztzo4dT>4jGCfC zhs$7(vPV(b5D6?wC#+{|9hYYA34ziwtSov@C%!}GYZ19WZOKtE6LE{*zzdRuphl(T zk{Fo1_>N^4>)AiR<>323rQFzey%!j%lQ50n0Q|9sp|%IvBiEEYhS4+`?qJ1!Go@d; z0I>=B4NJGHvvJ2@2mB(9F7QJf%!)Rt5vqd`Ezl4sC28=Q&TCgyr#{r?(l8A1DYQ_# zQCX1UIU7i=KB{TQKJwXEE1VdoR>c;f(!=71z3?Dp?!_5+zSZm~>WDaGb~I7uw2pfg zG!uu;Jfj8c$DevpaIA{WmM~7^$Hf8KqMwZgr=Oz2U|W@Gy@VYl-j8_6F)A6e5Wb4Fm57O{kSmSZ_0W&QS)L zXvEUOPY<{7q_ptRAgLOkI69(Q895dPAm!Vj9Gq$j+2A{2-18fL(Aw&YOZX-k%tWo( zYGwIv;fAsMop^_*CSzGJs(8#~EHP#>R($tbn0$*&#%lCaM2BWwiMI5ttGYDom$7_O zeKjc&yCgp6ZUaw?-{n0T*OjL__6Cek%u?!23*GK+6tj$lN6{%JWfOxGR@!|6J~Cd? zb}|Q`4Fd=pgNY~O&!pRSCOSQRdI=xjO3N%%9WcRwv$}N9Sp5a@hN(dw=2JRLWpY*K zrzBU$7F`gZ>h*wU3Kx>2!q8|(WHLV3tj3u5l2kB0XssW)9&^H(l;xxlJG9au0E|amxxe)RpwWWiYxKeo(eIu$UA=mn|{q8&HR|pBE}zL8Z#r;Fs*h zM;B!BkTZl}!IDO{(#x5Ih)fP0qjE2rLlUx8TIQZY23Km4lijEgO)>#j(~C>{vtTrK z`^E7k-f2k3R`d_XN&*=BE!xDbcwbe4{~K^Pdl%bXf#+BBz6!rb@HTwljQtirOp3(z z0MC63ob40-fT0^>ukb3uCOG)Vj%ps>IG%|8j~if%;6t@6h&oH^R7%x(R;l__P0l$C zg|i3WyR98%)^K)Hb$%Fk$ENkDMI7?%BXj(PJ3U;PPkrd*sv4=Gl`D&DsmFf*j`)-# zt799G${WIhuz(;4<>_t2MTi8-Rp5aS_HMI8Eo@s`Z_b}#TWE1;Y{kxaYjZo~&D6Bi zSmhfLq}VaGV<%J=PW_~})vVo$!4JVme zV$Lm9oDA%qDsI6+e)GHo|5Ert!^BR*4`m)}M%37CQG>3U-Yggz9eU}LTi~!-CkDlV zTl_^GZEdI(pvn~uN3wSe1tBP+R%HEnp5&gVXf=%JKK0QC* zj`?}4GOU6)+5GHK(45TGYmdRe<4xAtAf>3*HeMC(pIqa*E{#d83Axn&C znqp%QUK(#({_~i#BCa=eRmT=0w~*sViL8XrL_J84U=(KYv)~-KA-$}gt+LdEg$}ip zU5`1r=f?^jI#udewl1hq`HL(WU#J#}ohit2XB|qyy$V%Mjni86BJv6_keynkqsLL- z>Qnd18q>IS=Vh4So?ug6LTat5>pfGd#7v`(;8rsnbc}s~mAs6tq{-eL>h0tqV&4|+ zD8SK+D`iQ6YZ5So;?m82z$S`Q@oZa*ZJqRVmMQ0OKs8OMrbnohYglcS{v6o#%vUoD ziQ-R?+1!ctlQDc4rF6_OaH_*`^W}@2bSGpge!hI!6Gjrg9?GZFjVSc4^{>Ir?G=3c zyVl44cp0vp{0CadD91mEk-roCx_~nfb`)C{fE9vo23Tx+09FXT9e^BRX)M)YVq|}E zpqkX!?+7T&s4+C(?|K4D%+R!knCezjU^rnwm_G*OO1{4E5+;}dF~$IhIq&o+o3p+? z60O64m{~C(8V5nHbC04$7&Md;KwxqgPN8XATtz{XM2rkzUDzXM@iS zHtrWup~E*&vOfB^ZHmLrw}piu&iw|Nsxt+27nV@g$ZeZMn+tRZBdX6MDo@3Q!w|?Y zR(Nxf58-^{lyFQ$xz7hN(Otm0M-110rkN{sk`SWJg>4e#uR)60j{J)i>F^3;5LK>F z3P(jRxA!bRQ4(%jG-n0Zpl?}{0LvKwZ3juA+vF@^B=<`dIm?Ee$F)nANKKbXJ7ISt zm`%lSZ)vtYJJN&vaY~X&^epU!B~bV;JisY!A;eXN`UOy%q-|1}iNw5w^pz0*u`TnRxg4@s2B1wpfT|>S^)Tl-xF*f%l5j_j-Gx7aC|YwRbY9|t<;9-Cq>gaDF@I3F zS`p`75ik{+)zQopT$h$y0(^o2R9j$wp!uiH=ihyYM@E~rwv(btoA-i?nz_#%Gw9(MWZ_UpMS3Tm~ z^#MbnskrHnICIaW&|gM+!~?xTx5;e6$ZHtQ;r`7l)2cD~OSr%jbqdqRJIAO_t=48joOD_RWC-1JO~%7{U@P>({6ib1sdR3HZ3m0}QQ+M&?z4aA_^ zh(Q=)+zhgm94%7rxnNlK{!NoCe{&J2 z)P-mXa%3U}_$vXhpPr)mH!l4U|C8R3Mjp4dVGIKNPv#fvS;%R!A(qyv2mw?jkU;N3Wx-I8G%N9gL zrZ`+Ybep^+>|X@R90?Z>+#G-GCtT?1-(Jit3tt3Ew;?3#7mw%>7D;f`_w(?VBH=&} zlaz__3j&}`{epnLO`0J)9dD9EPKPN8!lo@LSe1mLQSD0LNJ%JPRT6~VDa;^ce$uTJ z$4wQA<2esr80IN$p?IjaMc9-EQ{*lVGMsL|u`reH^1zes!WebtAEjb3@$RXCKiwuP zf5Gy1&kH=6L4`Tc!aDGaaQmZBI!q=JE<8fZGLj2KyH}B)O1XqTG-`bkXU1o8&cMBm z0aTAoPt!*chVFp16X~LU4YV&49fe!Q$w9PxcOZv^nHrUYxHe<|OMveMa?ow$APhOa z7?mUE^i!=*eY5lESSyOjSXdfh=TH+>+T$yGg#Mo6D2y^=<-(~eNkn1~=~ zLFZh^+8Uu}|L(LM^BBe;9$XpyC2wE6K$T>&BEMj=E zg$yALiQs}Ls11=3KAoWDeUh-q|yQ|GGjcMv$Uu*XhBeQxOJd& zc75zW_m9P({cw0}^bZKpnwI69KHS67t3T2^eH#y2$yu9j=s#@ZZp7ZM;!DV-;u6G^ zmc`<_sxr^C^ku8cEBQ{j|I&ny=GME1A~?RCdpusa2n$;Y^g*KiQhEaqJlyRPhj`A1 zfU;GvE==%lAvhZAok_VSI6`q7Sv)Qj%fVkZJdLMXO{y30s0sPW{)q-9b8lU3aDJz% zd@NWIRb@`lRFzjSe&W7nj_p*H)ybS%HxrQIB#lrV5TK6NsN)Q()>(=S%=VYUseGLx z{t>5fYD1@SA{ip<1Co;TxoEMUMMVpxCgpYzdm=bTa<2fal+wOv-I+l+_t+4;GX(z% zu&0^3h4cJ~!jZZchVY*c!9C7I^wsLnkEsiHfk>f(V|XgU>N4xI^9znYvWq#1nR4+J zYn@i@JBZroEX5;JT{;OHCJrTNDNpPlJdDni&hM=V%tcofQ6!I%d+rtVPC17oi0mU( zQnXZi#Up^~yMwzh_GY*Gdn=OaxJb>w_UW=2;#}0?Os(WOwwtL>+h+7F*w=t}RXM1s z-K4E=X~0ImW6&f3sq_S9zWQPjkcVyT7hC z@JYgAvEe~=X-748FMGv-|Nm;I?C7SM*Xu4a*r8}d9)G0VVnu69y z3RX;`uw`P{$H)tP%TF}8dd?YH+ zEKj@aL!7&jqFyKJc8&s(b2J{A!%GWW4lgbvc!WJ@A0c}>hrsX%ISF7N;^l_sw%JR3 zpC$HU_+f_v?i^l>pvw`o?*UZU!}n%!dcvF*%7yY`Nl=ioIjm)=y|}U(+F2DqS434H zOHmJ3g$iV8v_O_d3uI|!fkP+TPyDd~ghwgd=BB(vy zJa4H9u_k&L+tJwUkHp(NHYa`6*aWo@l${}thgER255ZQ^L!{hT!b`ZpUNz`0^_q+_ zo6HhzfuPy`6SOny9K*so(mwJaDg`Uk6^i7@@YdD|WD*(1nm-ERr;zorV3l3`IO-g@ z6O(n0Bln~1qntCyJg=A^(tt*Jl*EVEPWXtb&rGq8G8^0H*;5p4{rRK~3xfO2)V}t! zgN?LG(+h*k7;pbE2y=x%OAV(fiWB|E?c^0sI*8)xE7{Bxm4OAFCP`&(@j)0z!xecJd9Q19A zxPdv8ZOc@)EemB^!CvMC`Bn(!+p@RHH|Z%dV1V^+X4{#7RJNUJYetf7%6}i(h9Dg2 zR_0rw)NUV*T6wg6bU5plJc-?Yk-pzO6eVN2>g_Bg)di`p`Ws>&L})^GgL{>PZC6C! z2pFkcc>4UYy|+W_El2;mySvL>E$_)vqPx3Q$%f8K8$0A_@X6PjlY|>3P)-Kv++N5J zZDA6~Jx}l*?v?WHbZ?e-mwr!nKOycZ?qe=bA{OVN;ptQw;li=JCe%3>3*@qiv02{j zZb9B1`rYX&e3yPtb{C8P6n8V<`-o(0_8rKCkO1y=pf*_^&dyK{Ml7W}C#3(UOk~gG zu2_V|M*crGk>A8pg*CEoQN2Pe!5ZK=HoPj@dKc3s$fWr4#QN$-H~LOkro zO5e1*oSqlbvw6Jd74*D|o_5F+4Mp|mo1ulWndJk&gR+RUEmhOLfq+F1GNj$?1>mtf zHx%ejT-v$Zfq|E{+ch92RHUjdL@34T%JJcICVa;}!{3Qeoo2tTVzejBXzbH>#iz+e z#c&m-oSEhJa-Ga2JoYd_>Lgd2FGRXBp}f%rG9P&NRC>0K_mo_{l%A8|iQ%G3bhuhi zvC~T4yFjKrXhRAm4`(Rf$HZ5UJIDFHEWS}Ax`M&fi}016n!tPySRZ*2i7Cc2=iY}Bn z$nkYbh^X~MmAN-x%xe*NveFj-eQQ#k2RvqA2_$GaMx6&d);`wR0;@q?J$J)1V;mH<5XfIo@YJ&puu;|EdbQNrLV( zq>Al$`*`#E5N0AHkpGY}7@kbcZP$RWhzB%F732t`?_CzPq6q_Ru*3{2Fk3OsN7agRBkJy<+jo=`g86- z+3dLHWt!cI_KD6`mfPjgay#)&%I&03xp_u`*e6bq)nFcJa3}f&cM{9(#Bt?jl-=nQ zkzOWwg);8t;WxOeeA-U^TKctrD1nypP+{Ozu% zJUZORE0{plwXu#gLU(6Q9{4QkNXyCU{NZYQwcM4EImKQrQ!Ls3>Gmn1?oVMWyFX1K zb4#nu!CUBGu?sRoEyLi3qD_vQS6N-g{CgNZg0)PX>tBNaoW-dOpd^V*!wg5K1Bd-t z7%vX>*)klRh8eD$hRLj*fajVh&ow?zVYw#Ca?LoFry>^*g8k=D1Nufl=Jb}+)lDh1 zo{K9uopGA3v*ALuPhlspmiqrxR>vXUg>YK9CZNQc+4mly`rebn=Rs9<^rx#|Livlz zpQt*<$`~~^nELp2xF)KPPYS-neMR1#Zo?&{>vBu-p6o7@_Y`-Fyz{m0yDowKxw|`` zcJENAI`?Y|sCSbb*SPxNI^*8BRLq@{;eV z*bCG*-42SNc2E~=9%v+Tvwd(kMoI^(ucw@0pP{~?v`)UEwB&bC7xo!m7&naA*t~}_ zb3yzMK$$ZE;Fx3dQ*T@B1MMwCnFH1?u^ZO=5UX3>9^-Dmu_*azuZ)574I z`7QRAvD@#9PdoE0`z*VAey2Uxo<6MoW3l;l_F0*3!DVLHkVk*hZ5jlgs^RH&kEU-O zoWIFF!0uU_Iom$lF3n$zfIT5*Gwrj-dcG9z_tv9!4MXN0yQrD;*t0d0>4WnJ?OFEh zwVAEUYnBHZD3;T{ZoA@B=Z&ar2RfKRk+TDnG^Hf-5u_m0Dz zYtI>nJI@}7;4sIRZgK1l5hQT-#t=q#2ixsUA*3956gLcmSmu1~Nyq#Ys@SHrnezj0 z*Pe%)O&6c~MEu zi%N3dI7uq{x$5-W>F5@XJ@D0d$z$gvUD1*Q|N6!o#s8vNzO$-w5@!pNxKBglMtBC3 z;OcCB&TYGty>y#fkaxQtEOzL@VyD|9crTa*Unqzv?vZ?llB656#F94Vj4_Q@OQ1Ga zx!d)mK!+ZkbZUbjFe1ca#(ch-JjmC2$_xobH*zBT=@H$b8&mo?bf3nQ-eK>M&V9iE z`_vJ*a2z-3oP=--yTd4ZhjTKJcwt7}%3J_ScHOkRFHrUI5pN~xjzU+O-Xy;bN+C3dPEW#?Ab|)gjlu4xfH-eI0D`H;-Ok@|oLoJk*?Ta0c%YEt& zUcKhGYZ-FnVzn%Gu?*d0IyO@d*8{WUVqf6v^UaV`XTq+BF+UN8g@do9FH@9UW!vT5 zW7b<~j9=tnhXYQtci3g#F}M?ku>FqR-IMK$E@7HO_Wf#4_}kOa7-Q0mtv`t&u-&BiSsG;l|PZs%i=x>Y-7@k?7?0tatJf9 zdhzkOQ;;=1s7Fhu$75&>u}AUPfHzLB`S(B&|3Gytb`<-nq!sme7Sl_sP=kAMD%@Ng z!2qwpjcr7+UuUBD&*4@Yu4mjjDT+Tci;F3MZ^}jSKZ5%a@c$qd1HEtrt8q;F%kXH# z?y)fbG3uvz+xKyN z_CzW6s7vbls`te9zeklzX0{c>U5T*|N?QK_(%u6@Y3|)$N5%~3MdnH3#Q`z?OkwGe zD=?W+UL>bP7|FVRWF2-t`k_CDyp1F2J_or;pkd^8w5aY{DHQoiA0Ax8w{pGX;hww70UYj+^~ei zWu47Po@zkmc$_dgnv%!{bx&HCITJ_E!mVh>h3lB$G@7J7Q%oOox{GkuIm2fS*|C?; z8aWdfXN#a&RE2MVM|5{Q(e7qH^70=spENGtNMz2lNq|d&e52d!N+*n{QKur=#;ReC z=@BlZAx7jFHL>n(N(u}Z6J;Xui=*fC1UE~cOOOx`24b+zwK-==QVHjrvwc{Uihsj$ z*J_%AId_e`vCJdycJ~>1ceq~5DJ1B059Qn0>a!8CqX*3yllyd#dvq7>MXSkA3Ktd8 z=6C^HgvkuT`3oXB@fYU5i!a8SdMq@hXif;OPv-<3Zp&qUMP@rQIYHRu1mXMzQTd5; zr!uTM8$q|1-xQTzF;T}s*m8~ZgbP&ikpvLy4hAgg&UZ)U7hFlpF9B8u@_SSXCIN&^ z0ticd(KHb4ZVn=(J3k|u1mfCsd{i?g354_Y5&8d$2?Jad81NICo@fFDcR+}tduxyY z>MkQcVIx0beEl$TQvq@AJwaq}d&$ZIAd$JJfH0aEdKuls&SCw83flJ<2+@tl3Qz|V ze1VV+Hgb3Y%?h1x_<#rNZUn*l7D2FnL=Y^|2!h!kK`{R!2qsG-JarnADmX$y9j6fL zMi{BF?hpKYN3I}D_}hq)LkH;860skmw!+ZEM@qhjzAjOOq`0l-@8B$Eq=Nxd8plsy zlqO~HDTqWFS6d{D5Yf_Z+z^J_qWR$#21u0uoAfwOVcZG&<4}Zg!z)?D!FY>T!mNUU ztNR>~BzYpl(BayJWYNQw5&FA`WtgnVBJRR41mKY@A~xej*kth^#!WkyseIa`Qc~vp z1W7G|6H>~ADK|<0ZYE5*Ul%tM<|#z81fT%QjVW6Mpv20Jo(OJAtc*qI2e_FN${!bp zz|EXcZcxI_1SmH$9d0H-x%Y}2Id!;Vh@9Gnstp^y2r2camskk$Nk@4rO9qmQCBwIG z$&9W1E-tb`?CDX8lo%0^1+6U?WSRjHvms#qXj-ORgUl5TGCkT1Vxelw1!*+|%pgsG z4;J}ordzXMo@9Ql_A~EhzQ3ARRJv)u~AqF{vY1n z1HP(aYagDOQ*u%u{g6g-PI{vRLJ1*2Oemp;8ft)05(q{JDlKdR7)4M)k$|9rf{KEQ zqJpA=QsgQMie9)NiWe0WMHCB)_&saQ&PjsY`@Y}*E8)kp=UHpb%$nJ2X3ySdmzWlR ztD3|!&*8F&*;+Qcu4K#wqwYgz^ybnoit`!ZQzh*vLUmq#!>Z4IrSD#MnX6v+WHl^W5yM}&{Z03b}KZv^y z{LK%D=|Nt-Y~eidi#_coUgrl5={i4jTKl2X+7C*{__|hJ7nmfTc?%V*yM(w4r-2xI zG#2n81^xOTlGi<~&Uv1$cZGPJD>S9JHXy%?K7P`$(WHqm&mAZ|^PBowiN~Zgezg(F zqU#(H-@vQr&1Tu;>1VVLivB6(fs zQF`VJ^%d$ojXRHJbuUZd4Cm@Aq?x>~QItlb-mI(8tdGPqe_2BFkM24WxN2b4xM7^V z?qxBYDe4Z-P9k0x7j(#|tCP|g5{=fZB%9fb^6L7CyY|wVeB%l^Z{6#mIMed_3W?WM zh+;eyQW}k+2-$dZ$qv`Am+pH#Ns0XY#4>G?KVkr=BX4lW7w9Z_8PI zlt@k|JJ(OB&lPyXr7eoSZY4_%Y_y`JE^Z{Cecebv(bh;nG2Tc(W2=#X#t zlP#JxE(0%rxBHy7B|2*#{Y1u^wROIcrTa#fu{W}exsj#oMwW3mvW&lxC7wU~yPxUx zmUL>N`>n-|EL+xF*7qNt$o_kot?MnFo-#8`%aDe>)TZ9DzRZLhm6>=W%aj{grn04f z81{30?Mbz_pOh`leUnTUDRv5rii+BkCuJ++A-d7%Nx;neDZ1+ESB>esI~!^J?rKCI zt)q&|s+fzfVTPLDE{eoe$qJmyqUgZN`^j;;vkI@uvtqN3jy2fgwF|UaLz|GQwb)b^ zp@^ABQSQvo0QPJ42avV$IdXS0shTW>DiQ$IsybB#tA>(lidNyJ<~o;Fy-upbT7{Q8 z+rzj-tqZ&xxtgUr@D&0mOuw*>8Xg@9N^T_9PFRu_5*yrKNQEPfk@+}yw6*CBC z609NzUjgXm+vic6(!K1>xQ&jAT&VChp_vtRC2Ko;oUJ!BO}vxOlR=z!awSSLT14u7 zvkTVV$pujH9@2TdgYj-wDz8~v)4tC9yBnEn+pxCYc*8Z-MV}$zT3STuN=7Zik6+e? zwHUu)x;}!3{u_QFkZ7RmYtuHu^{wh%l?jQ1+o?el04rnk~t?u|g3Ce7|a z7~rEB1}x<5)Z{Si@tHp@!%BcP6|-SK3u`+GQ>;v_sXf$8-{lwqbD-Fqgm2}f_SO>1 za$*(u@!N-ty8g|GB-a^D`>daqxq(|h&@Nv`wf-M<-;K$f7|Q zUqseoG#Ih?1jS;gt^*OsbyZeVP~!erYsY@1R`WU{%>hxBg%sM;iIithjMk}nB;Y{v zbHv29v^ipu+KzQJ!8hwj!F#@05$8(lc!Mn7*-N-As?$-i5h0VPi)@%vOVm7aq0Osl zLme@$w)o-z<*8T%kDVDe!BwWTMybNG)M{W1QkyUa-=G@mn#g9(ip9v5hLHVV*W0xz zhGL^STmP+26=qg>y1y#8L50ZBmV91<$`gWZRn)nGwoYp(d_%a~u_2*K9*9WLk=wtv z&DZ#-8SkHY?VzNnCPSJ=t2+>H^IvOc(>Z6fM;b`n!z1MD3NYWn2hO zoUAw)&ZDiRq9x2LYHC}blQFJ4$=$hX9&NS8S>cA>lI%+hrMqhJNv;_0rDXM%nzyvJ z_MA*M?k0M#s$7O`RTq?L4vu-0wp8Fw6`zXbWP>WN7j<%0u0b0TRMjKo6pkU|sYxv> zCt|a(E6IE;XyH5@Q>+&4OvOD^*i<#|CYH6;9^y@K)R22gH^EYYBLsRY4;Ff9%P%KD zxznlK{Ms4kaiRY{vRz<$8-33n9R8|$AcqRO2N!2#6=oZyC}R4-DQW(B0`A!grZYX-K9tc~)t5H5D(qFAbt*ar?~LkCytipwxdJ|E(TAS=19bj@I)5Q;np&#{ z@zz!~n78&S#DX>TfSJiz#hIK{oXJ`H*>^HRrHa}C-$x{mQ#D> z)J{3IPfqQUQ+wpp4mq_yPVJ6Ud*iH{q(;FRXVql*lsZz)n}TgZ3u|IXnr$BW6mrdS zy_o#FftY+HaSt_b7LqOSS&Pcr)8EQSgUUpAm(s>oRYn`^zNx&mRZZiqy{eqIUR4NC z=p9uz^VYkng10_ZxA4}tY6frps%G-GNmV6pn^sY!s;Pg~t-KAWx{bGiRkw$6BB*LM zZ-cAm@HV7sZtaQ-=>9vXquE8LcG0O_bZQTs+CiuG&#B#WYVVxdIj8o`sa#)?P zJ>kyAPHpVe#!hYP)TX=^xE-@E{h8S+S6Xnjv1%a|S$RK=JVYRp4^VOuCs$Dt!Duy? zj!Wv3=s9w}MsM!ao4aVk?=GuaO3ARQyR~8&HmSorjk}Me)~XfU40uGxhW6NQz_GzH zYaCp}N1|4c5o1e#z16Z2WvOZ&m0)MoALzJEC5Mx<4^XDc2Pyd=C3*TvZK*7XPQLI_ zmtF&{er?@aN|_c9HAt_cbp0&1o@j7u3w_UCU>$&i7>Rt;Jes_H>CVY!ExS5Xv1B2+ z7ti(@D^|lFtafX$@pJGebnrPaM@0zU<>piMFs!i_piIr`a;HT*cujYsQr4i92IV?i zKj@@`?8CKFF3Oag%k%M`rv*ZctHSxfbfgE*npt-9lu9@)p87_JFq}%q4IM1@%#n1v zqloJ{v;Ze6;t2SP+QS!Rl94R&?J$VYE&=C6EY2}-922Nm4950u{D%oIO3@LVhKkQ$ zW?{Pp|J|niR&z`FDUT2S<7|Mw876QysPMyoA!^b{N16-CWk#W0Oiw8T4oUiafWKIV zU;pjm!6ctAwDe5748YfjiJ`SSsG@i7-2)r};t zOe341QcAZ&5q5Eot7*^aeVs{r;U>a8Ju3p>bg!Z^yV#xoWLLY`QbxG1t3=+aWS}7? zCy=Rc3E{wYgfH8E?O+$b$X`47h@Yo^F~TnXkwo%gH?2ysv6IU{_;W4)anBSh7-IU^#(7kPx+dK1oK$!|s(kq3Gbj$~0BvQ3c{xcFrk zV{@qDCBt(%M~kK{9Bm@R=;3ol#ELt!1|jX}LAgHYK;+8Ae{S8N(Sdr--)CRi%``$+w1?2B(E;NkY+MPuO zJ;C^EB&E9+6K0`^MA59x?qLz4Cl~iga|f!KG2GeFN1Pc(*c55ISl>GkA?XuBD7Y_= z_ijEiLTv3d$H4ykf=SyFDs<;aLPrO!vklt50DCJ}at8_*R!ok%<7O45L?KH?M*@qZ`B>N^?K z)%RElr?I3}PfE{eL--6!&N8m$e##y|h2GPL+V_4w4bF$6DUz-sZ0+Jl1e%Z7-{T8Z z_CYto@Hh(9p(DxZE|A#8RVv>Q-MG-Vqp6yAc_^lgqM=yBLvaU>-3lHfxlZc9Z5*(_ z4x!4nL=r}hCfpgzagNw-D%y3X$oIL4$lzLOepL1mf zW95{b&P|2tL`8JUC@LEzpT}4jC2vA(kCHo)wu=K^Ih{wz&ybD~=^bfjWe*{28%Mk5 z_TD+2BSdM}IU^3r{%m87A+2pHVJgS}6TG_@8!(Cxtyvw4eG?&S5opn3tUpERlN@yG z5Ojtd+M6o7SU@({x)aXrMz}SbFgf?9QXlbS3GI%oCOMsBg*$E#Li=lUd;BC5wnOyA#wCP=59T5rErK=q8gT2H<8ZIWUV8Kh2Zp? zN(lsu0_!cLD?sdr<%3}Txs)eutw$Q$MW`6UrdP+c#+(!?ikMvvr(&W7T@VQnKjgL^ z8HQI%Vm$%Y4#kAwsE(_sU_T>Yx=6wY;Hbh=V9_EOH~SO&5~Z|;W3RBI2mGey=Ys8K z_LUvXz_rXhOglFrWqYxoGydhT*+JI*JCN8Iaf;a{wj3uuW7d<|c<}|ZFFD^l@h7uV zu5h0Ci&=Xcl~OH~q+T1$`HX5|WqO>es1|l+uduRO_@K7~L}awi=*R5qUc~MYO_`Oj zZkcGs`Kkty?tamRbr-s8mdVVONbE6@#q9Uq#P%AZ6Vsmt5d9qoPpYs57y6gz%xp0; zAK6`k3P%a8AwN4z|Q#(`QPdlq=+2%u1$_>2~=DvyUc|ZoAybY;z8=o$^^`$NG@vIeCWJ zmlKG6E5DZ27)@Qdl<(v@)_p#O@_i@IGg~=~*zfX3X;AC85ycgXU*T(juw`o&iq&v{ z*i#rVm~_n){qptVuzy&Q{i ze8YVh>zdu9S&_0BQ2L^xFwt3!V&yw1CQRh0G0KAKf5K`nS9DRu%Hj^dkAK9vsuH$* z0>dv?W6$8{F zW|KJIP_=}ZAs%NsMb)tK9%l2@a%Q`jEmZd^AN14jE+LxB#(EChwFoD=g zwUS#;_o3yAT6Mn)MU(oV5zXa#wT9Vb?3j9HjKJ0Rdb$PI-s{=jjWRB1r0eA=hW7nv z=yC;BAw|z&4@O9Fcksi&Ie8m_mt02>E*Gb6Mml)HQ>@(@VYlBAwAqLlws4vId0=S= z!bhCXL;Y+lWtg5vIBLiaNM4L3d8^4(#OYBanU}LOB0~)B{~FSB*m6SOH<7+|&_Uq3 z!XpuGaV_^mMB&0qZ8ihL+7f<*bcSd@<%q}-Q!|bT!!63fjtIBd4wT|x7@@K6 zuy0Vrz_9Ou25WU|ei---w1jSPI+1ccKaz5F>+>Tp6KD~ij{X%msE3HOi2NSp=>8@< zBAnv(A=b#kg?FZJM!IK_H8R5xPY-fLW{Bzp5Yej05s5|UWIteX5Mhr%!hZgQ*El_m zF`FeHGv4k;+NF~Nfx$t9Hw6+F_!Iul=_!mISaOuH(vP&pt`0}Ap1T}P*tCTcqS)e{ z5q5Dgh_F55>_AG7Z9>>}I^lLse;Y~Zww=O}YeUay;75bwfRhKe1oj`C1&ke>9~mef z9z`*+xL^NB+(6WCFz|GQGxH$S|;I+}Sfvz!&fc|5a1K%2S1b3~x6G6D3 zqfr7;pN@nl@*hOzucD}k>H(`EaiS*z#qJh_?*~)$Z#AQRxjbPl9NirMDDWZPeLcGD zfV_EM!ff7&Gx9f~^}1J<h(gC;P}NC}c76krZ%YXear)n!KA1_8huacnr_(NWGe!+{ zM&kU(e=><|g`{3GYA>)=^daD+==Xt7P9p8oEvU+YV<|fvViiHq%)?g=@Jjk{t5uPc}w8Z(^ns z`KrleV0jj$yEA^yMQq_B#HXL7}#v;1&dgNh!`75Cwai@3@Kj8l40p}%yx42(^@ z0@M-kC-3!=F-}N|cz@5~{rx;g$=`!l2W-<86dyVmn@3+m$e$ddjHtRu-afENloUUY zZ4RU&Bo7`b^x)C)zXQXsF53U{PeR@=hLhy#NWvwZ30LrBr2|=y5kDR&-;7Ly{hp$B zz>_1p19K;BjzIjsP&5?eSWz+XTu~V?ZbT*UYEe}b9ivu94qKR*e`nONg>eCdtr)Wz z2QqeN3}>9fcn9M;#%CD+#ki8u5=iBw5E=_#W6>4H!;C?!{*u!#aQb1!PEGG@;Lvgu z(YzgDOUA11l=kIxFHTQmyq9qVu)Z-XVL8W}&{*$9oA(`r2|k3q1mR)G>ocr|WJn<4 z$N<7S{RwSI*W0XvWSl zHBFG|2+czb=zcb;n5jzcRs2W^MjHC>|X6*o`m!V@DdAe{U zWthu2dB84ce{t=DTQmx!c(czTNGQUkusPoc295m)_>Sv1@Nxf7f!}l>Jm2RF;KHk8pqs=~Pwhs9uCF6tKCo%IK_Aw94v z6=}sbpl)#_uLQf5_&`FfN8SQ|q&xBaQOxNTjN7_|K(0%>E1uly8cDqp9~ln0@yIX| z$!0{p?Me89lW=$jVN^~uaOYswP9e*{9Kt(`sEBE!Nxq5NA$BktrIgm4Ox^oG_O9_? zySLQ{+BHpssk^+Q2%kwH?2$*9&aQ0lLFp%^5%gL7}iCO5~i|E3ne zr6b!wqGwp$AAQOV^7rZ#YR#vUDV-BbZR#7F1B~iK=`)=u{d#jsztf!3GfFAFvy{@h zphaV8CmxzYr~^$0-Nu0=S=EcMrkwC2wtOm;YFwEaPVcXCPb0+@7RCEhI*+q`Iq8?E zKykeDP0a(v<=CFUUnBZ9Z!0g^3xRR=VZaxBDgA(z@Kdi*z_Z?j6YYdQvplcqIHdPC zodits+Z@qW{^pp9bc7t$+-NKBaggaK`B7wBx!F$Yk|q_9eCR`kPWL74Vjt2jX-bki z{7B*@3gP8!*OWWO$;!Turvg;OIi(egmRk2OS0;k2ngvKawWnZo^1& z&nQ|Yq@~j2+MRI+*2K6(-Qi2gn#%^#fOUtSF|KNGl=kcUg7K=L+NX+sLF80M;IqEe#qEk)Ol1; zit`1mC2?M07f1aleU9?2*8i9|c}cX1aX#ZWO{ki1#+SK>p$?Mt^d)T0 zIF#j4jIVGX1T`h?Fh#hEaUr{VoO_E-NZ>k7NbGz%`Gf=uNIo^YhgU&2hmh8V8MAIhlrotX-3N!#GAo5~X|wOq6M8xSTKAuj zzQ8lVpdOU#aOqB3|7R*p0QFRvzvHGaCi(SJl(F1YeLEuwsLU@ zJLdNn{$i_U-hVYq5`kiu$;M1>4|agrS8_}UuI}PWFjeuDjPHq`B*ht%Jvrip3=$Vj zHax#ebg;N)vfI*5$WY-I%lW*G4wLrdL@?Z>3FS#(QOwTB1N{p;>?`?Y|L)OF(bCj) z3KY|+MJ5|OrC5ZEDJC13HVMBS-)gdaqZreyS^XLB zV`)japK!5OdZ%Si1zT>ie%Zyi9KDX&SMuf-rB0XFVzTln{i5CY1&G@u$DqtO@eZ?U zk&pQxUQCJ8?aj|VEfd6YlTFSx`b8&-bxdnSSn1&CWbu^AwoV%j_7bxLVs}QNOcw8H z9d_Dy=)Ts>@vqz|(JA7h$-c}j2m8%rS*}8vD&PVBY8<^>0U}j+o2+u`enX^*K$9+> zietQpFxl7RW=5xrSd+CMb~{)qvn8m0wMZB3J#2n-OOfwki=%Nx0oTK*!uhdzau{Lq|s_d?v7ms7KV2m(WuxmYHf4}Lwv}zM%*6NF}f3J@VC#l zBe7$2wz$l?#o}48&f-sIdROEK8!kqon6RV)34u8xiW&6`=224M?|_Zm_OK?@dJnVx zF=jJ#Mm%R+ExJcPAKg{7G1(tuc13p+*}4kx$C!Q5xndBr1KbyRVuEH4Dl|`&nK~*o zPpo2A!_IUU>l?969)>!_4#Pu_ME4Z?nNcggmA%F1TIZk;>MPDMJJ4-+(6?apXAx9I zq{1a)W(VZ$QI!f0dzfry)QM=^PGPdrsIQ}k2@7Hoalx()7k;U@{;TQt(ZhwIm5z>m ze~T^>cQRY-=-2mquob3mbD@kGA=WV4C*I2T1$&R#5;4IQ7&B5h(x}XR!rr-A%qS6M zvW?jiKj2I>*|qFuF=NH-H2(doTD+H49z9O9PG@Jtn2_k0@uI(n#m7t#MIM$GQzA+{ ztV7Hsaf^rL$CQfQ9yTbZTpaYUu`xG`V;(jwW`=OJ;J0tqpu#yZw~91S^aa?RF}I6s z4_h5GSM>0(%`x-EFb{hv#t@av_Q_YvcE>Cf^G)5N_`P7WTT+?T!YlHFm}+r~*-8=F z?4L1jRP2+B;(v-+E;h9$RxMnqzsKAwHf3lQl_gy(MCUe| zeU~EfP;$?<#8zVbc)M1Ma%MGR&B$QaL!w(d(k&MDya?AiafDg5h|Y_5JtDf%Jy^o1 z7HeZ$xgHmXI}lwd9+=t*4+vi{+53sP^hk0?ty}Ku<9bFMH`&;{*Sww;uj8(M%6IdS zL9XY-N1E}Vd_i1dR^xaerck~hu9z&l2QkTGy~c3ljGP($g7DVVQ802mSdhui=Z$f_ zC?ZUDDQ_xRoXHN2n~gUKr7>IVI6kflEK}0yTZd$GyGYTOsa@~%{U zwOAH%kNXGl8?zembKYXS!J;IWbc@BM@CV$+k772{YVjQCPvV@()`4BdoiMtX5AmDU z&*FAwdh+~5EN6BA18=eG7x7-cwmduV5%;fRd3Vj;9{4QSz#haFi?_n@BbGSJY^8WK z`Yrb#h8WnBl&eKj&O!IpM#r(g#E@R3)5o~K#2#j=#Z%osa{nbnL47_UC-OmHrTDA$ zakoV_JQ`Z%M_R{&(kiV3>hgW&wo0R+sh12WBxQ}GN55~~Ub2_TCV)BQLX&;g_eZz4 z+-b63!F=QoCVQpd6}PXXSNKpVN5TB$IL#2w0iuZ%qe)jSeux&aP33NbX$@MB(*b#W zoK^+|-DdZf7fhBAd~8!WGyOtpxQb;?^NXv^=?(_)*+ zvnG48Lz~zzIlqK-t3}(vS+U{Lffu{s5AO)+Qelsjy(SW?5vOnhfzwp{EfgzVox0}5 zM#)vI)4L*Go@GWmZCGr4qwY?YTPIUaZHc=prx4SY$&D=2B|f@N^=s2~iBGB%!}(rG zY`V-W*X;QLC9y5!ZzkJ2rX;qdTtA(3ROn~!Rx;{VZJF4&G`5YrU^3_U$d8eAY?F-96+UlVyxK?(QkinataL+}%s2-2qDj6_#~8 z?kp1Gs*uiq&-I@&%!Lw_Z1)k!}($@{vxL zGD(K4th4;gJ*kmpscdq8y{@!QN9TXI;ZC|d%Kfp^fD~$NO!CJb2aH`y81HqR+&~?$1cR)CP$depz3GKMVdN#wq4|yEti>W zOD?fBnqemYC3d#_(_|?fuEoxiP4KiSjhG4JaX+hUZn9r-BWjiGY_hMIbu(GePbFshV@z(>}#}-yGw30>9jG~aZBZo9)=%Z02#ftz5i zc&J^hl-5Ud6?yHl<5tRe50l~n+0tas<}7lolKoBg;ZR~Fn(^LVCD(h{h1dt>anq7! z*;<+TIMuLPRJvaCGHM$I%^Df-BUg`NddIDipKInghEtG-Wav+Hx8?T2MAJgJO)A`xyTZ~ zM|SR@*}kbw;@^@3IuctVmRdvN-TgSgAOIf!>49{;5=IX}zcvKvg$;1p)zpFSW{;+JVm5xKjd0^IDHg%kj?;ZcX z^fTG7@dLq*Fgw7P9zKw>^2qW4daZT*2aT%uKzikqZnfAGKODMzW;Kphd4=+b%)@zOEu%_kI35!NLOdMoSAm!h=?UBURLjOtTYVQKtvIh$FvqkHMh_>=Oo)`|47 zxXLeE4WOjT#%WW|`{)`MWbz9rb zj{mpZWU{}&zLfDpb-oi|U(0({}T&T(gG_Wc*If zFzE%bi*ljKCWOw8za)>DY#G>3a%+*!c&+_?@juJx5t{9Q+rP?0X8S}il9|MJsf0oNBg2v$=i#j89PA zOm;_)KjV|sLX#c9Zc0(J?$DNn*rRFcxXG4)wNU3w76jHx<=1G-WUvgi!DMfNwKddV zOl!ogA@n$r<4$eb4a#;Zz+}&Wbx>g@qj=6zE|XCtWUEysqj1hqcipW^IiF!m$WgPF zY4(Rd?uu379%413bB87g-ITG0=@PN7UueQjYQM?$_l-%&S3jC8xnD{`52e?0)ndfx zwh6tI>t0=G&t};Pz18z8b)mPSioR;r{lr#^ol{SXe(E=7H5in62?NxQ2ef7S*ibQ0 zbu`xKVmg-EMWHHvShIaNIu)wf8#KFw;Z&&3J*G3Zgx`f~=aX6|LnevA%I_)7ex5i< z3{gcU>p6Oo7^=>0(Yj1HI!sxg(QMTe+`6Nz+sqIeib9J#Eqnki9H~Zc)21_!akRQ( zvNyT}h_N2arCo5Nma;ypEgxcbkI6DHdlaj6Ci@YgH(qUGwpu(sxL-nv`u$lx{^@w0 zq@teF#ZWv?Ruk}opSp>4vRcCIEBQ|Blg`QNqSiTnz=5b#{bsT=Ir~MK!X-q?=a|rI zctV--Hd#@xu?bUEpk{mknyNCvaQs^>5~fasGN*xZrs}I1M)TZ+nQGQ|+OA2yTGTQy~L+(Y!;@Ae zEL9gw_V|?ikh@isMVIpOlvN3LtG*`tx_N%cGBw|1GI~|QGPPAR!_jwae#mll%2ZAn zyDDM1^0VqTI%Cp}R+wo2)!_f7rchy~*xQ{W#=a^`6Q8n3^AQpSo(Yrqfm> z+^5oQy39kV#lb7oXpg}qiKtW+0FHmp29s?WVi_kbh1;E9A<<>#Z>sn~6nS`}=vbFo{Yi!|B%;1?6tsP@cO zI}T3R1(w6C262%j)~Ep{TRhMR1)X5hubU@{wQ82hWOOLlohEyHN|Ja;tu@)pQ$oSE znyl~GB(YAtVX`Th+19C}CM)lig!{t(ZL)>ELcxA9*@iZShJ08#=m7wX*lleB@CdHc zWW(@N>?11KWH*tSM(q-PAihGOH9VYu@YLeKf zYE9O3TBtBKs;5nQD78>Nrgod`Olp95O#Q=TcK;;txH@IBQ2$V{izXXflq5DOd})Il zF})}hEW~6#cexh4StXjx(X~8av&u5rzVsFWPZ+9?Nk2-j40u9~HQ7s(lEjnh7Ly&G z6biQ3WR;1(1wW-8G}+?B60oOCc588x*rHxF*^1&&u)~^hJUy*GW~MLLJgtliOdk+a z+PstJ}ubIrl zRgrq#vlH8cZ8CL>@N4>3^^;*z`l)@Z`hyt_5(yR-Kvn4Kx2Yi}qw2S*{Gj^kx2bW= zboJXBRbQ{8>bI#WM3Zd%t9(}vfMDw<4%DTn4`2MqOtlOsop+o(P@gUhZ3Husl z+^-VMjHKJ&NOwSWGj-?1J?;Z)1T$Uz!A6z`)g;q$7Je;1NM3q=v^}JmA?TbZCE;CD zN#{J)#KRs|8Woawv{A+nJ-XYYViJyd*vzQN#7{ho^8Kq(zLOr^?NOl#pPI~|3eyrl z^C&6fztueUThG>K)w)LPWe>|vJgfG5m|c9Sj(J$O#4pt)59^tDP6>LT$~;p%uYxtB z^S{A~=T)x;%8P2ghmA?RsG^+p8Kq^HW{%9(NkUqVm`t`J_O;2@vF@75N@D}#q$L1P zaOvvjBudL3lkxS>P)kWzo$`UiP|FGr!vpn}-Hlk+@H)%&i4m3}W?#vc`9npNv79hjp;v%#S-vz`8%Ka}TP`uv zGeoRKFUj=`5of`}|7hVENi#&8rB*YZlHx24r=)t_vlET>Fr~LOJc!?|74F#kZ|UvO zD&gOjqBtXrEbk5`?1$etlz2araJ|HeN3iM&Qf5zFa#0CE z5!4f(j3lk@p@tZereJe3eu%<7q0P$R03qjW=~zapv7;V2q+|9a9E@$pS0 zN$*bD_sgzNXO1IzDqNA`N8S;-sr6}#C~t@P&nn1Niec>0guay4rn)od@E*N!_bPMu zX`AER44u}!b+Rj!*0ATUlNjhI64iC)|1#VEL9Xj6HcPAjNer;MajeGzq2i(~X3@H>4e^pI01Fg6@rQD#0d+lkHJs$f zV*&y!=6KO#;fH}#<|u>Xv!O@_V8d{$AJcW=b~=#?)t09dsi0nne~Ym94+yZDa-FN8 zi#G~SgFO8&@rgqJKMha3c-5@_#_?Gfo*$#T>V~BbRSHi8Q3E{7bAZZL>f{YO_CKXx#wGR1?Fh;1cu@T<2JAH#Rt-^m5-g>!hoG@U0gOg=U-7lq=M z>|yl6q7R{Uh{4b{Zor!fWUrfTG;DE0#ic=%?H$}KA;qyV*B7BPQ{O0&fun}x&iwDF z#N!yK+D3`^rsVqLMg!5^7-kmH7rVnMj-k_RH_G6#|DP(moK2;D0j#U?!05VkokULN z_}A?i(26nyS*dz*#U`eApw3(btjnNXTpvX?TYxsv1@?A9udZ&~$-0Lc_U;WO6#xID zQ6rm%_6^f@fw>A(EFQT4D)YFin-?&Fnn#NiJo1k;4#AjhG5=c-EXa^jkGrwIl_{Y# z9u@jOl2FNY<@~*t{a6uU{?`=~(Ox(9{hxer>V#E-60>-%aD_+5-&?7Bu_b0h3kRcB z*mJ1N51UZl?*mDG7Y;cR&z6ydmys0H0#*?Hj<`DmNfzxooRE|TWYP2rWMN=y+qCWyhib5X!gSks3FIIKC8_NI3 zG{yh_X!QT0&9w}2eKOC9)N?rhZ#9|Pr1y<>B?CusiQ3b_7tN0UM z*Eyt}&~4hshnc^-*sBed@WtP9>82XGi2px#L8H2i20_$Np%0x6CHM0j@Edopo)2`I zW(6g5x}kl;bS?Toio+P#N_659ei$1NAjQmJp8p$~diNpIr+BYxY^eR;q>u746#orH4VMzV+R!TRS~M*k|1pwQcFnp31lYvpya2q{>v>4L!~)nj#Lx(G`Y(TK zA5CHw(Wiv$b-VwQDHK2b|M~lf4BL6 zE@^cB`}gAXdBNix4H_Gs-#1)0(=1};wQ}8pmMkqiyJ)$s(fZ%aYp{`C`|7iRhJ`ki z>vgTU_T@=euXi=-ybU9%;dI_`jMS%Zc>epp%SLS_%eu4XhPfIJ)cQ14c6~0qM4#fn zV=Be%ARs2umQ?6{9S9dQKo2jGZ~!DbP^p$Mz^)Q>9r5eSEHr1ui>IK^~`2tC7`cZ0rV5s z;6xLV0&FVYM+yFhnBkzxM*31^ZGb_d5{h7v1q>1YghP0Kt0`4{EBe49UgVxwg9VvQ z{LXQp56u*Sb!W=@toc9TkC<5$i|76aMbs~%Tp)ee;JxfSM39VcyJQvgKt35h4c)@EXG{M0>(nd z5rl>qk4-2Fp$sz^=P<4W;x&grx7f?+L%6*I4|EE;$QL3GPs~7??!G*VbbCBX_htWX z;smfjoMHR(zyNUxcOdl#M&mxOOJY7AB$Rl75MRBJPvXfvyoBrK7DUSNsGgL!;SoKk zi}6^v{0>j#Ntur)@uZxLC-6o{_kb$grAUPqTFJQ)Ks*}B>7~H+Vk4)w0P*50#(Ytn znk4eYk(3Oed%!OAAi2KF+OzF`vCFWYEIbatg^tj^?@#KgffWz~D0p6BIX?mJ*2wHy)cykMXIYg9C2?bJP&af*~O8_GA z_NXYi3-!i9YvgCAU^6+JOx*)o0k=-eg8FDqS9zY@+GTxsLT_0ujL7|BuazDK+-1GF zc&NOBYDNK9xZ8`3;+f`Ea-%rw#;KR7*tH5!q@v zBq@;4gIk&6vGRUu6?bx`xR#x$sGo4A0v9XQXx^8jIV6{`)9ay)RGWYs)K=gdY8J~g zd6(_7+NPg{gm%*o%4=Xl@w`;_ZR2f8u_ca9!t38`qXH}$w)||TCChdsMR$0zg}S{J z@Zkxafq^KfnxD;F!jZq*3@i751(sd%{loyVOBTEF6fZCU<-a;;v!%dBqhp5E8n?|t zyYfYgvCEn@{RrNoM{UZI{t<6m2>)R@YF!+0!m^NcUdJ38$bEZG{Pe(-&w7Ex&JCx{~q^2h-u>A|K$~~*F-h{b?cRzCJ%gQv2j^|=LjH;L%TcH8;b_cR;EIUVkl#`A zt=+BOh`a>68!w@T*JDZwWrm!9$u*x_A1}R!&GYJu$DUVu4MCTFXU&I0n0&ay(^)&3 zwG&x;!j{_QHLpzh)wCZ(CT7eiInlJFNolkkp1;Sd93@ApE7+k&y=HMOQB;oMHJ=Mw z!UfIZYL;;3j+|ce8vq%lC15AG^K}M~YCnkL5d9vV$eNIlY_H`GO+jJ-E0_yk{Ux?)N^%MNkEM zyb1r|eTrLuinE>K;*LRr_f1&cQ``F-WBFz7@XIK)Qia>Ia_;aUyxS)i`};m0spuJo zE26OcL8MbUYyd7x*^k@FsGLHTpZ%)O2!;6n5*s2{xyq~jJbj?;`{35T<6&B1mu9C? z&>gakjPBrj$aYtLF0|BVDfVgpy?j&b`!o9cUWHqgYCQL!6?yRn18Zc&D999r-zn#8kxJlo?H znQ$>h{)GFrqo^)JM8S*wKyso?1x|M%+ecj~^Pn6`=P?#eNJaYV9==Vk^8U``Ld(s* zAy-!M?wiPxS&Ty%H?e#^r2KkXL0k^v*jkPaX#BDH+ZvzaU$a;6oCV%q?vyR zOSZCPz9Pwd-j_RgU+&<&xSO?jH72ynS-X$5`&j!PYnQNg32TqB_84n-S&esM1N^V> z5;2O^r&xW;RDX=+#%1onY7~U`=vbzgVP&VD>T{d_b~RJ!{;S&jKI6mK1+URleyk#P&-cE(+ddl@&X994|c{LAdP4@=l7eM{d-MnJg5 zJz$`9spVXka){Q)1c>v+y zL(yFU)##gh12(EJ@Z`!;i>t$f0bA7O9-*Sp9vihjV7uxwE);$6MOVUXPR~P`#&-2p z*`|QCT=aJJefwu%S`a!nU>EO{UFyD^7a*C{br;YTvj>&UO%KKXYlnW?s{#`b2JBTE zL*@oltK)g^2OL5<9|s(@?;ddySR45Tq}^WD2)tz6tDt$P7;kxh zcy?f}*Mg|rzydE{boF@4@!>rKcUi|p^n?6V^!9`LV!q$F$9X<1JNb&jgP5>gRqfaE{mc%urEaxfk*R%f7_Tfz@8_0ed0; ztK&PU^5L8hfNiFJfLw16KZYvFtt+Y@9<#W>HD16}X8Zs`v3-GOv9t(q3dR^{uZ_rw=UplS|s`gs#ekdr{D=~Ou&=#+&!{L^ftLRBc zR=c+a6?mmi-2pt^eFt&{6zvQeZ&^2aZ_qBvYXqIJj!78hYvp};8B)k8 zp2xS)vC(RwF>STbUbI^1NHN|*?I^JP7W5g4+Z}N(Xs_4Hy?zTi%B?K4?9cp0;98;X#EhmM4O0f&(2DJ&p)( z$5#_i$Sszu#mj@e9sdYg8@!zzU235P_;yQ90m9JnRWxoGa?BpJB{;)DXVxLymu|0l#wE90@+8M>bB5J`G-KNtlZJ?=j@Qg_iEUtX3=2 zTENkhxVf`h#TQyb_F71?*FusRmhW)q>I}WcR z&9L!f%qrl8yXlg-c*ci78WwYQXD)<9I|{^(jF*Ea9ptbJfXd)w)`svFAd^2 zD02jl>liZLc4S%?;O|lS!1oiM#`)7QG-F5w2z=rVH8N!445X-AATRTg*z7x1z4I42lbg6>^d8q@3F$2$%l6U0$!O-QrYU|AL zpKqbVXg*h;&lPv(v9OZ+=?q-_GL+hPF?20U&O`D`=y^-eDU-x`%dzeshn%;>;1W(0 zmvF{Hv)&9#YzFP1b)$>LB}@3!1gCMy@?~+F^Mv|w_#y{6aS?^SpP1>a7AdiJ-~f6C z$LXb(R=7PnoCnoX3mr_ud1x)Qw3$F-r)7Sra}M`eHRDFdGZSd75QF2$7C1NExzrLh zX|A)tQRT+m;0POTwz14}KAS4%C+aA>JkK2r%_&b`Zdl-pOW zPGsDNf==TQa)KpGEwg*9hNQgDYUf7Bg9@u-D5h}dBBhu_5(A!eJAS}?k(zPTk+`F*L z-C-%-bZUJF?U*Doyu(UYgl!bROxqY%hKipArjC0q%-cIZ8`rVCCujdISL)99jt_e! zEZ4C-cz@U|^kO$$)>|C$5nn^W3xIhonT1o&{YVdLi<>|)v@QtWy?)0--Z3%HJr{5?RWx)a4TEx;Ph_Bd^0Z%y!AZb zQ4SV-h6C^CS`hBQXDb#1)5TrDiQ*n$skk3lE^2}M#QN|cEJYUp&jXeCPHjZmZ6Qep z&?fG(ZUip3J`P-A-3(l1eG<6Fx&^r2`V4S`bsKP#^*P{E*6raoLu|Fa2=cu3CEyP0 zE5M!BUBKPe*MNJhuLJj4-vqv6-3xrr`Zn;0bwBW!^&s%L^$_rs^ zGLtPkvQ1~!cC-Bxmc3Zpm$d^q?+_bRS;X4WwiDFHHtO|K!$uumZaWFQ#r8SyEGy30 zXm?y-?GLQI%-Y{_6>GHt~c$1ZQfMScE*<&cQL*W3=(@8_cMOXIMioBcuV{$ zQIgaWKQ$QT*hGqDz&NoAm?ky=+u{pJEk(BY0*IG-0sG){dM)u;G?I^EED>EGnI=ks zmEzXqd@&C(G!8eFznNSjJ`xY5?6oX|n^TY4LQZDGk4mTci%hgfomC5QZsN?DeAo<-+bbRI^PsoqW4ttRYN z6RIqQB`GXPfuuS$uL--w@&c9@K;A8NVF1ZDFz#XeoKXal+{M_9aa`as1ESBFh#*q$ zaT0zWMkvBL17kPFaf}NY_uSM6eYUXIyRh8AC<-X;V(i8^j&UL52F5*%uHIzX4QL=@ znJi@7!076`SPV}QgSj}yaf}NYH!$MM4Dw<)eL_Wu68!pxA007oN`bq81Wv?6H*6y3iFcHDXYV5KiQY53=XgKn{fzf>-o`QSi{4kg zojyrEoqcZdDex)sncy?oXRgmvKHGd=@OjhcU7vsYeCzYO&p6*bzF+!|^4sK>(PUzi zTbnFt@<5ZHn?yD3()2=8Uw@~6lz*&$mVd5)Pyhb@BmKwuPxdeOukv5%|GU2ka0O%q zbPFg9_$y#=;J*TWf{q7W3+fv@HTZ$xM+XKU3jQSc%ite_V?q)_`h^S+`BzAKXur_8 zp?`)3Iy*ZHofDi7JGVLG!m`6k!}f$73OgQlI_&GP!tmkYq3BM5FH6uv`62!K0)OZ? zu=cZ2D#dV#1k7hi_?LNyQ; zI^NP$fnPN%QS{MfkKyP4c9Wj~rc8dCu$&~%r4csCeioS4`9)yXP4wEwzOL7S4^4gA zz~<_>y}(Yx4gfdBl45)w;W78Szy*`2xLG+=(9j`AfbWkY)HWJ*afN+9hI~sQ-RygT zE9ljO%K5(v`xf{%sw?lAJCbH3Te3BYiB0T8F(d@?8oi}7FKIMN0(RoW;SmT;jIAUh ze&Gj6fUG3jlt&X9T42JDq%~iGHWUhMp`uHmW?Krh;n%Rh0;SmxzLu8I%}3c^)8Ep^ z?)N`;B-u&nezNA=bI-kV?z!ild+uZI$OBgs|1!2JjCJe*!!{`}uh|+F~WY zGmo-?iib}xtQ7omO$g70vq4=i*7Uq6?iMVZ4XU*Pb`pd0jI4*NMpZvlEPfp0rq=MHiB~mYkcXRj(heshJ9>DX0$2qiW{MuG8iuv<}`0dw{CW2gN zQ?;^oC+6b9AC{B;o?LSQ@S|-P1761A!WzPJr!y41zJs8B7Qs8eNbo%lpSWZZ@QZg5 zMOc6pfpB;Y;dgZtd@4Yc=Cxr!?*(4~%umCkOyT5ps{xO;QI+ho2)@39;GJJ2cz6xL zrvg;5yK>x2-s1^RT*3`!7bOb|D0yKG;Wu1GuzF=Hn7+NR9q@F9DCWG)!0*{gxZFkg zjV?04sd!AIRQnQw-{5$6GxqQ*d6qmfPZ3_mxq({Y6+nh3^b(FHyozHBwZcyU8J^x_ z)+783hZkyv;c><_5o(a8vw7*iGv7s ziNk>1;%>kmaW7!6cr)ODxDPiE0^&F*=FRw#Ye1aI5b{ zvsI!ATwo?}k=+lR0pdjn_5g4jAYO!EM}Rv3nIN-o09OE^efA)57a+rby!s~aYCtBq z*~7p+fOxfteT(W;%f5w(mmLMJ0y3eVeFw0CJr2tGfJ~Ulo&aoQPXRWu?}0uGkO{Nd zGr)a-Oqj!pz?%V?Z~^-P@CyN%Fqb_Cd>$afKY@K7_}B8qK%CQIuL2JN!fx2lfJXpfH|#awF+e87+0TJ5 z1!VYVV6OvT28fgG>609hJ8(eM@`s{nEQi@gnaDSH=i1A7m!ot*^y68qnPx3LcZ zZ)d*(yn}rR_yGGY;1PDpeET}KK9?5mw9SQ2-*3AJ@SyEtz^~dC0N!I;1bClq3E&}{ zA3pyua;_G>j-0E7|Am~Zu|in@_y}^Y#yVvQ;G@Xdf`thFca#vY&jlnGdc6?AkMUkF z#Ow#*)9g=dxAgClWUIF=u%&F< zZBN*qxBaW_*S2Z)fMb~>?Pzn{;TVu_laI*XlAn@ak$)q9B7Y`JO0`m_%u+5={;W)M z2A%tz_d9D`8(f#W`dzIbTqy4Sil zy03B9c)sa*-1B|U4?VAVUibXM^DB>5bI7|>?N|4xFRLfiC#Ef){@v-vrvGgEYjyux z_q!QqX8iXIXZ^JLf2luRFEvy(T-dOvA=a>_AtgQ|E?tRcx=@A%w*h)sKVS{Z1J<&G zfL?Y2FsHKGxw0^gwE<3N+W_lW9`HPN0&oU<2e6)9HBW|p9|Am|y#qLty${&PJ_2lF zJ1>%9-+KUOvu6Q)Yyxl&I|10tYUj(aZ(AO|RgmG#a59X4uE!yG8-~Hkr?WxC(PcWg zHTmgmu>Kh(9nzZo+;XXjIJ!uDL+4eS4wws-SX8ZW=QLBT;rcxza;Ebt$}5QZBz zavF{qQBvviMvRcN3qH|YPNkglacu2bgoOWegCO(+A$%+7^Km$ga2@#j7LI>=jtKob zYfAVvZb+kxtcDnCP~)|@F2!{jt`=OaxYprXkE;!%!Umk^+9=dv%}4(xvktSQdf^IO zSJDrhx3kx#|Eax2ByV^upQS9Tsv`P zab3?R$aLK8N4rSK>B6gc8$rPI%cD|wPbs{wlzv|+{p}L{-BS2oDgC`t`bmnD{!f+& zSBe(H&86@f(JJs7(JJuTQv6!Pk7A^HNBA21C0+&p1+K4%hwNVwAH{Xd?q=`HZoK;Y zJ4Z$w5dN!vK$zV?f(k8tJNz2{Jx4Sh)3jhJ7z}7Dqee6k)x$}DI2jAY4Ba1&>FGpV zOD6sCSU45*`x9YJGg1Lf562=gJrV+kjfqxcV`7uBHEe!i2YWdejUZi%#Z#$(uEmq# zWW*nh`xEJ~ZbZXjBkB(%BB8ipL}Gz-I-X2Lf)U-Hh(`5bBpTFozYt9Y;<1=NnbwU| zA`(eO(_ud(2y3B8LXQUg!ALloO6y^N!U)HcNh1$#^UfNGAeD%7_L0dMHFS(qpM~ z%7`R_x*mzeB7smcmM|~e#XiYt!L+Vve%um^rlRpw$Oy$_(Qq=5O6c)eFs(&HM%oC3 z)3JoErSy~*3WsBXBs3V)wV)Oh0>MZi9!exrv1BR`*5a{rFp&yJBB4||5e?|zr|Icv zBp3}u^l&5^OB$(UBAy5b4L~gy4hm?ZkAjLQ;X{nf6xeP zktkH4#}ct{GM-96Ct;XMG?nrj$y7KHip1k7Xes3nY8nc{2Px*!ZgxX18i$b@aSdtF zpg-gfq=Eq>hSo}G5j_}#d4@v>3?muV!8D?$V!8L22veE z9UC({vV%jJflSxhq2W!Nf?**P4Mn2yC|V$v48gSU4XsF{J^X=S9O9<}K{Q7g!bc)T zBBlirp#&@srXSEkaeq84EX(wEtq5=33M|V*^JD14+}6Vq(_1^Jv~2H=*33{x7X%1s zG4mTe?BZ1ce>xe_{2?@PQVXIV!c3F8KkA1uN7C`M5sYXtJs5)K(G@`tB;x5%I2hJ+ z16?_4zTCsM=8R}85ZA&%7?}neOv9uT;Z(}-`y+v1G7|Lbi69xT-=8$la5`EsZs@^q z*q=(kW5k5^L~C1%)%!y+A(0NC+rnI7nlU{VhY=z#bOJe(et#_Dk7%%REd(*bk$_Q? z(9;?UgY4;OI+!*Cy=+SkwrM2%x<3FjNJm4_K*|V5^+XuWln6v3fdCpEJ=1_$pc87b zP&AQ>8CudvB!j6`EG{ft5f`+u-ycoI^@N^+2ZAi=5L_5KSR@@w!(OAIU>XJ=g97xp z5i#PaKmtx69!%R)hL%p72Yc}oW+A^3PZ$XREe4guL+J47fS!yJW4I}3E)ocbLK=Ks zC?1T&L!mU>S|AdHY|&_<+*MlJ5<)6F*qO=>WIBiX2KKfZn-W4gs2PD!2ppj;BMP4u zi$3d74J68=C4o;Uti?Ng2;nfCgG9-x*iO|nWa;3m*G%2nt)HzgIXe?1w!zxej^ z7zUaK&IWb}DfB3Yk5I_)N0Wg_SVyD#G5A1?Aeg3-!8mC+6^BT%h#u9nWFjpDQ@S2A z!Uh~|AYw!#(NGwTkbn#r1q^fmbl+ew1P=ozfe{LBH-+w}N8)ga3B7nrKQqOK);42x zU@I!NHF0@r?RtUYA&M8P2iSD6VPmGFt4(hgC|sXvFYC2!b#c}pJ1y329qi6_X4Ar#DSBBXJsd{U6aBXjPDBIDUy~%uX4?EA)9%9wS|K7v0 z;zfP^Ta($L-PyrRqPI&}of&G$_GS#AS9A%5!Cl$@HG#+;qi49gV<_9#Yx?eFSI?|?9hhu? zcgJ46ue)#HioWjQo=kD*PS(S!AcelOqqjHHJy`tZes+mCW9#28zeO!48NrqKU4qY;n_r>=S0cv@hE`RMa2BzbCFnN=d)#2Z}#= zgso(9GP`4K?@*>F9%a8~^i1QHz-Uhpg8Xz*^lt6p9y_Z&A<5`d)l#;wA*if`ZSwvKJqmC zks10&R_7?G@w#X5pQ&pgQ(29~v6v|`Ka=|gIx}5*NAa#7u*H&`Y~Pp}gkcnqu?|sQ z(>37N+PckYFR&|uJ2FFCTRXD7S7v*=`gX6`*q*&919R-@7!q*D=Oz`mzrgxf16TR> zq4u5G?L(yhjoBSLdCYucg3UP(l(jIKY-g6I8hbci{ErD1;BBzBw{xI4>z~;@iW6B| zhr5Te{lzDKivRpvEdGp*ROP(*U)R{D^kI(PfoXFuHXqBB7SM&7a;zP*bxipy@fOV7 z+we+>0jdu?EH3<>x^rnfr`(PSIo@V*x8gnnYlTJFFRc-$L<53YjK9skDp$MLVBd5j z_Ej}uLGjgpV}E41@?Ab5AS}i|(GeOhhqSwJ^$EKHsRkC`$r32<)EHjVoO$kDEvWZi zEFcC@mr2}*H4Oa;-V)7+3>HzSIXjymrSS&My5g}9SZ%QQhw0Ko%zR{q6vAF}rJ{j~ zio_|@&?L3^iy6}YWwU1cge4$eiaJqS3;}NkB)y#!AKI@&=*Rt8@ayGy_d!(bKUev%ckRP&f=>{ZOuZwGoC8=^Ww5OJ{QI#l zRlH!PG=KTRMP-w@k_+3#8)6NbtsU{3z`|nI10jaYwO2_wEV~~6)0OLf{`kb2?_P7o z{_oz&N*eVYRqE9I^UR~xml4hGW1`37@QI>#Bw^mXS?X}Uj%Bu^&YOST{BW}r z|Dq&|3euZAO_JmjJa&P3JRWDQ$dsm8_@7piOZ2cNk6X5@8|ZJApyuyov#>%H)eYW} zy*8hqZjiCnMu~Qx&_pp&9Vw_AY(BBkyya@Cd9_2fDT*XHkWg@XY}lUjAdv%)B9n+< z>O`pKAN1xQMw-XNC2y2bqXVMK!xzn8UoBlC9x!XJk!Ehcn@tmN6sb-fxtnTSE8BQh zk3v<${lu1X$TCw4F}0pJy(OyEJibh=x4A^rqE0Q$M_BEXa0|}?<+xD4S)vnZ3(bSq zNEftdbt+b+9tFyQc4}mam!US+sRpzomtqc-lz*3$O#yVFj@(a?k$XKZ*#WIWOz(6P zM>l58f6yUIYP|<~RTK&(?{trM`uQ>n-7Kp0XhAi^+i2YuY2k90Z1)Zt0Ifz53NO{1WOiIaScm9w~z}gXb;6{uY$e{Yw*qCs-lK^$@*5;iqb5hmip5y!R|sYSi|$S zs0I3RN(-m7sRjCSN?V`oYf|%n#g_t9?^@V90kt3!rFBYu*2k3DD@VYws++98UaT4smD9=mUrY`@5o8m9x5i1-RJVB zDafC4kVDA-)y}0RQbCmAYe4e2e2jd0=j;N=D7 z@289-XLw}f43B&U7lL$;nk%?(l@p&!yfQ2%e~KPUCAOoK|Ir3r4mP7Dv{` z@4=|zvgoj3%S7ppYPdx7C^#s5W!ctbR}?sHNwJXX$^NFeE|h1P=U*o^=W6ke3Uj$! zYN6lbtahT;Sv4s1D}qFx(9>v_B@c6!=|ktq7-3{5x-=YZVW(Q?!k2M{Vq7U~nMp%H zHxTOGlVu{JDAc!|%uXpvwd`^!IhPB#9UWb^gDIq!9S{XBf(#rk^jZ<_jv(}iCKk$K zY$Fd{=(pSL@EK0jLM?29Kn#K?3S37?_@+iD{1C53@c$7mTDN<9D z!kQG6%cCI3qTP8uPqljsw^&|50p^k+iQ+aN>6GT>=GtVj$xcqwg~7*#oEOtT4^7H0 z1-UN%}<2o2DlkG4z=K~YO+H}k`snBA$#)= zdGp`)<{$IspKXGV$mgE)=AQXJoya4MddW#~%l<1LH%IGG5;9q8a-*lY-6{qJ z>M>NL^<`GCB!bnM2m@PhA*@(IRk{BhBjhtC=bG#l!4J~F2^wz|HGilMDqsvEPc4Sg z%n1x#VCY7;7!v?*VL3^0ur#e8A2e~`JI-NSzd867Qb>I zuQ-H>GG#)8LFNy_7D~LS$Vnf!->QhE6%G$slY$DptcTp8mGUgKCe7#WK{KI7q%FnH z6sICnyP?~Z6mpE35T!D7n@PRb_#EQokfJb5%u4NtK?akA5=RWK7DnuOlK^qz2Qx}! zt9?)=W@Ho4`4oBG&|+EM<7MQ{GID~ZJ(e$occ-sMro-RdJC>qlEm5>Xj!v=89M!PQ zYCrWHDB>{reUIW+c*#V>r$Q%bAmis=Antd&8~8*F120$H)WWNI1C;1&&H&5LHlbbH;N|WB-!Y6gl+k?!Ce~+tqd{2~Z1cMtFvZM+mO_b0RlW6Wu;8qVP ztC>bEJ}MFdeOsF3iYH2iKM-q(LsM9J-ke2UvZDBkm6T``hKp&R$*f8^5$Ow{t#z!oWm0wzDeE>z7w{~XBmaR z@)`a(pW2T$kV7eClvT1yYYwOp^A6rw)k4-==m9K$Go{Hjx2GvpQjsI0W@zp*+5|s| z>JT~ysOVBK#l}#YRTN$ha&n=@XUpX(k^P-rR*o2AV1*HQ3&UVfyn%7H7hBRRu<*0H zH|S((R7dZKljmK;BcpyE6b@LyFa_!;EjmQ+=yIMh!D)q;ELzTxkp`)+wc=}e>S(JK z+h7HkTft^4;5pUN4q8!ES6j4<74%>>057dr+8@1vsOspj6OY=}!oB!%3HE`kjuv>P z91vRfDG2GADrG1<#j{zN`J{352&X*E;ZYQfuCYRHR0~I}FD3*Jidb_HEPbY`yyU+Q zDdj|joYmuwoOKh`1vKn>6dtbe+0@aeJnHB(hz2s!c8y4~y&Nx%v^9m1R zzJ@ztsPOpe=m!}5@Os$8)o$yy53%3iEj*6Kb0ejP7jT+)gr`7uVYQ9`&(EZpxdPYa zsI?Fn{fs(E;Z?f9GD>x1IV_>H1JYVc^R*1jAFcqwVOn3XT5sVsZ{c;iXUxvIjbT!R zbuwgOjhIYw&+#rM`#eh>sx*SR<5&Zt-rQId4r-O=Y>iYoD<66i^*3}u7$nxcBS#?_ zx+3_adte+yQIEG`L@As1x#Rw%_oeI}v;?|J70ij` zuz14dl|A=3ty;@VN4f#hh)PPHUQ&6vl#<^%+jWuWDtmSeB4yWU`B3g6$Ch*5Wfh-H zt%v%LL%q2YmVr;;7j9PO!AFvav}!9YgJ@-^X0f)b3@Dc49Wz?wno4G_U$yXQDcd>v zPzz(F1mb3K#!`=R5%FbHmA=9!&%!1pGNtb8m)$I#g+t^ z6H`lU@Vj@K>;%o_hAZ#kn)eny<38mxbOcWO47(ONl-Afdy*#n!I-x-Y+OT$TNDE;1 z7m|&P?f?xmo@$?6^i+GtHhageJX$?8xD$xv{(PQ+rldrcDI{d!an_4jM*1$cwNM-Ybq-wmNnzzL<7775Eyg6&;v> z(XJ@nM;=Wr)N`UFDSVn$plw_1uVNicORcwnVApjT?Yi=avkpeJ(EROy6rJ&4d7$X# zjPlqGLyy+X@SwDU*%Y&XP&!Yl5xwTEgVJKzhKdL_hxz27bgKD9><`-<&zG zu3`=+!7v{|!V3g$rKCR)_5%V50?!k85qFP>z(%-c z?wxGfdx_XYAWR?*P^hJ{_7gZr;1Gd(2tfJX!b1ecAuX9Z0@Jlys;a&pH)8Ldl=7B2 zXSXzMTZlIscFrlBkHMcWbMKkyB zmYy^2#~PShx|+YADg5~bTn$ak^3TxjqMHAKHM-=Val$`X+wTf(O42^OlaTz2=Kej> z!kG}2*4Yb7Pe44xq!cmV*dv8%--284XksOcIb*Ms6gBhey^_A7uc2w6AZBwbvV zZq-!jN)hwOO;e>SQ$M&#YOx*xOoM2s=j6i!kGcx85|2{Fg~2K|U+V$EPCGh~E^pD{ zb3x}rUtx_Ev{*r#6*v*7`BSJc)>5b7Tu)Jj;az~9CKv2NZ$CvL;s_`Qb}ukC@p*`# zxIj7LtmS`%RSTFH(1JVf;sU{c;)w`F5t3j|OMUrTr_&>lmh&(a>~?oK-MlJ@ZmEUY zOcxxK$Z}4^cH-*dmqi7|Zl{DUtDac8qn`Ba1uJnXgqKAaWQMP@ThHYF3Qvi(7{4B>yGn zFivX63iSpLLbb5DRQ3HO@?P_)eR%4%Pc7Vm37)rbcZq&K%{S4uKdXA0(EoD1Z3Qt=O2;Gou zjaCNu@|us(5*-e@PEfF5!i;Rh?H%!YN9w#I4TwQ3byK;S;o$j0-9~j}uKD;sNHcO_ z-f2hTG^-g|P7oek5S`?}=y5IXq$j=}n(J0#@Rj5Z@!%FFjMQmpI83#sK91CA(Vj;; zP+dxX6xDsEOs3j`d_`lKK#HJzFn8Memqcu?;~{9tLJQlR*wth*eP;SpsFt2Vpf8rU z^-H_@4Zi6}>oyPNq^=xIYO3dxlPrX5ZPLx+&&|!Z zOS5gZ=Z?9}-S{P>;%lcJr|^rc(qFz(=%#Bdi> zG(5B=)z^uWTD?PqTUKX>)(l^_C7D{=yS-0uS-YrrM=7$U@4D-^;1F7-V=zM`bLmHp zf4W<BbFs-Xv1oyzG?YR?%mA zKX%Msa1exXTuZVX{QJ8ShXCcpri@nVT!$K>6^Fm`%b556-Mxc8kL8R{{MK zB!hxTQ8+ooB_)YT+V}CJz}^L@=4?UuKD}*R{(J!B70?eOyiX9e--`VGz`F#&w+`WD zg7Cd0&T*}x*Gq8JDnA$DIiJv+M<2Ma;V;UXJC!)s{4F0l*3=zZstr^72>XUs5tVM@ z)sNywi7jbJs#UX#i9Pl|h&dZqp@t$TeVZucNdpHFLJQO$c?45FPJwEFJMxqI#}QH! z;02dFHNy#nc7z`xB=sY6o~)}LA!%1b=tPLufAa|N)+dJqET#V53M$kx{iEMbA#v2o z==+-+Ec!cRB;M4IMel$2Y&y!f623i?>M`|W(YGZ&pI*Q5^XX4s^Eq^svZq|*N)eoY zPME*#Z*QO^{8!+7B`-%TfsXy1{O|mo{1aW^pOl4)w)H7K z)$hD*6J^dfxPXp7~#O z3AdBQSj114yKZw*=7|Sf!cMX{i@xqxjvaglAU1CP$7Qasvg70Cms(uOD*Tfrfea8~ z@r@SONzop`A;+Nk%sSUgQUJ)l^{$_lXsvCoApXG|{gMVO`5~9^aaje3fAZW9m#y5> z)9t$vo)=zsd9#19*6hplcJ_5)U|QaM`KI)uShH_%sH3;5qr0y+v%Gn4X0UnX3T#oi zmURpcW_qsc-s=O4-ofR~!vnob2RnCWdO8Ld^<+B-`Ud;94=w8K%k?bn80=YmqrchL z)6tvVo*5j%N#Mx>!OiEZa9i62r=K0#dzMg(H4>~BXSA0$x9&~!)4^4KvUzbwe}D55 zi^I^s@F0#SZ|^%tpxoa`R1zrA%x^^DgPG3Z0Tfz_gEEl0VHkpDy4nV^H)gvtJ2Hdk zu>E^ngJrHhe27oZVr1+=A#@nIC39n@+t*ESd2KmS8_p&=F;Rdfw|8_8 yW=hrKT$X%}IAz(FoF)9SB^5Ow!?Go4BnNM$733UOi%y@6pWYUPWg&|HQT%`0fRZl& delta 147447 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 O6lFzw=)B8+t^PlLDswpi diff --git a/bin/BDInfo/System.Resources.Extensions.dll b/bin/BDInfo/System.Resources.Extensions.dll new file mode 100644 index 0000000000000000000000000000000000000000..939c9f5823eb53615d328e75767ebeebf0f703da GIT binary patch literal 137376 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�awL7&#+F|V-?Vnnm<3vY`W09lXvDR^=;~vMu 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%0D8=UvG}!Jk*|>(+^Z;U!t!U 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 YnHq2RHR*}FCNd*8WY+gj0VBxY0Q=3ipa1{> literal 0 HcmV?d00001 From e9968209acbb03b8391d8f9343c009fae91b34c9 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 20:21:54 +1000 Subject: [PATCH 25/33] Allow use, edit or discard uni3d description --- requirements.txt | 3 ++- src/trackers/COMMON.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b20352a43..c63a8c332 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,5 @@ pyoxipng rich Jinja2 pyotp -str2bool \ No newline at end of file +str2bool +click \ No newline at end of file diff --git a/src/trackers/COMMON.py b/src/trackers/COMMON.py index 02b059fe5..41413fa75 100644 --- a/src/trackers/COMMON.py +++ b/src/trackers/COMMON.py @@ -3,6 +3,7 @@ import requests import re import json +import click from src.bbcode import BBCODE from src.console import console @@ -184,6 +185,21 @@ async def unit3d_torrent_info(self, tracker, torrent_url, search_url, id=None, f bbcode = BBCODE() description, imagelist = bbcode.clean_unit3d_description(description, torrent_url) console.print(f"[green]Successfully grabbed description from {tracker}") + + # Allow user to edit or discard the description + console.print("[cyan]Do you want to edit or discard the description?[/cyan]") + edit_choice = input("[cyan]Enter 'e' to edit, 'd' to discard, or press Enter to keep it as is: [/cyan]") + + if edit_choice.lower() == 'e': + edited_description = click.edit(description) + if edited_description: + description = edited_description.strip() + console.print(f"[green]Final description after editing:[/green] {description}") + elif edit_choice.lower() == 'd': + description = None + console.print("[yellow]Description discarded.[/yellow]") + else: + console.print(f"[green]Keeping the original description.[/green]") else: console.print(f"[yellow]No description found for {tracker}.[/yellow]") else: @@ -193,6 +209,9 @@ async def unit3d_torrent_info(self, tracker, torrent_url, search_url, id=None, f console.print_exception() console.print(f"[yellow]Invalid Response from {tracker} API. Error: {str(e)}[/yellow]") + if description: # Ensure description is only printed if it's not None + console.print(f"[green]Final description to be returned:[/green] {description}") + return tmdb, imdb, tvdb, mal, description, category, infohash, imagelist, file_name async def parseCookieFile(self, cookiefile): From 413121c51429310b490d49df8d968f51a376e168 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 20:35:05 +1000 Subject: [PATCH 26/33] Silence consoles --- src/prep.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/prep.py b/src/prep.py index 95ba752d0..78b01d599 100644 --- a/src/prep.py +++ b/src/prep.py @@ -86,7 +86,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met manual_key = f"{tracker_key}_manual" found_match = False - console.print(f"[cyan]Attempting to search {tracker_name} with search_term: {search_term}[/cyan]") + # console.print(f"[cyan]Attempting to search {tracker_name} with search_term: {search_term}[/cyan]") if meta.get(tracker_key) is not None: meta[manual_key] = meta[tracker_key] @@ -131,12 +131,12 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met else: console.print(f"[yellow]No IMDb ID found on {tracker_name}[/yellow]") else: - console.print(f"[cyan]Searching {tracker_name} using search_term: {search_term}[/cyan]") + # console.print(f"[cyan]Searching {tracker_name} using search_term: {search_term}[/cyan]") imdb, tracker_id = None, None # Initialize variables if tracker_name == "PTP": imdb, tracker_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) elif tracker_name == "HDB": - console.print(f"[cyan]HDB search using folder/file: {search_term}[/cyan]") + # console.print(f"[cyan]HDB search using folder/file: {search_term}[/cyan]") imdb, tvdb_id, hdb_name, meta['ext_torrenthash'], tracker_id = await tracker_instance.search_filename(search_term, search_file_folder) meta['tvdb_id'] = str(tvdb_id) if tvdb_id else meta.get('tvdb_id') meta['hdb_name'] = hdb_name @@ -314,16 +314,16 @@ async def gather_prep(self, meta, mode): if str(self.config['TRACKERS'].get('PTP', {}).get('useAPI')).lower() == "true": ptp = PTP(config=self.config) - console.print(f"[cyan]Attempting to search PTP with search_term: {search_term}[/cyan]") + # console.print(f"[cyan]Attempting to search PTP with search_term: {search_term}[/cyan]") meta, found_match = await self.update_metadata_from_tracker('PTP', ptp, meta, search_term, search_file_folder) if not found_match and str(self.config['TRACKERS'].get('HDB', {}).get('useAPI')).lower() == "true": - console.print(f"[cyan]Attempting to search HDB with search_term: {search_term}[/cyan]") + # console.print(f"[cyan]Attempting to search HDB with search_term: {search_term}[/cyan]") hdb = HDB(config=self.config) meta, found_match = await self.update_metadata_from_tracker('HDB', hdb, meta, search_term, search_file_folder) if not found_match and str(self.config['TRACKERS'].get('BLU', {}).get('useAPI')).lower() == "true": - console.print(f"[cyan]Attempting to search BLU with search_term: {search_term}[/cyan]") + # console.print(f"[cyan]Attempting to search BLU with search_term: {search_term}[/cyan]") blu = BLU(config=self.config) meta, found_match = await self.update_metadata_from_tracker('BLU', blu, meta, search_term, search_file_folder) From 7da102b7c9d51fd37be533b7ab28b079f173468f Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 21:02:18 +1000 Subject: [PATCH 27/33] Keep or discard found image links --- src/prep.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/prep.py b/src/prep.py index 78b01d599..8c22bcce0 100644 --- a/src/prep.py +++ b/src/prep.py @@ -81,13 +81,15 @@ async def prompt_user_for_id_selection(self, blu_tmdb=None, blu_imdb=None, blu_t selection = input("Do you want to use this ID? (y/n): ").strip().lower() return selection == 'y' + async def prompt_user_for_confirmation(self, message): + selection = input(f"{message} (y/n): ").strip().lower() + return selection == 'y' + async def update_metadata_from_tracker(self, tracker_name, tracker_instance, meta, search_term, search_file_folder): tracker_key = tracker_name.lower() manual_key = f"{tracker_key}_manual" found_match = False - # console.print(f"[cyan]Attempting to search {tracker_name} with search_term: {search_term}[/cyan]") - if meta.get(tracker_key) is not None: meta[manual_key] = meta[tracker_key] console.print(f"[cyan]{tracker_name} ID found in meta, reusing existing ID: {meta[tracker_key]}[/cyan]") @@ -96,6 +98,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: console.print(f"[green]Valid data found on {tracker_name}, setting meta values[/green]") if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): + # Setting metadata based on found IDs if blu_tmdb not in [None, '0']: meta['tmdb_manual'] = blu_tmdb if blu_imdb not in [None, '0']: @@ -115,6 +118,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met found_match = True # Set flag if any relevant data is found else: console.print(f"[yellow]User skipped the found ID on {tracker_name}, moving to the next site.[/yellow]") + await self.handle_image_list(meta, tracker_name) return meta, found_match # Return immediately to skip the current site else: console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") @@ -131,12 +135,10 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met else: console.print(f"[yellow]No IMDb ID found on {tracker_name}[/yellow]") else: - # console.print(f"[cyan]Searching {tracker_name} using search_term: {search_term}[/cyan]") imdb, tracker_id = None, None # Initialize variables if tracker_name == "PTP": imdb, tracker_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) elif tracker_name == "HDB": - # console.print(f"[cyan]HDB search using folder/file: {search_term}[/cyan]") imdb, tvdb_id, hdb_name, meta['ext_torrenthash'], tracker_id = await tracker_instance.search_filename(search_term, search_file_folder) meta['tvdb_id'] = str(tvdb_id) if tvdb_id else meta.get('tvdb_id') meta['hdb_name'] = hdb_name @@ -145,6 +147,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: console.print(f"[green]Valid data found on {tracker_name} using file name, setting meta values[/green]") if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): + # Setting metadata based on found IDs if blu_tmdb not in [None, '0']: meta['tmdb_manual'] = blu_tmdb if blu_imdb not in [None, '0']: @@ -161,9 +164,10 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met meta['image_list'] = blu_imagelist if blu_filename: meta['blu_filename'] = blu_filename # Store the filename in meta for later use - found_match = True + found_match = True # Set flag if any relevant data is found else: console.print(f"[yellow]User skipped the found ID on {tracker_name}, moving to the next site.[/yellow]") + await self.handle_image_list(meta, tracker_name) return meta, found_match # Return immediately to skip the current site else: console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") @@ -182,8 +186,19 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met if tracker_id: meta[tracker_key] = tracker_id + await self.handle_image_list(meta, tracker_name) return meta, found_match + async def handle_image_list(self, meta, tracker_name): + if meta.get('image_list'): + keep_images = await self.prompt_user_for_confirmation(f"Do you want to keep the images found on {tracker_name}?") + if not keep_images: + meta['image_list'] = [] + console.print(f"[yellow]Images discarded from {tracker_name}[/yellow]") + else: + console.print(f"[green]Images retained from {tracker_name}[/green]") + + async def gather_prep(self, meta, mode): meta['mode'] = mode base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) @@ -202,7 +217,7 @@ async def gather_prep(self, meta, mode): meta['is_disc'], videoloc, bdinfo, meta['discs'] = await self.get_disc(meta) # Debugging information - console.print(f"Debug: meta['filelist'] before population: {meta.get('filelist', 'Not Set')}") + # console.print(f"Debug: meta['filelist'] before population: {meta.get('filelist', 'Not Set')}") if meta['is_disc'] == "BDMV": video, meta['scene'], meta['imdb'] = self.is_scene(meta['path'], meta.get('imdb', None)) @@ -306,7 +321,7 @@ async def gather_prep(self, meta, mode): meta['bdinfo'] = bdinfo # Debugging information after population - console.print(f"Debug: meta['filelist'] after population: {meta.get('filelist', 'Not Set')}") + # console.print(f"Debug: meta['filelist'] after population: {meta.get('filelist', 'Not Set')}") # Reuse information from trackers with fallback if search_term: # Ensure there's a valid search term From 6ac1fd5dfb5c32e466a93728d88781301a422736 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sat, 31 Aug 2024 23:28:22 +1000 Subject: [PATCH 28/33] Working auto PTP description with image links Need to fix the duplicate calls --- src/bbcode.py | 66 +++++++++++++------- src/prep.py | 148 +++++++++++++++++++++++++++++++++++--------- src/trackers/PTP.py | 17 ++++- 3 files changed, 177 insertions(+), 54 deletions(-) diff --git a/src/bbcode.py b/src/bbcode.py index 2821a7a03..2a512fe13 100644 --- a/src/bbcode.py +++ b/src/bbcode.py @@ -1,6 +1,7 @@ import re import html import urllib.parse +from src.console import console # Bold - KEEP # Italic - KEEP @@ -36,18 +37,22 @@ def __init__(self): pass def clean_ptp_description(self, desc, is_disc): + console.print(f"[yellow]Cleaning PTP description...") + # Convert Bullet Points to - desc = desc.replace("•", "-") # Unescape html desc = html.unescape(desc) - # End my suffering desc = desc.replace('\r\n', '\n') + # Debugging print + console.print(f"[yellow]Description after unescaping HTML:\n{desc[:500]}...") + # Remove url tags with PTP/HDB links url_tags = re.findall(r"(\[url[\=\]]https?:\/\/passthepopcorn\.m[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) - url_tags = url_tags + re.findall(r"(\[url[\=\]]https?:\/\/hdbits\.o[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) - if url_tags != []: + url_tags += re.findall(r"(\[url[\=\]]https?:\/\/hdbits\.o[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) + if url_tags: for url_tag in url_tags: url_tag = ''.join(url_tag) url_tag_removed = re.sub(r"(\[url[\=\]]https?:\/\/passthepopcorn\.m[^\]]+])", "", url_tag, flags=re.IGNORECASE) @@ -55,13 +60,16 @@ def clean_ptp_description(self, desc, is_disc): url_tag_removed = url_tag_removed.replace("[/url]", "") desc = desc.replace(url_tag, url_tag_removed) - # Remove links to PTP + # Debugging print + console.print(f"[yellow]Description after removing URL tags:\n{desc[:500]}...") + + # Remove links to PTP/HDB desc = desc.replace('http://passthepopcorn.me', 'PTP').replace('https://passthepopcorn.me', 'PTP') desc = desc.replace('http://hdbits.org', 'HDB').replace('https://hdbits.org', 'HDB') # Remove Mediainfo Tags / Attempt to regex out mediainfo - mediainfo_tags = re.findall(r"\[mediainfo\][\s\S]*?\[\/mediainfo\]", desc) - if len(mediainfo_tags) >= 1: + mediainfo_tags = re.findall(r"\[mediainfo\][\s\S]*?\[\/mediainfo\]", desc) + if mediainfo_tags: desc = re.sub(r"\[mediainfo\][\s\S]*?\[\/mediainfo\]", "", desc) elif is_disc != "BDMV": desc = re.sub(r"(^general\nunique)(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) @@ -70,7 +78,10 @@ def clean_ptp_description(self, desc, is_disc): desc = re.sub(r"(^(video|audio|text)( #\d+)?\nid)(.*?)^$", "", desc, flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) desc = re.sub(r"(^(menu)( #\d+)?\n)(.*?)^$", "", f"{desc}\n\n", flags=re.MULTILINE | re.IGNORECASE | re.DOTALL) elif any(x in is_disc for x in ["BDMV", "DVD"]): - return "" + return "", [] + + # Debugging print + console.print(f"[yellow]Description after removing mediainfo tags:\n{desc[:500]}...") # Convert Quote tags: desc = re.sub(r"\[quote.*?\]", "[code]", desc) @@ -102,7 +113,8 @@ def clean_ptp_description(self, desc, is_disc): for each in remove_list: desc = desc.replace(each, '') - # Catch Stray Images + # Catch Stray Images and Prepare Image List + imagelist = [] comps = re.findall(r"\[comparison=[\s\S]*?\[\/comparison\]", desc) hides = re.findall(r"\[hide[\s\S]*?\[\/hide\]", desc) comps.extend(hides) @@ -110,24 +122,33 @@ def clean_ptp_description(self, desc, is_disc): comp_placeholders = [] # Replace comparison/hide tags with placeholder because sometimes uploaders use comp images as loose images - for i in range(len(comps)): - nocomp = nocomp.replace(comps[i], '') - desc = desc.replace(comps[i], f"COMPARISON_PLACEHOLDER-{i} ") - comp_placeholders.append(comps[i]) + for i, comp in enumerate(comps): + nocomp = nocomp.replace(comp, '') + desc = desc.replace(comp, f"COMPARISON_PLACEHOLDER-{i} ") + comp_placeholders.append(comp) + + # Debugging print + console.print(f"[yellow]Description after processing comparisons and hides:\n{desc[:500]}...") # Remove Images in IMG tags: desc = re.sub(r"\[img\][\s\S]*?\[\/img\]", "", desc, flags=re.IGNORECASE) desc = re.sub(r"\[img=[\s\S]*?\]", "", desc, flags=re.IGNORECASE) - # Replace Images + + # Extract loose images and add to imagelist loose_images = re.findall(r"(https?:\/\/.*\.(?:png|jpg))", nocomp, flags=re.IGNORECASE) - if len(loose_images) >= 1: + if loose_images: + imagelist.extend(loose_images) + console.print(f"[yellow]Loose images found: {len(loose_images)}") for image in loose_images: desc = desc.replace(image, '') + + # Debugging print + console.print(f"[yellow]Final description after removing loose images:\n{desc[:500]}...") + # Re-place comparisons - if comp_placeholders != []: - for i, comp in enumerate(comp_placeholders): - comp = re.sub(r"\[\/?img[\s\S]*?\]", "", comp, flags=re.IGNORECASE) - desc = desc.replace(f"COMPARISON_PLACEHOLDER-{i} ", comp) + for i, comp in enumerate(comp_placeholders): + comp = re.sub(r"\[\/?img[\s\S]*?\]", "", comp, flags=re.IGNORECASE) + desc = desc.replace(f"COMPARISON_PLACEHOLDER-{i} ", comp) # Convert hides with multiple images to comparison desc = self.convert_collapse_to_comparison(desc, "hide", hides) @@ -139,9 +160,12 @@ def clean_ptp_description(self, desc, is_disc): desc = desc.replace('\n', '', 1) desc = desc.strip('\n') - if desc.replace('\n', '') == '': - return "" - return desc + if desc.replace('\n', '').strip() == '': + console.print(f"[yellow]Description is empty after cleaning.") + return "", imagelist + + console.print(f"[green]Returning cleaned description and {len(imagelist)} images.") + return desc, imagelist def clean_unit3d_description(self, desc, site): # Unescape html diff --git a/src/prep.py b/src/prep.py index 8c22bcce0..fbb177a9b 100644 --- a/src/prep.py +++ b/src/prep.py @@ -68,15 +68,21 @@ def __init__(self, screens, img_host, config): async def prompt_user_for_id_selection(self, blu_tmdb=None, blu_imdb=None, blu_tvdb=None, blu_filename=None, imdb=None): if imdb: imdb = str(imdb).zfill(7) # Convert to string and ensure IMDb ID is 7 characters long by adding leading zeros - console.print(f"[cyan]Found IMDb ID: https://www.imdb.com/title/tt{imdb}[/cyan]") + console.print(f"[cyan]Found IMDb ID: https://www.imdb.com/title/tt{imdb}") if blu_tmdb or blu_imdb or blu_tvdb: if blu_imdb: blu_imdb = str(blu_imdb).zfill(7) # Convert to string and ensure IMDb ID is 7 characters long by adding leading zeros - console.print("[cyan]Found the following IDs on BLU:[/cyan]") + console.print("[cyan]Found the following IDs on BLU:") console.print(f"TMDb ID: {blu_tmdb}") console.print(f"IMDb ID: https://www.imdb.com/title/tt{blu_imdb}") console.print(f"TVDb ID: {blu_tvdb}") console.print(f"Filename: {blu_filename}") + + if blu_imdb: # Assuming blu_imagelist would be linked with this function + if meta.get('image_list'): + console.print("[cyan]Found the following images:") + for img in meta['image_list']: + console.print(f"[blue]{img}") selection = input("Do you want to use this ID? (y/n): ").strip().lower() return selection == 'y' @@ -92,13 +98,12 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met if meta.get(tracker_key) is not None: meta[manual_key] = meta[tracker_key] - console.print(f"[cyan]{tracker_name} ID found in meta, reusing existing ID: {meta[tracker_key]}[/cyan]") + console.print(f"[cyan]{tracker_name} ID found in meta, reusing existing ID: {meta[tracker_key]}") if tracker_name == "BLU": blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, id=meta[tracker_key]) if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: - console.print(f"[green]Valid data found on {tracker_name}, setting meta values[/green]") + console.print(f"[green]Valid data found on {tracker_name}, setting meta values") if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): - # Setting metadata based on found IDs if blu_tmdb not in [None, '0']: meta['tmdb_manual'] = blu_tmdb if blu_imdb not in [None, '0']: @@ -115,27 +120,79 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met meta['image_list'] = blu_imagelist if blu_filename: meta['blu_filename'] = blu_filename # Store the filename in meta for later use - found_match = True # Set flag if any relevant data is found + found_match = True else: - console.print(f"[yellow]User skipped the found ID on {tracker_name}, moving to the next site.[/yellow]") + console.print(f"[yellow]User skipped the found ID on {tracker_name}, moving to the next site.") await self.handle_image_list(meta, tracker_name) - return meta, found_match # Return immediately to skip the current site + return meta, found_match else: - console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") + console.print(f"[yellow]No valid data found on {tracker_name}") + elif tracker_name == "PTP": + ptp_desc, ptp_imagelist = await tracker_instance.get_ptp_description(meta['ptp'], meta['is_disc']) + if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": + meta['description'] = ptp_desc + meta['image_list'] = ptp_imagelist + console.print(f"[green]PTP description and images added to metadata.") + + # Print images before asking for confirmation + if ptp_imagelist: + console.print("[cyan]Found the following images:") + for img in ptp_imagelist: + console.print(f"[blue]{img}") + + if await self.prompt_user_for_confirmation("Do you want to keep the description and images from PTP?"): + found_match = True + else: + console.print(f"[yellow]Description and images discarded from PTP") + meta['description'] = None + meta['image_list'] = [] + return meta, found_match + else: + console.print(f"[yellow]No valid data found on {tracker_name}") else: meta['imdb'], meta['ext_torrenthash'] = await tracker_instance.get_imdb_from_torrent_id(meta[tracker_key]) if meta['imdb']: - meta['imdb'] = str(meta['imdb']).zfill(7) # Pad IMDb ID with leading zeros + meta['imdb'] = str(meta['imdb']).zfill(7) if await self.prompt_user_for_id_selection(imdb=meta['imdb']): - console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}[/green]") + console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}") found_match = True + + if tracker_name == "PTP": + imdb, ptp_torrent_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) + if ptp_torrent_id: + meta['ptp'] = ptp_torrent_id # Store ptp_torrent_id in meta + if imdb: + imdb = str(imdb).zfill(7) + if await self.prompt_user_for_id_selection(imdb=imdb): + console.print(f"[green]{tracker_name} IMDb ID found: {imdb}") + meta['imdb'] = imdb + found_match = True + + ptp_desc, ptp_imagelist = await tracker_instance.get_ptp_description(meta['ptp'], meta['is_disc']) + if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": + meta['description'] = ptp_desc + meta['image_list'] = ptp_imagelist + console.print(f"[green]PTP description and images added to metadata.") + + if ptp_imagelist: + console.print("[cyan]Found the following images:") + for img in ptp_imagelist: + console.print(f"[blue]{img}") + + if await self.prompt_user_for_confirmation("Do you want to keep the description and images from PTP?"): + found_match = True + else: + console.print(f"[yellow]Description and images discarded from PTP") + meta['description'] = None + meta['image_list'] = [] + return meta, found_match else: - console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.[/yellow]") - return meta, found_match # Return immediately to skip the current site + console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.") + return meta, found_match else: - console.print(f"[yellow]No IMDb ID found on {tracker_name}[/yellow]") + console.print(f"[yellow]No IMDb ID found on {tracker_name}") else: - imdb, tracker_id = None, None # Initialize variables + imdb, tracker_id = None, None if tracker_name == "PTP": imdb, tracker_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) elif tracker_name == "HDB": @@ -145,13 +202,12 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met elif tracker_name == "BLU": blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, file_name=search_term) if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: - console.print(f"[green]Valid data found on {tracker_name} using file name, setting meta values[/green]") + console.print(f"[green]Valid data found on {tracker_name} using file name, setting meta values") if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): - # Setting metadata based on found IDs if blu_tmdb not in [None, '0']: meta['tmdb_manual'] = blu_tmdb if blu_imdb not in [None, '0']: - meta['imdb'] = str(blu_imdb).zfill(7) # Pad IMDb ID with leading zeros + meta['imdb'] = str(blu_imdb).zfill(7) if blu_tvdb not in [None, '0']: meta['tvdb_id'] = blu_tvdb if blu_mal not in [None, '0']: @@ -163,26 +219,56 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met if meta.get('image_list', []) == []: meta['image_list'] = blu_imagelist if blu_filename: - meta['blu_filename'] = blu_filename # Store the filename in meta for later use - found_match = True # Set flag if any relevant data is found + meta['blu_filename'] = blu_filename + found_match = True else: - console.print(f"[yellow]User skipped the found ID on {tracker_name}, moving to the next site.[/yellow]") + console.print(f"[yellow]User skipped the found ID on {tracker_name}, moving to the next site.") await self.handle_image_list(meta, tracker_name) - return meta, found_match # Return immediately to skip the current site + return meta, found_match else: - console.print(f"[yellow]No valid data found on {tracker_name}[/yellow]") + console.print(f"[yellow]No valid data found on {tracker_name}") else: imdb = tracker_id = None if imdb: - imdb = str(imdb).zfill(7) # Pad IMDb ID with leading zeros + imdb = str(imdb).zfill(7) if await self.prompt_user_for_id_selection(imdb=imdb): - console.print(f"[green]{tracker_name} IMDb ID found: {imdb}[/green]") + console.print(f"[green]{tracker_name} IMDb ID found: {imdb}") meta['imdb'] = imdb found_match = True + + if tracker_name == "PTP": + imdb, ptp_torrent_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) + if ptp_torrent_id: + meta['ptp'] = ptp_torrent_id # Store ptp_torrent_id in meta + if imdb: + imdb = str(imdb).zfill(7) + if await self.prompt_user_for_id_selection(imdb=imdb): + console.print(f"[green]{tracker_name} IMDb ID found: {imdb}") + meta['imdb'] = imdb + found_match = True + + ptp_desc, ptp_imagelist = await tracker_instance.get_ptp_description(meta['ptp'], meta['is_disc']) + if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": + meta['description'] = ptp_desc + meta['image_list'] = ptp_imagelist + console.print(f"[green]PTP description and images added to metadata.") + + if ptp_imagelist: + console.print("[cyan]Found the following images:") + for img in ptp_imagelist: + console.print(f"[blue]{img}") + + if await self.prompt_user_for_confirmation("Do you want to keep the description and images from PTP?"): + found_match = True + else: + console.print(f"[yellow]Description and images discarded from PTP") + meta['description'] = None + meta['image_list'] = [] + return meta, found_match else: - console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.[/yellow]") - return meta, found_match # Return immediately to skip the current site + console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.") + return meta, found_match if tracker_id: meta[tracker_key] = tracker_id @@ -191,13 +277,15 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met async def handle_image_list(self, meta, tracker_name): if meta.get('image_list'): + console.print("[cyan]Found the following images:") + for img in meta['image_list']: + console.print(f"[blue]{img}[/blue]") keep_images = await self.prompt_user_for_confirmation(f"Do you want to keep the images found on {tracker_name}?") if not keep_images: meta['image_list'] = [] - console.print(f"[yellow]Images discarded from {tracker_name}[/yellow]") + console.print(f"[yellow]Images discarded from {tracker_name}") else: - console.print(f"[green]Images retained from {tracker_name}[/green]") - + console.print(f"[green]Images retained from {tracker_name}") async def gather_prep(self, meta, mode): meta['mode'] = mode diff --git a/src/trackers/PTP.py b/src/trackers/PTP.py index 4e924cdaa..eae72bacb 100644 --- a/src/trackers/PTP.py +++ b/src/trackers/PTP.py @@ -175,13 +175,24 @@ async def get_ptp_description(self, ptp_torrent_id, is_disc): 'User-Agent': self.user_agent } url = 'https://passthepopcorn.me/torrents.php' + console.print(f"[yellow]Requesting description from {url} with ID {ptp_torrent_id}") response = requests.get(url, params=params, headers=headers) await asyncio.sleep(1) + ptp_desc = response.text + console.print(f"[yellow]Raw description received:\n{ptp_desc[:500]}...") # Show first 500 characters for brevity + bbcode = BBCODE() - desc = bbcode.clean_ptp_description(ptp_desc, is_disc) - console.print("[bold green]Successfully grabbed description from PTP") - return desc + desc, imagelist = bbcode.clean_ptp_description(ptp_desc, is_disc) + + console.print(f"[bold green]Successfully grabbed description from PTP") + console.print(f"[cyan]Description after cleaning:\n{desc[:500]}...") # Show first 500 characters for brevity + console.print(f"[cyan]Images found: {len(imagelist)}") + if imagelist: + for img in imagelist: + console.print(f"[blue]Image: {img}") + + return desc, imagelist async def get_group_by_imdb(self, imdb): params = { From 68c20f68e693910352c017e6ebd3212b1ebd8225 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sun, 1 Sep 2024 00:30:49 +1000 Subject: [PATCH 29/33] Fix error created with existing get_desc and remove dupe console todo: stop existing functions being called again when more screens are taken: Auto limit screens to those returned via site description. Stop many functions being called again when answering no to "Is this correct?" --- src/bbcode.py | 14 +-- src/prep.py | 235 ++++++++++++++++++++++---------------------- src/trackers/PTP.py | 6 +- 3 files changed, 124 insertions(+), 131 deletions(-) diff --git a/src/bbcode.py b/src/bbcode.py index 2a512fe13..a1324509d 100644 --- a/src/bbcode.py +++ b/src/bbcode.py @@ -37,7 +37,7 @@ def __init__(self): pass def clean_ptp_description(self, desc, is_disc): - console.print(f"[yellow]Cleaning PTP description...") + # console.print(f"[yellow]Cleaning PTP description...") # Convert Bullet Points to - desc = desc.replace("•", "-") @@ -47,7 +47,7 @@ def clean_ptp_description(self, desc, is_disc): desc = desc.replace('\r\n', '\n') # Debugging print - console.print(f"[yellow]Description after unescaping HTML:\n{desc[:500]}...") + # console.print(f"[yellow]Description after unescaping HTML:\n{desc[:500]}...") # Remove url tags with PTP/HDB links url_tags = re.findall(r"(\[url[\=\]]https?:\/\/passthepopcorn\.m[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) @@ -61,7 +61,7 @@ def clean_ptp_description(self, desc, is_disc): desc = desc.replace(url_tag, url_tag_removed) # Debugging print - console.print(f"[yellow]Description after removing URL tags:\n{desc[:500]}...") + # console.print(f"[yellow]Description after removing URL tags:\n{desc[:500]}...") # Remove links to PTP/HDB desc = desc.replace('http://passthepopcorn.me', 'PTP').replace('https://passthepopcorn.me', 'PTP') @@ -81,7 +81,7 @@ def clean_ptp_description(self, desc, is_disc): return "", [] # Debugging print - console.print(f"[yellow]Description after removing mediainfo tags:\n{desc[:500]}...") + # console.print(f"[yellow]Description after removing mediainfo tags:\n{desc[:500]}...") # Convert Quote tags: desc = re.sub(r"\[quote.*?\]", "[code]", desc) @@ -128,7 +128,7 @@ def clean_ptp_description(self, desc, is_disc): comp_placeholders.append(comp) # Debugging print - console.print(f"[yellow]Description after processing comparisons and hides:\n{desc[:500]}...") + # console.print(f"[yellow]Description after processing comparisons and hides:\n{desc[:500]}...") # Remove Images in IMG tags: desc = re.sub(r"\[img\][\s\S]*?\[\/img\]", "", desc, flags=re.IGNORECASE) @@ -143,7 +143,7 @@ def clean_ptp_description(self, desc, is_disc): desc = desc.replace(image, '') # Debugging print - console.print(f"[yellow]Final description after removing loose images:\n{desc[:500]}...") + # console.print(f"[yellow]Final description after removing loose images:\n{desc[:500]}...") # Re-place comparisons for i, comp in enumerate(comp_placeholders): @@ -164,7 +164,7 @@ def clean_ptp_description(self, desc, is_disc): console.print(f"[yellow]Description is empty after cleaning.") return "", imagelist - console.print(f"[green]Returning cleaned description and {len(imagelist)} images.") + # console.print(f"[green]Returning cleaned description and {len(imagelist)} images.") return desc, imagelist def clean_unit3d_description(self, desc, site): diff --git a/src/prep.py b/src/prep.py index fbb177a9b..7811fab6e 100644 --- a/src/prep.py +++ b/src/prep.py @@ -96,11 +96,17 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met manual_key = f"{tracker_key}_manual" found_match = False - if meta.get(tracker_key) is not None: - meta[manual_key] = meta[tracker_key] - console.print(f"[cyan]{tracker_name} ID found in meta, reusing existing ID: {meta[tracker_key]}") - if tracker_name == "BLU": - blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, id=meta[tracker_key]) + # Handle each tracker separately + if tracker_name == "BLU": + if meta.get(tracker_key) is not None: + meta[manual_key] = meta[tracker_key] + console.print(f"[cyan]{tracker_name} ID found in meta, reusing existing ID: {meta[tracker_key]}") + blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info( + "BLU", + tracker_instance.torrent_url, + tracker_instance.search_url, + id=meta[tracker_key] + ) if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: console.print(f"[green]Valid data found on {tracker_name}, setting meta values") if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): @@ -116,7 +122,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met meta['blu_desc'] = blu_desc if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() - if meta.get('image_list', []) == []: + if not meta.get('image_list'): meta['image_list'] = blu_imagelist if blu_filename: meta['blu_filename'] = blu_filename # Store the filename in meta for later use @@ -127,80 +133,14 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met return meta, found_match else: console.print(f"[yellow]No valid data found on {tracker_name}") - elif tracker_name == "PTP": - ptp_desc, ptp_imagelist = await tracker_instance.get_ptp_description(meta['ptp'], meta['is_disc']) - if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": - meta['description'] = ptp_desc - meta['image_list'] = ptp_imagelist - console.print(f"[green]PTP description and images added to metadata.") - - # Print images before asking for confirmation - if ptp_imagelist: - console.print("[cyan]Found the following images:") - for img in ptp_imagelist: - console.print(f"[blue]{img}") - - if await self.prompt_user_for_confirmation("Do you want to keep the description and images from PTP?"): - found_match = True - else: - console.print(f"[yellow]Description and images discarded from PTP") - meta['description'] = None - meta['image_list'] = [] - return meta, found_match - else: - console.print(f"[yellow]No valid data found on {tracker_name}") else: - meta['imdb'], meta['ext_torrenthash'] = await tracker_instance.get_imdb_from_torrent_id(meta[tracker_key]) - if meta['imdb']: - meta['imdb'] = str(meta['imdb']).zfill(7) - if await self.prompt_user_for_id_selection(imdb=meta['imdb']): - console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}") - found_match = True - - if tracker_name == "PTP": - imdb, ptp_torrent_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) - if ptp_torrent_id: - meta['ptp'] = ptp_torrent_id # Store ptp_torrent_id in meta - if imdb: - imdb = str(imdb).zfill(7) - if await self.prompt_user_for_id_selection(imdb=imdb): - console.print(f"[green]{tracker_name} IMDb ID found: {imdb}") - meta['imdb'] = imdb - found_match = True - - ptp_desc, ptp_imagelist = await tracker_instance.get_ptp_description(meta['ptp'], meta['is_disc']) - if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": - meta['description'] = ptp_desc - meta['image_list'] = ptp_imagelist - console.print(f"[green]PTP description and images added to metadata.") - - if ptp_imagelist: - console.print("[cyan]Found the following images:") - for img in ptp_imagelist: - console.print(f"[blue]{img}") - - if await self.prompt_user_for_confirmation("Do you want to keep the description and images from PTP?"): - found_match = True - else: - console.print(f"[yellow]Description and images discarded from PTP") - meta['description'] = None - meta['image_list'] = [] - return meta, found_match - else: - console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.") - return meta, found_match - else: - console.print(f"[yellow]No IMDb ID found on {tracker_name}") - else: - imdb, tracker_id = None, None - if tracker_name == "PTP": - imdb, tracker_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) - elif tracker_name == "HDB": - imdb, tvdb_id, hdb_name, meta['ext_torrenthash'], tracker_id = await tracker_instance.search_filename(search_term, search_file_folder) - meta['tvdb_id'] = str(tvdb_id) if tvdb_id else meta.get('tvdb_id') - meta['hdb_name'] = hdb_name - elif tracker_name == "BLU": - blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info("BLU", tracker_instance.torrent_url, tracker_instance.search_url, file_name=search_term) + # BLU tracker handling when tracker_key is not in meta + blu_tmdb, blu_imdb, blu_tvdb, blu_mal, blu_desc, blu_category, meta['ext_torrenthash'], blu_imagelist, blu_filename = await COMMON(self.config).unit3d_torrent_info( + "BLU", + tracker_instance.torrent_url, + tracker_instance.search_url, + file_name=search_term + ) if blu_tmdb not in [None, '0'] or blu_imdb not in [None, '0'] or blu_tvdb not in [None, '0']: console.print(f"[green]Valid data found on {tracker_name} using file name, setting meta values") if await self.prompt_user_for_id_selection(blu_tmdb, blu_imdb, blu_tvdb, blu_filename): @@ -216,7 +156,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met meta['blu_desc'] = blu_desc if blu_category.upper() in ['MOVIE', 'TV SHOW', 'FANRES']: meta['category'] = 'TV' if blu_category.upper() == 'TV SHOW' else blu_category.upper() - if meta.get('image_list', []) == []: + if not meta.get('image_list'): meta['image_list'] = blu_imagelist if blu_filename: meta['blu_filename'] = blu_filename @@ -227,51 +167,99 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met return meta, found_match else: console.print(f"[yellow]No valid data found on {tracker_name}") + + elif tracker_name == "PTP": + # Handle PTP separately to avoid duplication + if meta.get('ptp') is None: + # Only fetch if not already in meta + imdb, ptp_torrent_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) + if ptp_torrent_id: + meta['ptp'] = ptp_torrent_id # Store ptp_torrent_id in meta + meta['imdb'] = str(imdb).zfill(7) if imdb else None + + if meta.get('imdb') and await self.prompt_user_for_id_selection(imdb=meta['imdb']): + console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}") + found_match = True + + ptp_desc, ptp_imagelist = await tracker_instance.get_ptp_description(meta['ptp'], meta.get('is_disc', False)) + if ptp_desc.strip(): + meta['description'] = ptp_desc + meta['image_list'] = ptp_imagelist + console.print(f"[green]PTP description and images added to metadata.") + + if await self.prompt_user_for_confirmation("Do you want to keep the description from PTP?"): + meta['skip_gen_desc'] = True + found_match = True + else: + console.print(f"[yellow]Description discarded from PTP") + meta['description'] = None + return meta, found_match else: - imdb = tracker_id = None + console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.") + return meta, found_match - if imdb: - imdb = str(imdb).zfill(7) - if await self.prompt_user_for_id_selection(imdb=imdb): - console.print(f"[green]{tracker_name} IMDb ID found: {imdb}") - meta['imdb'] = imdb + elif tracker_name == "HDB": + if meta.get(tracker_key) is not None: + meta[manual_key] = meta[tracker_key] + console.print(f"[cyan]{tracker_name} ID found in meta, reusing existing ID: {meta[tracker_key]}") + imdb, tvdb_id, hdb_name, meta['ext_torrenthash'], tracker_id = await tracker_instance.search_filename(search_term, search_file_folder) + meta['tvdb_id'] = str(tvdb_id) if tvdb_id else meta.get('tvdb_id') + meta['hdb_name'] = hdb_name + if tracker_id: + meta[tracker_key] = tracker_id + found_match = True + else: + # Handle HDB when tracker_key is not in meta + imdb, tvdb_id, hdb_name, meta['ext_torrenthash'], tracker_id = await tracker_instance.search_filename(search_term, search_file_folder) + meta['tvdb_id'] = str(tvdb_id) if tvdb_id else meta.get('tvdb_id') + meta['hdb_name'] = hdb_name + if tracker_id: + meta[tracker_key] = tracker_id + found_match = True + + # Prompt user for confirmation on keeping HDB data + if found_match: + console.print(f"[green]{tracker_name} data found: IMDb ID: {imdb}, TVDb ID: {meta['tvdb_id']}, HDB Name: {meta['hdb_name']}") + if await self.prompt_user_for_confirmation(f"Do you want to keep the data found on {tracker_name}?"): + console.print(f"[green]{tracker_name} data retained.") + else: + console.print(f"[yellow]{tracker_name} data discarded.") + meta[tracker_key] = None + meta['tvdb_id'] = None + meta['hdb_name'] = None + found_match = False + + else: + # Handle other trackers if any + meta['imdb'], meta['ext_torrenthash'] = await tracker_instance.get_imdb_from_torrent_id(meta.get(tracker_key)) + if meta['imdb']: + meta['imdb'] = str(meta['imdb']).zfill(7) + if await self.prompt_user_for_id_selection(imdb=meta['imdb']): + console.print(f"[green]{tracker_name} IMDb ID found: {meta['imdb']}") found_match = True + # Additional PTP handling if needed if tracker_name == "PTP": - imdb, ptp_torrent_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) - if ptp_torrent_id: - meta['ptp'] = ptp_torrent_id # Store ptp_torrent_id in meta - if imdb: - imdb = str(imdb).zfill(7) - if await self.prompt_user_for_id_selection(imdb=imdb): - console.print(f"[green]{tracker_name} IMDb ID found: {imdb}") - meta['imdb'] = imdb - found_match = True - - ptp_desc, ptp_imagelist = await tracker_instance.get_ptp_description(meta['ptp'], meta['is_disc']) - if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": - meta['description'] = ptp_desc - meta['image_list'] = ptp_imagelist - console.print(f"[green]PTP description and images added to metadata.") - - if ptp_imagelist: - console.print("[cyan]Found the following images:") - for img in ptp_imagelist: - console.print(f"[blue]{img}") - - if await self.prompt_user_for_confirmation("Do you want to keep the description and images from PTP?"): - found_match = True - else: - console.print(f"[yellow]Description and images discarded from PTP") - meta['description'] = None - meta['image_list'] = [] - return meta, found_match + ptp_desc, ptp_imagelist = await tracker_instance.get_ptp_description(meta['ptp'], meta.get('is_disc', False)) + if ptp_desc.strip(): + meta['description'] = ptp_desc + meta['image_list'] = ptp_imagelist + console.print(f"[green]PTP description and images added to metadata.") + + if await self.prompt_user_for_confirmation("Do you want to keep the description from PTP?"): + meta['skip_gen_desc'] = True + found_match = True + else: + console.print(f"[yellow]Description discarded from PTP") + meta['description'] = None + return meta, found_match else: console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.") return meta, found_match - if tracker_id: - meta[tracker_key] = tracker_id + else: + console.print(f"[yellow]No IMDb ID found on {tracker_name}") + # Handle image list at the end await self.handle_image_list(meta, tracker_name) return meta, found_match @@ -2943,10 +2931,15 @@ def clean_filename(self, name): return name async def gen_desc(self, meta): + if meta.get('skip_gen_desc', False): + console.print("[cyan]Skipping description generation as PTP description was retained.") + return meta + desclink = meta.get('desclink', None) descfile = meta.get('descfile', None) ptp_desc = blu_desc = "" desc_source = [] + imagelist = [] with open(f"{meta['base_dir']}/tmp/{meta['uuid']}/DESCRIPTION.txt", 'w', newline="", encoding='utf8') as description: description.seek(0) if (desclink, descfile, meta['desc']) == (None, None, None): @@ -2961,11 +2954,12 @@ async def gen_desc(self, meta): if meta.get('ptp', None) is not None and str(self.config['TRACKERS'].get('PTP', {}).get('useAPI')).lower() == "true" and desc_source in ['PTP', None]: ptp = PTP(config=self.config) - ptp_desc = await ptp.get_ptp_description(meta['ptp'], meta['is_disc']) + ptp_desc, imagelist = await ptp.get_ptp_description(meta['ptp'], meta['is_disc']) if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": description.write(ptp_desc) description.write("\n") meta['description'] = 'PTP' + meta['imagelist'] = imagelist # Save the imagelist to meta if needed if ptp_desc == "" and meta.get('blu_desc', '').rstrip() not in [None, ''] and desc_source in ['BLU', None]: if meta.get('blu_desc', '').strip().replace('\r\n', '').replace('\n', '') != '': @@ -2988,6 +2982,7 @@ async def gen_desc(self, meta): description.write("[/code]") description.write("\n") meta['description'] = "CUSTOM" + if desclink is not None: parsed = urllib.parse.urlparse(desclink.replace('/raw/', '/')) split = os.path.split(parsed.path) @@ -3001,14 +2996,16 @@ async def gen_desc(self, meta): meta['description'] = "CUSTOM" if descfile is not None: - if os.path.isfile(descfile) is True: + if os.path.isfile(descfile): text = open(descfile, 'r').read() description.write(text) - meta['description'] = "CUSTOM" + meta['description'] = "CUSTOM" + if meta['desc'] is not None: description.write(meta['desc']) description.write("\n") meta['description'] = "CUSTOM" + description.write("\n") return meta diff --git a/src/trackers/PTP.py b/src/trackers/PTP.py index eae72bacb..910910fe4 100644 --- a/src/trackers/PTP.py +++ b/src/trackers/PTP.py @@ -186,11 +186,7 @@ async def get_ptp_description(self, ptp_torrent_id, is_disc): desc, imagelist = bbcode.clean_ptp_description(ptp_desc, is_disc) console.print(f"[bold green]Successfully grabbed description from PTP") - console.print(f"[cyan]Description after cleaning:\n{desc[:500]}...") # Show first 500 characters for brevity - console.print(f"[cyan]Images found: {len(imagelist)}") - if imagelist: - for img in imagelist: - console.print(f"[blue]Image: {img}") + console.print(f"[cyan]Description after cleaning:[yellow]\n{desc[:500]}...") # Show first 500 characters for brevity return desc, imagelist From a7311130c6ef0f7dcbebb80a06afc1e925737bc0 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sun, 1 Sep 2024 00:54:45 +1000 Subject: [PATCH 30/33] Skipping any part of auto ptp skips the original PTP description handling --- src/prep.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/prep.py b/src/prep.py index 7811fab6e..dc796923b 100644 --- a/src/prep.py +++ b/src/prep.py @@ -77,12 +77,6 @@ async def prompt_user_for_id_selection(self, blu_tmdb=None, blu_imdb=None, blu_t console.print(f"IMDb ID: https://www.imdb.com/title/tt{blu_imdb}") console.print(f"TVDb ID: {blu_tvdb}") console.print(f"Filename: {blu_filename}") - - if blu_imdb: # Assuming blu_imagelist would be linked with this function - if meta.get('image_list'): - console.print("[cyan]Found the following images:") - for img in meta['image_list']: - console.print(f"[blue]{img}") selection = input("Do you want to use this ID? (y/n): ").strip().lower() return selection == 'y' @@ -185,6 +179,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met if ptp_desc.strip(): meta['description'] = ptp_desc meta['image_list'] = ptp_imagelist + meta['skip_gen_desc'] = True console.print(f"[green]PTP description and images added to metadata.") if await self.prompt_user_for_confirmation("Do you want to keep the description from PTP?"): @@ -192,10 +187,12 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met found_match = True else: console.print(f"[yellow]Description discarded from PTP") + meta['skip_gen_desc'] = True meta['description'] = None return meta, found_match else: console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.") + meta['skip_gen_desc'] = True return meta, found_match elif tracker_name == "HDB": @@ -251,10 +248,12 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met found_match = True else: console.print(f"[yellow]Description discarded from PTP") + meta['skip_gen_desc'] = True meta['description'] = None return meta, found_match else: console.print(f"[yellow]User skipped the found IMDb ID on {tracker_name}, moving to the next site.") + meta['skip_gen_desc'] = True return meta, found_match else: console.print(f"[yellow]No IMDb ID found on {tracker_name}") @@ -2931,9 +2930,6 @@ def clean_filename(self, name): return name async def gen_desc(self, meta): - if meta.get('skip_gen_desc', False): - console.print("[cyan]Skipping description generation as PTP description was retained.") - return meta desclink = meta.get('desclink', None) descfile = meta.get('descfile', None) @@ -2953,6 +2949,9 @@ async def gen_desc(self, meta): desc_source = desc_source[0] if meta.get('ptp', None) is not None and str(self.config['TRACKERS'].get('PTP', {}).get('useAPI')).lower() == "true" and desc_source in ['PTP', None]: + if meta.get('skip_gen_desc', False): + console.print("[cyan]Skipping description generation as PTP description was retained.") + return meta ptp = PTP(config=self.config) ptp_desc, imagelist = await ptp.get_ptp_description(meta['ptp'], meta['is_disc']) if ptp_desc.replace('\r\n', '').replace('\n', '').strip() != "": From 1dd50d048e3b970f400027bd38f73c59a19bb982 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sun, 1 Sep 2024 01:09:39 +1000 Subject: [PATCH 31/33] Fix to skip HDB if no data is found --- src/prep.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/prep.py b/src/prep.py index dc796923b..d8e0addc1 100644 --- a/src/prep.py +++ b/src/prep.py @@ -168,7 +168,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met # Only fetch if not already in meta imdb, ptp_torrent_id, meta['ext_torrenthash'] = await tracker_instance.get_ptp_id_imdb(search_term, search_file_folder) if ptp_torrent_id: - meta['ptp'] = ptp_torrent_id # Store ptp_torrent_id in meta + meta['ptp'] = ptp_torrent_id meta['imdb'] = str(imdb).zfill(7) if imdb else None if meta.get('imdb') and await self.prompt_user_for_id_selection(imdb=meta['imdb']): @@ -214,18 +214,20 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met meta[tracker_key] = tracker_id found_match = True - # Prompt user for confirmation on keeping HDB data if found_match: - console.print(f"[green]{tracker_name} data found: IMDb ID: {imdb}, TVDb ID: {meta['tvdb_id']}, HDB Name: {meta['hdb_name']}") - if await self.prompt_user_for_confirmation(f"Do you want to keep the data found on {tracker_name}?"): - console.print(f"[green]{tracker_name} data retained.") + if imdb or tvdb_id or hdb_name: + console.print(f"[green]{tracker_name} data found: IMDb ID: {imdb}, TVDb ID: {meta['tvdb_id']}, HDB Name: {meta['hdb_name']}") + if await self.prompt_user_for_confirmation(f"Do you want to keep the data found on {tracker_name}?"): + console.print(f"[green]{tracker_name} data retained.") + else: + console.print(f"[yellow]{tracker_name} data discarded.") + meta[tracker_key] = None + meta['tvdb_id'] = None + meta['hdb_name'] = None + found_match = False else: - console.print(f"[yellow]{tracker_name} data discarded.") - meta[tracker_key] = None - meta['tvdb_id'] = None - meta['hdb_name'] = None + console.print(f"[yellow]Could not find a matching release on {tracker_name}.") found_match = False - else: # Handle other trackers if any meta['imdb'], meta['ext_torrenthash'] = await tracker_instance.get_imdb_from_torrent_id(meta.get(tracker_key)) From 08e369a03f25c88c25579f5c4b1f6747ce3c6bd1 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sun, 1 Sep 2024 01:33:16 +1000 Subject: [PATCH 32/33] Fix remove bot code from blu description Also show the cleaned description, rather than raw --- src/bbcode.py | 3 ++- src/prep.py | 2 +- src/trackers/COMMON.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bbcode.py b/src/bbcode.py index a1324509d..e23ef3075 100644 --- a/src/bbcode.py +++ b/src/bbcode.py @@ -212,7 +212,8 @@ def clean_unit3d_description(self, desc, site): desc = desc.replace(tag, '') # Remove bot signatures - desc = desc.replace("[img=35]https://blutopia/favicon.ico[/img] [b]Uploaded Using [url=https://github.com/HDInnovations/UNIT3D]UNIT3D[/url] Auto Uploader[/b] [img=35]https://blutopia/favicon.ico[/img]", '') + bot_signature_regex = r"\[center\]\s*\[img=\d+\]https:\/\/blutopia\.xyz\/favicon\.ico\[\/img\]\s*\[b\]Uploaded Using \[url=https:\/\/github\.com\/HDInnovations\/UNIT3D\]UNIT3D\[\/url\] Auto Uploader\[\/b\]\s*\[img=\d+\]https:\/\/blutopia\.xyz\/favicon\.ico\[\/img\]\s*\[\/center\]" + desc = re.sub(bot_signature_regex, "", desc, flags=re.IGNORECASE) desc = re.sub(r"\[center\].*Created by L4G's Upload Assistant.*\[\/center\]", "", desc, flags=re.IGNORECASE) # Replace spoiler tags diff --git a/src/prep.py b/src/prep.py index d8e0addc1..a3fb02f1f 100644 --- a/src/prep.py +++ b/src/prep.py @@ -266,7 +266,7 @@ async def update_metadata_from_tracker(self, tracker_name, tracker_instance, met async def handle_image_list(self, meta, tracker_name): if meta.get('image_list'): - console.print("[cyan]Found the following images:") + console.print(f"[cyan]Found the following images from {tracker_name}:") for img in meta['image_list']: console.print(f"[blue]{img}[/blue]") keep_images = await self.prompt_user_for_confirmation(f"Do you want to keep the images found on {tracker_name}?") diff --git a/src/trackers/COMMON.py b/src/trackers/COMMON.py index 41413fa75..1fbcd8767 100644 --- a/src/trackers/COMMON.py +++ b/src/trackers/COMMON.py @@ -178,13 +178,12 @@ async def unit3d_torrent_info(self, tracker, torrent_url, search_url, id=None, f imdb = attributes.get('imdb_id') infohash = attributes.get('info_hash') - console.print(f"[blue]Extracted description: {description}[/blue]") - # Process the description and imagelist if the description exists if description: bbcode = BBCODE() description, imagelist = bbcode.clean_unit3d_description(description, torrent_url) console.print(f"[green]Successfully grabbed description from {tracker}") + console.print(f"[blue]Extracted description: [yellow]{description}") # Allow user to edit or discard the description console.print("[cyan]Do you want to edit or discard the description?[/cyan]") From c7da5d0963947b1aa0c0960a13e8b9ba397d3a42 Mon Sep 17 00:00:00 2001 From: Audionut Date: Sun, 1 Sep 2024 02:14:52 +1000 Subject: [PATCH 33/33] Fix PTP image return and skip more screenshots if at least 3 images already exist --- src/bbcode.py | 30 +++++++---------------- src/prep.py | 58 +++++++++++++++++++++++++++++---------------- src/trackers/PTP.py | 2 +- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/src/bbcode.py b/src/bbcode.py index e23ef3075..0dfa3f352 100644 --- a/src/bbcode.py +++ b/src/bbcode.py @@ -46,9 +46,6 @@ def clean_ptp_description(self, desc, is_disc): desc = html.unescape(desc) desc = desc.replace('\r\n', '\n') - # Debugging print - # console.print(f"[yellow]Description after unescaping HTML:\n{desc[:500]}...") - # Remove url tags with PTP/HDB links url_tags = re.findall(r"(\[url[\=\]]https?:\/\/passthepopcorn\.m[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) url_tags += re.findall(r"(\[url[\=\]]https?:\/\/hdbits\.o[^\]]+)([^\[]+)(\[\/url\])?", desc, flags=re.IGNORECASE) @@ -60,9 +57,6 @@ def clean_ptp_description(self, desc, is_disc): url_tag_removed = url_tag_removed.replace("[/url]", "") desc = desc.replace(url_tag, url_tag_removed) - # Debugging print - # console.print(f"[yellow]Description after removing URL tags:\n{desc[:500]}...") - # Remove links to PTP/HDB desc = desc.replace('http://passthepopcorn.me', 'PTP').replace('https://passthepopcorn.me', 'PTP') desc = desc.replace('http://hdbits.org', 'HDB').replace('https://hdbits.org', 'HDB') @@ -80,9 +74,6 @@ def clean_ptp_description(self, desc, is_disc): elif any(x in is_disc for x in ["BDMV", "DVD"]): return "", [] - # Debugging print - # console.print(f"[yellow]Description after removing mediainfo tags:\n{desc[:500]}...") - # Convert Quote tags: desc = re.sub(r"\[quote.*?\]", "[code]", desc) desc = desc.replace("[/quote]", "[/code]") @@ -127,23 +118,21 @@ def clean_ptp_description(self, desc, is_disc): desc = desc.replace(comp, f"COMPARISON_PLACEHOLDER-{i} ") comp_placeholders.append(comp) - # Debugging print - # console.print(f"[yellow]Description after processing comparisons and hides:\n{desc[:500]}...") - # Remove Images in IMG tags: desc = re.sub(r"\[img\][\s\S]*?\[\/img\]", "", desc, flags=re.IGNORECASE) desc = re.sub(r"\[img=[\s\S]*?\]", "", desc, flags=re.IGNORECASE) - # Extract loose images and add to imagelist + # Extract loose images and add to imagelist as dictionaries loose_images = re.findall(r"(https?:\/\/.*\.(?:png|jpg))", nocomp, flags=re.IGNORECASE) if loose_images: - imagelist.extend(loose_images) - console.print(f"[yellow]Loose images found: {len(loose_images)}") - for image in loose_images: - desc = desc.replace(image, '') - - # Debugging print - # console.print(f"[yellow]Final description after removing loose images:\n{desc[:500]}...") + for img_url in loose_images: + image_dict = { + 'img_url': img_url, + 'raw_url': img_url, + 'web_url': img_url # Since there is no distinction here, use the same URL for all + } + imagelist.append(image_dict) + desc = desc.replace(img_url, '') # Re-place comparisons for i, comp in enumerate(comp_placeholders): @@ -164,7 +153,6 @@ def clean_ptp_description(self, desc, is_disc): console.print(f"[yellow]Description is empty after cleaning.") return "", imagelist - # console.print(f"[green]Returning cleaned description and {len(imagelist)} images.") return desc, imagelist def clean_unit3d_description(self, desc, site): diff --git a/src/prep.py b/src/prep.py index a3fb02f1f..b3c5bb1d8 100644 --- a/src/prep.py +++ b/src/prep.py @@ -1158,11 +1158,22 @@ def _is_vob_good(n, loops, num_screens): os.remove(smallest) def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=None): + # Ensure the image list is initialized and preserve existing images + if 'image_list' not in meta: + meta['image_list'] = [] + + # Check if there are already at least 3 image links in the image list + existing_images = [img for img in meta['image_list'] if isinstance(img, dict) and img.get('img_url', '').startswith('http')] + if len(existing_images) >= 3: + console.print("[yellow]There are already at least 3 images in the image list. Skipping additional screenshots.") + return + + # Determine the number of screenshots to take if num_screens is None: - num_screens = self.screens - len(meta.get('image_list', [])) - if num_screens == 0: - # or len(meta.get('image_list', [])) >= num_screens: + num_screens = self.screens - len(existing_images) + if num_screens <= 0: return + with open(f"{base_dir}/tmp/{folder_id}/MediaInfo.json", encoding='utf-8') as f: mi = json.load(f) video_track = mi['media']['track'][1] @@ -1208,17 +1219,17 @@ def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=Non ss_times = [] screen_task = progress.add_task("[green]Saving Screens...", total=num_screens + 1) for i in range(num_screens + 1): - image = os.path.abspath(f"{base_dir}/tmp/{folder_id}/{filename}-{i}.png") - if not os.path.exists(image) or retake is not False: + image_path = os.path.abspath(f"{base_dir}/tmp/{folder_id}/{filename}-{i}.png") + if not os.path.exists(image_path) or retake is not False: retake = False try: - ss_times = self.valid_ss_time(ss_times, num_screens+1, length) + ss_times = self.valid_ss_time(ss_times, num_screens + 1, length) ff = ffmpeg.input(path, ss=ss_times[-1]) if w_sar != 1 or h_sar != 1: ff = ff.filter('scale', int(round(width * w_sar)), int(round(height * h_sar))) ( ff - .output(image, vframes=1, pix_fmt="rgb24") + .output(image_path, vframes=1, pix_fmt="rgb24") .overwrite_output() .global_args('-loglevel', loglevel) .run(quiet=debug) @@ -1226,14 +1237,14 @@ def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=Non except Exception: console.print(traceback.format_exc()) - self.optimize_images(image) - if os.path.getsize(Path(image)) <= 75000: + self.optimize_images(image_path) + if os.path.getsize(Path(image_path)) <= 75000: console.print("[yellow]Image is incredibly small, retaking") retake = True time.sleep(1) - if os.path.getsize(Path(image)) <= 31000000 and self.img_host == "imgbb" and retake is False: + if os.path.getsize(Path(image_path)) <= 31000000 and self.img_host == "imgbb" and retake is False: i += 1 - elif os.path.getsize(Path(image)) <= 10000000 and self.img_host in ["imgbox", 'pixhost'] and retake is False: + elif os.path.getsize(Path(image_path)) <= 10000000 and self.img_host in ["imgbox", 'pixhost'] and retake is False: i += 1 elif self.img_host in ["ptpimg", "lensdump", "ptscreens"] and retake is False: i += 1 @@ -1249,15 +1260,22 @@ def screenshots(self, path, filename, folder_id, base_dir, meta, num_screens=Non else: i += 1 progress.advance(screen_task) - # remove smallest image - smallest = "" - smallestsize = 99 ** 99 - for screens in glob.glob1(f"{base_dir}/tmp/{folder_id}/", f"{filename}-*"): - screensize = os.path.getsize(screens) - if screensize < smallestsize: - smallestsize = screensize - smallest = screens - os.remove(smallest) + + # Add new images to the meta['image_list'] as dictionaries + new_images = glob.glob(f"{filename}-*.png") + for image in new_images: + img_dict = { + 'img_url': image, + 'raw_url': image, + 'web_url': image # Assuming local path, but you might need to update this if uploading + } + meta['image_list'].append(img_dict) + + # Remove the smallest image if there are more than needed + if len(meta['image_list']) > self.screens: + smallest = min(meta['image_list'], key=lambda x: os.path.getsize(x['img_url'])) + os.remove(smallest['img_url']) + meta['image_list'].remove(smallest) def valid_ss_time(self, ss_times, num_screens, length): valid_time = False diff --git a/src/trackers/PTP.py b/src/trackers/PTP.py index 910910fe4..f74a54c91 100644 --- a/src/trackers/PTP.py +++ b/src/trackers/PTP.py @@ -180,7 +180,7 @@ async def get_ptp_description(self, ptp_torrent_id, is_disc): await asyncio.sleep(1) ptp_desc = response.text - console.print(f"[yellow]Raw description received:\n{ptp_desc[:500]}...") # Show first 500 characters for brevity + # console.print(f"[yellow]Raw description received:\n{ptp_desc[:500]}...") # Show first 500 characters for brevity bbcode = BBCODE() desc, imagelist = bbcode.clean_ptp_description(ptp_desc, is_disc)