From d2931e61da17a7225250be7b7bafd3d2f7f295a0 Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sat, 7 Jan 2023 23:37:16 -0500 Subject: [PATCH 01/10] v1.0 --- plugins/movieFromScene/README.md | 37 ++ plugins/movieFromScene/movieFromScene.yml | 31 + .../py_plugins/movieFromScene.config | 1 + .../py_plugins/movieFromScene.py | 143 +++++ .../py_plugins/movieFromSceneDef.py | 289 ++++++++++ .../py_plugins/movieFromSceneGui.py | 34 ++ .../py_plugins/movieFromSceneGuiDef.py | 528 ++++++++++++++++++ 7 files changed, 1063 insertions(+) create mode 100644 plugins/movieFromScene/README.md create mode 100644 plugins/movieFromScene/movieFromScene.yml create mode 100644 plugins/movieFromScene/py_plugins/movieFromScene.config create mode 100644 plugins/movieFromScene/py_plugins/movieFromScene.py create mode 100644 plugins/movieFromScene/py_plugins/movieFromSceneDef.py create mode 100644 plugins/movieFromScene/py_plugins/movieFromSceneGui.py create mode 100644 plugins/movieFromScene/py_plugins/movieFromSceneGuiDef.py diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md new file mode 100644 index 00000000..b4fcf3a6 --- /dev/null +++ b/plugins/movieFromScene/README.md @@ -0,0 +1,37 @@ +# StashPlugins +A collection of python plugins for stash + +Minimum stash version: v0.4.0-71 + +### Currently available plugins: + +Plugin config | Description | Notes +----------------------- | --------------------------------------------------- | -------- +set_ph_urls.yml | Add urls to pornhub scenes downloaded by Youtube-dl | +gallerytags.yml | Copy information from attached scene to galleries | +bulk_url_scraper.yml | Bulk scene and gallery url scraping | Config (/py_plugins/config.py) has to be edited manually, until plugin parameters get implemented +update_image_titles.yml | Update all image titles (Fixes natural sort) | +yt-dl_downloader.yml | Download Videos automated with yt-dl and add the scrape tag for burl_url_scraper | Config files in yt-dl_downloader/ folder. Add all urls line by line to urls.txt and change download dir in config.ini | + +### Download instructions: +Drop the py_plugins folder as well as all desired plugin configurations in stash's plugin folder +and press the `Reload plugins` button in the Plugin settings + +All plugins require python 3, as well as the requests module, which can be installed with the command `pip install requests`. +If the python installation requires you to call python via `python3`, you have to change python to python3 in the exec block of each plugin config. + + +### Docker instructions: +To use the plugins with a stash instance running in a (remote-) docker container it is required to install python inside of it: +- Open a shell in the docker container: `docker exec -it sh` (get the container id from `docker ps -a`) +- In the container execute the following commands: + ```shell + apt update + apt install python3 + apt install python3-pip + pip3 install requests + ``` +- Leave the container via `Ctrl+P,Ctrl+Q` +- Drop the py_plugins folder as well as all desired plugin configurations in stash's plugin folder located in `config/plugins`. Create the plugins folder if it is not already there +- Change `python` to `python3` in the plugin configuration (.yml) files +- Press the `Reload plugins` button in stash's plugin settings diff --git a/plugins/movieFromScene/movieFromScene.yml b/plugins/movieFromScene/movieFromScene.yml new file mode 100644 index 00000000..e8aed34d --- /dev/null +++ b/plugins/movieFromScene/movieFromScene.yml @@ -0,0 +1,31 @@ +name: Auto-Create Movie From Scene +description: Automatically create a movie from a scene when scene gets updated. Require Python 3.10 and above. If you use the config GUI, tkinter should be installed. +url: https://github.com/stashapp/CommunityScripts +version: 1.0 +exec: + - python + - "{pluginDir}/py_plugins/movieFromScene.py" +interface: raw +hooks: + - name: hook_create_movie_from_scene + description: Create a movie from a scene if it fits certain criteria + triggeredBy: + - Scene.Update.Post +tasks: + - name: 'Disable' + description: Don't auto-create movies. Save this setting in the config file. + defaultArgs: + mode: disable + - name: 'Enable' + description: Auto-Create a movie when scene is updated and fits the criteria. Save this setting in the config file. + defaultArgs: + mode: enable + - name: 'Dry Run' + description: Run but not create any movies. Only show in the log. Save this setting in the config file. + defaultArgs: + mode: dryrun + - name: 'Show Config' + description: Use tkinter to show detailed config GUI for this plugin. Make sure tkinter is installed before this. + defaultArgs: + mode: config + \ No newline at end of file diff --git a/plugins/movieFromScene/py_plugins/movieFromScene.config b/plugins/movieFromScene/py_plugins/movieFromScene.config new file mode 100644 index 00000000..5a909eca --- /dev/null +++ b/plugins/movieFromScene/py_plugins/movieFromScene.config @@ -0,0 +1 @@ +{"mode": "disable", "criteria": {"no movie": true, "title": true, "URL": false, "date": false, "studio": false, "performer": true, "tag": false, "details": true, "organized": false}} \ No newline at end of file diff --git a/plugins/movieFromScene/py_plugins/movieFromScene.py b/plugins/movieFromScene/py_plugins/movieFromScene.py new file mode 100644 index 00000000..3db12289 --- /dev/null +++ b/plugins/movieFromScene/py_plugins/movieFromScene.py @@ -0,0 +1,143 @@ +import json +import os +import sys +import time +import log + +try: + import movieFromSceneDef as defs +except Exception: + output_json = {"output":"", "error": 'Import module movieFromSceneDef.py failed.'} + sys.exit + +# APP/DB Schema version prior to files refactor PR +# 41 is v0.18.0 (11/30/2022) +API_VERSION_BF_FILES = 41 + +# check python version +p_version = sys.version +if float( p_version[:p_version.find(".",2)] ) < 3.1 : + defs.exit_plugin("", "Error: You need at least python 3.10 for this plugin.") + +# Get data from Stash +FRAGMENT = json.loads(sys.stdin.read()) +# log.LogDebug("Fragment:" + json.dumps(FRAGMENT)) + + +FRAGMENT_SERVER = FRAGMENT["server_connection"] +FRAGMENT_SCENE = FRAGMENT["args"].get("hookContext") + +# Graphql properties in a dict +g = { "port": FRAGMENT_SERVER['Port'], + "scheme": FRAGMENT_SERVER['Scheme'], + "session": FRAGMENT_SERVER['SessionCookie']['Value'], + "args": FRAGMENT["args"], + "plugin_dir": FRAGMENT_SERVER['PluginDir'], + "dir": FRAGMENT_SERVER['Dir']} +# log.LogDebug("g:" + str(g) ) + +if FRAGMENT_SERVER['Host'] == "0.0.0.0": + g['host'] = 'localhost' +else: + g['host'] = FRAGMENT_SERVER['Host'] + +system_status = defs.get_api_version(g) +# log.LogDebug(json.dumps(system_status)) + +api_version = system_status["appSchema"] + +if api_version < API_VERSION_BF_FILES: # Only needed for versions after files refactor + defs.exit_plugin( + f"Stash with API version:{api_version} is not supported. You need at least {API_VERSION_BF_FILES}" + ) + +# load config file. +configFile = g['plugin_dir']+"/py_plugins/movieFromScene.config" +try: + f = open( configFile,"r" ) + config = json.load( f ) + f.close() +except FileNotFoundError as e: + # This is the default config, when the config file is missing. + configStr = """ + { + "mode": "disable", + "criteria" : { + "no movie" : true, + "title" : true, + "URL" : false, + "date" : false, + "studio" : false, + "performer" : true, + "tag" : false, + "details" : true, + "organized" : false + } + } + """ + f = open( configFile, "w") + f.write(configStr) + f.close() + config = json.loads(configStr) +except: + defs.exit_plugin("","Error in config file: movieFromScene.config. Err:" + str(Exception) ) + +if not FRAGMENT_SCENE: + match g["args"]["mode"]: + case "config": + # log.LogDebug("run the gui process.") + # Require tkinter module installed in Python. + import subprocess + DETACHED_PROCESS = 0x00000008 + g['dir'] = str( g['dir'] ).replace('\\', '/') + guiPy = g['dir'] + '/' + g['plugin_dir']+"/py_plugins/movieFromSceneGui.py" + # log.LogDebug("guiPy:" + guiPy) + subprocess.Popen([sys.executable, guiPy], creationflags=DETACHED_PROCESS) + defs.exit_plugin("Config GUI launched.") + + case "disable": + # Disable the plugin and save the setting. + config['mode'] = "disable" + bSuccess = defs.SaveSettings(configFile, config) + if bSuccess: + defs.exit_plugin("Plugin Disabled.") + else: + defs.exit_plugin("Error saving settings.") + # log.LogDebug("hit the disable button.") + case "enable": + config['mode'] = "enable" + bSuccess = defs.SaveSettings(configFile, config) + if bSuccess: + defs.exit_plugin("Plugin Enabled.") + else: + defs.exit_plugin("Error saving settings.") + + # log.LogDebug("hit the enable button.") + case "dryrun": + config['mode'] = "disable" + bSuccess = defs.SaveSettings(configFile, config) + if bSuccess: + defs.exit_plugin("Plugin in Dry Run mode.") + else: + defs.exit_plugin("Error saving settings.") + + # log.LogDebug("hit the dryrun button.") + case "batch": + log.LogDebug("hit the batch button.") + +# The above is for settings run in Tasks. +# The below is for auto movie creation. + +if config['mode'] == 'disable': + defs.exit_plugin("Plugin disabled. Not doing anything.") + + +scene_id = FRAGMENT_SCENE["id"] +if not scene_id: + defs.exit_plugin("", "No Scene ID found!") + +SCENE = defs.get_scene( scene_id, g ) +# Get the scene info +# log.LogDebug("scene:" + json.dumps(SCENE)) + +defs.create_Movie_By_Config(SCENE, config, g) \ No newline at end of file diff --git a/plugins/movieFromScene/py_plugins/movieFromSceneDef.py b/plugins/movieFromScene/py_plugins/movieFromSceneDef.py new file mode 100644 index 00000000..61f29307 --- /dev/null +++ b/plugins/movieFromScene/py_plugins/movieFromSceneDef.py @@ -0,0 +1,289 @@ +import json +import sys +import log +import requests + +def SaveSettings(configFile, config): + try: + f = open(configFile, "w") + json.dump(config, f) + f.close() + except Exception: + return False + + # Success. + return True + +def exit_plugin(msg=None, err=None): + if msg is None and err is None: + msg = "plugin ended" + output_json = {"output": msg, "error": err} + print(json.dumps(output_json)) + sys.exit() + +def doRequest(query, variables, g, raise_exception=True): + # Session cookie for authentication + graphql_host = g['host'] + graphql_port = g['port'] + graphql_scheme = g['scheme'] + graphql_cookies = { + 'session': g['session'] + } + + graphql_headers = { + "Accept-Encoding": "gzip, deflate", + "Content-Type": "application/json", + "Accept": "application/json", + "Connection": "keep-alive", + "DNT": "1" + } + + # Stash GraphQL endpoint + graphql_url = graphql_scheme + "://" + graphql_host + ":" + str(graphql_port) + "/graphql" + + json = {'query': query} + if variables is not None: + json['variables'] = variables + try: + response = requests.post(graphql_url, json=json,headers=graphql_headers, cookies=graphql_cookies, timeout=20) + except Exception as e: + exit_plugin(err=f"[FATAL] Exception with GraphQL request. {e}") + if response.status_code == 200: + result = response.json() + if result.get("error"): + for error in result["error"]["errors"]: + if raise_exception: + raise Exception(f"GraphQL error: {error}") + else: + log.LogError(f"GraphQL error: {error}") + return None + if result.get("data"): + return result.get("data") + elif response.status_code == 401: + exit_plugin(err="HTTP Error 401, Unauthorised.") + else: + raise ConnectionError(f"GraphQL query failed: {response.status_code} - {response.content}") + +def get_scene(scene_id, g): + query = """ + query FindScene($id: ID!, $checksum: String) { + findScene(id: $id, checksum: $checksum) { + id, + title, + files{ + basename, + duration + }, + movies{ + movie{ + id, + duration + } + }, + date, + url, + details, + studio{ + id, + name + }, + performers{ + id, + name + } + tags{ + id, + name + } + paths{ + screenshot + } + } + } + """ + variables = { + "id": scene_id + } + result = doRequest(query=query, variables=variables, g=g) + return result['findScene'] + +def get_api_version(g): + query = """ + query SystemStatus { + systemStatus { + databaseSchema + appSchema + } + } + """ + result = doRequest(query, None, g) + return result['systemStatus'] + +def create_movie_from_scene(s, g): + # scene is the whole scene dictionary with all the info + # A movie must have a valid title. + title = s["title"] + if not title: + title = getDict(s, "files", "basename" ) + + # Special treatment for duration. + if not s["files"]: + duration = None + else: + duration = int( s["files"][0].get("duration") ) + + query = """ + mutation movieCreate( + $name: String!, + $duration: Int, + $date: String, + $studio_id: ID, + $synopsis: String, + $url: String, + $front_image: String + ) + { + movieCreate( + input: { + name: $name, + duration: $duration, + date: $date, + studio_id: $studio_id, + synopsis: $synopsis, + url: $url, + front_image: $front_image + }) + { + id + } + } + """ + + variables = { + "name": title, + "duration": duration, + "date": getDict(s,"date", type = "date"), + "studio_id": getDict( s, "studio", "id", type="number"), + "synopsis": getDict(s, "details"), + "url": getDict( s, "link"), + "front_image": getDict( s,"paths","screenshot") + } + log.LogDebug( "movie var:" + json.dumps(variables)) + try: + result = doRequest(query, variables, g) + return result['movieCreate']["id"] + except Exception: + exit_plugin("", "Error in creating movie.") + +def sceneLinkMovie(scene_id, movie_id, g): + query = """ + mutation SceneUpdate($scene_id: ID!, $movie_id: ID! ){ + sceneUpdate( + input:{ + id: $scene_id, + movies:[ + { movie_id : $movie_id} + ] + } + ) { id } + } + """ + vars = { + "scene_id": scene_id, + "movie_id": movie_id + } + try: + result = doRequest(query, vars, g) + return result['sceneUpdate']["id"] + except Exception: + exit_plugin("", "error in linking movie to scene") + +def create_Movie_By_Config(s, config, g): + # s = SCENE + # ct = the criteria + ct = config['criteria'] + scene_id = s["id"] + # Does this scene have a movie? + bDryRun = ( config['mode'] == 'dryrun' ) + + if ct["no movie"] and s["movies"]: + # Criteria is "Have No movie", yet scene has movie + exit_plugin("Skip. Scene " + str(scene_id) + " has movie already.") + + if ct["title"] and not s["title"]: + # Criteria is "has title", yet scene has no title + exit_plugin("Skip. Scene " + str(scene_id) + " has no title.") + + if ct["URL"] and not s["url"]: + # Criteria is "has URL", yet scene has no url + exit_plugin("Skip. Scene " + str(scene_id) + " has no URL.") + + if ct["date"] and not s["date"]: + # Criteria is "has date", yet scene has no date + exit_plugin("Skip. Scene " + str(scene_id) + " has no date.") + + if ct["studio"] and not s["studio"]: + # Criteria is "has studio", yet scene has no studio + exit_plugin("Skip. Scene " + str(scene_id) + " has no studio.") + + if ct["performer"] and not s["performers"]: + # Criteria is "has performer", yet scene has no performer + exit_plugin("Skip. Scene " + str(scene_id) + " has no performers.") + + if ct["tag"] and not s["tags"]: + # Criteria is "has tag", yet scene has no tag + exit_plugin("Skip. Scene " + str(scene_id) + " has no URL.") + + if ct["details"] and not s["details"]: + # Criteria is "has details", yet scene has no details + exit_plugin("Skip. Scene " + str(scene_id) + " has no details.") + + if ct["organized"] and not s["organized"]: + # Criteria is "Is Organized", yet scene is not organized + exit_plugin("Skip. Scene " + str(scene_id) + " is not organized.") + + # Pass all of the above. Now the scene can be made into a movie. + + # Try to get something like "http://localhost:9999/scenes/1234" from screenshot + strScreen = s["paths"]["screenshot"] + link = str( strScreen[:strScreen.rfind("/")]) # Add the scene's "link" item to the dict + s["link"] = link.replace("scene", "scenes") + + # Create a movie and set the detail from + if not bDryRun: + movie_id = create_movie_from_scene(s,g) + + # Link this movie to the scene. + if not bDryRun: + sceneLinkMovie(scene_id, movie_id, g) + + if bDryRun: + exit_plugin("Dryrun: Movie with scene id: " + scene_id + " created.") + else: + exit_plugin("Movie with scene id: " + scene_id + " created. Movie id is:" + movie_id) + +def getDict( rootDict, firstLevel, secondLevel="", type="string" ): + # Safely reference the first level and second level of dictionary object + if not rootDict: + return empty(type) + + if not rootDict.get(firstLevel): + return empty(type) + + if secondLevel == "": + # No second level + return rootDict.get(firstLevel) + else: + # Has second level + if not rootDict[firstLevel].get(secondLevel): + return empty(type) + else: + return rootDict[firstLevel][secondLevel] + +def empty(type): + # return empty according to type. + match type: + case "string": + return "" + case "date" | "number": + return None diff --git a/plugins/movieFromScene/py_plugins/movieFromSceneGui.py b/plugins/movieFromScene/py_plugins/movieFromSceneGui.py new file mode 100644 index 00000000..7d2977b0 --- /dev/null +++ b/plugins/movieFromScene/py_plugins/movieFromSceneGui.py @@ -0,0 +1,34 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Support module generated by PAGE version 7.6 +# in conjunction with Tcl version 8.6 +# Jan 07, 2023 02:18:52 AM EST platform: Windows NT + +import tkinter as tk +from tkinter.constants import * + +import movieFromSceneGuiDef as myGuiDef + +_debug = True # False to eliminate debug printing from callback functions. + +def main(): + # '''Main entry point for the application.''' + global root + root = tk.Tk() + root.protocol( 'WM_DELETE_WINDOW' , root.destroy) + # Creates a toplevel widget. + global _top1, _w1 + _top1 = root + _w1 = myGuiDef.Toplevel1(_top1) + root.mainloop() + +# Start the gui from here + +if __name__ == '__main__': + myGuiDef.start_up() + + + + + diff --git a/plugins/movieFromScene/py_plugins/movieFromSceneGuiDef.py b/plugins/movieFromScene/py_plugins/movieFromSceneGuiDef.py new file mode 100644 index 00000000..432fda57 --- /dev/null +++ b/plugins/movieFromScene/py_plugins/movieFromSceneGuiDef.py @@ -0,0 +1,528 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# GUI module generated by PAGE version 7.6 +# in conjunction with Tcl version 8.6 +# Jan 07, 2023 02:18:01 AM EST platform: Windows NT + +import sys +import json +import log +import tkinter as tk +from tkinter import messagebox +from tkinter.constants import * +import os.path + +_script = sys.argv[0] +_location = os.path.dirname(_script).replace( '\\' , '/' ) +_configFile = _location + "/movieFromScene.config" + +import movieFromSceneGui as myGui + +_bgcolor = '#d9d9d9' # X11 color: 'gray85' +_fgcolor = '#000000' # X11 color: 'black' +_compcolor = 'gray40' # X11 color: #666666 +_ana1color = '#c3c3c3' # Closest X11 color: 'gray76' +_ana2color = 'beige' # X11 color: #f5f5dc +_tabfg1 = 'black' +_tabfg2 = 'black' +_tabbg1 = 'grey75' +_tabbg2 = 'grey89' +_bgmode = 'light' + +class Toplevel1: + def __init__(self, top): + '''This class configures and populates the toplevel window. + top is the toplevel containing window.''' + self.config = getConfig() + self.runmode = tk.StringVar(value=self.config['mode']) + ct = self.config['criteria'] + self.bHasNoMovie = tk.BooleanVar( value=ct['no movie'] ) + self.bHasTitle = tk.BooleanVar( value=ct['title'] ) + self.bHasURL = tk.BooleanVar( value=ct['URL'] ) + self.bHasDate = tk.BooleanVar( value=ct['date'] ) + self.bHasStudio = tk.BooleanVar( value=ct['studio'] ) + self.bHasPerfromer = tk.BooleanVar( value=ct['performer'] ) + self.bHasTag = tk.BooleanVar( value=ct['tag'] ) + self.bHasDetails = tk.BooleanVar( value=ct['details'] ) + self.bIsOrganized = tk.BooleanVar( value=ct['organized'] ) + + top.geometry("427x527+283+169") + top.minsize(120, 1) + top.maxsize(2198, 1215) + top.resizable(1, 1) + top.title("Configuration") + top.configure(background="#d9d9d9") + + self.top = top + + # ==================== Frame 1 =========================== + self.frame1 = tk.LabelFrame(self.top) + self.frame1.place(relx=0.047, rely=0.076, relheight=0.167 + , relwidth=0.831) + self.frame1.configure(relief='groove') + self.frame1.configure(font="-family {Segoe UI} -size 12") + self.frame1.configure(foreground="#000000") + self.frame1.configure(text='Run Mode') + self.frame1.configure(background="#d9d9d9") + self.frame1.configure(cursor="fleur") + + self.RadioDisable = tk.Radiobutton(self.frame1, variable=self.runmode, value='disable') + self.RadioDisable.place(relx=0.085, rely=0.341, relheight=0.386 + , relwidth=0.239, bordermode='ignore') + self.RadioDisable.configure(activebackground="beige") + self.RadioDisable.configure(activeforeground="black") + self.RadioDisable.configure(anchor='w') + self.RadioDisable.configure(background="#d9d9d9") + self.RadioDisable.configure(compound='left') + self.RadioDisable.configure(disabledforeground="#a3a3a3") + self.RadioDisable.configure(font="-family {Segoe UI} -size 11") + self.RadioDisable.configure(foreground="#000000") + self.RadioDisable.configure(highlightbackground="#d9d9d9") + self.RadioDisable.configure(highlightcolor="black") + self.RadioDisable.configure(justify='left') + self.RadioDisable.configure(selectcolor="#d9d9d9") + self.RadioDisable.configure(text='Disable') + self.RadioDisable_tooltip = \ + ToolTip(self.RadioDisable, 'The plugin is currently disabled.') + + self.RadioEnable = tk.Radiobutton(self.frame1, variable=self.runmode, value='enable') + self.RadioEnable.place(relx=0.366, rely=0.341, relheight=0.398 + , relwidth=0.237, bordermode='ignore') + self.RadioEnable.configure(activebackground="beige") + self.RadioEnable.configure(activeforeground="black") + self.RadioEnable.configure(anchor='w') + self.RadioEnable.configure(background="#d9d9d9") + self.RadioEnable.configure(compound='left') + self.RadioEnable.configure(disabledforeground="#a3a3a3") + self.RadioEnable.configure(font="-family {Segoe UI} -size 11") + self.RadioEnable.configure(foreground="#000000") + self.RadioEnable.configure(highlightbackground="#d9d9d9") + self.RadioEnable.configure(highlightcolor="black") + self.RadioEnable.configure(justify='left') + self.RadioEnable.configure(selectcolor="#d9d9d9") + self.RadioEnable.configure(text='''Enable''') + self.RadioEnable_tooltip = \ + ToolTip(self.RadioEnable, 'The plugin is currently enabled.') + + self.RadioDryrun = tk.Radiobutton(self.frame1, variable=self.runmode, value='dryrun') + self.RadioDryrun.place(relx=0.704, rely=0.341, relheight=0.398 + , relwidth=0.237, bordermode='ignore') + self.RadioDryrun.configure(activebackground="beige") + self.RadioDryrun.configure(activeforeground="black") + self.RadioDryrun.configure(anchor='w') + self.RadioDryrun.configure(background="#d9d9d9") + self.RadioDryrun.configure(compound='left') + self.RadioDryrun.configure(disabledforeground="#a3a3a3") + self.RadioDryrun.configure(font="-family {Segoe UI} -size 11") + self.RadioDryrun.configure(foreground="#000000") + self.RadioDryrun.configure(highlightbackground="#d9d9d9") + self.RadioDryrun.configure(highlightcolor="black") + self.RadioDryrun.configure(justify='left') + self.RadioDryrun.configure(selectcolor="#d9d9d9") + self.RadioDryrun.configure(text='Dry Run') + self.RadioDryrun_tooltip = \ + ToolTip(self.RadioDryrun, 'The plugin will run, but will not create any movies.') + + # ==================== Frame 2 =========================== + self.Frame2 = tk.LabelFrame(self.top) + self.Frame2.place(relx=0.047, rely=0.266, relheight=0.7, relwidth=0.82) + self.Frame2.configure(relief='groove') + self.Frame2.configure(font="-family {Segoe UI} -size 12") + self.Frame2.configure(foreground="#000000") + self.Frame2.configure(text='Criteria') + self.Frame2.configure(background="#d9d9d9") + self.Label1 = tk.Label(self.Frame2) + self.Label1.place(relx=0.057, rely=0.081, height=50, width=284 + , bordermode='ignore') + self.Label1.configure(anchor='w') + self.Label1.configure(background="#d9d9d9") + self.Label1.configure(compound='left') + self.Label1.configure(disabledforeground="#a3a3a3") + self.Label1.configure(font="-family {Segoe UI} -size 11") + self.Label1.configure(foreground="#000000") + self.Label1.configure(justify='left') + self.Label1.configure(text='''The plugin will auto-create a movie +when the following criteria are all true:''') + + self.chkTitle = tk.Checkbutton(self.Frame2, variable=self.bHasTitle, onvalue=True, offvalue=False) + self.chkTitle.place(relx=0.057, rely=0.298, relheight=0.084 + , relwidth=0.431, bordermode='ignore') + self.chkTitle.configure(activebackground="beige") + self.chkTitle.configure(activeforeground="black") + self.chkTitle.configure(anchor='w') + self.chkTitle.configure(background="#d9d9d9") + self.chkTitle.configure(compound='left') + self.chkTitle.configure(disabledforeground="#a3a3a3") + self.chkTitle.configure(font="-family {Segoe UI} -size 12") + self.chkTitle.configure(foreground="#000000") + self.chkTitle.configure(highlightbackground="#d9d9d9") + self.chkTitle.configure(highlightcolor="black") + self.chkTitle.configure(justify='left') + self.chkTitle.configure(selectcolor="#d9d9d9") + self.chkTitle.configure(text='Has Title') + self.chkTitle_tooltip = \ + ToolTip(self.chkTitle, 'The criteria fits if the scene has a title.') + + self.chkNoMovie = tk.Checkbutton(self.Frame2, variable=self.bHasNoMovie, onvalue=True, offvalue=False) + self.chkNoMovie.place(relx=0.057, rely=0.217, relheight=0.084 + , relwidth=0.431, bordermode='ignore') + self.chkNoMovie.configure(activebackground="beige") + self.chkNoMovie.configure(activeforeground="black") + self.chkNoMovie.configure(anchor='w') + self.chkNoMovie.configure(background="#d9d9d9") + self.chkNoMovie.configure(compound='left') + self.chkNoMovie.configure(disabledforeground="#a3a3a3") + self.chkNoMovie.configure(font="-family {Segoe UI} -size 12") + self.chkNoMovie.configure(foreground="#000000") + self.chkNoMovie.configure(highlightbackground="#d9d9d9") + self.chkNoMovie.configure(highlightcolor="black") + self.chkNoMovie.configure(justify='left') + self.chkNoMovie.configure(selectcolor="#d9d9d9") + self.chkNoMovie.configure(text='Has No Movie') + self.chkNoMovie_tooltip = \ + ToolTip(self.chkNoMovie, 'The criteria fits if the scene does not have any movie yet.') + + self.chkURL = tk.Checkbutton(self.Frame2, variable=self.bHasURL, onvalue=True, offvalue=False) + self.chkURL.place(relx=0.057, rely=0.379, relheight=0.084, relwidth=0.431 + , bordermode='ignore') + self.chkURL.configure(activebackground="beige") + self.chkURL.configure(activeforeground="black") + self.chkURL.configure(anchor='w') + self.chkURL.configure(background="#d9d9d9") + self.chkURL.configure(compound='left') + self.chkURL.configure(disabledforeground="#a3a3a3") + self.chkURL.configure(font="-family {Segoe UI} -size 12") + self.chkURL.configure(foreground="#000000") + self.chkURL.configure(highlightbackground="#d9d9d9") + self.chkURL.configure(highlightcolor="black") + self.chkURL.configure(justify='left') + self.chkURL.configure(selectcolor="#d9d9d9") + self.chkURL.configure(text='Has URL') + self.chkURL_tooltip = \ + ToolTip(self.chkURL, 'The criteria fits if the scene has URL.') + + self.chkDate = tk.Checkbutton(self.Frame2, variable=self.bHasDate, onvalue=True, offvalue=False) + self.chkDate.place(relx=0.057, rely=0.461, relheight=0.084 + , relwidth=0.431, bordermode='ignore') + self.chkDate.configure(activebackground="beige") + self.chkDate.configure(activeforeground="black") + self.chkDate.configure(anchor='w') + self.chkDate.configure(background="#d9d9d9") + self.chkDate.configure(compound='left') + self.chkDate.configure(disabledforeground="#a3a3a3") + self.chkDate.configure(font="-family {Segoe UI} -size 12") + self.chkDate.configure(foreground="#000000") + self.chkDate.configure(highlightbackground="#d9d9d9") + self.chkDate.configure(highlightcolor="black") + self.chkDate.configure(justify='left') + self.chkDate.configure(selectcolor="#d9d9d9") + self.chkDate.configure(text='Has Date') + self.chkDate_tooltip = \ + ToolTip(self.chkDate, 'The criteria fits if the scene has Date data.') + + self.chkStudio = tk.Checkbutton(self.Frame2, variable=self.bHasStudio, onvalue=True, offvalue=False) + self.chkStudio.place(relx=0.057, rely=0.542, relheight=0.084 + , relwidth=0.431, bordermode='ignore') + self.chkStudio.configure(activebackground="beige") + self.chkStudio.configure(activeforeground="black") + self.chkStudio.configure(anchor='w') + self.chkStudio.configure(background="#d9d9d9") + self.chkStudio.configure(compound='left') + self.chkStudio.configure(disabledforeground="#a3a3a3") + self.chkStudio.configure(font="-family {Segoe UI} -size 12") + self.chkStudio.configure(foreground="#000000") + self.chkStudio.configure(highlightbackground="#d9d9d9") + self.chkStudio.configure(highlightcolor="black") + self.chkStudio.configure(justify='left') + self.chkStudio.configure(selectcolor="#d9d9d9") + self.chkStudio.configure(text='Has Studio') + self.chkStudio_tooltip = \ + ToolTip(self.chkStudio, 'The criteria fits if the scene has Studio data.') + + self.chkPerformer = tk.Checkbutton(self.Frame2, variable=self.bHasPerfromer, onvalue=True, offvalue=False) + self.chkPerformer.place(relx=0.057, rely=0.626, relheight=0.084 + , relwidth=0.431, bordermode='ignore') + self.chkPerformer.configure(activebackground="beige") + self.chkPerformer.configure(activeforeground="black") + self.chkPerformer.configure(anchor='w') + self.chkPerformer.configure(background="#d9d9d9") + self.chkPerformer.configure(compound='left') + self.chkPerformer.configure(disabledforeground="#a3a3a3") + self.chkPerformer.configure(font="-family {Segoe UI} -size 12") + self.chkPerformer.configure(foreground="#000000") + self.chkPerformer.configure(highlightbackground="#d9d9d9") + self.chkPerformer.configure(highlightcolor="black") + self.chkPerformer.configure(justify='left') + self.chkPerformer.configure(selectcolor="#d9d9d9") + self.chkPerformer.configure(text='Has Performer(s)') + self.chkPerformer_tooltip = \ + ToolTip(self.chkPerformer, 'The criteria fits if the scene has Performer(s).') + + self.chkTag = tk.Checkbutton(self.Frame2, variable=self.bHasTag, onvalue=True, offvalue=False) + self.chkTag.place(relx=0.057, rely=0.707, relheight=0.084, relwidth=0.431 + , bordermode='ignore') + self.chkTag.configure(activebackground="beige") + self.chkTag.configure(activeforeground="black") + self.chkTag.configure(anchor='w') + self.chkTag.configure(background="#d9d9d9") + self.chkTag.configure(compound='left') + self.chkTag.configure(disabledforeground="#a3a3a3") + self.chkTag.configure(font="-family {Segoe UI} -size 12") + self.chkTag.configure(foreground="#000000") + self.chkTag.configure(highlightbackground="#d9d9d9") + self.chkTag.configure(highlightcolor="black") + self.chkTag.configure(justify='left') + self.chkTag.configure(selectcolor="#d9d9d9") + self.chkTag.configure(text='Has Tag(s)') + self.chkTag_tooltip = \ + ToolTip(self.chkTag, 'The criteria fits if the scene has Tag(s).') + + self.chkDetails = tk.Checkbutton(self.Frame2, variable=self.bHasDetails, onvalue=True, offvalue=False) + self.chkDetails.place(relx=0.057, rely=0.789, relheight=0.084 + , relwidth=0.431, bordermode='ignore') + self.chkDetails.configure(activebackground="beige") + self.chkDetails.configure(activeforeground="black") + self.chkDetails.configure(anchor='w') + self.chkDetails.configure(background="#d9d9d9") + self.chkDetails.configure(compound='left') + self.chkDetails.configure(disabledforeground="#a3a3a3") + self.chkDetails.configure(font="-family {Segoe UI} -size 12") + self.chkDetails.configure(foreground="#000000") + self.chkDetails.configure(highlightbackground="#d9d9d9") + self.chkDetails.configure(highlightcolor="black") + self.chkDetails.configure(justify='left') + self.chkDetails.configure(selectcolor="#d9d9d9") + self.chkDetails.configure(text='Has Details') + self.chkDetails_tooltip = \ + ToolTip(self.chkDetails, 'The criteria fits if the scene has Details.') + + self.chkOrganized = tk.Checkbutton(self.Frame2, variable=self.bIsOrganized, onvalue=True, offvalue=False) + self.chkOrganized.place(relx=0.057, rely=0.867, relheight=0.084 + , relwidth=0.431, bordermode='ignore') + self.chkOrganized.configure(activebackground="beige") + self.chkOrganized.configure(activeforeground="black") + self.chkOrganized.configure(anchor='w') + self.chkOrganized.configure(background="#d9d9d9") + self.chkOrganized.configure(compound='left') + self.chkOrganized.configure(disabledforeground="#a3a3a3") + self.chkOrganized.configure(font="-family {Segoe UI} -size 12") + self.chkOrganized.configure(foreground="#000000") + self.chkOrganized.configure(highlightbackground="#d9d9d9") + self.chkOrganized.configure(highlightcolor="black") + self.chkOrganized.configure(justify='left') + self.chkOrganized.configure(selectcolor="#d9d9d9") + self.chkOrganized.configure(text='Is Organized') + self.chkOrganized_tooltip = \ + ToolTip(self.chkOrganized, "The criteria fits if the scene's Organized data is checked.") + + self.btnSave = tk.Button(self.Frame2, command=self.Save) + self.btnSave.place(relx=0.571, rely=0.488, height=44, width=117 + , bordermode='ignore') + self.btnSave.configure(activebackground="beige") + self.btnSave.configure(activeforeground="black") + self.btnSave.configure(background="#d9d9d9") + self.btnSave.configure(compound='left') + self.btnSave.configure(disabledforeground="#a3a3a3") + self.btnSave.configure(font="-family {Segoe UI} -size 12") + self.btnSave.configure(foreground="#000000") + self.btnSave.configure(highlightbackground="#d9d9d9") + self.btnSave.configure(highlightcolor="black") + self.btnSave.configure(pady="0") + self.btnSave.configure(text='Save Config') + self.btnSave_tooltip = \ + ToolTip(self.btnSave, 'Save the config file for this plugin.') + + self.btnCancel = tk.Button(self.Frame2, command=self.Cancel) + self.btnCancel.place(relx=0.571, rely=0.715, height=44, width=117 + , bordermode='ignore') + self.btnCancel.configure(activebackground="beige") + self.btnCancel.configure(activeforeground="black") + self.btnCancel.configure(background="#d9d9d9") + self.btnCancel.configure(compound='left') + self.btnCancel.configure(disabledforeground="#a3a3a3") + self.btnCancel.configure(font="-family {Segoe UI} -size 12") + self.btnCancel.configure(foreground="#000000") + self.btnCancel.configure(highlightbackground="#d9d9d9") + self.btnCancel.configure(highlightcolor="black") + self.btnCancel.configure(pady="0") + self.btnCancel.configure(text='Cancel') + + self.Label2 = tk.Label(self.top) + self.Label2.place(relx=0.117, rely=0.0, height=41, width=294) + self.Label2.configure(anchor='w') + self.Label2.configure(background="#d9d9d9") + self.Label2.configure(compound='left') + self.Label2.configure(disabledforeground="#a3a3a3") + self.Label2.configure(font="-family {Segoe UI} -size 12 -weight bold") + self.Label2.configure(foreground="#000000") + self.Label2.configure(text='Auto-Create Move By Scene Update') + + # Save button + def Save(self): + c = self.config['criteria'] + match self.runmode.get(): + case "disable": + self.config['mode'] = "disable" + case "enable": + self.config['mode'] = "enable" + case "dryrun": + self.config['mode'] = "dryrun" + c['no movie'] = self.bHasNoMovie.get() + c['title'] = self.bHasTitle.get() + c['URL'] = self.bHasURL.get() + c['date'] = self.bHasDate.get() + c['studio'] = self.bHasStudio.get() + c['performer'] = self.bHasPerfromer.get() + c['tag'] = self.bHasTag.get() + c['details'] = self.bHasDetails.get() + c['organized'] = self.bIsOrganized.get() + + try: + f = open( _configFile, "w") + json.dump(self.config, f) + f.close + except Exception: + messagebox.showerror("Error Saving", "Error in saving config file: " + _configFile ) + + sys.exit() + + def Cancel(self): + sys.exit() + +from time import time, localtime, strftime +class ToolTip(tk.Toplevel): + """ Provides a ToolTip widget for Tkinter. """ + def __init__(self, wdgt, msg=None, msgFunc=None, delay=0.5, + follow=True): + self.wdgt = wdgt + self.parent = self.wdgt.master + tk.Toplevel.__init__(self, self.parent, bg='black', padx=1, pady=1) + self.withdraw() + self.overrideredirect(True) + self.msgVar = tk.StringVar() + if msg is None: + self.msgVar.set('No message provided') + else: + self.msgVar.set(msg) + self.msgFunc = msgFunc + self.delay = delay + self.follow = follow + self.visible = 0 + self.lastMotion = 0 + self.msg = tk.Message(self, textvariable=self.msgVar, bg=_bgcolor, + fg=_fgcolor, font="TkDefaultFont", + aspect=1000) + self.msg.grid() + self.wdgt.bind('', self.spawn, '+') + self.wdgt.bind('', self.hide, '+') + self.wdgt.bind('', self.move, '+') + def spawn(self, event=None): + self.visible = 1 + self.after(int(self.delay * 1000), self.show) + def show(self): + if self.visible == 1 and time() - self.lastMotion > self.delay: + self.visible = 2 + if self.visible == 2: + self.deiconify() + def move(self, event): + self.lastMotion = time() + if self.follow is False: + self.withdraw() + self.visible = 1 + self.geometry('+%i+%i' % (event.x_root + 20, event.y_root - 10)) + try: + self.msgVar.set(self.msgFunc()) + except: + pass + self.after(int(self.delay * 1000), self.show) + def hide(self, event=None): + self.visible = 0 + self.withdraw() + def update(self, msg): + self.msgVar.set(msg) + def configure(self, **kwargs): + backgroundset = False + foregroundset = False + # Get the current tooltip text just in case the user doesn't provide any. + current_text = self.msgVar.get() + # to clear the tooltip text, use the .update method + if 'debug' in kwargs.keys(): + debug = kwargs.pop('debug', False) + if debug: + for key, value in kwargs.items(): + print(f'key: {key} - value: {value}') + if 'background' in kwargs.keys(): + background = kwargs.pop('background') + backgroundset = True + if 'bg' in kwargs.keys(): + background = kwargs.pop('bg') + backgroundset = True + if 'foreground' in kwargs.keys(): + foreground = kwargs.pop('foreground') + foregroundset = True + if 'fg' in kwargs.keys(): + foreground = kwargs.pop('fg') + foregroundset = True + + fontd = kwargs.pop('font', None) + if 'text' in kwargs.keys(): + text = kwargs.pop('text') + if (text == '') or (text == "\n"): + text = current_text + else: + self.msgVar.set(text) + reliefd = kwargs.pop('relief', 'flat') + justifyd = kwargs.pop('justify', 'left') + padxd = kwargs.pop('padx', 1) + padyd = kwargs.pop('pady', 1) + borderwidthd = kwargs.pop('borderwidth', 2) + wid = self.msg # The message widget which is the actual tooltip + if backgroundset: + wid.config(bg=background) + if foregroundset: + wid.config(fg=foreground) + wid.config(font=fontd) + wid.config(borderwidth=borderwidthd) + wid.config(relief=reliefd) + wid.config(justify=justifyd) + wid.config(padx=padxd) + wid.config(pady=padyd) +# End of Class ToolTip + +# ================== Def =================== + +# Return configuration in a Dict +def getConfig(): + try: + f = open( _configFile,"r" ) + config = json.load( f ) + f.close() + except Exception: + # This is the default config, when the config file has a problem. + # print( "error opening file: " + _configFile ) + configStr = """ + { + "mode": "disable", + "criteria" : { + "no movie" : true, + "title" : true, + "URL" : false, + "date" : false, + "studio" : false, + "performer" : true, + "tag" : false, + "details" : true, + "organized" : false + } + } + """ + return json.loads(configStr) + # file open and read successfully + return config + +def start_up(): + myGui.main() + + + From 06336bd7f553b4f0f358777b938306fb77a712b1 Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:08:01 -0500 Subject: [PATCH 02/10] Update README.md --- plugins/movieFromScene/README.md | 80 +++++++++++++++++--------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md index b4fcf3a6..5583aceb 100644 --- a/plugins/movieFromScene/README.md +++ b/plugins/movieFromScene/README.md @@ -1,37 +1,43 @@ -# StashPlugins -A collection of python plugins for stash - -Minimum stash version: v0.4.0-71 - -### Currently available plugins: - -Plugin config | Description | Notes ------------------------ | --------------------------------------------------- | -------- -set_ph_urls.yml | Add urls to pornhub scenes downloaded by Youtube-dl | -gallerytags.yml | Copy information from attached scene to galleries | -bulk_url_scraper.yml | Bulk scene and gallery url scraping | Config (/py_plugins/config.py) has to be edited manually, until plugin parameters get implemented -update_image_titles.yml | Update all image titles (Fixes natural sort) | -yt-dl_downloader.yml | Download Videos automated with yt-dl and add the scrape tag for burl_url_scraper | Config files in yt-dl_downloader/ folder. Add all urls line by line to urls.txt and change download dir in config.ini | - -### Download instructions: -Drop the py_plugins folder as well as all desired plugin configurations in stash's plugin folder -and press the `Reload plugins` button in the Plugin settings - -All plugins require python 3, as well as the requests module, which can be installed with the command `pip install requests`. -If the python installation requires you to call python via `python3`, you have to change python to python3 in the exec block of each plugin config. - - -### Docker instructions: -To use the plugins with a stash instance running in a (remote-) docker container it is required to install python inside of it: -- Open a shell in the docker container: `docker exec -it sh` (get the container id from `docker ps -a`) -- In the container execute the following commands: - ```shell - apt update - apt install python3 - apt install python3-pip - pip3 install requests - ``` -- Leave the container via `Ctrl+P,Ctrl+Q` -- Drop the py_plugins folder as well as all desired plugin configurations in stash's plugin folder located in `config/plugins`. Create the plugins folder if it is not already there -- Change `python` to `python3` in the plugin configuration (.yml) files -- Press the `Reload plugins` button in stash's plugin settings +# Auto-Create Movie From Scene Update +A python plugin with GUI config. + +Tested under: +Stash v0.18.0 +Python 3.10 with tkinter installed +Windows 11 + +### Purpose of the plugin: +There are lots of scrapers which can retrieve information for scenes, but there are only a few of them come with movie scrapers. No one have patience to scrape the scene then copy the information from scenes to movies. Therefore, this is where the plugin comes handy: you specify the criteria when to automatically create a movie for your scene. Once the settings are set, and a scene has enough information to fit the criteria, it will automatically copy the scene information to create a new movie for you. + +### install instructions: +Drop the py_plugins folder and the "movieFromScene.yml" file in stash's plugin folder, and press the `Reload plugins` button in the Plugin settings + +This plugin requires python 3.10 for the new "match" statement. Because "if elif...else..." is so lame! +It also comes with a GUI that can help you set the criteria and run mode easily, but then you need to install "tkinter" in Python. Or you can just edit the config file manually. + +### How to use it: +Once installed, you will find under the "Settings->Tasks->Plugin Tasks" a new task called "Auto-Create Movie From Scene" like below: +

+ +

+Here you can hit "Disable" to disable the plugin, or "Enable" to activate it, or "Dryrun" to see how it runs in the console log. +If you have installed tkinter, you can click on "Show Config" to see the detail settings. +It's the same thing as you run "movieFromSceneGui.py" directly from file browser or a console. Anyway, you will end up with the screen: +

+ +

+* The run mode is obvious: Disable, Enable or just Dry Runs. +* The criteria defines when this plugin will automatically create a scene. +As the above example, it will only create a movie when: +1. The scene has no movies. +2. The scene has title. ( This is not the same as the file name. ) +3. The scene has at least one performer. +4. The scene has some details text. +Only when all 4 condictions are met, and after the scene is updated with something, then a movie will be created. + +### I got a problem with xxx... +This is only version 1.0. So please raise an issue and let me know. I am not a Linux or Docker guy, so please don't expect me to solve +problems related to that. In fact, this is my first time to build a Python GUI program. + + + From 401cd428d6fb4312fe6fcbc26794f66d5603967b Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:15:56 -0500 Subject: [PATCH 03/10] Update README.md --- plugins/movieFromScene/README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md index 5583aceb..9176db82 100644 --- a/plugins/movieFromScene/README.md +++ b/plugins/movieFromScene/README.md @@ -1,18 +1,18 @@ # Auto-Create Movie From Scene Update -A python plugin with GUI config. +### A python plugin for Stash with GUI config. -Tested under: -Stash v0.18.0 -Python 3.10 with tkinter installed +Tested under:
+Stash v0.18.0
+Python 3.10 with tkinter installed
Windows 11 ### Purpose of the plugin: -There are lots of scrapers which can retrieve information for scenes, but there are only a few of them come with movie scrapers. No one have patience to scrape the scene then copy the information from scenes to movies. Therefore, this is where the plugin comes handy: you specify the criteria when to automatically create a movie for your scene. Once the settings are set, and a scene has enough information to fit the criteria, it will automatically copy the scene information to create a new movie for you. +There are lots of scrapers which can retrieve information for scenes, but there are only a few of them came with movie scrapers. No one have patience to scrape the scene then copy the information from scenes to movies bit by bit. Therefore, this is where the plugin comes in handy: you specify the criteria when to automatically create a movie for your scene. Once the settings are set, and a scene has enough information to fit the criteria, it will automatically use the scene information to create a new movie for you. ### install instructions: -Drop the py_plugins folder and the "movieFromScene.yml" file in stash's plugin folder, and press the `Reload plugins` button in the Plugin settings +Drop the py_plugins folder and the "movieFromScene.yml" file in stash's plugin folder, and press the `Reload plugins` button in the Plugin settings.

