Как стать автором
Поиск
Написать публикацию
Обновить

Как не увидеть то, что не хотелось бы видеть, чтобы потом не нужно было развидеть

Уровень сложностиПростой
Время на прочтение8 мин
Количество просмотров1.5K

Приветствую! Хочу рассказать про свой мини pet-проект «Just Skip It!», который я разработала (и надеюсь буду развивать), чтобы автоматически пропускать нежелательные сцены в видео.

Поводом для создания проекта, послужило желание избавиться от «неинтересных» эпизодов, которые, по моему мнению, «не улучшают» семейную коллекцию кинофильмов. Сначала использовались варианты редактирования файлов, от комбайнов - видеоредакторов до батников + ffmpeg, довольно быстро я поняла, что этот метод «не очень», так как неисправимо портит оригинальный файл. Хотелось более гибкого решения, которое позволит быстро и неинвазивно вносить изменения в процесс цензурирования.

Так и родился проект «Just Skip It!». В предлагаемой мной реализации, я использовала медиаплеер VLC, и утилиту на Python, которая управляет плеером через его RC-интерфейс.

Что такое Just Skip It!

Just Skip It! — это утилита, которая позволяет при воспроизведении видеофайла пропускать заранее определённые сегменты видео. Вы просто создаёте для своего видеофайла специальный JSON-конфиг с тайм-кодами, перетаскиваете видео в окно утилиты, и она сама запускает VLC и отслеживает воспроизведение. Когда подходит время метки, утилита автоматически «перематывает» плеер на указанный в метке интервал.

Ключевые возможности:

  • Окно с поддержкой Drag and Drop для выбора видео.

  • Пропуски задаются в простом JSON-файле.

  • Утилита сама находит и проверяет конфиг, запускает VLC и контролирует процесс.

  • Оригинальные видеофайлы остаются нетронутыми.

Архитектура проекта

Проект состоит из нескольких ключевых модулей.

1. Графический интерфейс

Интерфейс создан с использованием Tkinter для реализации Drag and Drop. Основное окно — это область, куда можно перетащить видеофайл.

Код
# ... existing code ...
class VideoDropWindow:
    # ... existing code ...
    def setup_drop_area(self):
        """Create area for file drag and drop"""
        # ... existing code ...
        
        # Setup drag-and-drop
        self.drop_frame.drop_target_register(tkdnd.DND_FILES)
        self.drop_frame.dnd_bind('<<Drop>>', self.on_drop)
        self.drop_frame.dnd_bind('<<DragEnter>>', self.on_drag_enter)
        self.drop_frame.dnd_bind('<<DragLeave>>', self.on_drag_leave)
        
        # ... existing code ...
        
    def on_drag_enter(self, event):
        """Handle entering the drop zone"""
        self.drop_frame.config(bg="lightblue")
        self.label.config(bg="lightblue", text="Release file here")
        
    def on_drag_leave(self, event):
        """Handle leaving the drop zone"""
        self.drop_frame.config(bg="lightgray")
        self.label.config(
            bg="lightgray", 
            text="Drop video file here"
        )
        
    def on_drop(self, event):
        """Handle file drop"""
        # Get file path
        file_path = event.data.strip('{}')  # Remove curly braces if present

        # Check if it's a video file
        if self.is_video_file(file_path):
            self.process_video_file(file_path)
        else:
            messagebox.showerror(
                "Error", 
                "This is not a video file or format is not supported!"
            )
        
        # Return to normal appearance
        self.on_drag_leave(event)
        
    def is_video_file(self, file_path):
        """Check if the file is a video file"""
        video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}
        file_extension = os.path.splitext(file_path)[1].lower()
        return file_extension in video_extensions
        
    def process_video_file(self, file_path):
        """Process video file - extract path and name"""
        # Save file path
        self.current_video_path = os.path.abspath(file_path)
        
        # ... existing code ...
        # Check for JSON file
        json_check_result = check_video_file(self.current_video_path)
        
        if json_check_result:
            # JSON file found and valid
            # ... existing code ...
            
            # Show confirm button only if JSON file is valid
            self.show_confirm_button()
        else:
            # JSON file not found or invalid
            # ... existing code ...
    
    def show_confirm_button(self):
        """Show confirmation button"""
        # Create frame for button if it doesn't exist yet
        if not hasattr(self, 'button_frame'):
            self.button_frame = tk.Frame(self.root)
            self.button_frame.pack(pady=10)
            
            self.ok_button = tk.Button(
                self.button_frame,
                text="Launch VLC",
                command=self.on_confirm,
                bg="green",
                fg="white",
                font=("Arial", 12, "bold"),
                padx=20,
                pady=5
            )
            self.ok_button.pack()
        else:
            # If frame already exists, just show it
            self.button_frame.pack(pady=10)
            
    def on_confirm(self):
        """Handle confirmation button click"""
        if self.current_video_path:
            try:
                # Hide button
                self.button_frame.pack_forget()
    
                # ... existing code ...
                def run_vlc():
                    # Call main function from launcher
                    vlc_main(self.current_video_path)
    
                # Launch in a separate thread
                self.vlc_thread = threading.Thread(target=run_vlc)
                self.vlc_thread.daemon = False  # Thread will continue after main application closes
                self.vlc_thread.start()
    
                # Close main window
                self.root.destroy()
        
                # Create a new window with stop button
                self.create_stop_window()
    
            except Exception as e:
                messagebox.showerror("Error", f"Launch error: {str(e)}")
                self.info_label.config(text="Launch error", fg="red")
                self.show_confirm_button()  # Show button again

    # ... existing code ...

После того как файл «брошен» в окно, утилита проверяет, является ли он видеофайлом, а затем ищет для него одноименный JSON-конфиг. Если всё в порядке, появляется кнопка «Запустить VLC».

2. Конфигурация сегментов (JSON)

Для каждого видео создаётся свой JSON-файл с таким же именем (например, my_movie.mp4 и my_movie.json).

Структура файла
{
  "version": "1.0",
  "video_info": {
    "filename": "video_name.mp4",
    "duration": "01:30:45"
  },
  "time_segments": [
    {
      "id": 1,
      "name": "Skip intro",
      "trigger_time": "00:01:30",
      "jump_to_time": "00:03:45",
      "enabled": true
    },
    {
      "id": 2,
      "name": "Skip credits",
      "trigger_time": "01:28:00",
      "jump_to_time": "01:29:50",
      "enabled": true
    }
  ],
  "settings": {
    "loop_segments": false,
    "show_notifications": true
  }
}
  • version: Версия формата конфигурации

  • video_info: Основная информация о видео

    • filename: Имя видеофайла

    • duration: Общая продолжительность видео в формате ЧЧ:ММ:СС

  • time_segments: Массив сегментов для пропуска

    • id: Уникальный идентификатор сегмента

    • name: Описание сегмента (например, «Пропустить вступление»)

    • trigger_time: Время активации пропуска (ЧЧ:ММ:СС)

    • jump_to_time: Время, куда нужно перейти (ЧЧ:ММ:СС)

    • enabled: Нужно ли пропускать этот сегмент (true/false)

  • settings: Дополнительные настройки

    • loop_segments: Определяет, должны ли перемотки срабатывать повторно (пока не реализовано полностью)

    • show_notifications: Показывать уведомления во время пропуска (пока не реализовано полностью)

3. Поиск и валидация JSON

Поиск и валидация JSON файлов осуществляется в модулях:

Модуль json_finder.py просто ищет одноимённый файл в той же директории.

Код
# ... existing code ...
def find_json_file(video_path):
    """
    Searches for a JSON file with the same name as the video file
    """
    directory = os.path.dirname(video_path)
    filename_without_ext = os.path.splitext(os.path.basename(video_path))[0]
    json_path = os.path.join(directory, f"{filename_without_ext}.json")
    
    return json_path if os.path.exists(json_path) else None
# ... existing code ...

Модуль json_validator.py — это валидатор, который проверяет JSON на соответствие структуре, корректность форматов (например, время ЧЧ:ММ:СС), отсутствие дубликатов id и другие правила. Это помогает избежать падения утилиты из-за опечатки в конфиге.

Код
# ... existing code ...
class VideoConfigValidator:
    def __init__(self):
	# ... existing code ...
    
    def validate_time_format(self, time_str: str) -> bool:
	# ... existing code ...
    
    def validate_structure(self, data: Dict[Any, Any, structure: Dict[Any, Any], path: str = "") -> List[str]:
	# ... existing code ...
    
    def validate_business_rules(self, data: Dict[Any, Any]) -> List[str]:
	# ... existing code ...
    
    def validate_json_file(self, file_path: str) -> Dict[str, Any]:
	# ... existing code ...

4. Глобальные настройки (config.ini)

Чтобы не хардкодить настройки в скриптах, путь к VLC и параметры подключения вынесены в config.ini.

config.ini
[VLC]
executable_path = C:\apps\VLC\vlc.exe
rc_host = localhost
rc_port = 4212
rc_password =

[TIMEOUTS]
rc_check_interval = 1
rc_connection_timeout = 60
  • executable_path — путь к vlc.exe.

  • rc_host, rc_port, rc_password — данные для подключения к RC-интерфейсу VLC.

  • rc_check_interval — как часто приложение проверяет состояние VLC (в секундах)

  • rc_connection_timeout — максимальное время ожидания подключения к VLC (в секундах)

5. Взаимодействие с VLC

Лаунчер (launcher.py)

Эта часть запускает VLC и ждёт, когда его RC-интерфейс станет доступен для подключения.

Код
# ... existing code ...
def main(video_path):
	# ... existing code ...
    # Launch VLC
    vlc_process = start_vlc(config['vlc_path'], video_path)
	# ... existing code ...
    # Wait and check RC interface
    for attempt in range(max_attempts):
		# ... existing code ...
        if test_rc_connection(config['rc_host'], config['rc_port'], 1, config.get('rc_password', '')):
            print("RC interface available!")

            # Get path to JSON file from video path
            json_file_path = video_path.rsplit(".", 1)[0] + ".json"
            
            from src.vlc.controller import main as skip_controller_main
            
            print("Starting skip controller...")
            skip_controller_main(json_file_path)
            return
        
        time.sleep(config['check_interval'])
# ... rest of code ...

Контроллер (controller.py)

После успешного запуска контроллер подключается к VLC через сокет и в цикле выполняет две основные команды: get_time (получить текущее время) и seek (перемотать).

Код
# ... existing code ...
    def send_vlc_command(self, command):
        """Sends command to VLC through RC interface"""
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
			# ... existing code ...
            sock.connect((self.vlc_host, self.vlc_port))
			# ... existing code ...
            sock.send(f"{command}\n".encode())
# ... rest of code ...

# ... existing code ...
    def check_segments(self):
        """Checks if video needs to be skipped"""
		# ... existing code ...
        current_time = self.get_current_time()
        if current_time is None:
            return
		# ... existing code ...
        for segment in self.segments:
            trigger_seconds = self.time_to_seconds(segment['trigger_time'])
            jump_seconds = self.time_to_seconds(segment['jump_to_time'])
    
            # Check if we are in the range between trigger_time and jump_to_time
            if trigger_seconds <= current_time < jump_seconds:
                print(f"Segment activated: {segment['name']}")
                self.seek_to_time(jump_seconds)
                break
# ... rest of code ...

Логика проста: если текущее время воспроизведения попадает в интервал между trigger_time и jump_to_time, контроллер отправляет команду на перемотку.

Руководство по использованию

1. Установка

  1. Клонируйте или скачайте репозиторий.

    git clone https://github.com/S0fiya-dev/just-skip-it
  2. Установите зависимости:

    pip install -r requirements.txt
  3. Настройте config.ini, указав путь к вашему VLC.

2. Настройка VLC

Чтобы программа могла управлять VLC плеером, в нём нужно активировать RC-интерфейс (Remote Control).

  1. В VLC откройте Инструменты → Настройки.

  2. В левом нижнем углу выберите Показать настройки: Все.

  3. Перейдите в Интерфейс → Основные интерфейсы.

  4. Поставьте галочку Интерфейс удалённого управления.

  5. Перейдите в Интерфейс → Основные интерфейсы → RC и настройте хост/порт (обычно: localhost:4212), если необходимо - пароль (если установите его необходимо добавить в config.ini).

3. Создание JSON-файла

Для вашего видео (например, movie.mp4) создайте в той же папке файл movie.json и заполните его по примеру, который я приводила выше, также пример можно скопировать из папки docs, в папке проекта.

После этого можно запускать main.py, в открывшееся окно «кидать» видео и наслаждаться просмотром без лишних сцен.

Планы на будущее

  • Встроенный редактор JSON. Чтобы не создавать файлы вручную, можно добавить в интерфейс редактор тайм-кодов.

  • Уведомления. Реализовать всплывающие уведомления о пропуске сегмента (пока show_notifications в конфиге — это задел на будущее).

  • Кроссплатформенность. Тестировалось на Windows, в планах протестировать на Linux (на пишке).

  • Онлайн-база тайм-кодов. Если появится интерес пользователей, создать онлайн-базу, где они могли бы делиться готовыми конфигами для популярных фильмов и сериалов.

Заключение

Just Skip It! стал для меня отличным упражнением в написании многокомпонентной утилиты и работе со сторонними программами. Проект решил мою первоначальную задачу и оказался вполне юзабельным.

Буду рада, если кому-то он покажется полезным или интересным. Ссылку на GitHub-репозиторий оставлю ниже. Открыта для предложений по улучшению, так как намерена развивать проект.

Ссылка на GitHub

Ссылка на видео

Спасибо за внимание!

Теги:
Хабы:
+6
Комментарии9

Публикации

Ближайшие события