Приветствую! Хочу рассказать про свой мини 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-репозиторий оставлю ниже. Открыта для предложений по улучшению, так как намерена развивать проект.
Спасибо за внимание!