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

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

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

Комментарии (9)


  1. IZh
    29.07.2025 18:18

    Без централизованной базы, где кто-то уже заранее разметил «неинтересный» контент, при первом просмотре хотя бы один раз всё-таки придётся это увидеть самому. (А может и два, в процессе создания разметки.)

    Тут требуется информационный сервис с категоризацией «неинтересного» контента по темам, чтобы можно было отключать выборочно.


  1. MAXH0
    29.07.2025 18:18

    Забавно.. "Неинвазивная цензура! " ... В рамках своей семьи/секты/клуба - почему бы и нет. Необходимо только помнить о важностии консенсуса мнений.


  1. novoselov
    29.07.2025 18:18

    И как теперь развидеть 2 больших пальца у девушки?


    1. Squoworode
      29.07.2025 18:18

      Отключите картинки и посмотрите ещё раз, так вы их не увидите, и вам не придётся развидеть их!


  1. Neo5
    29.07.2025 18:18

    Есть несколько замечаний:

    1. Для того, чтобы разметить эти тайм-коды, их надо знать. А для того, чтобы их узнать надо просмотреть то, что нужно пропустить. То есть, задача "не увидеть" изначально провальная

    2. Даже если эти коды будут подтягиваться откуда-то извне, задача "не увидеть" всё равно провальная, ведь кто-то всё-таки увидел то, что не следует

    3. А кто определяет то, что не надо увидеть? Не возникает ли здесь цензура?


    1. hira
      29.07.2025 18:18

      1-2. Ну я так понимаю, что целью ставится исключить из показа сцены, не предназначенные, например, для детей. В целом фильм неплох (и родители его смотрели, причём, возможно, не раз), но вот, допустим, обнажёнка, или некоторые сцены насилия ребёнку показывать не хотелось бы.

      3. Как будто бы в этом есть что-то плохое.


  1. HardWrMan
    29.07.2025 18:18

    Автор изобрёл sponsorblock для локальных файлов?


  1. pda0
    29.07.2025 18:18

    Буквально "у нас уже есть SponsorBlock дома". :-)


  1. goldexer
    29.07.2025 18:18

    Когда-то очень давно делал что-то подобное на Visual Basic 6.0. На форму дропался Windows Media Player ActiveX control, И, хоть drag-n-drop формы поддерживают из коробки (код пишется в обработках), я просто ассоциировал утилиту (скомпилированный exe) с видеофайлами и она при открытии сама искала файл с таймкодами. Соответственно, если не находила, файл воспроизводился в обычном режиме. Я тогда был ещё школьником 6 класса, меня подкупала простота создания интерфейса и написания кода в VB6, тогда как в том же Pascal, Python, Visual C++ надо было сильно поплясать. Delphi тогда тоже очень выделялся и подкупал мощью, а вот Borland C++ я так и не понял, не моё видимо. Так что да, VB6, но зато оно работало и хорошо работало.

    Забыл добавить: таймкоды писались в файл автоматически по хоткеям и так же навигация по ним, удаление. Приходилось отслеживать глобально состояние клавиш, но щито поделать...