diff --git a/core/logger.py b/core/logger.py new file mode 100644 index 0000000..c0cd247 --- /dev/null +++ b/core/logger.py @@ -0,0 +1,35 @@ +import logging +import os +from datetime import datetime + + +class Logger: + def __init__(self): + self.log_messages = [] + + def log(self, level, message): + """Добавление сообщения в лог""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_entry = f"[{timestamp}] [{level}] {message}" + self.log_messages.append(log_entry) + print(log_entry) # Также выводим в консоль + + def save_log(self, file_path): + """Сохранение лога в файл""" + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.write("VIDEO EDITOR PROCESSING LOG\n") + f.write("=" * 50 + "\n") + f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write("=" * 50 + "\n\n") + + for log_entry in self.log_messages: + f.write(log_entry + "\n") + + self.log("INFO", f"Лог сохранен: {file_path}") + except Exception as e: + self.log("ERROR", f"Ошибка сохранения лога: {str(e)}") + + def clear_log(self): + """Очистка лога""" + self.log_messages.clear() \ No newline at end of file diff --git a/core/video_processor.py b/core/video_processor.py new file mode 100644 index 0000000..0a2e0a8 --- /dev/null +++ b/core/video_processor.py @@ -0,0 +1,111 @@ +import os +from moviepy.editor import VideoFileClip, concatenate_videoclips +import tempfile +from datetime import datetime +from core.logger import Logger + + +class VideoProcessor: + def __init__(self): + self.logger = Logger() + + def process_videos(self, video_data, output_dir, output_format="mp4", + quality="high", progress_callback=None): + """ + Основной метод обработки видео + """ + self.logger.log("INFO", f"Начало обработки {len(video_data)} видео файлов") + + clips = [] + total_steps = sum(len(data['time_ranges']) for data in video_data) + 2 + current_step = 0 + + try: + # Обработка каждого видео файла + for i, data in enumerate(video_data): + video_path = data['path'] + time_ranges = data['time_ranges'] + + self.logger.log("INFO", f"Обработка файла: {os.path.basename(video_path)}") + + with VideoFileClip(video_path) as video: + # Вырезаем указанные диапазоны + for start, end in time_ranges: + if progress_callback: + progress = current_step / total_steps + progress_callback(progress, + f"Вырезание фрагмента {i + 1}/{len(video_data)}") + + # Обрезаем видео по таймкодам + clip = video.subclip(start, end) + clips.append(clip) + current_step += 1 + + self.logger.log("INFO", + f"Вырезан фрагмент: {self.format_time(start)} - {self.format_time(end)}") + + if not clips: + raise ValueError("Нет видео фрагментов для объединения") + + # Объединение клипов + if progress_callback: + progress_callback(current_step / total_steps, "Объединение видео фрагментов") + + self.logger.log("INFO", f"Объединение {len(clips)} фрагментов") + final_clip = concatenate_videoclips(clips) + + # Настройка качества + bitrate = self.get_bitrate(quality) + + # Генерация имени выходного файла + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_filename = f"edited_video_{timestamp}.{output_format}" + output_path = os.path.join(output_dir, output_filename) + + # Сохранение результата + if progress_callback: + progress_callback((total_steps - 1) / total_steps, "Сохранение итогового файла") + + self.logger.log("INFO", f"Сохранение файла: {output_path}") + final_clip.write_videofile( + output_path, + codec='libx264', + bitrate=bitrate, + audio_codec='aac', + verbose=False, + logger=None + ) + + # Закрытие клипов + final_clip.close() + for clip in clips: + clip.close() + + self.logger.log("SUCCESS", f"Обработка завершена успешно: {output_path}") + return output_path + + except Exception as e: + self.logger.log("ERROR", f"Ошибка обработки: {str(e)}") + # Закрытие клипов в случае ошибки + for clip in clips: + try: + clip.close() + except: + pass + raise + + def get_bitrate(self, quality): + """Определение битрейта в зависимости от качества""" + quality_settings = { + 'low': '1000k', + 'medium': '3000k', + 'high': '8000k' + } + return quality_settings.get(quality, '3000k') + + def format_time(self, seconds): + """Форматирование времени в читаемый вид""" + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + seconds = int(seconds % 60) + return f"{hours:02d}:{minutes:02d}:{seconds:02d}" \ No newline at end of file diff --git a/gui/main_window.py b/gui/main_window.py new file mode 100644 index 0000000..eb19198 --- /dev/null +++ b/gui/main_window.py @@ -0,0 +1,167 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import os +from gui.video_item import VideoItem +from core.video_processor import VideoProcessor +from core.logger import Logger + + +class MainWindow: + def __init__(self, root): + self.root = root + self.root.title("Video Editor Pro") + self.root.geometry("900x700") + + self.video_items = [] + self.setup_ui() + self.logger = Logger() + + def setup_ui(self): + # Main frame + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Add files section + files_frame = ttk.LabelFrame(main_frame, text="Видео файлы", padding="5") + files_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) + + ttk.Button(files_frame, text="Добавить файлы", + command=self.add_video_files).pack(side=tk.LEFT, padx=(0, 10)) + ttk.Button(files_frame, text="Очистить все", + command=self.clear_all).pack(side=tk.LEFT) + + # Scrollable frame for video items + canvas = tk.Canvas(main_frame) + scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview) + self.scrollable_frame = ttk.Frame(canvas) + + self.scrollable_frame.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + scrollbar.grid(row=1, column=1, sticky=(tk.N, tk.S)) + + # Output settings + output_frame = ttk.LabelFrame(main_frame, text="Настройки вывода", padding="5") + output_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0)) + + ttk.Label(output_frame, text="Формат:").grid(row=0, column=0, padx=(0, 5)) + self.format_var = tk.StringVar(value="mp4") + format_combo = ttk.Combobox(output_frame, textvariable=self.format_var, + values=["mp4", "avi", "mov", "mkv"], state="readonly") + format_combo.grid(row=0, column=1, padx=(0, 20)) + + ttk.Label(output_frame, text="Качество:").grid(row=0, column=2, padx=(0, 5)) + self.quality_var = tk.StringVar(value="high") + quality_combo = ttk.Combobox(output_frame, textvariable=self.quality_var, + values=["low", "medium", "high"], state="readonly") + quality_combo.grid(row=0, column=3, padx=(0, 20)) + + ttk.Button(output_frame, text="Выбрать папку для сохранения", + command=self.select_output_dir).grid(row=0, column=4, padx=(0, 10)) + + self.output_dir_var = tk.StringVar(value=os.getcwd()) + ttk.Label(output_frame, textvariable=self.output_dir_var).grid(row=0, column=5) + + # Process button + ttk.Button(main_frame, text="Выполнить", + command=self.process_videos, style="Accent.TButton").grid(row=3, column=0, pady=20) + + # Progress bar + self.progress_var = tk.DoubleVar() + self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100) + self.progress_bar.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E)) + + # Status label + self.status_var = tk.StringVar(value="Готов к работе") + ttk.Label(main_frame, textvariable=self.status_var).grid(row=5, column=0, columnspan=2) + + # Configure grid weights + main_frame.columnconfigure(0, weight=1) + main_frame.rowconfigure(1, weight=1) + + def add_video_files(self): + files = filedialog.askopenfilenames( + title="Выберите видео файлы", + filetypes=[ + ("Видео файлы", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm"), + ("Все файлы", "*.*") + ] + ) + + for file_path in files: + video_item = VideoItem(self.scrollable_frame, file_path) + video_item.pack(fill=tk.X, padx=5, pady=2) + self.video_items.append(video_item) + + self.update_status(f"Добавлено файлов: {len(files)}") + + def clear_all(self): + for item in self.video_items: + item.destroy() + self.video_items.clear() + self.update_status("Все файлы удалены") + + def select_output_dir(self): + directory = filedialog.askdirectory(title="Выберите папку для сохранения") + if directory: + self.output_dir_var.set(directory) + + def update_status(self, message): + self.status_var.set(message) + self.root.update() + + def process_videos(self): + if not self.video_items: + messagebox.showerror("Ошибка", "Добавьте хотя бы один видео файл") + return + + # Collect video data + video_data = [] + for item in self.video_items: + time_ranges = item.get_time_ranges() + if not time_ranges: # Use entire video if no ranges specified + time_ranges = [(0, item.get_duration())] + video_data.append({ + 'path': item.file_path, + 'time_ranges': time_ranges + }) + + output_format = self.format_var.get() + output_quality = self.quality_var.get() + output_dir = self.output_dir_var.get() + + try: + processor = VideoProcessor() + + def progress_callback(progress, message): + self.progress_var.set(progress * 100) + self.update_status(message) + + output_path = processor.process_videos( + video_data=video_data, + output_dir=output_dir, + output_format=output_format, + quality=output_quality, + progress_callback=progress_callback + ) + + # Save log + log_path = output_path.replace(f".{output_format}", "_log.txt") + self.logger.save_log(log_path) + + self.update_status(f"Готово! Файл сохранен: {output_path}") + messagebox.showinfo("Успех", f"Обработка завершена!\nФайл: {output_path}") + + except Exception as e: + error_msg = f"Ошибка обработки: {str(e)}" + self.update_status(error_msg) + self.logger.log("ERROR", error_msg) + messagebox.showerror("Ошибка", error_msg) + finally: + self.progress_var.set(0) \ No newline at end of file diff --git a/gui/video_item.py b/gui/video_item.py new file mode 100644 index 0000000..f2de4c7 --- /dev/null +++ b/gui/video_item.py @@ -0,0 +1,108 @@ +import tkinter as tk +from tkinter import ttk +import os +from moviepy.editor import VideoFileClip + + +class VideoItem(ttk.Frame): + def __init__(self, parent, file_path): + super().__init__(parent) + self.file_path = file_path + self.time_ranges = [] + self.setup_ui() + + def setup_ui(self): + # File info + file_name = os.path.basename(self.file_path) + ttk.Label(self, text=file_name, font=('Arial', 9, 'bold')).grid(row=0, column=0, sticky=tk.W) + + # Duration info + try: + with VideoFileClip(self.file_path) as clip: + duration = clip.duration + duration_str = self.format_time(duration) + ttk.Label(self, text=f"Длительность: {duration_str}").grid(row=1, column=0, sticky=tk.W) + except: + duration_str = "Неизвестно" + ttk.Label(self, text="Ошибка чтения файла").grid(row=1, column=0, sticky=tk.W) + + # Time ranges frame + ranges_frame = ttk.LabelFrame(self, text="Временные диапазоны") + ranges_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5) + + self.ranges_container = ttk.Frame(ranges_frame) + self.ranges_container.pack(fill=tk.X, padx=5, pady=5) + + # Add range button + ttk.Button(ranges_frame, text="+ Добавить диапазон", + command=self.add_time_range).pack(pady=5) + + # Buttons + ttk.Button(self, text="Удалить", + command=self.destroy).grid(row=0, column=2, rowspan=2, padx=5) + + self.columnconfigure(0, weight=1) + + def format_time(self, seconds): + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + seconds = int(seconds % 60) + return f"{hours:02d}:{minutes:02d}:{seconds:02d}" + + def add_time_range(self, start_time=0, end_time=0): + range_frame = ttk.Frame(self.ranges_container) + range_frame.pack(fill=tk.X, pady=2) + + ttk.Label(range_frame, text="От:").pack(side=tk.LEFT) + start_var = tk.StringVar(value=self.format_time(start_time)) + start_entry = ttk.Entry(range_frame, textvariable=start_var, width=8) + start_entry.pack(side=tk.LEFT, padx=(0, 10)) + + ttk.Label(range_frame, text="До:").pack(side=tk.LEFT) + end_var = tk.StringVar(value=self.format_time(end_time)) + end_entry = ttk.Entry(range_frame, textvariable=end_var, width=8) + end_entry.pack(side=tk.LEFT, padx=(0, 10)) + + def remove_range(): + range_frame.destroy() + self.time_ranges = [r for r in self.time_ranges if r['frame'] != range_frame] + + ttk.Button(range_frame, text="×", width=3, + command=remove_range).pack(side=tk.LEFT) + + self.time_ranges.append({ + 'frame': range_frame, + 'start_var': start_var, + 'end_var': end_var + }) + + def get_time_ranges(self): + ranges = [] + for range_data in self.time_ranges: + try: + start_time = self.parse_time(range_data['start_var'].get()) + end_time = self.parse_time(range_data['end_var'].get()) + + if start_time < end_time: + ranges.append((start_time, end_time)) + except: + continue + return ranges + + def parse_time(self, time_str): + parts = time_str.split(':') + if len(parts) == 3: # HH:MM:SS + hours, minutes, seconds = map(int, parts) + return hours * 3600 + minutes * 60 + seconds + elif len(parts) == 2: # MM:SS + minutes, seconds = map(int, parts) + return minutes * 60 + seconds + else: # SS + return int(time_str) + + def get_duration(self): + try: + with VideoFileClip(self.file_path) as clip: + return clip.duration + except: + return 0 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..b776ef1 --- /dev/null +++ b/main.py @@ -0,0 +1,10 @@ +import tkinter as tk +from gui.main_window import MainWindow + +def main(): + root = tk.Tk() + app = MainWindow(root) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e72adb6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +moviepy==1.0.3 +Pillow==10.0.0 \ No newline at end of file diff --git a/utils/file_utils.py b/utils/file_utils.py new file mode 100644 index 0000000..35ff14d --- /dev/null +++ b/utils/file_utils.py @@ -0,0 +1,26 @@ +import os +import shutil + + +def get_file_size(file_path): + """Получение размера файла в читаемом формате""" + size_bytes = os.path.getsize(file_path) + for unit in ['B', 'KB', 'MB', 'GB']: + if size_bytes < 1024.0: + return f"{size_bytes:.2f} {unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.2f} TB" + + +def ensure_directory(directory): + """Создание директории если не существует""" + if not os.path.exists(directory): + os.makedirs(directory) + + +def get_valid_filename(filename): + """Очистка имени файла от недопустимых символов""" + invalid_chars = '<>:"/\\|?*' + for char in invalid_chars: + filename = filename.replace(char, '_') + return filename \ No newline at end of file