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 } """, }