Приветствую! Хочу рассказать про свой мини 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. Установка
-
Клонируйте или скачайте репозиторий.
git clone https://github.com/S0fiya-dev/just-skip-it -
Установите зависимости:
pip install -r requirements.txt -
Настройте
config.ini, указав путь к вашему VLC.
2. Настройка VLC
Чтобы программа могла управлять VLC плеером, в нём нужно активировать RC-интерфейс (Remote Control).
-
В VLC откройте Инструменты → Настройки.
-
В левом нижнем углу выберите Показать настройки: Все.
-
Перейдите в Интерфейс → Основные интерфейсы.
-
Поставьте галочку Интерфейс удалённого управления.
-
Перейдите в Интерфейс → Основные интерфейсы → RC и настройте хост/порт (обычно: localhost:4212), если необходимо — пароль (если установите его необходимо добавить в
config.ini).
3. Создание JSON-файла
Для вашего видео (например, movie.mp4) создайте в той же папке файл movie.json и заполните его по примеру, который я приводила выше, также пример можно скопировать из папки docs, в папке проекта.
После этого можно запускать main.py, в открывшееся окно «кидать» видео и наслаждаться просмотром без лишних сцен.
Планы на будущее
-
Встроенный редактор JSON. Чтобы не создавать файлы вручную, можно добавить в интерфейс редактор тайм-кодов.
-
Уведомления. Реализовать всплывающие уведомления о пропуске сегмента (пока
show_notificationsв конфиге — это задел на будущее). -
Кроссплатформенность. Тестировалось на Windows, в планах протестировать на Linux (на пишке).
-
Онлайн-база тайм-кодов. Если появится интерес пользователей, создать онлайн-базу, где они могли бы делиться готовыми конфигами для популярных фильмов и сериалов.
Заключение
Just Skip It! стал для меня отличным упражнением в написании многокомпонентной утилиты и работе со сторонними программами. Проект решил мою первоначальную задачу и оказался вполне юзабельным.
Буду рада, если кому-то он покажется полезным или интересным. Ссылку на GitHub-репозиторий оставлю ниже. Открыта для предложений по улучшению, так как намерена развивать проект.
Спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/articles/932194/
Добавить комментарий