📌 Giới thiệu

Bài viết này hướng dẫn cài đặt và sử dụng SotiMediaOrganizer (SMO) – một công cụ mạnh mẽ để tự động phân loại ảnh và video theo ngày tháng, phát hiện và xử lý file trùng lặp trên Ubuntu Server.

Ưu điểm

  • 🚀 Phân loại tự động theo metadata (ngày chụp)

  • 📁 Cấu trúc thư mục rõ ràng: Năm/Tháng/Ngày/

  • 🔍 Phát hiện và xử lý file trùng lặp

  • 🎬 Hỗ trợ cả ẢNH và VIDEO

  • 💾 Di chuyển file (move) thay vì copy, tiết kiệm dung lượng

  • 🖥️ Chạy được trên cấu hình thấp (Intel N5105, 8GB RAM vẫn tốt)


🖥️ Yêu cầu hệ thống

  • Hệ điều hành: Ubuntu 20.04 hoặc mới hơn

  • CPU: 2 nhân trở lên (khuyến nghị 4 nhân)

  • RAM: Tối thiểu 4GB (khuyến nghị 8GB)

  • Dung lượng trống: Đủ để chứa ảnh sau khi xử lý


📦 Cài đặt công cụ

Bước 1: Cập nhật hệ thống và cài đặt dependencies

bash
sudo apt update
sudo apt upgrade -y
sudo apt install -y curl git exiftool

Bước 2: Cài đặt Node.js (bản 18 LTS)

bash
# Thêm repository NodeSource
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo bash -

# Cài đặt Node.js
sudo apt install -y nodejs

# Kiểm tra
node --version  # Phải hiển thị v18.x.x

Bước 3: Cài đặt SMO (SotiMediaOrganizer)

bash
# Cấu hình npm để cài trong thư mục user (không cần sudo)
mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'

# Thêm vào PATH
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

# Cài đặt SMO
npm install -g @sotilab/smo

# Kiểm tra
smo --help

📁 Chuẩn bị cấu trúc thư mục

Tạo các thư mục cần thiết

bash
# Thư mục gốc (điều chỉnh theo đường dẫn thực tế của bạn)
cd /home/your_user/Data

# Tạo cấu trúc thư mục đích
mkdir -p ANH_CHUP_PROCESSED/{Images,Videos,Others}
mkdir -p DUPLICATES/{Images,Videos,Others}
mkdir -p ERRORS
mkdir -p LOGS

Giải thích các thư mục:

  • ANH_CHUP: Thư mục chứa ảnh/video gốc (cần xử lý)

  • ANH_CHUP_PROCESSED: Thư mục chứa kết quả đã phân loại

    • Images: Ảnh đã phân loại theo ngày

    • Videos: Video đã phân loại theo ngày

    • Others: File khác (RAW, PDF, DOC…)

  • DUPLICATES: File trùng lặp được di chuyển vào đây

  • ERRORS: File bị lỗi (hỏng, không đọc được metadata)

  • LOGS: Log file chi tiết của quá trình xử lý


🐍 Script xử lý toàn diện (Ảnh + Video + File khác)

Tạo file script với nội dung bên dưới:

bash
nano ~/organize_media.py
python
#!/usr/bin/env python3
import os
import shutil
import subprocess
from pathlib import Path
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import sys

# ============================================
# CẤU HÌNH - CHỈNH SỬA THEO ĐƯỜNG DẪN CỦA BẠN
# ============================================
SOURCE = "/home/your_user/Data/ANH_CHUP"
DEST_IMAGES = "/home/your_user/Data/ANH_CHUP_PROCESSED/Images"
DEST_VIDEOS = "/home/your_user/Data/ANH_CHUP_PROCESSED/Videos"
DEST_OTHERS = "/home/your_user/Data/ANH_CHUP_PROCESSED/Others"
DUPLICATES_IMAGES = "/home/your_user/Data/DUPLICATES/Images"
DUPLICATES_VIDEOS = "/home/your_user/Data/DUPLICATES/Videos"
DUPLICATES_OTHERS = "/home/your_user/Data/DUPLICATES/Others"
ERRORS = "/home/your_user/Data/ERRORS"
LOG_FILE = "/home/your_user/Data/organize_media_log.txt"

