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

от автора

Приветствую! Хочу рассказать про свой мини 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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/932194/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *