Skip to content

Commit 602c9a1

Browse files
authored
Add [Video Chapter Markers] plugin to extract and create scene markers from video chapters (#518)
1 parent 9863f74 commit 602c9a1

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Video Chapter Markers
2+
3+
This plugin extracts chapter information embedded in video files using `ffprobe` and creates markers in Stash. It processes scenes by checking if no markers are present and then adds chapter markers based on the video file's chapter data. This tool is especially useful for automating the import of chapter markers when a scene is updated or processing multiple scenes at once.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
stashapp-tools>=0.2.58
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import stashapi.log as log
2+
from stashapi.stashapp import StashInterface
3+
import stashapi.marker_parse as mp
4+
import sys
5+
import json
6+
import time
7+
from pathlib import Path
8+
import subprocess
9+
import re
10+
11+
def extract_chapters_from_video(video_path):
12+
try:
13+
cmd = [
14+
"ffprobe",
15+
"-v", "quiet",
16+
"-print_format", "json",
17+
"-show_chapters",
18+
video_path
19+
]
20+
result = subprocess.run(cmd, capture_output=True, text=True)
21+
22+
if result.returncode != 0:
23+
log.error(f"Error extracting chapters from {video_path}: {result.stderr}")
24+
return []
25+
26+
chapters_data = json.loads(result.stdout)
27+
28+
if "chapters" not in chapters_data or len(chapters_data["chapters"]) == 0:
29+
log.debug(f"No chapters found in {video_path}")
30+
return []
31+
32+
markers = []
33+
for chapter in chapters_data["chapters"]:
34+
title = chapter.get("tags", {}).get("title", "Chapter")
35+
36+
start_time = float(chapter.get("start_time", 0))
37+
38+
marker = {
39+
"seconds": start_time,
40+
"primary_tag": "From Chapter",
41+
"tags": [],
42+
"title": title,
43+
}
44+
log.debug(marker)
45+
markers.append(marker)
46+
47+
return markers
48+
49+
except Exception as e:
50+
log.error(f"Error processing {video_path}: {str(e)}")
51+
return []
52+
53+
def processScene(scene):
54+
log.debug(scene["scene_markers"])
55+
# Only process scenes without existing markers
56+
if len(scene["scene_markers"]) == 0:
57+
for f in scene["files"]:
58+
video_path = f["path"]
59+
log.debug(f"Processing video: {video_path}")
60+
61+
markers = extract_chapters_from_video(video_path)
62+
63+
if len(markers) > 0:
64+
if len(markers) == 1 and markers[0]["seconds"] == 0:
65+
log.info(f"Single chapter at the beginning in {video_path}, skipping import.")
66+
else:
67+
log.info(f"Found {len(markers)} chapters in {video_path}")
68+
mp.import_scene_markers(stash, markers, scene["id"], 15)
69+
else:
70+
log.info(f"No chapters found in {video_path}")
71+
72+
def processAll():
73+
query = {
74+
"has_markers": "false",
75+
}
76+
per_page = 100
77+
log.info("Getting scene count")
78+
count = stash.find_scenes(
79+
f=query,
80+
filter={"per_page": 1},
81+
get_count=True,
82+
)[0]
83+
log.info(str(count) + " scenes to process.")
84+
85+
for r in range(1, int(count / per_page) + 2):
86+
i = (r - 1) * per_page
87+
log.info(
88+
"fetching data: %s - %s %0.1f%%"
89+
% (
90+
(r - 1) * per_page,
91+
r * per_page,
92+
(i / count) * 100,
93+
)
94+
)
95+
scenes = stash.find_scenes(
96+
f=query,
97+
filter={"page": r, "per_page": per_page},
98+
)
99+
for s in scenes:
100+
processScene(s)
101+
i = i + 1
102+
log.progress((i / count))
103+
104+
json_input = json.loads(sys.stdin.read())
105+
106+
FRAGMENT_SERVER = json_input["server_connection"]
107+
stash = StashInterface(FRAGMENT_SERVER)
108+
109+
if "mode" in json_input["args"]:
110+
PLUGIN_ARGS = json_input["args"]["mode"]
111+
if "processAll" == PLUGIN_ARGS:
112+
processAll()
113+
elif "hookContext" in json_input["args"]:
114+
_id = json_input["args"]["hookContext"]["id"]
115+
_type = json_input["args"]["hookContext"]["type"]
116+
if _type == "Scene.Update.Post":
117+
scene = stash.find_scene(_id)
118+
processScene(scene)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Video Chapter Markers
2+
description: Create markers from chapter information embedded in video files
3+
version: 1.0
4+
url: https://github.com/stashapp/CommunityScripts/tree/main/plugins/videoChapterMarkers
5+
exec:
6+
- python
7+
- "{pluginDir}/videoChapterMarkers.py"
8+
interface: raw
9+
tasks:
10+
- name: "Process All Videos"
11+
description: Extract chapters from all video files without existing markers and create Stash markers
12+
defaultArgs:
13+
mode: processAll
14+
15+
hooks:
16+
- name: Create Markers on Scene Update
17+
description: Extract chapters and create markers when a scene is updated
18+
triggeredBy:
19+
- Scene.Update.Post

0 commit comments

Comments
 (0)