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 diff --git a/plugins/movieFromScene/README.md b/plugins/movieFromScene/README.md new file mode 100644 index 00000000..21090b80 --- /dev/null +++ b/plugins/movieFromScene/README.md @@ -0,0 +1,48 @@ +# Auto-Create Movie From Scene Update +### A python plugin for Stash 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 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.

+ +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 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 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 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 +problems related to that. In fact, this is my first time to build a Python GUI program. Please be understanding. + + + 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() + + +