renamerOnUpdate: segment config options into different file#18
Closed
jthrow0451 wants to merge 2 commits intostashapp:mainfrom
Closed
renamerOnUpdate: segment config options into different file#18jthrow0451 wants to merge 2 commits intostashapp:mainfrom
jthrow0451 wants to merge 2 commits intostashapp:mainfrom
Conversation
Removed config options inside main file
Added config file
Collaborator
|
If we move that we might as well move the rest of the user configurable options. README.md # SQLITE Renamer for Stash
Use metadata from your stash to rename your scene file.
## Requirement
- Python (Tested on Python v3.9.1 64bit, Win10)
- Request Module
- Stash
- Windows 10 ? (No idea if this works for all OS)
- Log.py
## Usage
- Everytime you update a scene, the plugin will check/update your scene's filename.
- You can set a path for [STASH_LOGFILE](config.py), so you will have a file with all changes that the plugin made. Useful if you want to revert the changes.
## Filename template selectors
Available: `$date` `$performer` `$title` `$studio` `$height` `$parent_studio`
The script will replace these fields with the data from the database.
Example:
| Template | Result
| ------------- |:-------------:
$title|Her Fantasy Ball.mp4
$title $height|Her Fantasy Ball 1080p.mp4
$date $title|2016-12-29 Her Fantasy Ball.mp4
$date $performer - $title [$studio]|2016-12-29 Eva Lovia - Her Fantasy Ball [Sneaky Sex].mp4
$parent_studio $date $performer - $title|RealityKings 2016-12-29 Eva Lovia - Her Fantasy Ball.mp4
Note:
- A regex will remove illegal characters for Windows.
- If your resulting path is be more than 240 characters, the script will try to reduce it. It will only use Date + Title.
- If the height of the video is 2160/4320, it will be replaced by `4k`/`8k` else it will be `height + p` (240p,720p,1080p...)
- If the scene contains more than 3 performers, $performer will be not be replaced.
## Change scene filename by studios/tags
If you want differents formats by studios/tags check [config.py](config.py) and adjust as needed. If studio name templates are configured then they will be used instead of the tag ones.
A default template can also be used if no suitable studio/tag is found. You just need to adjust the `default_template` and `use_default_template` in [config.py](config.py)config.py # Available template selectors
# -----------------------------------------------------------------
# $date $performer $title $studio $height $parent_studio
# -----------------------------------------------------------------
# e.g.:
# $title == SSNI-000.mp4
# $date $title == 2017-04-27 Oni Chichi.mp4
# $date $title $height == 2017-04-27 Oni Chichi 1080p.mp4
# $date $performer - $title [$studio] == 2016-12-29 Eva Lovia - Her Fantasy Ball [Sneaky Sex].mp4
# $parent_studio $date $performer - $title == Reality Kings 2016-12-29 Eva Lovia - Her Fantasy Ball.mp4
# -----------------------------------------------------------------
# templates to use for given tags
# add or remove as needed
tag_templates = {
"!1. Western": "$date $performer - $title [$studio]",
"!1. JAV": "$title",
"!1. Anime": "$date $title",
}
studio_templates = None
# uncomment and adjust the below if you want to use studio names instead of tags for the renaming templates
#
#studio_templates = {
# "STUDIO NAME": "$date $performer - $title [$studio]",
# "STUDIO NAME 2": "$parent_studio $date $performer - $title"
#}
# default template, adjust as needed
default_template = "$date $title"
# change to True to use the default template if no specific tag/studio is found
use_default_template = False
# Log File
# File to save what is renamed, can be useful if you need to revert changes
# Will look like: IDSCENE|OLD_FILENAME|NEW_FILENAME
# Leave Blank or use None if you don't want to use a log file, or a working path like this: C:\Users\Winter\.stash\plugins\Hooks\rename_log.txt
STASH_LOGFILE = r""renamerOnUpdate.py import json
import os
import re
import sqlite3
import sys
import requests
import log
import config
log.LogDebug("--Starting Hook 'Update' Plugin--")
FRAGMENT = json.loads(sys.stdin.read())
FRAGMENT_SERVER = FRAGMENT["server_connection"]
FRAGMENT_SCENE_ID = FRAGMENT["args"]["hookContext"]["id"]
def callGraphQL(query, variables=None):
# Session cookie for authentication
graphql_port = FRAGMENT_SERVER['Port']
graphql_scheme = FRAGMENT_SERVER['Scheme']
graphql_cookies = {
'session': FRAGMENT_SERVER.get('SessionCookie').get('Value')
}
graphql_headers = {
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json",
"Accept": "application/json",
"Connection": "keep-alive",
"DNT": "1"
}
if FRAGMENT_SERVER.get('Domain'):
graphql_domain = FRAGMENT_SERVER['Domain']
else:
graphql_domain = 'localhost'
# Stash GraphQL endpoint
graphql_url = graphql_scheme + "://" + \
graphql_domain + ":" + str(graphql_port) + "/graphql"
json = {'query': query}
if variables is not None:
json['variables'] = variables
response = requests.post(graphql_url, json=json,
headers=graphql_headers, cookies=graphql_cookies)
if response.status_code == 200:
result = response.json()
if result.get("error"):
for error in result["error"]["errors"]:
raise Exception("GraphQL error: {}".format(error))
if result.get("data"):
return result.get("data")
elif response.status_code == 401:
sys.exit("HTTP Error 401, Unauthorised.")
else:
raise ConnectionError("GraphQL query failed:{} - {}. Query: {}. Variables: {}".format(
response.status_code, response.content, query, variables))
def graphql_getscene(scene_id):
query = """
query FindScene($id: ID!, $checksum: String) {
findScene(id: $id, checksum: $checksum) {
...SceneData
}
}
fragment SceneData on Scene {
id
checksum
oshash
title
details
url
date
rating
o_counter
organized
path
phash
interactive
file {
size
duration
video_codec
audio_codec
width
height
framerate
bitrate
}
paths {
screenshot
preview
stream
webp
vtt
chapters_vtt
funscript
}
studio {
...SlimStudioData
}
movies {
movie {
...MovieData
}
scene_index
}
tags {
...SlimTagData
}
performers {
...PerformerData
}
}
fragment SlimStudioData on Studio {
id
name
image_path
stash_ids {
endpoint
stash_id
}
parent_studio {
id
name
}
details
rating
}
fragment MovieData on Movie {
id
checksum
name
aliases
duration
date
rating
director
studio {
...SlimStudioData
}
synopsis
url
front_image_path
back_image_path
scene_count
}
fragment SlimTagData on Tag {
id
name
aliases
image_path
}
fragment PerformerData on Performer {
id
checksum
name
url
gender
twitter
instagram
birthdate
ethnicity
country
eye_color
height
measurements
fake_tits
career_length
tattoos
piercings
aliases
favorite
image_path
scene_count
image_count
gallery_count
tags {
...SlimTagData
}
stash_ids {
stash_id
endpoint
}
rating
details
death_date
hair_color
weight
}
"""
variables = {
"id": scene_id
}
result = callGraphQL(query, variables)
return result.get('findScene')
def graphql_configuration():
query = """
query Configuration {
configuration {
...ConfigData
}
}
fragment ConfigData on ConfigResult {
general {
...ConfigGeneralData
}
interface {
...ConfigInterfaceData
}
dlna {
...ConfigDLNAData
}
}
fragment ConfigGeneralData on ConfigGeneralResult {
stashes {
path
excludeVideo
excludeImage
}
databasePath
generatedPath
configFilePath
cachePath
calculateMD5
videoFileNamingAlgorithm
parallelTasks
previewAudio
previewSegments
previewSegmentDuration
previewExcludeStart
previewExcludeEnd
previewPreset
maxTranscodeSize
maxStreamingTranscodeSize
apiKey
username
password
maxSessionAge
logFile
logOut
logLevel
logAccess
createGalleriesFromFolders
videoExtensions
imageExtensions
galleryExtensions
excludes
imageExcludes
scraperUserAgent
scraperCertCheck
scraperCDPPath
stashBoxes {
name
endpoint
api_key
}
}
fragment ConfigInterfaceData on ConfigInterfaceResult {
menuItems
soundOnPreview
wallShowTitle
wallPlayback
maximumLoopDuration
autostartVideo
showStudioAsText
css
cssEnabled
language
slideshowDelay
handyKey
}
fragment ConfigDLNAData on ConfigDLNAResult {
serverName
enabled
whitelistedIPs
interfaces
}
"""
result = callGraphQL(query)
return result.get('configuration')
def makeFilename(scene_information, query):
# Query exemple:
# Available: $date $performer $title $studio $height $parent_studio
# $title == SSNI-000.mp4
# $date $title == 2017-04-27 Oni Chichi.mp4
# $date $title $height == 2017-04-27 Oni Chichi 1080p.mp4
# $date $performer - $title [$studio] == 2016-12-29 Eva Lovia - Her Fantasy Ball [Sneaky Sex].mp4
# $parent_studio $date $performer - $title == RealityKings 2016-12-29 Eva Lovia - Her Fantasy Ball.mp4
new_filename = str(query)
if "$date" in new_filename:
if scene_information.get('date') == "" or scene_information.get('date') is None:
new_filename = re.sub('\$date\s*', '', new_filename)
else:
new_filename = new_filename.replace("$date", scene_information["date"])
if "$performer" in new_filename:
if scene_information.get('performer') == "" or scene_information.get('performer') is None:
new_filename = re.sub('\$performer\s*', '', new_filename)
else:
new_filename = new_filename.replace("$performer", scene_information["performer"])
if "$title" in new_filename:
if scene_information.get('title') == "" or scene_information.get('title') is None:
new_filename = re.sub('\$title\s*', '', new_filename)
else:
new_filename = new_filename.replace("$title", scene_information["title"])
if "$studio" in new_filename:
if scene_information.get('studio') == "" or scene_information.get('studio') is None:
new_filename = re.sub('\$studio\s*', '', new_filename)
else:
new_filename = new_filename.replace("$studio", scene_information["studio"])
if "$parent_studio" in new_filename:
if scene_information.get('parent_studio') == "" or scene_information.get('parent_studio') is None:
new_filename = re.sub('\$parent_studio\s*', '', new_filename)
else:
new_filename = new_filename.replace("$parent_studio", scene_information["parent_studio"])
if "$height" in new_filename:
if scene_information.get('height') == "" or scene_information.get('height') is None:
new_filename = re.sub('\$height\s*', '', new_filename)
else:
new_filename = new_filename.replace("$height", scene_information["height"])
new_filename = re.sub('^\s*-\s*', '', new_filename)
new_filename = re.sub('\s*-\s*$', '', new_filename)
new_filename = re.sub('\[\W*]', '', new_filename)
new_filename = re.sub('\s{2,}', ' ', new_filename)
new_filename = new_filename.strip()
return new_filename
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()
scene_info = graphql_getscene(FRAGMENT_SCENE_ID)
stash_config = graphql_configuration()
stash_database = stash_config["general"]["databasePath"]
log.LogDebug("Scene ID: {}".format(FRAGMENT_SCENE_ID))
#log.LogDebug("Scene Info: {}".format(scene_info))
log.LogDebug("Database Path: {}".format(stash_database))
result_template = None
# use default template if enabled
if config.use_default_template:
result_template = config.default_template
# if studio templates are defined use them instead of the tags
if config.studio_templates is not None:
if scene_info.get("studio"):
if config.studio_templates.get(scene_info["studio"]["name"]):
result_template = config.studio_templates[scene_info["studio"]["name"]]
elif scene_info.get("tags"):
for tag in scene_info["tags"]:
if config.tag_templates.get(tag["name"]):
result_template = config.tag_templates[tag["name"]]
break
if result_template is None:
exit_plugin("No template for this file.")
else:
log.LogDebug("Using this template: {}".format(result_template))
current_path = scene_info["path"]
file_extension = os.path.splitext(current_path)[1]
# Note: basename contains the extension
current_filename = os.path.basename(current_path)
current_directory = os.path.dirname(current_path)
# Grabbing things from Stash
scene_information = {}
# Grab Title (without extension if present)
if scene_info.get("title"):
# Remove extension
scene_information["title"] = re.sub(file_extension + '$', '', scene_info["title"])
# Grab Date
scene_information["date"] = scene_info.get("date")
# Grab Performer (Dani Daniels Riley Reid)
if scene_info.get("performers"):
perf_list = ""
if len(scene_info["performers"]) > 3:
log.LogWarning("More than 3 performers.")
else:
for performer in scene_info["performers"]:
if performer.get("name"):
perf_list += performer["name"] + " "
else:
continue
perf_list = perf_list.strip()
scene_information["performer"] = perf_list
# Grab Studio name
if scene_info.get("studio"):
scene_information["studio"] = scene_info["studio"].get("name")
# Grab Parent name
if scene_info["studio"].get("parent_studio"):
scene_information["parent_studio"] = scene_info["studio"]["parent_studio"]["name"]
# Grab Height (720p,1080p,4k...)
if scene_info["file"]["height"] == '4320':
scene_information["height"] = '8k'
else:
if scene_info["file"]["height"] == '2160':
scene_information["height"] = '4k'
else:
scene_information["height"] = "{}p".format(scene_info["file"]["height"])
log.LogDebug("[DEBUG] Scene information: {}".format(scene_information))
# Create the new filename
new_filename = makeFilename(scene_information, result_template) + file_extension
# Remove illegal characters for Windows ('#' and ',' is not illegal you can remove it)
new_filename = re.sub('[\\/:"*?<>|#,]+', '', new_filename)
# Use typewriter instead of Apostrophe
new_filename = re.sub("[’‘]+", "'", new_filename)
# Replace the old filename with the new in the filepath
new_path = current_path.replace(current_filename,new_filename)
#new_path = re.sub('{}$'.format(current_filename), new_filename, current_path)
# Try to prevent error with long path for Win10
# https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd
if len(new_path) > 240:
log.LogWarning("The Path is too long...", new_path)
# We only use the date and title to get a shorter file (eg: 2017-04-27 - Oni Chichi.mp4)
if scene_info.get("date"):
reducePath = len(current_directory + scene_info["title"] + scene_info["date"] + file_extension) + 3
else:
reducePath = len(current_directory + scene_info["title"] + file_extension) + 3
if reducePath < 240:
if scene_info.get("date"):
new_filename = makeFilename(scene_information, "$date - $title") + file_extension
else:
new_filename = makeFilename(scene_information, "$title") + file_extension
new_path = current_path.replace(current_filename,new_filename)
#new_path = re.sub('{}$'.format(current_filename), new_filename, current_path)
log.LogInfo("Reduced filename to: {}", new_filename)
else:
exit_plugin(err="Can't reduce the path, operation aborted.")
log.LogDebug("Filename: {} -> {}".format(current_filename,new_filename))
log.LogDebug("Path: {} -> {}".format(current_path,new_path))
if (new_path == current_path):
exit_plugin("Filename already correct.")
# Connect to the DB
try:
sqliteConnection = sqlite3.connect(stash_database)
cursor = sqliteConnection.cursor()
log.LogDebug("Python successfully connected to SQLite\n")
except sqlite3.Error as error:
exit_plugin(err="FATAL SQLITE Error: {}".format(error))
# Looking for duplicate filename
cursor.execute("SELECT id FROM scenes WHERE path LIKE ? AND NOT id=?;", ["%" + new_filename, FRAGMENT_SCENE_ID])
dupl_check = cursor.fetchall()
if len(dupl_check) > 0:
for dupl_row in dupl_check:
log.LogError("Same filename: [{}]".format(dupl_row[0]))
exit_plugin(err="Duplicate filename detected, check log!")
# OS Rename
if (os.path.isfile(current_path) == True):
os.rename(current_path, new_path)
if (os.path.isfile(new_path) == True):
log.LogInfo("[OS] File Renamed!")
if config.STASH_LOGFILE:
with open(config.STASH_LOGFILE, 'a', encoding='utf-8') as f:
f.write("{}|{}|{}\n".format(FRAGMENT_SCENE_ID, current_path, new_path))
else:
exit_plugin(err="[OS] File failed to rename ? {}".format(new_path))
else:
exit_plugin(err="[OS] File doesn't exist in your Disk/Drive ({})".format(current_path))
# Database rename
cursor.execute("UPDATE scenes SET path=? WHERE id=?;", [new_path, FRAGMENT_SCENE_ID])
# Save the UPDATE
sqliteConnection.commit()
# Close DB
cursor.close()
sqliteConnection.close()
log.LogInfo("[SQLITE] Database updated!")
exit_plugin("Successful!") |
Merged
Collaborator
|
Superceded by #20 |
feederbox826
pushed a commit
to feederbox826/CommunityScripts
that referenced
this pull request
Oct 25, 2023
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.