diff --git a/src/kemonodownloader/creator_downloader.py b/src/kemonodownloader/creator_downloader.py index eacfa91..5eef105 100644 --- a/src/kemonodownloader/creator_downloader.py +++ b/src/kemonodownloader/creator_downloader.py @@ -22,7 +22,7 @@ import aiohttp from aiohttp import ClientSession, ClientTimeout import threading - +import base64 class ThreadSettings: """Settings container for thread operations""" @@ -453,7 +453,7 @@ def get_effective_extension(file_path, file_name): if 'f=' not in file_url and file_name: file_url += f"?f={file_name}" self.log.emit(translate("log_debug", translate("checking_main_file", file_name, file_ext)), "INFO") - if '.jpg' in allowed_extensions and file_ext in ['.jpg', '.jpeg']: + if '.jpg' in allowed_extensions and file_ext in ['.jpg', '.jpeg', '.jfif']: self.log.emit(translate("log_debug", translate("added_main_file", file_name)), "INFO") files_to_download.append((file_name, file_url)) elif file_ext in allowed_extensions: @@ -471,7 +471,7 @@ def get_effective_extension(file_path, file_name): if 'f=' not in attachment_url and attachment_name: attachment_url += f"?f={attachment_name}" self.log.emit(translate("log_debug", translate("checking_attachment", attachment_name, attachment_ext)), "INFO") - if '.jpg' in allowed_extensions and attachment_ext in ['.jpg', '.jpeg']: + if '.jpg' in allowed_extensions and attachment_ext in ['.jpg', '.jpeg', '.jfif']: self.log.emit(translate("log_debug", translate("added_attachment", attachment_name)), "INFO") files_to_download.append((attachment_name, attachment_url)) elif attachment_ext in allowed_extensions: @@ -486,13 +486,43 @@ def get_effective_extension(file_path, file_name): img_ext = os.path.splitext(img_url)[1].lower() img_name = os.path.basename(img_url) self.log.emit(translate("log_debug", translate("checking_content_image", img_name, img_ext)), "INFO") - if '.jpg' in allowed_extensions and img_ext in ['.jpg', '.jpeg']: + if '.jpg' in allowed_extensions and img_ext in ['.jpg', '.jpeg', '.jfif']: self.log.emit(translate("log_debug", translate("added_content_image", img_name)), "INFO") files_to_download.append((img_name, img_url)) elif img_ext in allowed_extensions: self.log.emit(translate("log_debug", translate("added_content_image", img_name)), "INFO") files_to_download.append((img_name, img_url)) + # External link detection from 'embed' object + if 'embed' in post and post['embed'] and 'url' in post['embed']: + embed_url = post['embed']['url'] + subject = post['embed'].get('subject', 'external_link') + link_filename = f"{sanitize_filename(subject)}_link.txt" + + # Use a special prefix to identify this as a link to be saved, not downloaded + special_url = f"save_link_to_txt:{embed_url}" + + # Check if TXT files are allowed + if '.txt' in allowed_extensions: + self.log.emit(translate("log_debug", f"Added external link: {link_filename}"), "INFO") + files_to_download.append((link_filename, special_url)) + + if self.creator_content_check and 'content' in post and post['content']: + soup = BeautifulSoup(post['content'], 'html.parser') + plain_text = soup.get_text(separator='\n').strip() + if plain_text: + content_filename = "post_content.txt" + + # Encode the content to safely pass it in the URL-like string + encoded_content = base64.b64encode(plain_text.encode('utf-8')).decode('utf-8') + special_url = f"save_content_to_txt:{encoded_content}" + + if '.txt' in allowed_extensions: + self.log.emit(translate("log_debug", f"Added text content file: {content_filename}"), "INFO") + files_to_download.append((content_filename, special_url)) + + + self.log.emit(translate("log_debug", translate("total_files_detected", len(files_to_download))), "INFO") return list(dict.fromkeys(files_to_download)) @@ -572,7 +602,7 @@ def run(self): post_id, detected_files = result for file_name, file_url in detected_files: self.log.emit(translate("log_debug", translate("detected_file", file_name, file_url)), "INFO") - files_to_download.append(file_url) + files_to_download.append((file_name, file_url)) files_to_posts_map[file_url] = post_id completed_posts += 1 progress = min(int((completed_posts / total_posts) * 100), 100) @@ -649,7 +679,7 @@ def _get_domain_config_from_files(self): def build_post_files_map(self): post_files_map = {post_id: [] for post_id in self.selected_posts} - for file_url in self.files_to_download: + for _, file_url in self.files_to_download: post_id = self.files_to_posts_map.get(file_url) if post_id in post_files_map: post_files_map[post_id].append(file_url) @@ -714,8 +744,8 @@ def fetch_creator_and_post_info(self): def stop(self): self.is_running = False - async def download_file(self, file_url, folder, file_index, total_files, session): - if not self.is_running or file_url not in self.files_to_download: + async def download_file(self, filename, file_url, folder, file_index, total_files, session): + if not self.is_running: self.log.emit(translate("log_info", f"Skipping {file_url}"), "INFO") return @@ -735,27 +765,60 @@ async def download_file(self, file_url, folder, file_index, total_files, session self.check_post_completion(file_url) return - filename = file_url.split('f=')[-1] if 'f=' in file_url else file_url.split('/')[-1].split('?')[0] - # Apply auto rename if enabled if self.auto_rename_enabled: - # Initialize counter for this post if not exists with self.post_file_counters_lock: if post_id not in self.post_file_counters: self.post_file_counters[post_id] = 0 - - # Increment counter for this post self.post_file_counters[post_id] += 1 file_counter = self.post_file_counters[post_id] - - # Get file extension file_ext = os.path.splitext(filename)[1] - # Get original filename without extension original_name = os.path.splitext(filename)[0] - # Create new filename with counter and original name filename = f"{file_counter}_{original_name}{file_ext}" - + full_path = os.path.join(post_folder, filename.replace('/', '_')) + + # Handle saving links to .txt files + if file_url.startswith("save_link_to_txt:"): + real_url = file_url.replace("save_link_to_txt:", "", 1) + try: + with open(full_path, 'w', encoding='utf-8') as f: + f.write(real_url) + self.log.emit(translate("log_info", f"Saved link to {full_path}"), "INFO") + with self.completed_files_lock: + self.completed_files.add(file_url) + self.file_completed.emit(file_index, file_url, True) + self.check_post_completion(file_url) + except Exception as e: + error_msg = f"Failed to save link to {full_path}: {str(e)}" + self.log.emit(translate("log_error", error_msg), "ERROR") + with self.failed_files_lock: + self.failed_files[file_url] = str(e) + self.file_completed.emit(file_index, file_url, False) + self.check_post_completion(file_url) + return + + elif file_url.startswith("save_content_to_txt:"): + encoded_content = file_url.replace("save_content_to_txt:", "", 1) + try: + content_bytes = base64.b64decode(encoded_content) + with open(full_path, 'wb') as f: # Write as bytes + f.write(content_bytes) + self.log.emit(translate("log_info", f"Saved post content to {full_path}"), "INFO") + with self.completed_files_lock: + self.completed_files.add(file_url) + self.file_completed.emit(file_index, file_url, True) + self.check_post_completion(file_url) + except Exception as e: + error_msg = f"Failed to save post content to {full_path}: {str(e)}" + self.log.emit(translate("log_error", error_msg), "ERROR") + with self.failed_files_lock: + self.failed_files[file_url] = str(e) + self.file_completed.emit(file_index, file_url, False) + self.check_post_completion(file_url) + return + + # Normal file download logic url_hash = hashlib.md5(file_url.encode()).hexdigest() with self.file_hashes_lock: @@ -818,18 +881,15 @@ async def download_file(self, file_url, folder, file_index, total_files, session file_handle.close() file_handle = None - # Validate downloaded size matches content-length if file_size > 0 and downloaded_size != file_size: error_msg = translate("size_mismatch_error", downloaded_size, file_size, file_url) self.log.emit(translate("log_warning", error_msg), "WARNING") - # Delete incomplete file if os.path.exists(full_path): try: os.remove(full_path) self.log.emit(translate("log_info", translate("deleted_incomplete_file", full_path)), "INFO") except OSError as e: self.log.emit(translate("log_error", translate("failed_to_delete_incomplete_file", full_path, str(e))), "ERROR") - # Raise exception to trigger retry raise Exception(f"Size mismatch: downloaded {downloaded_size} bytes, expected {file_size} bytes") with open(full_path, 'rb') as f: @@ -879,7 +939,6 @@ async def download_file(self, file_url, folder, file_index, total_files, session if file_handle: file_handle.close() file_handle = None - def check_post_completion(self, file_url): post_id = self.files_to_posts_map.get(file_url) if post_id in self.post_files_map: @@ -890,8 +949,8 @@ def check_post_completion(self, file_url): async def download_worker(self, queue, folder, total_files, session): while self.is_running: try: - file_index, file_url = await queue.get() - await self.download_file(file_url, folder, file_index, total_files, session) + file_index, filename, file_url = await queue.get() + await self.download_file(filename, file_url, folder, file_index, total_files, session) queue.task_done() except asyncio.QueueEmpty: break @@ -923,8 +982,9 @@ def run(self): asyncio.set_event_loop(loop) try: queue = asyncio.Queue() - for i, file_url in enumerate(self.files_to_download): - queue.put_nowait((i, file_url)) + for i, (filename, file_url) in enumerate(self.files_to_download): + queue.put_nowait((i, filename, file_url)) + async def main(): async with ClientSession() as session: @@ -1257,7 +1317,7 @@ def setup_ui(self): creator_ext_layout.setHorizontalSpacing(20) creator_ext_layout.setVerticalSpacing(10) self.creator_ext_checks = { - '.jpg': QCheckBox("JPG/JPEG"), + '.jpg': QCheckBox("JPG/JPEG/JFIF"), '.png': QCheckBox("PNG"), '.zip': QCheckBox("ZIP"), '.mp4': QCheckBox("MP4"), @@ -1272,12 +1332,13 @@ def setup_ui(self): '.psd': QCheckBox("PSD"), '.clip': QCheckBox("CLIP"), '.jpe':QCheckBox("JPE"), - '.webp':QCheckBox("WEBP") + '.webp':QCheckBox("WEBP"), + '.txt': QCheckBox("TXT") } for i, (ext, check) in enumerate(self.creator_ext_checks.items()): check.setChecked(True) check.stateChanged.connect(self.filter_items) - creator_ext_layout.addWidget(check, i // 5, i % 5) + creator_ext_layout.addWidget(check, i // 6, i % 6) self.creator_ext_group.setLayout(creator_ext_layout) creator_options_layout.addWidget(self.creator_ext_group) self.creator_options_group.setLayout(creator_options_layout) diff --git a/src/kemonodownloader/post_downloader.py b/src/kemonodownloader/post_downloader.py index 7e668d6..2b594b1 100644 --- a/src/kemonodownloader/post_downloader.py +++ b/src/kemonodownloader/post_downloader.py @@ -23,7 +23,7 @@ from fake_useragent import UserAgent import gzip import threading - +import base64 class ThreadSettings: """Settings container for thread operations""" @@ -102,7 +102,7 @@ def run(self): cache_path = os.path.join(self.cache_dir, cache_key) if os.path.exists(cache_path): - if ext in ['.jpg', '.jpeg', '.png']: + if ext in ['.jpg', '.jpeg', '.png', '.jfif']: pixmap = QPixmap() if pixmap.load(cache_path): self.preview_ready.emit(self.url, pixmap) @@ -128,7 +128,7 @@ def run(self): progress = int((self.downloaded_size / self.total_size) * 100) self.progress.emit(min(progress, 100)) - if ext in ['.jpg', '.jpeg', '.png']: + if ext in ['.jpg', '.jpeg', '.png', '.jfif']: pixmap = QPixmap() if not pixmap.loadFromData(downloaded_data): self.error.emit(f"{translate('error_loading_image')}: {self.url}: {translate('invalid_image_data')}") @@ -254,7 +254,7 @@ def init_ui(self): def start_preview(self): ext = os.path.splitext(self.media_url.lower())[1] - if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: + if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.jfif']: self.preview_thread = PreviewThread(self.media_url, self.cache_dir) self.preview_thread.preview_ready.connect(self.display_image) self.preview_thread.progress.connect(self.update_progress) @@ -616,7 +616,7 @@ def parse_response_content(self, response): def detect_files(self, post): detected_files = [] allowed_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.zip', '.mp4', '.pdf', '.7z', - '.mp3', '.wav', '.rar', '.mov', '.docx', '.psd', '.clip', '.jpe', '.webp'] + '.mp3', '.wav', '.rar', '.mov', '.docx', '.psd', '.clip', '.jpe', '.webp', '.jfif'] def get_effective_extension(file_path, file_name): name_ext = os.path.splitext(file_name)[1].lower() @@ -654,6 +654,23 @@ def get_effective_extension(file_path, file_name): if img_ext in allowed_extensions: detected_files.append((img_name, img_url)) + if 'content' in post and post['content']: + soup = BeautifulSoup(post['content'], 'html.parser') + plain_text = soup.get_text(separator='\n').strip() + if plain_text: + content_filename = "post_content.txt" + encoded_content = base64.b64encode(plain_text.encode('utf-8')).decode('utf-8') + special_url = f"save_content_to_txt:{encoded_content}" + detected_files.append((content_filename, special_url)) + + + if 'embed' in post and post['embed'] and 'url' in post['embed']: + embed_url = post['embed']['url'] + subject = post['embed'].get('subject', 'external_link') + link_filename = f"{sanitize_filename(subject)}_link.txt" + special_url = f"save_link_to_txt:{embed_url}" + detected_files.append((link_filename, special_url)) + return list(dict.fromkeys(detected_files)) class FilePreparationThread(QThread): @@ -733,6 +750,18 @@ def get_effective_extension(file_path, file_name): self.log.emit(translate("log_debug", f"Added content image: {img_name}"), "INFO") files_to_download.append((img_name, img_url)) + # External link detection from 'embed' object + if 'embed' in post and post['embed'] and 'url' in post['embed']: + embed_url = post['embed']['url'] + subject = post['embed'].get('subject', 'external_link') + link_filename = f"{sanitize_filename(subject)}_link.txt" + special_url = f"save_link_to_txt:{embed_url}" + + if '.txt' in allowed_extensions: + self.log.emit(translate("log_debug", f"Added external link: {link_filename}"), "INFO") + files_to_download.append((link_filename, special_url)) + + self.log.emit(translate("log_debug", f"Total files detected: {len(files_to_download)}"), "INFO") return list(dict.fromkeys(files_to_download)) @@ -833,7 +862,7 @@ def run(self): post_id, detected_files = result for file_name, file_url in detected_files: self.log.emit(translate("log_debug", f"Detected file: {file_name} from {file_url}"), "INFO") - files_to_download.append(file_url) + files_to_download.append((file_name, file_url)) files_to_posts_map[file_url] = post_id completed_posts += 1 progress = min(int((completed_posts / total_posts) * 100), 100) @@ -960,7 +989,7 @@ def extract_service_from_url(self, url): def build_post_files_map(self): post_files_map = {self.post_id: []} - for file_url in self.selected_files: + for _, file_url in self.selected_files: post_id = self.files_to_posts_map.get(file_url) if post_id == self.post_id: post_files_map[post_id].append(file_url) @@ -989,8 +1018,8 @@ def stop(self): self.is_running = False self.log.emit(translate("log_info", "DownloadThread cancellation initiated"), "INFO") - def download_file(self, file_url, folder, file_index, total_files): - if not self.is_running or file_url not in self.selected_files: + def download_file(self, filename, file_url, folder, file_index, total_files): + if not self.is_running: self.log.emit(translate("log_info", f"Skipping {file_url} due to cancellation"), "INFO") return @@ -999,8 +1028,6 @@ def download_file(self, file_url, folder, file_index, total_files): post_folder_name = f"{post_id}_{self.post_title}" post_folder = os.path.join(service_folder, post_folder_name) os.makedirs(post_folder, exist_ok=True) - - filename = file_url.split('f=')[-1] if 'f=' in file_url else file_url.split('/')[-1].split('?')[0] # Handle auto rename if enabled if hasattr(self, 'auto_rename') and self.auto_rename: @@ -1009,6 +1036,39 @@ def download_file(self, file_url, folder, file_index, total_files): filename = f"{file_index + 1}_{base_name}{file_extension}" full_path = os.path.join(post_folder, filename.replace('/', '_')) + + # Handle saving links to .txt files + if file_url.startswith("save_link_to_txt:"): + real_url = file_url.replace("save_link_to_txt:", "", 1) + try: + with open(full_path, 'w', encoding='utf-8') as f: + f.write(real_url) + self.log.emit(translate("log_info", f"Saved link to {full_path}"), "INFO") + with self.completed_files_lock: + self.completed_files.add(file_url) + self.file_completed.emit(file_index, file_url) + self.check_post_completion(file_url) + except Exception as e: + self.log.emit(translate("log_error", f"Failed to save link to {full_path}: {str(e)}"), "ERROR") + self.file_progress.emit(file_index, 0) + return + elif file_url.startswith("save_content_to_txt:"): + encoded_content = file_url.replace("save_content_to_txt:", "", 1) + try: + content_bytes = base64.b64decode(encoded_content) + with open(full_path, 'wb') as f: # Write as bytes + f.write(content_bytes) + self.log.emit(translate("log_info", f"Saved post content to {full_path}"), "INFO") + with self.completed_files_lock: + self.completed_files.add(file_url) + self.file_completed.emit(file_index, file_url) + self.check_post_completion(file_url) + except Exception as e: + self.log.emit(translate("log_error", f"Failed to save post content to {full_path}: {str(e)}"), "ERROR") + self.file_progress.emit(file_index, 0) + return + + # Normal file download logic url_hash = hashlib.md5(file_url.encode()).hexdigest() with self.file_hashes_lock: @@ -1056,18 +1116,15 @@ def download_file(self, file_url, folder, file_index, total_files): if progress == 100: self.file_completed.emit(file_index, file_url) - # Validate downloaded size matches content-length if file_size > 0 and downloaded_size != file_size: error_msg = translate("size_mismatch_error", downloaded_size, file_size, file_url) self.log.emit(translate("log_warning", error_msg), "WARNING") - # Delete incomplete file if os.path.exists(full_path): try: os.remove(full_path) self.log.emit(translate("log_info", translate("deleted_incomplete_file", full_path)), "INFO") except OSError as e: self.log.emit(translate("log_error", translate("failed_to_delete_incomplete_file", full_path, str(e))), "ERROR") - # Raise exception to trigger retry raise Exception(f"Size mismatch: downloaded {downloaded_size} bytes, expected {file_size} bytes") with open(full_path, 'rb') as f: @@ -1120,8 +1177,8 @@ def run(self): if total_files > 0: with ThreadPoolExecutor(max_workers=self.max_concurrent) as executor: - futures = {executor.submit(self.download_file, file_url, self.download_folder, i, total_files): i - for i, file_url in enumerate(self.selected_files)} + futures = {executor.submit(self.download_file, filename, file_url, self.download_folder, i, total_files): i + for i, (filename, file_url) in enumerate(self.selected_files)} for future in as_completed(futures): if not self.is_running: break @@ -1235,6 +1292,7 @@ def __init__(self, parent): self.current_post_url = None self.all_files_map = {} self.all_detected_posts = [] + self.url_file_map = {} self.post_url_map = {} self.total_files_to_download = 0 self.completed_files = set() @@ -1372,12 +1430,13 @@ def setup_ui(self): '.pdf': QCheckBox("PDF"), '.7z': QCheckBox("7Z"), '.mp3': QCheckBox("MP3"), '.wav': QCheckBox("WAV"), '.rar': QCheckBox("RAR"), '.mov': QCheckBox("MOV"), '.docx': QCheckBox("DOCX"), '.psd': QCheckBox("PSD"), - '.clip': QCheckBox("CLIP"), '.jpe':QCheckBox("JPE"), '.webp':QCheckBox("WEBP") + '.clip': QCheckBox("CLIP"), '.jpe':QCheckBox("JPE"), '.jfif': QCheckBox("JFIF"), + '.webp':QCheckBox("WEBP"), '.txt': QCheckBox("TXT") } for i, (ext, check) in enumerate(self.post_filter_checks.items()): check.setChecked(True) check.stateChanged.connect(self.filter_items) - filter_layout.addWidget(check, i // 4, i % 4) + filter_layout.addWidget(check, i // 5, i % 5) self.post_filter_group.setLayout(filter_layout) file_list_layout.addWidget(self.post_filter_group) @@ -1668,8 +1727,9 @@ def display_files_for_post(self, url): post_data = self.parse_response_content(response) post = post_data if isinstance(post_data, dict) and 'post' not in post_data else post_data.get('post', {}) allowed_extensions = [ext.lower() for ext, check in self.post_filter_checks.items() if check.isChecked()] - self.all_detected_files = self.detect_files(post, allowed_extensions) + self.all_detected_files = self.detect_files(post, allowed_extensions) self.file_url_map = {file_name: file_url for file_name, file_url in self.all_detected_files} + self.url_file_map = {file_url: file_name for file_name, file_url in self.all_detected_files} self.checked_urls.clear() for file_name, file_url in self.all_detected_files: @@ -1760,6 +1820,25 @@ def get_effective_extension(file_path, file_name): elif img_ext in allowed_extensions: detected_files.append((img_name, img_url)) + if 'content' in post and post['content']: + soup = BeautifulSoup(post['content'], 'html.parser') + plain_text = soup.get_text(separator='\n').strip() + if plain_text: + content_filename = "post_content.txt" + encoded_content = base64.b64encode(plain_text.encode('utf-8')).decode('utf-8') + special_url = f"save_content_to_txt:{encoded_content}" + if '.txt' in allowed_extensions: + detected_files.append((content_filename, special_url)) + + + if 'embed' in post and post['embed'] and 'url' in post['embed']: + embed_url = post['embed']['url'] + subject = post['embed'].get('subject', 'external_link') + link_filename = f"{sanitize_filename(subject)}_link.txt" + special_url = f"save_link_to_txt:{embed_url}" + if '.txt' in allowed_extensions: + detected_files.append((link_filename, special_url)) + return list(dict.fromkeys(detected_files)) def check_all_posts(self): @@ -1900,25 +1979,24 @@ def update_background_progress(self, value): def on_file_preparation_finished(self, urls, files_to_download, files_to_posts_map): self.append_log_to_console(translate("log_debug", f"Files prepared for URLs: {urls}, Total files: {len(files_to_download)}"), "INFO") - for file_url in files_to_download: + for file_name, file_url in files_to_download: + self.url_file_map[file_url] = file_name if file_url not in self.checked_urls: self.checked_urls[file_url] = True self.append_log_to_console(translate("log_debug", f"Updated checked_urls after preparation: {self.checked_urls}"), "INFO") active_filters = [ext.lower() for ext, check in self.post_filter_checks.items() if check.isChecked()] - checked_files = [] - for file_url in files_to_download: + checked_files_with_names = [] + for file_name, file_url in files_to_download: if not self.checked_urls.get(file_url, False): continue - file_name = file_url.split('f=')[-1] if 'f=' in file_url else file_url.split('/')[-1] file_ext = os.path.splitext(file_name)[1].lower() if not active_filters or file_ext in active_filters or (file_ext == '.jpeg' and '.jpg' in active_filters): - checked_files.append(file_url) + checked_files_with_names.append((file_name, file_url)) - self.append_log_to_console(translate("log_debug", f"Checked files after filtering: {len(checked_files)}"), "INFO") - self.append_log_to_console(translate("log_debug", f"Checked files list: {checked_files}"), "INFO") + self.append_log_to_console(translate("log_debug", f"Checked files after filtering: {len(checked_files_with_names)}"), "INFO") - if not checked_files: + if not checked_files_with_names: self.append_log_to_console(translate("log_warning", f"No files to download for URLs: {urls}. Proceeding to next post."), "WARNING") self.process_next_post(urls[1:] if len(urls) > 1 else []) return @@ -1932,7 +2010,8 @@ def on_file_preparation_finished(self, urls, files_to_download, files_to_posts_m settings = self._create_thread_settings() max_concurrent = settings.simultaneous_downloads auto_rename = self.auto_rename_checkbox.isChecked() - self.thread = DownloadThread(url, self.parent.download_folder, checked_files, files_to_posts_map, + + self.thread = DownloadThread(url, self.parent.download_folder, checked_files_with_names, files_to_posts_map, self.post_console, self.other_files_dir, post_id, settings, max_concurrent, auto_rename) self.active_threads.append(self.thread) self.thread.file_progress.connect(self.update_file_progress) @@ -2243,7 +2322,7 @@ def view_current_item(self): if self.current_preview_url: ext = os.path.splitext(self.current_preview_url.lower())[1] unsupported_extensions = ['.zip', '.psd', '.docx', '.7z', '.rar', '.clip','jpe'] - supported_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mov', '.mp3', '.wav', '.webp'] + supported_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mov', '.mp3', '.wav', '.webp', '.jfif'] if ext in unsupported_extensions: self.append_log_to_console(translate("log_warning", translate("preview_not_supported", ext, self.current_preview_url)), "WARNING")