-This plugin requires python 3.10 for the new "match" statement. Because "if elif...else..." is so lame! +This plugin requires python 3.10 for the new "match" statement. Because "if elif...else..." is so lame!
It also comes with a GUI that can help you set the criteria and run mode easily, but then you need to install "tkinter" in Python. Or you can just edit the config file manually. ### How to use it: @@ -26,8 +26,10 @@ It's the same thing as you run "movieFromSceneGui.py" directly from file browser

+ * The run mode is obvious: Disable, Enable or just Dry Runs. * The criteria defines when this plugin will automatically create a scene. + As the above example, it will only create a movie when: 1. The scene has no movies. 2. The scene has title. ( This is not the same as the file name. ) From 44a8c98ab9364e4f6943fc308c6dbaf4305d8d0f Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:17:30 -0500 Subject: [PATCH 04/10] Update README.md --- plugins/movieFromScene/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md index 9176db82..5a2cb9c0 100644 --- a/plugins/movieFromScene/README.md +++ b/plugins/movieFromScene/README.md @@ -39,7 +39,7 @@ Only when all 4 condictions are met, and after the scene is updated with somethi ### I got a problem with xxx... This is only version 1.0. So please raise an issue and let me know. I am not a Linux or Docker guy, so please don't expect me to solve -problems related to that. In fact, this is my first time to build a Python GUI program. +problems related to that. In fact, this is my first time to build a Python GUI program. Please be understanding. From f70b4129551fee80594ab1dff292ea3c9bbaeeab Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:34:21 -0500 Subject: [PATCH 05/10] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4c902a77..e72c6ad8 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Scanning|Scene.Create
Gallery.Create|[filenameParser](plugins/filenameParse Scanning|Scene.Create|[pathParser](plugins/pathParser)|Updates scene info based on the file path.|v0.17 Scanning|Scene.Create|[titleFromFilename](plugins/titleFromFilename)|Sets the scene title to its filename|v0.17 Reporting||[TagGraph](plugins/tagGraph)|Creates a visual of the Tag relations.|v0.7 +Movies|Scene.Update|[movieFromScene](plugins/movieFromScene)|Auto-create movies when scenes get updated.|v0.18 ## Themes From 829644e6dbc2a120d331a868799014d50f9b934a Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:41:37 -0500 Subject: [PATCH 06/10] Update README.md --- plugins/movieFromScene/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md index 5a2cb9c0..b7870e72 100644 --- a/plugins/movieFromScene/README.md +++ b/plugins/movieFromScene/README.md @@ -28,7 +28,7 @@ It's the same thing as you run "movieFromSceneGui.py" directly from file browser

* The run mode is obvious: Disable, Enable or just Dry Runs. -* The criteria defines when this plugin will automatically create a scene. +* The criteria defines under what condition this plugin will automatically create a movie. As the above example, it will only create a movie when: 1. The scene has no movies. From 20ca01a75027ef08db7a145e092cbf72aebd82f7 Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:44:29 -0500 Subject: [PATCH 07/10] Update README.md --- plugins/movieFromScene/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md index b7870e72..984c56c3 100644 --- a/plugins/movieFromScene/README.md +++ b/plugins/movieFromScene/README.md @@ -31,10 +31,12 @@ It's the same thing as you run "movieFromSceneGui.py" directly from file browser * The criteria defines under what condition this plugin will automatically create a movie. As the above example, it will only create a movie when: + 1. The scene has no movies. 2. The scene has title. ( This is not the same as the file name. ) 3. The scene has at least one performer. 4. The scene has some details text. + Only when all 4 condictions are met, and after the scene is updated with something, then a movie will be created. ### I got a problem with xxx... From c812e78b1eb684c6fb5d20bad80712ac49424de7 Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:45:26 -0500 Subject: [PATCH 08/10] Update README.md --- plugins/movieFromScene/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md index 984c56c3..98759d0c 100644 --- a/plugins/movieFromScene/README.md +++ b/plugins/movieFromScene/README.md @@ -37,7 +37,7 @@ As the above example, it will only create a movie when: 3. The scene has at least one performer. 4. The scene has some details text. -Only when all 4 condictions are met, and after the scene is updated with something, then a movie will be created. +Only when all 4 conditions are met, and after the scene is updated with something, then a movie will be created and linked to that scene. ### I got a problem with xxx... This is only version 1.0. So please raise an issue and let me know. I am not a Linux or Docker guy, so please don't expect me to solve From d3cad0f60da60ab10736ac889afb0e4fffb2a0f5 Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:51:08 -0500 Subject: [PATCH 09/10] Update README.md --- plugins/movieFromScene/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md index 98759d0c..ac2a8f0b 100644 --- a/plugins/movieFromScene/README.md +++ b/plugins/movieFromScene/README.md @@ -37,7 +37,8 @@ As the above example, it will only create a movie when: 3. The scene has at least one performer. 4. The scene has some details text. -Only when all 4 conditions are met, and after the scene is updated with something, then a movie will be created and linked to that scene. +Only when all 4 conditions are met, and after the scene is updated with something, then a movie will be created and linked to that scene. The new movie will try to copy as much information as possible, including title, studio, duration, date, details and front cover.
+The new movie will not copy the URL from scene directly, instead it will copy the scene's internal URL, like "http://localhost:9999/scenes/1234" . Because I am planning on create a scraper that will allow you to update the movie information from this URL. It doesn't make much sense to direct-copy the URL if the scraper cannot scrape the URL for movies. ### I got a problem with xxx... This is only version 1.0. So please raise an issue and let me know. I am not a Linux or Docker guy, so please don't expect me to solve From 540be9b8da22352ce130aea520a709b034acebb5 Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sun, 8 Jan 2023 00:52:56 -0500 Subject: [PATCH 10/10] Update README.md --- plugins/movieFromScene/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md index ac2a8f0b..21090b80 100644 --- a/plugins/movieFromScene/README.md +++ b/plugins/movieFromScene/README.md @@ -38,7 +38,7 @@ As the above example, it will only create a movie when: 4. The scene has some details text. Only when all 4 conditions are met, and after the scene is updated with something, then a movie will be created and linked to that scene. The new movie will try to copy as much information as possible, including title, studio, duration, date, details and front cover.
-The new movie will not copy the URL from scene directly, instead it will copy the scene's internal URL, like "http://localhost:9999/scenes/1234" . Because I am planning on create a scraper that will allow you to update the movie information from this URL. It doesn't make much sense to direct-copy the URL if the scraper cannot scrape the URL for movies. +The new movie will not copy the URL from scene directly, instead it will copy the scene's internal URL, like "http://localhost:9999/scenes/1234" . Because I am planning on create a scraper that will allow you to update the movie information from this URL. It doesn't make much sense to direct-copy the external URL if the scraper cannot scrape it for movies. ### I got a problem with xxx... This is only version 1.0. So please raise an issue and let me know. I am not a Linux or Docker guy, so please don't expect me to solve