├── deovr-plugin.yml ├── README.md └── deovr-plugin.py /deovr-plugin.yml: -------------------------------------------------------------------------------- 1 | name: deovr-plugin 2 | description: Creates the json files that deovr is expecting 3 | version: 0.1 4 | url: https://github.com/tweeticoats/stash-deovr-plugin 5 | exec: 6 | - python3 7 | - "{pluginDir}/deovr-plugin.py" 8 | - api 9 | interface: raw 10 | tasks: 11 | - name: Setup tags 12 | description: Create tags used by plugin, ie export_deovr 13 | defaultArgs: 14 | mode: setup 15 | - name: create json 16 | description: run scrapers on tagged scenes 17 | defaultArgs: 18 | mode: json 19 | 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deovr-plugin 2 | This plugin generates json files used by deovr allowing you to play 2d and 3d video's using the player. 3 | Deovr looks for an index file /deovr of scenes as json files containing information on scenes. 4 | This plugin generates these 5 | 6 | Stash can act as a web server and serve files from a custom url, we will be using this to serve the json files generated by this plugin. 7 | Edit config.yml and add a custom served folder option like the below, this will serve files from http://stash_url/custom/deovr/ 8 | ``` 9 | custom_served_folders: 10 | /deovr: /root/.stash/deovr 11 | ``` 12 | 13 | Unfortunatly stash does not allow you serve the index url /deovr so you will need a web service to serve this index file from this base url. 14 | You need to map /deovr to /custom/deovr/deovr.json 15 | See the nginx configuration on how to proxy this one url. 16 | 17 | 18 | # Installation 19 | Download the git repo 20 | To use copy the plugin to the plugins/ folder in your stash. 21 | Reload plugins in stash and run the setup task to create tags in stash used for configuration. 22 | 23 | # Plugin usage 24 | This plugin is configured by looking at tags applied to scenes. 25 | Run the setup task to create these tags. 26 | * **export_deovr** - apply this tag to include it in the index 27 | * **FLAT** - Mark the video as 2d. This is the default if other projection tags are not configured. 28 | * **DOME** - 3D 180° projection, this is what most VR video's use. 29 | * **SPHERE** - 3D 360° projection used by some earlier videos 30 | * **FISHEYE** - Fish Eye lense projection 31 | * **MKX200** - 3D 200° projection used by SLR 32 | * **SBS** - Side by Side with the left eye taking up the left half of the video. This is the default for 3d video's. 33 | * **TB** - Up Down with the left eye taking up the top half of the video. 34 | 35 | # plugin configuration 36 | You will need to configure the domain / ip address so the urls are correct. 37 | Edit deovr-plugin and specify the domain or ip address of the instance of the stash instance. 38 | 39 | ``` 40 | domain='stash.home' 41 | ``` 42 | 43 | # Nginx configuration 44 | 45 | ``` 46 | server { 47 | listen 80; 48 | listen [::]:80; 49 | 50 | server_name stash.home; 51 | client_max_body_size 0; 52 | location = /deovr { 53 | proxy_pass http://192.168.0.xx:9999/custom/deovr/deovr.json; 54 | 55 | } 56 | location / { 57 | proxy_pass http://192.168.0.xx:9999; 58 | proxy_http_version 1.1; 59 | 60 | proxy_set_header Upgrade $http_upgrade; 61 | proxy_set_header Connection "Upgrade"; 62 | proxy_set_header Host $host; 63 | proxy_set_header X-Real-IP $remote_addr; 64 | proxy_set_header X-Forwarded-For $remote_addr; 65 | proxy_set_header X-Forwarded-Port $server_port; 66 | proxy_set_header X-Forwarded-Proto $scheme; 67 | } 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /deovr-plugin.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import sys 3 | import json 4 | import datetime 5 | 6 | 7 | class deovr: 8 | headers = { 9 | "Accept-Encoding": "gzip, deflate, br", 10 | "Content-Type": "application/json", 11 | "Accept": "application/json", 12 | "Connection": "keep-alive", 13 | "DNT": "1" 14 | } 15 | domain='stash.home' 16 | 17 | url_prefix='http://'+domain 18 | # In deovr, we are adding the lists: VR and 2D 19 | # You can add a string for studio or performers you want to add as lists in deovr 20 | # pinned_studio=['Wankz VR'] 21 | pinned_studio=[] 22 | pinned_performers=[] 23 | 24 | def __init__(self, url): 25 | self.url = url 26 | 27 | self.path='/root/.stash/deovr/' 28 | 29 | 30 | 31 | def __callGraphQL(self, query, variables=None): 32 | json = {} 33 | json['query'] = query 34 | if variables != None: 35 | json['variables'] = variables 36 | 37 | # handle cookies 38 | response = requests.post(self.url, json=json, headers=self.headers) 39 | 40 | if response.status_code == 200: 41 | result = response.json() 42 | if result.get("error", None): 43 | for error in result["error"]["errors"]: 44 | raise Exception("GraphQL error: {}".format(error)) 45 | if result.get("data", None): 46 | return result.get("data") 47 | else: 48 | raise Exception( 49 | "GraphQL query failed:{} - {}. Query: {}. Variables: {}".format(response.status_code, response.content, 50 | query, variables)) 51 | 52 | def get_scenes_with_tag(self, tag): 53 | tagID = self.findTagIdWithName(tag) 54 | query = """query findScenes($scene_filter: SceneFilterType!) { 55 | findScenes(scene_filter: $scene_filter filter: {sort: "date",direction: DESC,per_page: -1} ) { 56 | count 57 | scenes { 58 | id 59 | checksum 60 | oshash 61 | title 62 | details 63 | url 64 | date 65 | rating 66 | organized 67 | o_counter 68 | path 69 | file { 70 | size 71 | duration 72 | video_codec 73 | audio_codec 74 | width 75 | height 76 | framerate 77 | bitrate 78 | } 79 | paths { 80 | screenshot 81 | preview 82 | stream 83 | webp 84 | vtt 85 | chapters_vtt 86 | sprite 87 | funscript 88 | } 89 | galleries { 90 | id 91 | checksum 92 | path 93 | title 94 | url 95 | date 96 | details 97 | rating 98 | organized 99 | studio { 100 | id 101 | name 102 | url 103 | } 104 | image_count 105 | tags { 106 | id 107 | name 108 | image_path 109 | scene_count 110 | } 111 | } 112 | performers { 113 | id 114 | name 115 | gender 116 | url 117 | twitter 118 | instagram 119 | birthdate 120 | ethnicity 121 | country 122 | eye_color 123 | country 124 | height 125 | measurements 126 | fake_tits 127 | career_length 128 | tattoos 129 | piercings 130 | aliases 131 | } 132 | studio{ 133 | id 134 | name 135 | url 136 | stash_ids{ 137 | endpoint 138 | stash_id 139 | } 140 | } 141 | tags{ 142 | id 143 | name 144 | } 145 | 146 | stash_ids{ 147 | endpoint 148 | stash_id 149 | } 150 | } 151 | } 152 | }""" 153 | 154 | variables = {"scene_filter": {"tags": {"value": [tagID], "modifier": "INCLUDES"}}} 155 | result = self.__callGraphQL(query, variables) 156 | return result["findScenes"]["scenes"] 157 | 158 | def createTagWithName(self, name): 159 | query = """ 160 | mutation tagCreate($input:TagCreateInput!) { 161 | tagCreate(input: $input){ 162 | id 163 | } 164 | } 165 | """ 166 | variables = {'input': { 167 | 'name': name 168 | }} 169 | 170 | result = self.__callGraphQL(query, variables) 171 | return result["tagCreate"]["id"] 172 | 173 | def findTagIdWithName(self, name): 174 | query = """query { 175 | allTags { 176 | id 177 | name 178 | } 179 | }""" 180 | 181 | result = self.__callGraphQL(query) 182 | 183 | for tag in result["allTags"]: 184 | if tag["name"] == name: 185 | return tag["id"] 186 | return None 187 | 188 | def setup(self): 189 | tags=["VR","SBS","TB","export_deovr","FLAT","DOME","SPHERE","FISHEYE","MKX200"] 190 | for t in tags: 191 | tag = self.findTagIdWithName(t) 192 | if tag is None: 193 | self.createTagWithName(t) 194 | 195 | def run(self): 196 | scenes = self.get_scenes_with_tag("export_deovr") 197 | data = {} 198 | index = 1 199 | 200 | if scenes is not None: 201 | recent=[] 202 | vr=[] 203 | flat=[] 204 | studio_cache={} 205 | performer_cache={} 206 | for s in scenes: 207 | r={} 208 | r["title"] = s["title"] 209 | r["videoLength"]=int(s["file"]["duration"]) 210 | url=s["paths"]["screenshot"] 211 | r["thumbnailUrl"] =self.url_prefix+url[21:] 212 | r["video_url"]=self.url_prefix+'/custom/deovr/'+s["id"]+'.json' 213 | recent.append(r) 214 | 215 | if "180_180x180_3dh_LR" in s["path"]: 216 | vr.append(r) 217 | elif [x for x in ['DOME','SPHERE','FISHEYE','MKX200','VR'] if x in [x["name"] for x in s["tags"]]]: 218 | vr.append(r) 219 | else: 220 | flat.append(r) 221 | 222 | scene={} 223 | scene["id"]=s["id"] 224 | scene["title"]=s["title"] 225 | scene["authorized"]=1 226 | scene["description"]=s["details"] 227 | if "studio" in s and s["studio"] is not None: 228 | scene["paysite"]={"id": 1,"name": s["studio"]["name"],"is3rdParty": True} 229 | if s["studio"]["name"] in studio_cache: 230 | studio_cache[s["studio"]["name"]].append(r) 231 | else: 232 | studio_cache[s["studio"]["name"]]=[r] 233 | scene["thumbnailUrl"]=s["paths"]["screenshot"] 234 | url= s["paths"]["screenshot"] 235 | scene["thumbnailUrl"] =self.url_prefix+url[21:] 236 | scene["isFavorite"]= False 237 | scene["isScripted"]= False 238 | scene["isWatchlist"]= False 239 | 240 | vs={} 241 | vs["resolution"]=s["file"]["height"] 242 | vs["height"]=s["file"]["height"] 243 | vs["width"]=s["file"]["width"] 244 | vs["size"]=s["file"]["size"] 245 | url=s["paths"]["stream"] 246 | vs["url"]=self.url_prefix+url[21:] 247 | scene["encodings"]=[{"name":s["file"]["video_codec"],"videoSources":[vs]}] 248 | 249 | 250 | if "180_180x180_3dh_LR" in s["path"]: 251 | scene["is3d"] = True 252 | scene["screenType"]="dome" 253 | scene["stereoMode"]="sbs" 254 | else: 255 | scene["screenType"]="flat" 256 | scene["is3d"]=False 257 | if 'SBS' in [x["name"] for x in s["tags"]]: 258 | scene["stereoMode"] = "sbs" 259 | elif 'TB' in [x["name"] for x in s["tags"]]: 260 | scene["stereoMode"] = "tb" 261 | 262 | if 'FLAT' in [x["name"] for x in s["tags"]]: 263 | scene["screenType"]="flat" 264 | scene["is3d"]=False 265 | elif 'DOME' in [x["name"] for x in s["tags"]]: 266 | scene["is3d"] = True 267 | scene["screenType"]="dome" 268 | elif 'SPHERE' in [x["name"] for x in s["tags"]]: 269 | scene["is3d"] = True 270 | scene["screenType"]="sphere" 271 | elif 'FISHEYE' in [x["name"] for x in s["tags"]]: 272 | scene["is3d"] = True 273 | scene["screenType"]="fisheye" 274 | elif 'MKX200' in [x["name"] for x in s["tags"]]: 275 | scene["is3d"] = True 276 | scene["screenType"]="mkx200" 277 | 278 | scene["timeStamps"]=None 279 | 280 | actors=[] 281 | for p in s["performers"]: 282 | #actors.append({"id":p["id"],"name":p["name"]}) 283 | actors.append({"id": p["id"], "name": p["name"]}) 284 | scene["actors"]=actors 285 | 286 | for p in self.pinned_performers: 287 | # print([x["name"] for x in s["performers"]]) 288 | if p.lower() in [x["name"].lower() for x in s["performers"]]: 289 | if p in performer_cache: 290 | performer_cache[p].append(r) 291 | else: 292 | performer_cache[p]=[r] 293 | 294 | scene["fullVideoReady"]= True 295 | scene["fullAccess"]= True 296 | 297 | with open(self.path+s["id"]+'.json', 'w') as outfile: 298 | json.dump(scene, outfile) 299 | outfile.close() 300 | 301 | 302 | data["scenes"]=[{"name":"Recent","list":recent},{"name":"VR","list":vr},{"name":"2D","list":flat}] 303 | for sn in self.pinned_studio: 304 | if sn in studio_cache: 305 | data["scenes"].append({"name":sn,"list":studio_cache[sn]}) 306 | for pn in self.pinned_performers: 307 | if pn in performer_cache: 308 | data["scenes"].append({"name":pn,"list":performer_cache[pn]}) 309 | 310 | 311 | with open(self.path+"deovr.json", 'w') as outfile: 312 | json.dump(data, outfile) 313 | outfile.close() 314 | 315 | if __name__ == '__main__': 316 | if len(sys.argv) > 1: 317 | url = "http://localhost:9999/graphql" 318 | if len(sys.argv) > 2: 319 | url = sys.argv[2] 320 | 321 | if sys.argv[1] == "setup": 322 | client = deovr(url) 323 | client.setup() 324 | 325 | elif sys.argv[1] =="json": 326 | client = deovr(url) 327 | client.setup() 328 | client.run() 329 | elif sys.argv[1]== "api": 330 | fragment = json.loads(sys.stdin.read()) 331 | scheme=fragment["server_connection"]["Scheme"] 332 | port=fragment["server_connection"]["Port"] 333 | domain="localhost" 334 | if "Domain" in fragment["server_connection"]: 335 | domain = fragment["server_connection"]["Domain"] 336 | if not domain: 337 | domain='localhost' 338 | url = scheme + "://" + domain + ":" +str(port) + "/graphql" 339 | client = deovr(url) 340 | mode=fragment["args"]["mode"] 341 | # client.debug("Mode: "+mode) 342 | if mode == "setup": 343 | client.setup() 344 | elif mode == "json": 345 | client.run() 346 | --------------------------------------------------------------------------------