Skip to content

Commit 0e7673a

Browse files
authored
E621 tagger (#555)
1 parent a96d38a commit 0e7673a

File tree

4 files changed

+280
-0
lines changed

4 files changed

+280
-0
lines changed

plugins/e621_tagger/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# e621 tagger
2+
3+
Just a quick script to tag your uploadings
4+
5+
Took some code from bulkImageScrape as example, because I'm not a python dev
6+
7+
https://github.com/stashapp/CommunityScripts/blob/main/plugins/bulkImageScrape/bulkImageScrape.py
8+
9+
## How to use
10+
11+
Go to Tasks -> e621_tagger -> Press Tag Everything
12+
13+
## Configuration
14+
15+
You can configure which tags it will skip. By default, it will skip `e621_tagged` tag
16+
17+
## Rate limit
18+
19+
Be aware, that e621 has rate limit. In script it's hardcoded 2 seconds on wait time

plugins/e621_tagger/e621_tagger.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import hashlib
2+
import re
3+
import sys
4+
import json
5+
import time
6+
import requests
7+
import stashapi.log as log
8+
from stashapi.stashapp import StashInterface
9+
10+
11+
12+
def get_all_images(
13+
client: StashInterface,
14+
skip_tags: list[str],
15+
exclude_organized: bool
16+
) -> list[dict]:
17+
"""
18+
Get all images with proper tag exclusion and organization filter
19+
"""
20+
image_filter = {}
21+
pagination = {
22+
"page": 1,
23+
"per_page": -1, # -1 gets all results at once
24+
"sort": "created_at",
25+
"direction": "ASC",
26+
}
27+
28+
# Convert tag names to IDs
29+
tag_ids = []
30+
for tag_name in skip_tags:
31+
tag = get_or_create_tag(client, tag_name)
32+
if tag:
33+
tag_ids.append(tag["id"])
34+
35+
if tag_ids:
36+
image_filter["tags"] = {
37+
"value": [],
38+
"excludes": tag_ids,
39+
"modifier": "INCLUDES_ALL",
40+
"depth": -1,
41+
}
42+
43+
if exclude_organized:
44+
image_filter["organized"] = False # Correct field name
45+
46+
# Maintain original parameter structure
47+
return client.find_images(f=image_filter, filter=pagination)
48+
49+
50+
def process_e621_post(stash: StashInterface, image_id: str, image_md5: str) -> None:
51+
"""Process e621 metadata and update Stash records"""
52+
# Skip already processed images
53+
image = stash.find_image(image_id)
54+
if any(tag["name"] == "e621_tagged" for tag in image.get("tags", [])):
55+
return
56+
57+
try:
58+
time.sleep(2) # Rate limiting
59+
response = requests.get(
60+
f"https://e621.net/posts.json?md5={image_md5}",
61+
headers={"User-Agent": "Stash-e621-Tagger/1.0"},
62+
timeout=10
63+
)
64+
response.raise_for_status()
65+
post_data = response.json().get("post", {})
66+
except Exception as e:
67+
log.error(f"e621 API error: {str(e)}")
68+
return
69+
70+
if not post_data:
71+
return
72+
73+
# Create essential entities
74+
e621_tag = get_or_create_tag(stash, "e621_tagged")
75+
post_url = f"https://e621.net/posts/{post_data['id']}"
76+
77+
# Process tags
78+
tag_ids = [e621_tag["id"]]
79+
for category in ["general", "species", "character", "artist", "copyright"]:
80+
for tag in post_data.get("tags", {}).get(category, []):
81+
# Clean and validate tag
82+
clean_tag = tag.strip()
83+
if not clean_tag:
84+
continue
85+
86+
stash_tag = get_or_create_tag(stash, clean_tag)
87+
if stash_tag:
88+
tag_ids.append(stash_tag["id"])
89+
90+
# Process studio
91+
studio_id = None
92+
if artists := post_data.get("tags", {}).get("artist"):
93+
studio = get_or_create_studio(stash, artists[0])
94+
studio_id = studio["id"]
95+
96+
# Process performers
97+
performer_ids = []
98+
for char_tag in post_data.get("tags", {}).get("character", []):
99+
performer_name = char_tag.split('_(')[0]
100+
performer = get_or_create_performer(stash, performer_name)
101+
performer_ids.append(performer["id"])
102+
103+
# Update image
104+
try:
105+
stash.update_image({
106+
"id": image_id,
107+
"urls": [post_url],
108+
"tag_ids": list(set(tag_ids)),
109+
"studio_id": studio_id,
110+
"performer_ids": performer_ids
111+
})
112+
113+
log.info("Image updated: ${image_id}")
114+
except Exception as e:
115+
log.error(f"Update failed: {str(e)}")
116+
117+
118+
def get_or_create_tag(stash: StashInterface, tag_name: str) -> dict:
119+
"""Find or create tag with hierarchy handling"""
120+
# Validate tag name
121+
tag_name = tag_name.strip()
122+
if not tag_name:
123+
log.error("Attempted to create tag with empty name")
124+
return None
125+
126+
existing = stash.find_tags(f={"name": {"value": tag_name, "modifier": "EQUALS"}})
127+
if existing:
128+
return existing[0]
129+
130+
parts = tag_name.split(":")
131+
parent_id = None
132+
for i in range(len(parts)):
133+
current_name = ":".join(parts[:i+1]).strip()
134+
if not current_name:
135+
continue
136+
137+
existing = stash.find_tags(f={"name": {"value": current_name, "modifier": "EQUALS"}})
138+
if not existing:
139+
create_data = {"name": current_name}
140+
if parent_id:
141+
create_data["parent_ids"] = [parent_id]
142+
try:
143+
new_tag = stash.create_tag(create_data)
144+
if not new_tag:
145+
log.error(f"Failed to create tag: {current_name}")
146+
return None
147+
parent_id = new_tag["id"]
148+
except Exception as e:
149+
log.error(f"Error creating tag {current_name}: {str(e)}")
150+
return None
151+
else:
152+
parent_id = existing[0]["id"]
153+
return {"id": parent_id}
154+
155+
def get_or_create_studio(stash: StashInterface, name: str) -> dict:
156+
"""Find or create studio"""
157+
studios = stash.find_studios(f={"name": {"value": name, "modifier": "EQUALS"}})
158+
return studios[0] if studios else stash.create_studio({"name": name})
159+
160+
161+
def get_or_create_performer(stash: StashInterface, name: str) -> dict:
162+
"""Find or create performer"""
163+
performers = stash.find_performers(f={"name": {"value": name, "modifier": "EQUALS"}})
164+
return performers[0] if performers else stash.create_performer({"name": name})
165+
166+
167+
def scrape_image(client: StashInterface, image_id: str) -> None:
168+
"""Main scraping handler"""
169+
image = client.find_image(image_id)
170+
if not image or not image.get("visual_files"):
171+
return
172+
173+
file_data = image["visual_files"][0]
174+
filename = file_data["basename"]
175+
filename_md5 = filename.split('.')[0]
176+
final_md5 = None
177+
178+
# First try filename-based MD5
179+
if re.match(r"^[a-f0-9]{32}$", filename_md5):
180+
final_md5 = filename_md5
181+
log.info(f"Using filename MD5: {final_md5}")
182+
else:
183+
# Fallback to content-based MD5
184+
try:
185+
file_path = file_data["path"]
186+
log.info(f"Generating MD5 from file content: {file_path}")
187+
188+
md5_hash = hashlib.md5()
189+
with open(file_path, "rb") as f:
190+
# Read file in 64kb chunks for memory efficiency
191+
for chunk in iter(lambda: f.read(65536), b""):
192+
md5_hash.update(chunk)
193+
194+
final_md5 = md5_hash.hexdigest()
195+
log.info(f"Generated content MD5: {final_md5}")
196+
except Exception as e:
197+
log.error(f"Failed to generate MD5: {str(e)}")
198+
return
199+
200+
if final_md5:
201+
process_e621_post(client, image_id, final_md5)
202+
else:
203+
log.warning("No valid MD5 available for processing")
204+
205+
# Plugin setup and execution
206+
# In the main execution block:
207+
if __name__ == "__main__":
208+
json_input = json.loads(sys.stdin.read())
209+
stash = StashInterface(json_input["server_connection"])
210+
211+
config = stash.get_configuration().get("plugins", {})
212+
settings = {
213+
"SkipTags": "e621_tagged", # Add automatic filtering
214+
"ExcludeOrganized": False
215+
}
216+
settings.update(config.get("e621_tagger", {}))
217+
218+
log.info(settings)
219+
220+
# Get e621_tagged ID for filtering
221+
e621_tag = get_or_create_tag(stash, "e621_tagged")
222+
223+
# Existing tags + automatic e621_tagged exclusion
224+
skip_tags = [t.strip() for t in settings["SkipTags"].split(",") if t.strip()]
225+
skip_tags.append(e621_tag["id"]) # Filter by ID instead of name
226+
227+
images = get_all_images(stash, skip_tags, settings["ExcludeOrganized"])
228+
229+
# Rest of the loop remains the same
230+
for i, image in enumerate(images, 1):
231+
image_tag_names = [tag["name"] for tag in image.get("tags", [])]
232+
if any(tag in image_tag_names for tag in skip_tags):
233+
log.info(f"Skipping image {image['id']} - contains skip tag")
234+
continue
235+
236+
log.progress(i/len(images))
237+
scrape_image(stash, image["id"])
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: e621_tagger
2+
description: Finding images and videos on e621 and tagging them.
3+
version: 0.1
4+
url: https://github.com/stashapp/CommunityScripts/
5+
exec:
6+
- python
7+
- "{pluginDir}/e621_tagger.py"
8+
9+
interface: raw
10+
11+
settings:
12+
SkipTags:
13+
displayName: List of tags to skip (comma separated). Default - e621_tagged
14+
type: STRING
15+
ExcludeOrganized:
16+
displayName: Exclude images that are set as organized (default is to include)
17+
type: BOOLEAN
18+
19+
tasks:
20+
- name: "Tag everything"
21+
description: "Tag everything (Warning: can take a while)"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
requests
2+
# stashapi has to be installed from source until stashapp-tools is updated to include the latest version
3+
stashapi @ git+https://github.com/stg-annon/stashapi.git

0 commit comments

Comments
 (0)