This commit is contained in:
Christoph Califice
2025-10-09 20:05:31 -03:00
parent ed22ef22bc
commit 0a5f88d75a
1442 changed files with 101562 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
import re, sys, copy, json
try:
import requests
except ModuleNotFoundError:
print("You need to install the requests module. (https://docs.python-requests.org/en/latest/user/install/)", file=sys.stderr)
print("If you have pip (normally installed with python), run this command in a terminal (cmd): pip install requests", file=sys.stderr)
sys.exit()
try:
import py_common.log as log
except ModuleNotFoundError:
print("You need to download the folder 'py_common' from the community repo! (CommunityScrapers/tree/master/scrapers/py_common)", file=sys.stderr)
sys.exit()
try:
from traxxx_interface import TraxxxInterface
except ModuleNotFoundError:
print("You need to download the file 'traxxx_interface.py' from the community repo! (CommunityScrapers/tree/master/scrapers/traxxx_interface.py)", file=sys.stderr)
sys.exit()
def main():
global traxxx
mode = sys.argv[1]
traxxx = TraxxxInterface()
fragment = json.loads(sys.stdin.read())
data = None
log.info(mode)
if mode == 'scene_name':
data = scene_by_name(fragment)
if mode == 'scene_url':
data = scene_query_fragment(fragment)
if mode == 'scene_query_fragment':
data = scene_query_fragment(fragment)
if mode == 'scene_fragment':
data = scene_fragment(fragment)
if mode == 'performer_lookup':
data = performer_lookup(fragment)
if mode == 'performer_fragment':
data = performer_fragment(fragment)
if mode == 'performer_url':
data = performer_url(fragment)
# log.info(json.dumps(data))
print(json.dumps(data))
def search_traxxx_for_scene(fragment):
title = fragment.get("title")
if not title:
title = fragment.get("name")
if not title:
return
return traxxx.search_scenes(title)
# Return a list of scenes from a search
def scene_by_name(fragment):
scenes = search_traxxx_for_scene(fragment)
if scenes:
return [traxxx.parse_to_stash_scene_search(s) for s in scenes]
else:
log.warning("No scene results from Traxxx")
return []
# extract TraxxxID from passed fragment and return new fragment
def scene_query_fragment(fragment):
traxxx_url = fragment.get("url", "")
m = re.search(r'traxxx.me/scene/(\d+)/', traxxx_url)
if not m:
log.warning(f'could not parse scene ID from URL: {traxxx_url}')
return
scene_id = m.group(1)
scene = traxxx.get_scene(scene_id)
return traxxx.parse_to_stash_scene(scene)
# return first result from scene_name
def scene_fragment(fragment):
scenes = search_traxxx_for_scene(fragment)
if scenes:
return traxxx.parse_to_stash_scene(scenes[0])
# Return a list of possible performer matches
def performer_lookup(fragment):
performers = traxxx.search_performers(fragment["name"])
if performers:
return [traxxx.parse_to_stash_performer_search(p) for p in performers]
else:
log.warning("No performer results from Traxxx")
return []
# Return a single best guess for performer based on fragment
def performer_fragment(fragment):
# check if fragment has Traxxx URL
performer = performer_url(fragment)
if performer:
return performer
# search and take first result from lookup
performer = performer_lookup(fragment)[0]
return performer
# Get PerformerID from URL and do a lookup on it
def performer_url(fragment):
m = re.search(r'traxxx.me/actor/(\d+)/', fragment['url'])
if not m:
return
performer_id = m.group(1)
performer = traxxx.get_performer(performer_id)
return traxxx.parse_to_stash_performer(performer)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,51 @@
name: "Traxxx"
# requires: py_common
sceneByURL:
- url:
- traxxx.me/scene/
action: script
script:
- python
- Traxxx.py
- scene_url
sceneByName:
action: script
script:
- python
- Traxxx.py
- scene_name
sceneByFragment:
action: script
script:
- python
- Traxxx.py
- scene_fragment
sceneByQueryFragment:
action: script
script:
- python
- Traxxx.py
- scene_query_fragment
performerByName:
action: script
script:
- python
- Traxxx.py
- performer_lookup
performerByFragment:
action: script
script:
- python
- Traxxx.py
- performer_fragment
performerByURL:
- url:
- traxxx.me/actor/
action: script
script:
- python
- Traxxx.py
- performer_url
# Last Updated April 24, 2023

View File

@@ -0,0 +1,11 @@
id: Traxxx
name: Traxxx
metadata: {}
version: 3479c8b
date: "2023-11-22 01:14:42"
requires: []
source_repository: https://stashapp.github.io/CommunityScrapers/stable/index.yml
files:
- traxxx_interface.py
- Traxxx.py
- Traxxx.yml