# Định dạng file được hỗ trợ
IMAGE_EXT = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.heic', '.jfif'}
VIDEO_EXT = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v', '.3gp', '.mpg', '.mpeg', '.webm'}
# Các file còn lại sẽ vào thư mục Others

# ============================================
# CÁC HÀM XỬ LÝ (KHÔNG CẦN THAY ĐỔI)
# ============================================
def log_message(msg):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    log_line = f"[{timestamp}] {msg}"
    print(log_line)
    with open(LOG_FILE, 'a') as f:
        f.write(log_line + '\n')

def get_file_type(filepath):
    ext = Path(filepath).suffix.lower()
    if ext in IMAGE_EXT:
        return 'image'
    elif ext in VIDEO_EXT:
        return 'video'
    else:
        return 'other'

def get_date_taken(filepath):
    try:
        result = subprocess.run(
            ['exiftool', '-DateTimeOriginal', '-d', '%Y/%m/%d', filepath],
            capture_output=True, text=True, timeout=3
        )
        if result.returncode == 0 and result.stdout and ':' in result.stdout:
            date_str = result.stdout.split(': ')[1].strip()
            if date_str and len(date_str) == 10:
                return date_str
    except:
        pass
    
    try:
        result = subprocess.run(
            ['exiftool', '-MediaCreateDate', '-d', '%Y/%m/%d', filepath],
            capture_output=True, text=True, timeout=3
        )
        if result.returncode == 0 and result.stdout and ':' in result.stdout:
            date_str = result.stdout.split(': ')[1].strip()
            if date_str and len(date_str) == 10:
                return date_str
    except:
        pass
    
    mtime = os.path.getmtime(filepath)
    return datetime.fromtimestamp(mtime).strftime('%Y/%m/%d')

def process_file(filepath):
    try:
        file_type = get_file_type(filepath)
        
        if file_type == 'image':
            dest_base = DEST_IMAGES
            dup_base = DUPLICATES_IMAGES
        elif file_type == 'video':
            dest_base = DEST_VIDEOS
            dup_base = DUPLICATES_VIDEOS
        else:
            dest_base = DEST_OTHERS
            dup_base = DUPLICATES_OTHERS
        
        # File Others: không phân theo ngày
        if file_type == 'other':
            os.makedirs(dest_base, exist_ok=True)
            dest_path = os.path.join(dest_base, os.path.basename(filepath))
            
            if os.path.exists(dest_path):
                os.makedirs(dup_base, exist_ok=True)
                shutil.move(filepath, os.path.join(dup_base, os.path.basename(filepath)))
                return 'dup_other'
            else:
                shutil.move(filepath, dest_path)
                return 'ok_other'
        
        # Image và Video: phân loại theo ngày
        date_path = get_date_taken(filepath)
        dest_dir = os.path.join(dest_base, date_path)
        os.makedirs(dest_dir, exist_ok=True)
        dest_path = os.path.join(dest_dir, os.path.basename(filepath))
        
        if os.path.exists(dest_path):
            dup_dir = os.path.join(dup_base, date_path)
            os.makedirs(dup_dir, exist_ok=True)
            shutil.move(filepath, os.path.join(dup_dir, os.path.basename(filepath)))
            return f'dup_{file_type}'
        else:
            shutil.move(filepath, dest_path)
            return f'ok_{file_type}'
            
    except Exception as e:
        os.makedirs(ERRORS, exist_ok=True)
        try:
            shutil.move(filepath, os.path.join(ERRORS, os.path.basename(filepath)))
        except:
            pass
        return f'error_{str(e)[:50]}'

