Skip to content

Commit

Permalink
Update mtz_extractor.py
Browse files Browse the repository at this point in the history
Update the terminal view
  • Loading branch information
ryshaal authored Nov 10, 2024
1 parent 7f78f1e commit 0cb14cc
Showing 1 changed file with 192 additions and 79 deletions.
271 changes: 192 additions & 79 deletions mtz_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,85 @@
import logging
import time
import threading
import random
from typing import Set, Optional
from pathlib import Path
from itertools import cycle
import contextlib
from datetime import datetime


class ColorText:
"""Class untuk menangani warna text di terminal"""

HEADER = "\033[95m"
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
END = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"

@staticmethod
def blue(text: str) -> str:
return f"{ColorText.BLUE}{text}{ColorText.END}"

@staticmethod
def green(text: str) -> str:
return f"{ColorText.GREEN}{text}{ColorText.END}"

@staticmethod
def red(text: str) -> str:
return f"{ColorText.RED}{text}{ColorText.END}"

@staticmethod
def yellow(text: str) -> str:
return f"{ColorText.YELLOW}{text}{ColorText.END}"

@staticmethod
def cyan(text: str) -> str:
return f"{ColorText.CYAN}{text}{ColorText.END}"

@staticmethod
def bold(text: str) -> str:
return f"{ColorText.BOLD}{text}{ColorText.END}"


class LoadingAnimation:
"""Class untuk menangani animasi loading di terminal"""
"""Class untuk menangani animasi loading di terminal dengan efek smooth"""

def __init__(self, description: str = "Processing"):
self.description = description
self.is_running = False
self.animation_thread = None
self.progress = 0
self.animations = {
"dots": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
"line": ["-", "\\", "|", "/"],
"arrow": ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
"pulse": [
"█▁▁▁▁▁▁▁",
"██▁▁▁▁▁▁",
"███▁▁▁▁▁",
"████▁▁▁▁",
"█████▁▁▁",
"██████▁▁",
"███████▁",
"████████",
"smooth_bar": [
"▰▱▱▱▱▱▱▱▱▱",
"▰▰▱▱▱▱▱▱▱▱",
"▰▰▰▱▱▱▱▱▱▱",
"▰▰▰▰▱▱▱▱▱▱",
"▰▰▰▰▰▱▱▱▱▱",
"▰▰▰▰▰▰▱▱▱▱",
"▰▰▰▰▰▰▰▱▱▱",
"▰▰▰▰▰▰▰▰▱▱",
"▰▰▰▰▰▰▰▰▰▱",
"▰▰▰▰▰▰▰▰▰▰",
],
"wave": ["≋", "≈", "≋", "≈", "≋"],
"spinner": ["◜", "◠", "◝", "◞", "◡", "◟"],
"pulse": ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
}
self.animation_chars = self.animations["dots"]
self.colors = [
ColorText.BLUE,
ColorText.CYAN,
ColorText.GREEN,
ColorText.YELLOW,
]
self.current_color = random.choice(self.colors)
self.animation_chars = self.animations["smooth_bar"]

def start(self):
"""Memulai animasi loading"""
Expand All @@ -47,17 +97,19 @@ def stop(self):
self.is_running = False
if self.animation_thread:
self.animation_thread.join()
sys.stdout.write("\r" + " " * (len(self.description) + 20) + "\r")
sys.stdout.write("\r" + " " * (len(self.description) + 30) + "\r")
sys.stdout.flush()

def _animate(self):
"""Fungsi internal untuk menjalankan animasi"""
for char in cycle(self.animation_chars):
if not self.is_running:
break
sys.stdout.write(f"\r{char} {self.description}")
sys.stdout.flush()
time.sleep(0.1)
"""Fungsi internal untuk menjalankan animasi dengan efek smooth"""
while self.is_running:
for frame in self.animation_chars:
if not self.is_running:
break
animation = f"{self.current_color}{frame}{ColorText.END}"
sys.stdout.write(f"\r{animation} {self.description} ")
sys.stdout.flush()
time.sleep(0.1)


@contextlib.contextmanager
Expand All @@ -83,31 +135,70 @@ def __init__(self, allowed_extensions: Set[str] = None):
".RSA",
}
self.setup_logging()
self.stats = {
"start_time": None,
"total_files": 0,
"total_size": 0,
"extracted_size": 0,
}

def setup_logging(self) -> None:
"""Mengatur logging ke file log"""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("mtz_extractor.log", mode="w"),
],
)
logging.info("MTZ Extractor initialized")
log_folder = Path("logs")
log_folder.mkdir(exist_ok=True)

timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = log_folder / f"extraction_{timestamp}.log"

# Konfigurasi logger khusus untuk instance ini
self.logger = logging.getLogger(f'MTZExtractor_{timestamp}')
self.logger.setLevel(logging.INFO)

# Buat file handler dan set formatnya
file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8')
file_handler.setLevel(logging.INFO)

# Buat formatter
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)

# Tambahkan handler ke logger
self.logger.addHandler(file_handler)

# Pastikan log tidak duplikat ke root logger
self.logger.propagate = False

self.logger.info("MTZ Extractor started")
self.logger.info(f"Log file created at: {log_file}")

def print_banner(self):
"""Menampilkan banner aplikasi"""
banner = f"""
{ColorText.cyan('╔═════════════════════════════════════════╗')}
{ColorText.cyan('║')} {ColorText.bold(' MTZ Extractor v2.0 ')} {ColorText.cyan('║')}
{ColorText.cyan('║')} {ColorText.yellow(' BY Ryhshall ')} {ColorText.cyan('║')}
{ColorText.cyan('╚═════════════════════════════════════════╝')}
"""
print(banner)

def format_size(self, size: int) -> str:
"""Format ukuran file dalam bentuk yang mudah dibaca"""
for unit in ["B", "KB", "MB", "GB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} TB"

def validate_mtz_file(self, file_path: str) -> bool:
"""Validasi apakah file adalah MTZ"""
if not os.path.exists(file_path):
logging.error("File tidak ditemukan!")
print("\n❌ File tidak ditemukan!")
print(f"\n{ColorText.red('❌ File tidak ditemukan!')}")
return False

if not file_path.endswith(".mtz"):
logging.error("File bukan format MTZ!")
print("\n❌ File bukan format MTZ!")
print(f"\n{ColorText.red('❌ File bukan format MTZ!')}")
return False

logging.info("File validasi berhasil: %s", file_path)
return True

def create_extract_folder(self, file_path: str) -> Optional[str]:
Expand All @@ -123,90 +214,112 @@ def create_extract_folder(self, file_path: str) -> Optional[str]:
counter += 1

extract_folder.mkdir(parents=True, exist_ok=True)
logging.info("Folder ekstraksi dibuat: %s", extract_folder)
return str(extract_folder)

except Exception as e:
logging.error("Gagal membuat folder ekstraksi: %s", str(e))
print(f"\n❌ Gagal membuat folder: {str(e)}")
print(f"\n{ColorText.red(f'❌ Gagal membuat folder: {str(e)}')}")
return None

def extract_mtz(self, file_path: str, extract_folder: str) -> bool:
"""Ekstraksi file MTZ ke folder"""
try:
with loading_animation("Mengekstrak"):
self.stats["start_time"] = time.time()
self.stats["total_size"] = os.path.getsize(file_path)

with loading_animation(
f"Mengekstrak {ColorText.yellow(os.path.basename(file_path))} ({self.format_size(self.stats['total_size'])})"
):
with zipfile.ZipFile(file_path, "r") as zip_ref:
zip_ref.extractall(extract_folder)
logging.info("File berhasil diekstrak ke %s", extract_folder)
self.stats["total_files"] = len(zip_ref.namelist())
return True
except Exception as e:
logging.error("Gagal ekstrak: %s", str(e))
print(f"\n❌ Gagal ekstrak: {str(e)}")
print(f"\n{ColorText.red(f'❌ Gagal ekstrak: {str(e)}')}")
return False

def process_files(self, folder: str) -> None:
"""Proses file setelah diekstrak"""
with loading_animation("Memproses"):
with loading_animation(f"Memproses {ColorText.yellow(os.path.basename(folder))}"):
self._add_zip_extension_to_files(folder)
self._unzip_files_to_folders(folder)
self._cleanup_empty_folders(folder)
self.stats["extracted_size"] = self.calculate_folder_size(folder)

def calculate_folder_size(self, folder: str) -> int:
"""Menghitung ukuran total folder"""
total_size = 0
for dirpath, _, filenames in os.walk(folder):
for f in filenames:
fp = os.path.join(dirpath, f)
total_size += os.path.getsize(fp)
return total_size

def _add_zip_extension_to_files(self, folder: str) -> None:
"""Tambahkan ekstensi .zip pada file yang bukan ekstensi yang diizinkan"""
try:
for file_path in Path(folder).rglob("*"):
if file_path.is_file() and file_path.suffix not in self.allowed_extensions:
new_path = file_path.with_suffix(file_path.suffix + ".zip")
file_path.rename(new_path)
logging.info("Ditambahkan ekstensi .zip: %s", new_path)
except Exception as e:
logging.error("Error menambahkan ekstensi .zip: %s", str(e))
for file_path in Path(folder).rglob("*"):
if file_path.is_file() and file_path.suffix not in self.allowed_extensions:
new_path = file_path.with_suffix(file_path.suffix + ".zip")
file_path.rename(new_path)

def _unzip_files_to_folders(self, folder: str) -> None:
"""Ekstraksi file .zip ke folder masing-masing"""
try:
for file_path in Path(folder).rglob("*.zip"):
if file_path.is_file():
folder_path = file_path.with_suffix("")
folder_path.mkdir(exist_ok=True)
for file_path in Path(folder).rglob("*.zip"):
if file_path.is_file():
folder_path = file_path.with_suffix("")
folder_path.mkdir(exist_ok=True)

try:
with zipfile.ZipFile(file_path, "r") as zip_ref:
zip_ref.extractall(folder_path)
logging.info("File .zip diekstrak: %s", folder_path)

file_path.unlink() # Hapus file .zip setelah diekstrak
except Exception as e:
logging.error("Error mengekstrak file .zip: %s", str(e))
file_path.unlink()
except Exception:
continue

def _cleanup_empty_folders(self, folder: str) -> None:
"""Hapus folder kosong"""
try:
for dir_path in Path(folder).rglob("*"):
if dir_path.is_dir() and not any(dir_path.iterdir()):
dir_path.rmdir()
logging.info("Folder kosong dihapus: %s", dir_path)
except Exception as e:
logging.error("Error menghapus folder kosong: %s", str(e))
for dirpath, dirnames, filenames in os.walk(folder, topdown=False):
if not dirnames and not filenames:
try:
os.rmdir(dirpath)
except OSError:
continue

def show_completion(self, extract_folder: str):
"""Menampilkan pesan selesai"""
"""Menampilkan pesan selesai dengan statistik"""
os.system("cls" if os.name == "nt" else "clear")
print("\n✨ Ekstraksi Berhasil! ✨")
print(f"📁 Tersimpan di: {extract_folder}\n")
logging.info("Ekstraksi selesai. Tersimpan di %s", extract_folder)
completion_time = time.time() - self.stats["start_time"]
expansion_ratio = (
(self.stats["extracted_size"] / self.stats["total_size"]) * 100
if self.stats["total_size"] > 0
else 0
)

print(f"\n{ColorText.green('✨ Ekstraksi Berhasil! ✨')}")
print(f"\n{ColorText.cyan('📊 Statistik Ekstraksi:')}")
print(f"├─ Total file: {ColorText.yellow(str(self.stats['total_files']))}")
print(
f"├─ Ukuran awal: {ColorText.yellow(self.format_size(self.stats['total_size']))}"
)
print(
f"├─ Ukuran akhir: {ColorText.yellow(self.format_size(self.stats['extracted_size']))}"
)
print(f"├─ Rasio ekspansi: {ColorText.yellow(f'{expansion_ratio:.1f}%')}")
print(f"├─ Waktu proses: {ColorText.yellow(f'{completion_time:.1f} detik')}")
print(f"└─ Lokasi: {ColorText.yellow(extract_folder)}\n")


def get_user_input() -> str:
"""Fungsi untuk mendapatkan input dari user"""
return input("Masukan lokasi file .mtz: ").strip()
return input(
f"{ColorText.cyan('📂')} Masukkan lokasi file MTZ: "
).strip()


def main():
"""Fungsi utama"""
os.system("cls" if os.name == "nt" else "clear")
print("🗂️ MTZ Extractor by Ryshall\n")

extractor = MTZExtractor()
extractor.print_banner()

try:
file_path = get_user_input()
Expand All @@ -218,6 +331,8 @@ def main():
if not extract_folder:
sys.exit(1)

print(f"\n{ColorText.cyan('⏳')} Memulai proses ekstraksi...\n")

if not extractor.extract_mtz(file_path, extract_folder):
sys.exit(1)

Expand All @@ -226,12 +341,10 @@ def main():

except KeyboardInterrupt:
os.system("cls" if os.name == "nt" else "clear")
print("\n⚠️ Dibatalkan!\n")
logging.info("Proses dibatalkan oleh user.")
print(f"\n{ColorText.yellow('⚠️ Dibatalkan!')}\n")
sys.exit(0)
except Exception as e:
logging.error("Error: %s", str(e))
print(f"\n❌ Error: {str(e)}")
print(f"\n{ColorText.red('❌ Error:')} {str(e)}\n")
sys.exit(1)


Expand Down

0 comments on commit 0cb14cc

Please sign in to comment.