📌 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
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)
# 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)
# 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
# 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:
nano ~/organize_media.py
#!/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
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)
# 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)
# 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)
# 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ý
# 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
# 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:
with ThreadPoolExecutor(max_workers=2) as executor:
Thay đổi max_workers:
-
CPU 2 nhân:
max_workers=1 -
CPU 4 nhân (N5105):
max_workers=2 -
CPU 8 nhân trở lên:
max_workers=4
🕐 Chạy tự động hàng đêm với Cron
# 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:
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:
sudo apt install -y exiftool
Lỗi: ModuleNotFoundError: No module named 'concurrent'
Cách fix (Python 3 đã có sẵn concurrent):
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:
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
-
Sao lưu dữ liệu trước khi chạy script lần đầu
-
Script sử dụng
move– file sẽ được di chuyển, không copy -
File trùng lặp được đưa vào thư mục
DUPLICATES, kiểm tra kỹ trước khi xóa -
Chạy thử với một thư mục nhỏ trước khi xử lý toàn bộ
-
Sử dụng screen để tránh mất kết nối SSH trong quá trình chạy dài