# ============================================
# CHƯƠNG TRÌNH CHÍNH
# ============================================
def main():
    log_message("=" * 70)
    log_message("🚀 BẮT ĐẦU XỬ LÝ ẢNH + VIDEO + FILE KHÁC")
    log_message("=" * 70)
    log_message(f"📁 Nguồn: {SOURCE}")
    log_message("")
    
    # Đếm và phân loại file
    log_message("Đang quét và phân loại file...")
    images, videos, others = [], [], []
    
    for root, dirs, files in os.walk(SOURCE):
        for file in files:
            filepath = os.path.join(root, file)
            file_type = get_file_type(filepath)
            if file_type == 'image':
                images.append(filepath)
            elif file_type == 'video':
                videos.append(filepath)
            else:
                others.append(filepath)
    
    total = len(images) + len(videos) + len(others)
    log_message(f"📊 KẾT QUẢ QUÉT:")
    log_message(f"   🖼️  Ảnh: {len(images)} file")
    log_message(f"   🎬 Video: {len(videos)} file")
    log_message(f"   📄 File khác: {len(others)} file")
    log_message(f"   📦 Tổng cộng: {total} file")
    log_message("")
    
    if total == 0:
        log_message("⚠️ Không có file nào cần xử lý!")
        return
    
    # Xử lý file
    log_message("🔄 BẮT ĐẦU XỬ LÝ...")
    start_time = time.time()
    
    ok_img = ok_vid = ok_oth = 0
    dup_img = dup_vid = dup_oth = 0
    errors = 0
    
    all_files = images + videos + others
    
    with ThreadPoolExecutor(max_workers=2) as executor:
        futures = {executor.submit(process_file, f): f for f in all_files}
        for i, future in enumerate(as_completed(futures), 1):
            result = future.result()
            
            if result.startswith('ok_image'): ok_img += 1
            elif result.startswith('ok_video'): ok_vid += 1
            elif result.startswith('ok_other'): ok_oth += 1
            elif result.startswith('dup_image'): dup_img += 1
            elif result.startswith('dup_video'): dup_vid += 1
            elif result.startswith('dup_other'): dup_oth += 1
            else: errors += 1
            
            if i % 200 == 0 or i == total:
                elapsed = time.time() - start_time
                rate = i / elapsed if elapsed > 0 else 0
                remaining = (total - i) / rate if rate > 0 else 0
                log_message(f"📊 {i}/{total} ({i*100//total}%) - "
                           f"🖼️:{ok_img+dup_img} 🎬:{ok_vid+dup_vid} 📄:{ok_oth+dup_oth} "
                           f"- Tốc độ: {rate:.1f} file/s - Còn: {remaining/3600:.1f}h")
    
    # Tổng kết
    log_message("")
    log_message("=" * 70)
    log_message("✅ HOÀN TẤT XỬ LÝ!")
    log_message("=" * 70)
    log_message(f"   🖼️  Ảnh: {ok_img} OK, {dup_img} trùng")
    log_message(f"   🎬 Video: {ok_vid} OK, {dup_vid} trùng")
    log_message(f"   📄 File khác: {ok_oth} OK, {dup_oth} trùng")
    log_message(f"   ❌ Lỗi: {errors} file")
    log_message("=" * 70)

if __name__ == "__main__":
    try:
        if os.path.exists(LOG_FILE):
            os.remove(LOG_FILE)
        main()
    except KeyboardInterrupt:
        log_message("\n⚠️ Người dùng dừng chương trình")
        sys.exit(0)

Phân quyền cho script

bash
chmod +x ~/organize_media.py

🚀 Chạy xử lý

Sử dụng Screen (chạy nền, tránh mất kết nối SSH)

bash
# Tạo screen session
screen -S organize-media

# Chạy script
python3 ~/organize_media.py

# Detach screen (để chạy nền): Ctrl + A, sau đó D
# Quay lại xem: screen -r organize-media

Kiểm tra tiến độ (terminal khác)

bash
# Xem log real-time
tail -f /home/your_user/Data/organize_media_log.txt

# Kiểm tra số lượng đã xử lý
watch -n 30 'echo "🖼️ Images: $(find /home/your_user/Data/ANH_CHUP_PROCESSED/Images -type f 2>/dev/null | wc -l) | 🎬 Videos: $(find /home/your_user/Data/ANH_CHUP_PROCESSED/Videos -type f 2>/dev/null | wc -l) | 📄 Others: $(find /home/your_user/Data/ANH_CHUP_PROCESSED/Others -type f 2>/dev/null | wc -l)"'

📊 Các lệnh quản lý hữu ích

Dọn dẹp thư mục cũ (trước khi chạy lại)

