2024-05-23 21:26:54 +02:00

1242 lines
25 KiB
Python

import requests
import py_common.log as log
from py_common.config import get_config
from py_common.util import dig
config = get_config(
default="""
# URL for your local Stash server
url = http://localhost:9999
# API key can be generated in Stash's settings page: `Settings > Security > Authentication`
api_key =
"""
)
def callGraphQL(query: str, variables: dict | None = None):
api_key = config.api_key
url = config.url
if not url:
log.error("You need to set the URL in 'config.ini'")
return None
elif "stashdb.org" in url:
log.error("You need to set the URL in 'config.ini' to your own stash server")
return None
stash_url = f"{url}/graphql"
headers = {
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/json",
"Accept": "application/json",
"Connection": "keep-alive",
"DNT": "1",
"ApiKey": api_key,
}
json: dict[str, str | dict] = {"query": query}
if variables:
json["variables"] = variables
response = requests.post(stash_url, json=json, headers=headers)
if response.status_code == 200:
result = response.json()
if errors := result.get("error"):
errors = "\n".join(errors)
log.error(f"[GraphQL] {errors}")
return result.get("data")
elif response.status_code == 401:
if not api_key:
log.error(
"[GraphQL] Unable to authenticate with Stash: you need to set the API Key in 'config.ini' in the 'py_common' folder"
)
else:
log.error(
"[GraphQL] Unable to authenticate with Stash: make sure 'config.ini' in the 'py_common' folder has the correct API Key"
)
return None
elif response.status_code == 404:
log.error(
"[GraphQL] Unable to connect to Stash: make sure 'config.ini' in the 'py_common' folder has the correct URL"
)
return None
raise ConnectionError(
f"[GraphQL] Unexpected error: {response.status_code} - {response.content}"
)
def configuration() -> dict | None:
query = """
query Configuration {
configuration {
...ConfigData
}
}
fragment ConfigData on ConfigResult {
general {
...ConfigGeneralData
}
interface {
...ConfigInterfaceData
}
dlna {
...ConfigDLNAData
}
scraping {
...ConfigScrapingData
}
defaults {
...ConfigDefaultSettingsData
}
}
fragment ConfigGeneralData on ConfigGeneralResult {
stashes {
path
excludeVideo
excludeImage
}
databasePath
generatedPath
metadataPath
cachePath
calculateMD5
videoFileNamingAlgorithm
parallelTasks
previewAudio
previewSegments
previewSegmentDuration
previewExcludeStart
previewExcludeEnd
previewPreset
maxTranscodeSize
maxStreamingTranscodeSize
writeImageThumbnails
apiKey
username
password
maxSessionAge
logFile
logOut
logLevel
logAccess
createGalleriesFromFolders
videoExtensions
imageExtensions
galleryExtensions
excludes
imageExcludes
customPerformerImageLocation
stashBoxes {
name
endpoint
api_key
}
}
fragment ConfigInterfaceData on ConfigInterfaceResult {
menuItems
soundOnPreview
wallShowTitle
wallPlayback
maximumLoopDuration
noBrowser
autostartVideo
autostartVideoOnPlaySelected
continuePlaylistDefault
showStudioAsText
css
cssEnabled
language
imageLightbox {
slideshowDelay
displayMode
scaleUp
resetZoomOnNav
scrollMode
scrollAttemptsBeforeChange
}
disableDropdownCreate {
performer
tag
studio
}
handyKey
funscriptOffset
}
fragment ConfigDLNAData on ConfigDLNAResult {
serverName
enabled
whitelistedIPs
interfaces
}
fragment ConfigScrapingData on ConfigScrapingResult {
scraperUserAgent
scraperCertCheck
scraperCDPPath
excludeTagPatterns
}
fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
scan {
scanGeneratePreviews
scanGenerateImagePreviews
scanGenerateSprites
scanGeneratePhashes
scanGenerateThumbnails
}
identify {
sources {
source {
...ScraperSourceData
}
options {
...IdentifyMetadataOptionsData
}
}
options {
...IdentifyMetadataOptionsData
}
}
autoTag {
performers
studios
tags
__typename
}
generate {
sprites
previews
imagePreviews
previewOptions {
previewSegments
previewSegmentDuration
previewExcludeStart
previewExcludeEnd
previewPreset
}
markers
markerImagePreviews
markerScreenshots
transcodes
phashes
}
deleteFile
deleteGenerated
}
fragment ScraperSourceData on ScraperSource {
stash_box_endpoint
scraper_id
}
fragment IdentifyMetadataOptionsData on IdentifyMetadataOptions {
fieldOptions {
...IdentifyFieldOptionsData
}
setCoverImage
setOrganized
includeMalePerformers
}
fragment IdentifyFieldOptionsData on IdentifyFieldOptions {
field
strategy
createMissing
}
"""
result = callGraphQL(query) or {}
return dig(result, "configuration")
def getScene(scene_id: str | int) -> dict | None:
query = """
query FindScene($id: ID!, $checksum: String) {
findScene(id: $id, checksum: $checksum) {
...SceneData
}
}
fragment SceneData on Scene {
id
title
code
details
urls
date
rating100
o_counter
organized
interactive
files {
path
size
duration
video_codec
audio_codec
width
height
frame_rate
bit_rate
}
paths {
screenshot
preview
stream
webp
vtt
sprite
funscript
}
scene_markers {
...SceneMarkerData
}
galleries {
...SlimGalleryData
}
studio {
...SlimStudioData
}
movies {
movie {
...MovieData
}
scene_index
}
tags {
...SlimTagData
}
performers {
...PerformerData
}
stash_ids {
endpoint
stash_id
}
}
fragment SceneMarkerData on SceneMarker {
id
title
seconds
stream
preview
screenshot
scene {
id
}
primary_tag {
id
name
aliases
}
tags {
id
name
aliases
}
}
fragment SlimGalleryData on Gallery {
id
title
code
date
urls
details
photographer
rating100
organized
image_count
cover {
paths {
thumbnail
}
}
studio {
id
name
image_path
}
tags {
id
name
}
performers {
id
name
gender
favorite
image_path
}
scenes {
id
title
files {
path
basename
}
}
}
fragment SlimStudioData on Studio {
id
name
image_path
stash_ids {
endpoint
stash_id
}
parent_studio {
id
}
details
rating100
aliases
}
fragment MovieData on Movie {
id
name
aliases
duration
date
rating100
director
studio {
...SlimStudioData
}
synopsis
url
front_image_path
back_image_path
scene_count
scenes {
id
title
files {
path
}
}
}
fragment SlimTagData on Tag {
id
name
aliases
image_path
}
fragment PerformerData on Performer {
id
name
url
gender
twitter
instagram
birthdate
ethnicity
country
eye_color
height_cm
measurements
fake_tits
career_length
tattoos
piercings
alias_list
favorite
image_path
scene_count
image_count
gallery_count
movie_count
tags {
...SlimTagData
}
stash_ids {
stash_id
endpoint
}
rating100
details
death_date
hair_color
weight
}
"""
variables = {"id": str(scene_id)}
result = callGraphQL(query, variables) or {}
return dig(result, "findScene")
def getSceneScreenshot(scene_id: str | int) -> str | None:
query = """
query FindScene($id: ID!, $checksum: String) {
findScene(id: $id, checksum: $checksum) {
id
paths {
screenshot
}
}
}
"""
variables = {"id": str(scene_id)}
result = callGraphQL(query, variables) or {}
return dig(result, "findScene", "paths", "screenshot")
def getSceneByPerformerId(performer_id: str | int) -> dict | None:
query = """
query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {
findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {
count
filesize
duration
scenes {
...SceneData
__typename
}
__typename
}
}
fragment SceneData on Scene {
id
title
details
urls
date
rating100
o_counter
organized
files {
path
size
duration
video_codec
audio_codec
width
height
frame_rate
bit_rate
__typename
}
interactive
interactive_speed
captions {
language_code
caption_type
__typename
}
created_at
updated_at
paths {
screenshot
preview
stream
webp
vtt
sprite
funscript
interactive_heatmap
caption
__typename
}
scene_markers {
...SceneMarkerData
__typename
}
galleries {
...SlimGalleryData
__typename
}
studio {
...SlimStudioData
__typename
}
movies {
movie {
...MovieData
__typename
}
scene_index
__typename
}
tags {
...SlimTagData
__typename
}
performers {
...PerformerData
__typename
}
stash_ids {
endpoint
stash_id
__typename
}
sceneStreams {
url
mime_type
label
__typename
}
__typename
}
fragment SceneMarkerData on SceneMarker {
id
title
seconds
stream
preview
screenshot
scene {
id
__typename
}
primary_tag {
id
name
aliases
__typename
}
tags {
id
name
aliases
__typename
}
__typename
}
fragment SlimGalleryData on Gallery {
id
title
code
date
urls
details
photographer
rating100
organized
image_count
cover {
paths {
thumbnail
__typename
}
__typename
}
studio {
id
name
image_path
__typename
}
tags {
id
name
__typename
}
performers {
id
name
gender
favorite
image_path
__typename
}
scenes {
id
title
files {
path
}
__typename
}
__typename
}
fragment SlimStudioData on Studio {
id
name
image_path
stash_ids {
endpoint
stash_id
__typename
}
parent_studio {
id
__typename
}
details
rating100
aliases
__typename
}
fragment MovieData on Movie {
id
name
aliases
duration
date
rating100
director
studio {
...SlimStudioData
__typename
}
synopsis
url
front_image_path
back_image_path
scene_count
scenes {
id
title
files {
path
}
__typename
}
__typename
}
fragment SlimTagData on Tag {
id
name
aliases
image_path
__typename
}
fragment PerformerData on Performer {
id
name
url
gender
twitter
instagram
birthdate
ethnicity
country
eye_color
height_cm
measurements
fake_tits
career_length
tattoos
piercings
alias_list
favorite
ignore_auto_tag
image_path
scene_count
image_count
gallery_count
movie_count
tags {
...SlimTagData
__typename
}
stash_ids {
stash_id
endpoint
__typename
}
rating100
details
death_date
hair_color
weight
__typename
}
"""
variables = {
"filter": {"page": 1, "per_page": 20, "sort": "title", "direction": "ASC"},
"scene_filter": {
"performers": {"value": [str(performer_id)], "modifier": "INCLUDES_ALL"}
},
}
result = callGraphQL(query, variables) or {}
return dig(result, "findScenes")
def getSceneIdByPerformerId(performer_id: str | int) -> dict | None:
query = """
query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {
findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {
scenes {
id
title
files {
path
}
paths {
screenshot
}
}
}
}
"""
variables = {
"filter": {"page": 1, "per_page": 20, "sort": "id", "direction": "DESC"},
"scene_filter": {
"performers": {"value": [str(performer_id)], "modifier": "INCLUDES_ALL"}
},
}
result = callGraphQL(query, variables) or {}
return dig(result, "findScenes")
def getPerformersByName(performer_name: str) -> dict | None:
query = """
query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {
findPerformers(filter: $filter, performer_filter: $performer_filter) {
count
performers {
...PerformerData
__typename
}
__typename
}
}
fragment PerformerData on Performer {
id
name
url
gender
twitter
instagram
birthdate
ethnicity
country
eye_color
height_cm
measurements
fake_tits
career_length
tattoos
piercings
alias_list
favorite
ignore_auto_tag
image_path
scene_count
image_count
gallery_count
movie_count
tags {
...SlimTagData
__typename
}
stash_ids {
stash_id
endpoint
__typename
}
rating100
details
death_date
hair_color
weight
__typename
}
fragment SlimTagData on Tag {
id
name
aliases
image_path
__typename
}
"""
variables = {
"filter": {
"q": performer_name,
"page": 1,
"per_page": 20,
"sort": "name",
"direction": "ASC",
},
"performer_filter": {},
}
result = callGraphQL(query, variables) or {}
return dig(result, "findPerformers")
def getPerformersIdByName(performer_name: str) -> dict | None:
query = """
query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {
findPerformers(filter: $filter, performer_filter: $performer_filter) {
count
performers {
...PerformerData
}
}
}
fragment PerformerData on Performer {
id
name
alias_list
}
"""
variables = {
"filter": {
"q": performer_name,
"page": 1,
"per_page": 20,
"sort": "name",
"direction": "ASC",
},
"performer_filter": {},
}
result = callGraphQL(query, variables) or {}
return dig(result, "findPerformers")
def getGallery(gallery_id: str | int) -> dict | None:
query = """
query FindGallery($id: ID!) {
findGallery(id: $id) {
...GalleryData
}
}
fragment GalleryData on Gallery {
id
created_at
updated_at
title
code
date
urls
details
photographer
rating100
organized
folder {
path
}
cover {
...SlimImageData
}
studio {
...SlimStudioData
}
tags {
...SlimTagData
}
performers {
...PerformerData
}
scenes {
...SlimSceneData
}
}
fragment SlimImageData on Image {
id
title
rating100
organized
o_counter
visual_files {
... on ImageFile {
path
size
height
width
}
}
paths {
thumbnail
image
}
galleries {
id
files {
path
}
title
}
studio {
id
name
image_path
}
tags {
id
name
}
performers {
id
name
gender
favorite
image_path
}
}
fragment SlimStudioData on Studio {
id
name
image_path
stash_ids {
endpoint
stash_id
}
parent_studio {
id
}
details
rating100
aliases
}
fragment SlimTagData on Tag {
id
name
aliases
image_path
}
fragment PerformerData on Performer {
id
name
url
gender
twitter
instagram
birthdate
ethnicity
country
eye_color
height_cm
measurements
fake_tits
career_length
tattoos
piercings
alias_list
favorite
image_path
scene_count
image_count
gallery_count
movie_count
tags {
...SlimTagData
}
stash_ids {
stash_id
endpoint
}
rating100
details
death_date
hair_color
weight
}
fragment SlimSceneData on Scene {
id
title
code
details
urls
date
rating100
o_counter
organized
interactive
files {
path
size
duration
video_codec
audio_codec
width
height
frame_rate
bit_rate
}
paths {
screenshot
preview
stream
webp
vtt
sprite
funscript
}
scene_markers {
id
title
seconds
}
galleries {
id
title
files {
path
}
}
studio {
id
name
image_path
}
movies {
movie {
id
name
front_image_path
}
scene_index
}
tags {
id
name
}
performers {
id
name
gender
favorite
image_path
}
stash_ids {
endpoint
stash_id
}
}
"""
variables = {"id": gallery_id}
result = callGraphQL(query, variables) or {}
return dig(result, "findGallery")
def getGalleryPath(gallery_id: str | int) -> str | None:
query = """
query FindGallery($id: ID!) {
findGallery(id: $id) {
folder {
path
}
files {
path
}
}
}
"""
variables = {"id": gallery_id}
result = callGraphQL(query, variables) or {}
# Galleries can either be a folder full of files or a zip file
return dig(result, "findGallery", "folder", "path") or dig(
result, "findGallery", "files", 0, "path"
)
# GraphQL introspection
#
# if a graphQL API changes, you can use this as the query string value to
# discover available API fields, queries, etc.
GRAPHQL_INTROSPECTION = """
fragment FullType on __Type {
kind
name
fields(includeDeprecated: true) {
name
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
type {
...TypeRef
}
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
types {
...FullType
}
directives {
name
locations
args {
...InputValue
}
}
}
}
"""