222 lines
6.0 KiB
Python
222 lines
6.0 KiB
Python
"""
|
|
DeviantArt Stash upload module for ComfyUI node.
|
|
|
|
Handles uploading files to DeviantArt Stash via /stash/submit endpoint.
|
|
Returns itemid, stack, and stackid for subsequent publish operation.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import requests
|
|
|
|
# API URL
|
|
API_STASH_SUBMIT_URL = "https://www.deviantart.com/api/v1/oauth2/stash/submit"
|
|
|
|
|
|
def get_content_type(file_path: Path) -> str:
|
|
"""
|
|
Get MIME content type based on file extension.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
|
|
Returns:
|
|
MIME type string
|
|
"""
|
|
ext = file_path.suffix.lower()
|
|
content_types = {
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".png": "image/png",
|
|
".gif": "image/gif",
|
|
".bmp": "image/bmp",
|
|
}
|
|
return content_types.get(ext, "application/octet-stream")
|
|
|
|
|
|
def to_base36(number: int) -> str:
|
|
"""
|
|
Convert integer to base36 string for sta.sh URL.
|
|
|
|
Args:
|
|
number: Integer itemid from API response
|
|
|
|
Returns:
|
|
Base36 encoded string (lowercase)
|
|
"""
|
|
if number == 0:
|
|
return "0"
|
|
|
|
chars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
result = []
|
|
|
|
while number:
|
|
number, remainder = divmod(number, 36)
|
|
result.append(chars[remainder])
|
|
|
|
return "".join(reversed(result))
|
|
|
|
|
|
class StashUploadResult:
|
|
"""Result of a Stash upload operation."""
|
|
|
|
def __init__(
|
|
self,
|
|
success: bool,
|
|
itemid: Optional[int] = None,
|
|
stack: Optional[str] = None,
|
|
stackid: Optional[int] = None,
|
|
stash_url: Optional[str] = None,
|
|
error: Optional[str] = None,
|
|
):
|
|
"""
|
|
Initialize upload result.
|
|
|
|
Args:
|
|
success: Whether upload was successful
|
|
itemid: Stash item ID (required for publish)
|
|
stack: Stack name (optional)
|
|
stackid: Stack ID (optional)
|
|
stash_url: URL to view item in Stash
|
|
error: Error message if failed
|
|
"""
|
|
self.success = success
|
|
self.itemid = itemid
|
|
self.stack = stack
|
|
self.stackid = stackid
|
|
self.stash_url = stash_url
|
|
self.error = error
|
|
|
|
|
|
def upload_to_stash(
|
|
file_path: str,
|
|
access_token: str,
|
|
title: Optional[str] = None,
|
|
artist_comments: Optional[str] = None,
|
|
tags: Optional[list[str]] = None,
|
|
original_url: Optional[str] = None,
|
|
is_dirty: bool = False,
|
|
noai: bool = False,
|
|
is_ai_generated: bool = True,
|
|
stack: Optional[str] = None,
|
|
stackid: Optional[int] = None,
|
|
) -> StashUploadResult:
|
|
"""
|
|
Upload a file to DeviantArt Stash.
|
|
|
|
Args:
|
|
file_path: Path to the image file
|
|
access_token: Valid OAuth access token
|
|
title: Title for the deviation (max 50 chars)
|
|
artist_comments: Artist's description/comments
|
|
tags: List of tags
|
|
original_url: URL to original source
|
|
is_dirty: Whether content contains adult themes
|
|
noai: Opt out of AI training
|
|
is_ai_generated: Whether content is AI generated
|
|
stack: Stack name to add item to
|
|
stackid: Stack ID to add item to
|
|
|
|
Returns:
|
|
StashUploadResult with itemid on success, error on failure
|
|
"""
|
|
path = Path(file_path)
|
|
|
|
if not path.exists():
|
|
return StashUploadResult(
|
|
success=False,
|
|
error=f"File not found: {file_path}"
|
|
)
|
|
|
|
# Build request data as list of tuples to properly handle arrays
|
|
# DeviantArt API expects array params as repeated keys: tags[]=x&tags[]=y
|
|
data: list[tuple[str, object]] = [("access_token", access_token)]
|
|
|
|
if title:
|
|
# Truncate title to 50 characters (API limit)
|
|
data.append(("title", title[:50]))
|
|
|
|
if artist_comments:
|
|
data.append(("artist_comments", artist_comments))
|
|
|
|
if tags:
|
|
for tag in tags:
|
|
data.append(("tags[]", tag))
|
|
|
|
if original_url:
|
|
data.append(("original_url", original_url))
|
|
|
|
if is_dirty:
|
|
data.append(("is_dirty", "1"))
|
|
|
|
if noai:
|
|
data.append(("noai", "1"))
|
|
|
|
if is_ai_generated:
|
|
data.append(("is_ai_generated", "1"))
|
|
|
|
if stack:
|
|
data.append(("stack", stack))
|
|
|
|
if stackid:
|
|
data.append(("stackid", stackid))
|
|
|
|
try:
|
|
print(f"Uploading file to Stash: {path.name}")
|
|
if tags:
|
|
print(f"Tags being sent: {tags}")
|
|
|
|
with path.open("rb") as fh:
|
|
files = {
|
|
"file": (path.name, fh, get_content_type(path)),
|
|
}
|
|
|
|
response = requests.post(
|
|
API_STASH_SUBMIT_URL,
|
|
data=data,
|
|
files=files,
|
|
timeout=120, # Longer timeout for file upload
|
|
)
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
|
|
if result.get("status") == "success":
|
|
itemid = result.get("itemid")
|
|
if not itemid:
|
|
return StashUploadResult(
|
|
success=False,
|
|
error="No itemid in response"
|
|
)
|
|
|
|
# Generate stash URL from itemid
|
|
base36_id = to_base36(itemid)
|
|
stash_url = f"https://sta.sh/0{base36_id}"
|
|
|
|
print(f"File uploaded to Stash successfully!")
|
|
print(f" ItemID: {itemid}")
|
|
print(f" URL: {stash_url}")
|
|
|
|
return StashUploadResult(
|
|
success=True,
|
|
itemid=itemid,
|
|
stack=result.get("stack"),
|
|
stackid=result.get("stackid"),
|
|
stash_url=stash_url,
|
|
)
|
|
|
|
error_msg = result.get(
|
|
"error_description", result.get("error", "Unknown error")
|
|
)
|
|
return StashUploadResult(success=False, error=error_msg)
|
|
|
|
except requests.RequestException as e:
|
|
error_msg = f"Stash upload request failed: {e}"
|
|
print(f"ERROR: {error_msg}")
|
|
return StashUploadResult(success=False, error=error_msg)
|
|
except Exception as e:
|
|
error_msg = f"Unexpected error during stash upload: {e}"
|
|
print(f"ERROR: {error_msg}")
|
|
return StashUploadResult(success=False, error=error_msg)
|