bash
# Xóa toàn bộ dữ liệu đã xử lý
rm -rf /home/your_user/Data/ANH_CHUP_PROCESSED/*
rm -rf /home/your_user/Data/DUPLICATES/*
rm -rf /home/your_user/Data/ERRORS/*

# Tạo lại cấu trúc
mkdir -p /home/your_user/Data/ANH_CHUP_PROCESSED/{Images,Videos,Others}
mkdir -p /home/your_user/Data/DUPLICATES/{Images,Videos,Others}
mkdir -p /home/your_user/Data/ERRORS

Kiểm tra kết quả sau khi xử lý

bash
# Xem cấu trúc thư mục đã phân loại
tree -L 3 /home/your_user/Data/ANH_CHUP_PROCESSED/Images/ | head -30

# Xem file trùng lặp
ls -la /home/your_user/Data/DUPLICATES/Images/

# Xóa file trùng (sau khi kiểm tra kỹ)
rm -rf /home/your_user/Data/DUPLICATES

Xem log lỗi chi tiết

bash
# Tìm các dòng lỗi trong log
grep -i error /home/your_user/Data/organize_media_log.txt

# Xem 50 dòng cuối của log
tail -50 /home/your_user/Data/organize_media_log.txt

⚙️ Tùy chỉnh cho cấu hình yếu

Nếu máy chủ có cấu hình thấp (CPU 2-4 nhân, RAM 4-8GB), điều chỉnh số luồng xử lý:

Trong file organize_media.py, tìm dòng:

python
with ThreadPoolExecutor(max_workers=2) as executor:

Thay đổi max_workers:

  • CPU 2 nhânmax_workers=1

  • CPU 4 nhân (N5105)max_workers=2

  • CPU 8 nhân trở lênmax_workers=4


🕐 Chạy tự động hàng đêm với Cron

bash
# Mở crontab
crontab -e

# Thêm dòng sau (chạy lúc 2h sáng)
0 2 * * * /usr/bin/python3 /home/your_user/organize_media.py >> /home/your_user/Data/cron_log.txt 2>&1

📝 Các định dạng file được hỗ trợ

Loại Định dạng Thư mục đích
Ảnh jpg, jpeg, png, gif, bmp, tiff, webp, heic, jfif Images/Năm/Tháng/Ngày/
Video mp4, avi, mov, mkv, flv, wmv, m4v, 3gp, mpg, mpeg, webm Videos/Năm/Tháng/Ngày/
File khác raw, psd, pdf, doc, txt, zip, v.v. Others/ (không phân theo ngày)

🐛 Xử lý lỗi thường gặp

Lỗi: EACCES: permission denied

Nguyên nhân: Thiếu quyền ghi vào thư mục

Cách fix:

bash
sudo chown -R your_user:your_user /home/your_user/Data
chmod -R 755 /home/your_user/Data

Lỗi: exiftool: command not found

Cách fix:

bash
sudo apt install -y exiftool

Lỗi: ModuleNotFoundError: No module named 'concurrent'

Cách fix (Python 3 đã có sẵn concurrent):

bash
python3 --version  # Phải >= 3.6

🎯 Kết quả mong đợi

Sau khi chạy thành công, thư mục ANH_CHUP_PROCESSED sẽ có cấu trúc:

text
ANH_CHUP_PROCESSED/
├── Images/
│   ├── 2024/
│   │   ├── 01/
│   │   │   ├── 15/
│   │   │   │   ├── family.jpg
│   │   │   │   └── sunset.png
│   │   │   └── 20/
│   │   └── 02/
│   └── 2025/
├── Videos/
│   ├── 2024/
│   │   └── 01/
│   │       └── 15/
│   │           └── vacation.mp4
│   └── 2025/
└── Others/
    ├── raw_file.CR2
    └── document.pdf

📌 Lưu ý quan trọng

  1. Sao lưu dữ liệu trước khi chạy script lần đầu

  2. Script sử dụng move – file sẽ được di chuyển, không copy

  3. File trùng lặp được đưa vào thư mục DUPLICATES, kiểm tra kỹ trước khi xóa

  4. Chạy thử với một thư mục nhỏ trước khi xử lý toàn bộ

  5. Sử dụng screen để tránh mất kết nối SSH trong quá trình chạy dài


🔗 Tham khảo

By admin

Leave a Reply

Your email address will not be published. Required fields are marked *