View File

@@ -0,0 +1,608 @@
import re, sys
# local modules
try:
import requests
except ModuleNotFoundError:
print("You need to install the requests module. (https://docs.python-requests.org/en/latest/user/install/)", file=sys.stderr)
print("If you have pip (normally installed with python), run this command in a terminal (cmd): pip install requests", file=sys.stderr)
sys.exit()
try:
import py_common.log as log
except ModuleNotFoundError:
print("You need to download the folder 'py_common' from the community repo! (CommunityScrapers/tree/master/scrapers/py_common)", file=sys.stderr)
sys.exit()
def parse_response(json_input):
if isinstance(json_input, dict):
for key, value in json_input.items():
if isinstance(value, dict):
json_input[key] = transform_type(value)
parse_response(json_input[key])
else:
parse_response(value)
elif isinstance(json_input, list):
for item in json_input:
parse_response(item)
def transform_type(value):
if value.get("__typename") == "Media":
if value.get("isS3"):
return f'https://cdndev.traxxx.me/{value.get("path")}'
else:
return f'https://traxxx.me/media/{value.get("path")}'
return value
class TraxxxInterface:
port = ""
url = ""
headers = {
"Content-Type": "application/json",
"User-Agent": "stash/1.0.0"
}
cookies = {}
def __init__(self, fragments={}):
scheme = "https"
domain = 'traxxx.me'
if self.port:
domain = f'{domain}:{self.port}'
# Stash GraphQL endpoint
self.url = f'{scheme}://{domain}/graphql'
log.debug(f"Using GraphQl endpoint at {self.url}")
self.fragments = fragments
self.fragments.update(traxxx_gql_fragments)
def __resolveFragments(self, query):
fragmentReferences = list(set(re.findall(r'(?<=\.\.\.)\w+', query)))
fragments = []
for ref in fragmentReferences:
fragments.append({
"fragment": ref,
"defined": bool(re.search("fragment {}".format(ref), query))
})
if all([f["defined"] for f in fragments]):
return query
else:
for fragment in [f["fragment"] for f in fragments if not f["defined"]]:
if fragment not in self.fragments:
raise Exception(f'GraphQL error: fragment "{fragment}" not defined')
query += self.fragments[fragment]
return self.__resolveFragments(query)
def __callGraphQL(self, query, variables=None):
query = self.__resolveFragments(query)
json_request = {'query': query}
if variables is not None:
json_request['variables'] = variables
response = requests.post(self.url, json=json_request, headers=self.headers, cookies=self.cookies)
if response.status_code == 200:
result = response.json()
if result.get("errors"):
for error in result["errors"]:
log.error(f"GraphQL error: {error}")
if result.get("error"):
for error in result["error"]["errors"]:
log.error(f"GraphQL error: {error}")
if result.get("data"):
data = result['data']
parse_response(data)
return data
elif response.status_code == 401:
sys.exit("HTTP Error 401, Unauthorized. Cookie authentication most likely failed")
else:
raise ConnectionError(
"GraphQL query failed:{} - {}. Query: {}. Variables: {}".format(
response.status_code, response.content, query, variables)
)
def search_scenes(self, search, numResults=20):
query = """
query SearchReleases(
$query: String!
$limit: Int = 20
) {
scenes: searchReleases(
query: $query
first: $limit
orderBy: RANK_DESC
filter: {
rank: {
greaterThan: 0.045
}
}
) {
release {
...traxScene
}
rank
}
}
"""
variables = {
'query': search,
'limit': int(numResults)
}
result = self.__callGraphQL(query, variables)
log.info(f'scene search "{search}" returned {len(result["scenes"])} results')
return [s["release"] for s in result["scenes"]]
def search_performers(self, search, numResults=20):
query = """
query SearchActors(
$query: String!
$limit: Int = 20
) {
actors: searchActors(
query: $query
first: $limit
) {
...traxActor
}
}
"""
variables = {
'query': search,
'limit': int(numResults)
}
results = self.__callGraphQL(query, variables).get("actors")
log.info(f'performer search "{search}" returned {len(results)} results')
return results
# shootID refers to a media sources uniqueID e.x. a LegalPorno shootID might be "GIO0001"
def get_scene_by_shootID(self, shootId):
query = """
query Releases(
$idList: [String!]
){ releases( filter: {shootId: { in:$idList } }
){
...traxScene
}
}
"""
variables = {'idList': [shootId]}
response = self.__callGraphQL(query, variables).get("releases")
log.info(f'scene shootID lookup "{shootId}" returned {len(response)} results')
return next(iter(response), None)
def get_scene(self, traxxx_scene_id):
query = """
query Releases(
$sceneId: Int!
) {
releases(
filter:{id:{equalTo:$sceneId}}
) {
...traxScene
}
}
"""
variables = {'sceneId': int(traxxx_scene_id)}
response = self.__callGraphQL(query, variables).get("releases")
log.info(f'scene traxxxID lookup "{traxxx_scene_id}" returned {len(response)} results')
return next(iter(response), None)
def get_performer(self, traxxx_performer_id):
query = """
query Actors(
$actorId: Int!
) {
actors: actors(
filter:{id:{equalTo:$actorId}}
) {
...traxActor
}
}
"""
variables = {'actorId': int(traxxx_performer_id)}
response = self.__callGraphQL(query, variables).get("actors")
log.info(f'performer traxxxID lookup "{traxxx_performer_id}" returned {len(response)} results')
return next(iter(response), None)
def parse_to_stash_scene_search(self, s):
fragment = {}
if s.get("poster"):
if s["poster"].get("image"):
fragment["image"] = s["poster"]["image"]
# search returns url as traxxx url for later parsing by scene parser
if s.get("slug"):
fragment["url"] = f'https://traxxx.me/scene/{s["id"]}/{s["slug"]}/'
if s.get("shootId"):
fragment["code"] = s["shootId"]
if s.get("date"):
fragment["date"] = s["date"].split("T")[0]
if s.get("title"):
fragment["title"] = s["title"]
if s.get("entity"):
if s["entity"].get("name"):
fragment["studio"] = { "name": s["entity"]["name"] }
if s.get("description"):
fragment["details"] = s["description"]
# #tags take too much space in the results page
#if s.get("tags"):
# fragment["tags"] = [{"name": t["tag"]["name"]} for t in s.get("tags",{}) if t["tag"] and t["tag"].get("name")]
if s.get("actors"):
fragment["performers"] = [{"name": a["actor"]["name"]} for a in s["actors"] if a["actor"] and a["actor"].get("name")]
return fragment
def parse_to_stash_scene(self, s):
fragment = {}
if s.get("shootId"):
fragment["code"] = s["shootId"]
if s.get("title"):
fragment["title"] = s["title"]
if s.get("description"):
fragment["details"] = s["description"]
if s.get("url"):
fragment["url"] = s.get("url")
if s.get("date"):
fragment["date"] = s["date"].split("T")[0]
if s.get("poster"):
if s["poster"].get("image"):
fragment["image"] = s["poster"]["image"]
if s.get("tags"):
fragment["tags"] = [{"name": t["tag"]["name"]} for t in s.get("tags",{}) if t["tag"] and t["tag"].get("name")]
if s.get("actors"):
fragment["performers"] = [{"name": a["actor"]["name"]} for a in s["actors"] if a["actor"] and a["actor"].get("name")]
if s.get("movies"):
movies = []
for m in s["movies"]:
m = m["movie"]
if m.get("title"):
movie = {
"name": m["title"]
}
if m.get("date"):
movie["date"] = m["date"]
if m.get("url"):
movie["url"] = m["url"]
if m.get("description"):
movie["synopsis"] = m["description"]
if m.get("covers"):
covers = m["covers"]
if len(covers) >= 1:
movie["front_image"] = covers[0]["media"]
if len(covers) >= 2:
movie["back_image"] = covers[1]["media"]
movies.append(movie)
fragment["movies"] = movies
if s.get("entity"):
if s["entity"].get("name"):
studio = {'name':s["entity"]["name"]}
if s["entity"].get("url"):
studio['url'] = s["entity"]["url"]
fragment["studio"] = studio
return fragment
def parse_to_stash_performer_search(self, p):
fragment = {}
if p.get("name"):
fragment["name"] = p["name"]
if p.get("slug"):
fragment["url"] = f'https://traxxx.me/actor/{p["id"]}/{p["slug"]}/'
fragment["images"] = []
if p.get("image"):
fragment["images"].append( p["image"] )
for profile in p["profiles"]:
if profile.get("image"):
fragment["images"].append( profile["image"] )
return fragment
def parse_to_stash_performer(self, p):
fragment = {}
if p.get("name"):
fragment["name"] = p["name"]
if p.get("slug"):
fragment["url"] = f'https://traxxx.me/actor/{p["id"]}/{p["slug"]}/'
if p.get("gender"):
fragment["gender"] = p["gender"]
if p.get("birthdate"):
fragment["birthdate"] = p["birthdate"]
if p.get("dateOfDeath"):
fragment["death_date"] = p["dateOfDeath"]
if p.get("eyes"):
fragment["eye_color"] = p["eyes"]
if p.get("hairColor"):
fragment["hair_color"] = p["hairColor"]
if p.get("heightMetric"):
fragment["height"] = p["heightMetric"]
if p.get("weightMetric"):
fragment["weight"] = p["weightMetric"]
if p.get("tattoos"):
fragment["tattoos"] = p["tattoos"]
if p.get("piercings"):
fragment["piercings"] = p["piercings"]
if p["naturalBoobs"] is False:
fragment["fake_tits"] = "Augmented"
if p["naturalBoobs"] is True:
fragment["fake_tits"] = "Natural"
if all( k in p for k in ['cup','bust','waist','hip'] ):
fragment['measurements'] = f"{p['bust']}{p['cup']}-{p['waist']}-{p['hip']}"
if p.get("ethnicity"):
fragment["ethnicity"] = p["ethnicity"]
if p.get("birthCountry"):
if p["birthCountry"].get("alpha2"):
country = p["birthCountry"]["alpha2"]
fragment["country"] = country
fragment["images"] = []
if p.get("image"):
fragment["images"].append( p["image"] )
for profile in p["profiles"]:
if profile.get("image"):
fragment["images"].append( profile["image"] )
# descriptions = []
# for profile in p["profiles"]:
# if profile.get("description"):
# if profile.get("entity"):
# if profile["entity"].get("name"):
# descriptions.append(f'{profile["entity"]["name"]}:\n{profile["description"]}')
# if descriptions != []:
# fragment["description"] = "\n\n".join(descriptions)
if p.get("aliasFor"):
# TODO: when traxxx has any performers with an alias
# fragment["aliases"]
pass
if p.get("socials"):
# TODO: when traxxx has more data to work with
# fragment["twitter"]
# fragment["instagram"]
pass
# TODO: implement this if traxxx ever adds career data to performers
# if p.get("careerLength")
# fragment["career_length"] = p["careerLength"]
return fragment
traxxx_gql_fragments = {
"traxActorMin": """
fragment traxActorMin on Actor {
id
slug
name
gender
aliasFor: actorByAliasFor {
id
name
slug
}
}
""",
"traxActor":"""
fragment traxActor on Actor {
id
slug
name
gender
dateOfBirth
dateOfDeath
ethnicity
cup
bust
waist
hip
naturalBoobs
heightMetric: height(units:METRIC)
weightMetric: weight(units:METRIC)
hairColor
eyes
hasTattoos
tattoos
hasPiercings
piercings
socials: actorsSocials {
id
url
platform
}
description
aliasFor: actorByAliasFor {
id
name
slug
}
entity { ...traxEntity }
image: avatarMedia {
...traxMedia
}
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
profiles: actorsProfiles(orderBy: PRIORITY_DESC) {
...traxActorProfile
}
}
""",
"traxActorProfile":"""
fragment traxActorProfile on ActorsProfile {
image: avatarMedia { ...traxMedia }
description
entity { ...traxEntity }
}
""",
"traxScene":"""
fragment traxScene on Release{
id
title
date
datePrecision
slug
shootId
productionDate
comment
createdAt
url
actors: releasesActors(filter:{actor:{gender:{distinctFrom:"male"}}}) {
actor {...traxActorMin}
}
tags: releasesTags(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) {
tag {
name
priority
__typename
}
}
chapters: chapters(orderBy: CHAPTERS_POSTER_BY_CHAPTER_ID__CHAPTER_ID_DESC){
title
description
duration
time
tags: chaptersTags{ tag { id, name, slug } }
__typename
}
poster: releasesPoster {
image: media{ ...traxMedia }
}
covers: releasesCovers(orderBy: MEDIA_BY_MEDIA_ID__INDEX_ASC) {
image: media{ ...traxMedia }
}
photos: releasesPhotos(orderBy: MEDIA_BY_MEDIA_ID__INDEX_ASC) {
image: media{ ...traxMedia }
}
isNew
isFavorited
entity { ...traxEntity }
studio {
id
name
slug
url
__typename
}
movies: moviesScenesBySceneId {
movie {
id
title
slug
url
description
date
datePrecision
covers: moviesCovers(orderBy: MEDIA_BY_MEDIA_ID__INDEX_DESC) {
media {
...traxMedia
}
}
__typename
}
__typename
}
}
""",
"traxMedia":"""
fragment traxMedia on Media {
id
index
path
thumbnail
width
height
thumbnailWidth
thumbnailHeight
lazy
isS3
comment
__typename
}
""",
"traxEntity":"""
fragment traxEntity on Entity {
id
name
slug
url
type
independent
parent {
id
slug
url
type
__typename
}
__typename
}
""",
}