mirror of
https://github.com/stashapp/CommunityScripts.git
synced 2026-04-20 11:06:18 -05:00
Create renamer as Task Plugin (#21)
This commit is contained in:
@@ -7,6 +7,7 @@ This list keeps track of scripts and plugins in this repository. Please ensure t
|
||||
Category|Triggers|Plugin Name|Description|Minimum Stash version
|
||||
--------|-----------|-----------|-----------|---------------------
|
||||
Scraper|Task|[GHScraper_Checker](plugins/GHScraper_Checker)|Compare local file against github file from the community scraper repo.|v0.8
|
||||
Maintenance|Task|[renamer](plugins/renamer)|Rename your file based on Stash metadata in bulk.|v0.7
|
||||
Maintenance|Scene.Update|[renamerOnUpdate](plugins/renamerOnUpdate)|Rename your file based on Stash metadata.|v0.7
|
||||
Scenes|SceneMarker.Create<br />SceneMarker.Update|[markerTagToScene](plugins/markerTagToScene)|Adds primary tag of Scene Marker to the Scene on marker create/update.|v0.8 ([46bbede](https://github.com/stashapp/stash/commit/46bbede9a07144797d6f26cf414205b390ca88f9))
|
||||
Scanning|Scene.Create<br />Gallery.Create|[filenameParser](plugins/filenameParser)|Tries to parse filenames, primarily in {studio}.{year}.{month}.{day}.{performer1firstname}.{performer1lastname}.{performer2}.{title} format, into the respective fields|v0.10
|
||||
|
||||
74
plugins/renamer/README.md
Normal file
74
plugins/renamer/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
|
||||
# SQLITE Renamer for Stash (Task)
|
||||
Using metadata from your stash to rename your file.
|
||||
|
||||
## Requirement
|
||||
- Stash
|
||||
- Python 3+ (Tested on Python v3.9.1 64bit, Win10)
|
||||
- Request Module (https://pypi.org/project/requests/)
|
||||
- Windows 10 ? (No idea if this work for all OS)
|
||||
|
||||
## Installation
|
||||
|
||||
- Download the whole folder 'renamer' (config.py, log.py, renamerTask.py/.yml)
|
||||
- Place it in your **plugins** folder (where the `config.yml` is)
|
||||
- Reload plugins (Settings > Plugins)
|
||||
- renamerTask should appear.
|
||||
|
||||
### :exclamation: Make sure to configure the plugin by editing `config.py` before running it :exclamation:
|
||||
|
||||
## Usage
|
||||
|
||||
- You have tasks (Settings > Task):
|
||||
- **Dry-Run 🔍**: Don't edit any file, just show in log. It will create `renamer_scan.txt` that contains every edit.
|
||||
- **[DRYRUN] Check 10 scenes**: Check 10 scenes (by newest updated).
|
||||
- **[DRYRUN] Check all scenes**: Check all scenes.
|
||||
- **Process :pencil2:**: Edit your files, **don't touch Stash while doing this task**.
|
||||
- **Process scanned scene from Dry-Run task**: Read `renamer_scan.txt` instead of checking all scenes.
|
||||
- **Process 10 scenes**: Check 10 scenes (by newest updated).
|
||||
- **Process all scenes**: Check all scenes.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Read/Edit `config.py`
|
||||
- I recommend setting the **log_file** as it can be useful to revert.
|
||||
- If you have the **renamerOnUpdate** plugin, you can copy the `config.py` from it.
|
||||
|
||||
### Example
|
||||
|
||||
> Note: The priority is Tag > Studio > Default
|
||||
|
||||
The config will be:
|
||||
```py
|
||||
# Change filename for scenes from 'Vixen' or 'Slayed' studio.
|
||||
studio_templates = {
|
||||
"Slayed": "$date $performer - $title [$studio]",
|
||||
"Vixen": "$performer - $title [$studio]"
|
||||
}
|
||||
# Change filename if the tag 'rename_tag' is present.
|
||||
tag_templates = {
|
||||
"rename_tag": "$year $title - $studio $resolution $video_codec",
|
||||
}
|
||||
# Change filename no matter what
|
||||
use_default_template = True
|
||||
default_template = "$date $title"
|
||||
# Use space as a performer separator
|
||||
performer_splitchar = " "
|
||||
# If the scene has more than 3 performers, the $performer field will be ignored.
|
||||
performer_limit = 3
|
||||
```
|
||||
The scene was just scanned, everything is default (Title = Filename).
|
||||
|
||||
Current filename: `Slayed.21.09.02.Ariana.Marie.Emily.Willis.And.Eliza.Ibarra.XXX.1080p.mp4`
|
||||
|
||||
|Stash Field | Value | Filename | Trigger template |
|
||||
|--|:---:|--|--|
|
||||
| - | *Default* |`Slayed.21.09.02.Ariana.Marie.Emily.Willis.And.Eliza.Ibarra.XXX.1080p.mp4` | default_template
|
||||
| ~Title| **Driver**| `Driver.mp4` | default_template
|
||||
| +Date| **2021-09-02**| `2021-09-02 Driver.mp4` | default_template
|
||||
| +Performer | **Ariana Marie<br>Emily Willis<br>Eliza Ibarra**| `2021-09-02 Driver.mp4` | default_template
|
||||
| +Studio | **Vixen**| `Ariana Marie Emily Willis Eliza Ibarra - Driver [Vixen].mp4` | studio_templates [Vixen]
|
||||
| ~Studio | **Slayed**| `2021-09-02 Ariana Marie Emily Willis Eliza Ibarra - Driver [Slayed].mp4` | studio_templates [Slayed]
|
||||
| +Performer | **Elsa Jean**| `2021-09-02 Driver [Slayed].mp4` | studio_templates [Slayed]<br>**Reach performer_limit**.
|
||||
| +Tag | **rename_tag**| `2021 Driver - Slayed HD h264.mp4` | tag_templates [rename_tag]
|
||||
|
||||
88
plugins/renamer/config.py
Normal file
88
plugins/renamer/config.py
Normal file
@@ -0,0 +1,88 @@
|
||||
###################################################################
|
||||
#
|
||||
# -----------------------------------------------------------------
|
||||
# Available: $date $year $performer $title $height $resolution $studio $parent_studio $studio_family $video_codec $audio_codec
|
||||
# -note:
|
||||
# $studio_family: If parent studio exist use it, else use the studio name.
|
||||
# $performer: If more than * performers, this field will be ignored. Limit to fix at Settings section below (default: 3)
|
||||
# $resolution: SD/HD/UHD/VERTICAL (for phone) | $height: 720p 1080p 4k 8k
|
||||
# -----------------------------------------------------------------
|
||||
# e.g.:
|
||||
# $title == Her Fantasy Ball
|
||||
# $date $title == 2016-12-29 Her Fantasy Ball
|
||||
# $year $title $height == 2016 Her Fantasy Ball 1080p
|
||||
# $date $performer - $title [$studio] == 2016-12-29 Eva Lovia - Her Fantasy Ball [Sneaky Sex]
|
||||
# $parent_studio $date $performer - $title == Reality Kings 2016-12-29 Eva Lovia - Her Fantasy Ball
|
||||
#
|
||||
####################################################################
|
||||
# TEMPLATE #
|
||||
|
||||
# Priority : Tags > Studios > Default
|
||||
|
||||
# templates to use for given tags
|
||||
# add or remove as needed
|
||||
tag_templates = {
|
||||
"!1. Western": "$date $performer - $title [$studio]",
|
||||
"!1. JAV": "$title",
|
||||
"!1. Anime": "$title $date [$studio]"
|
||||
}
|
||||
|
||||
# adjust the below if you want to use studio names instead of tags for the renaming templates
|
||||
studio_templates = {
|
||||
|
||||
}
|
||||
|
||||
# change to True to use the default template if no specific tag/studio is found
|
||||
use_default_template = False
|
||||
# default template, adjust as needed
|
||||
default_template = "$date $title"
|
||||
|
||||
######################################
|
||||
# Logging #
|
||||
|
||||
# File to save what is renamed, can be useful if you need to revert changes.
|
||||
# Will look like: IDSCENE|OLD_PATH|NEW_PATH
|
||||
# Leave Blank ("") or use None if you don't want to use a log file, or a working path like: C:\Users\USERNAME\.stash\plugins\Hooks\rename_log.txt
|
||||
log_file = r""
|
||||
|
||||
######################################
|
||||
# Settings #
|
||||
|
||||
# Character to use as a performer separator.
|
||||
performer_splitchar = " "
|
||||
# Maximum number of performer names in the filename. If there are more than that in a scene the filename will not include any performer names!
|
||||
performer_limit = 3
|
||||
# ignore male performers.
|
||||
performer_ignore_male = False
|
||||
|
||||
# If $performer is before $title, prevent having duplicate text.
|
||||
# e.g.:
|
||||
# Template used: $year $performer - $title
|
||||
# 2016 Dani Daniels - Dani Daniels in ***.mp4 --> 2016 Dani Daniels in ***.mp4
|
||||
prevent_title_performer = False
|
||||
|
||||
# Only rename 'Organized' scenes.
|
||||
only_organized = False
|
||||
# Field to remove if the path is too long. First in list will be removed then second then ... if length is still too long.
|
||||
order_field = ["$video_codec", "$audio_codec", "$resolution", "$height", "$studio_family", "$studio", "$parent_studio","$performer"]
|
||||
# Alternate way to show diff. Not useful at all.
|
||||
alt_diff_display = False
|
||||
|
||||
######################################
|
||||
# Module Related #
|
||||
|
||||
# ! OPTIONAL module settings. Not needed for basic operation !
|
||||
|
||||
# = psutil module (https://pypi.org/project/psutil/) =
|
||||
# Gets a list of all processes instead of stopping after the first one. Enabling it slows down the plugin
|
||||
process_getall = False
|
||||
# If the file is used by a process, the plugin will kill it. IT CAN MAKE STASH CRASH TOO.
|
||||
process_kill_attach = False
|
||||
# =========================
|
||||
|
||||
# = Unidecode module (https://pypi.org/project/Unidecode/) =
|
||||
# Check site mentioned for more details.
|
||||
# TL;DR: Prevent having non common characters by replacing them.
|
||||
# Warning: If you have non-latin characters (Cyrillic, Kanji, Arabic, ...), the result will be extremely different.
|
||||
use_ascii = False
|
||||
# =========================
|
||||
52
plugins/renamer/log.py
Normal file
52
plugins/renamer/log.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import sys
|
||||
|
||||
|
||||
# Log messages sent from a plugin instance are transmitted via stderr and are
|
||||
# encoded with a prefix consisting of special character SOH, then the log
|
||||
# level (one of t, d, i, w, e, or p - corresponding to trace, debug, info,
|
||||
# warning, error and progress levels respectively), then special character
|
||||
# STX.
|
||||
#
|
||||
# The LogTrace, LogDebug, LogInfo, LogWarning, and LogError methods, and their equivalent
|
||||
# formatted methods are intended for use by plugin instances to transmit log
|
||||
# messages. The LogProgress method is also intended for sending progress data.
|
||||
#
|
||||
|
||||
def __prefix(level_char):
|
||||
start_level_char = b'\x01'
|
||||
end_level_char = b'\x02'
|
||||
|
||||
ret = start_level_char + level_char + end_level_char
|
||||
return ret.decode()
|
||||
|
||||
|
||||
def __log(level_char, s):
|
||||
if level_char == "":
|
||||
return
|
||||
|
||||
print(__prefix(level_char) + s + "\n", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def LogTrace(s):
|
||||
__log(b't', s)
|
||||
|
||||
|
||||
def LogDebug(s):
|
||||
__log(b'd', s)
|
||||
|
||||
|
||||
def LogInfo(s):
|
||||
__log(b'i', s)
|
||||
|
||||
|
||||
def LogWarning(s):
|
||||
__log(b'w', s)
|
||||
|
||||
|
||||
def LogError(s):
|
||||
__log(b'e', s)
|
||||
|
||||
|
||||
def LogProgress(p):
|
||||
progress = min(max(0, p), 1)
|
||||
__log(b'p', str(progress))
|
||||
638
plugins/renamer/renamerTask.py
Normal file
638
plugins/renamer/renamerTask.py
Normal file
@@ -0,0 +1,638 @@
|
||||
import difflib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
try:
|
||||
import psutil # pip install psutil
|
||||
MODULE_PSUTIL = True
|
||||
except:
|
||||
MODULE_PSUTIL = False
|
||||
|
||||
try:
|
||||
import unidecode # pip install Unidecode
|
||||
MODULE_UNIDECODE = True
|
||||
except:
|
||||
MODULE_UNIDECODE = False
|
||||
|
||||
import config
|
||||
import log
|
||||
|
||||
|
||||
FRAGMENT = json.loads(sys.stdin.read())
|
||||
FRAGMENT_SERVER = FRAGMENT["server_connection"]
|
||||
PLUGIN_DIR = FRAGMENT_SERVER["PluginDir"]
|
||||
PLUGIN_ARGS = FRAGMENT['args'].get("mode")
|
||||
|
||||
log.LogDebug("--Starting Plugin 'Renammer'--")
|
||||
|
||||
#log.LogDebug("{}".format(FRAGMENT))
|
||||
|
||||
def callGraphQL(query, variables=None, raise_exception=True):
|
||||
# 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"
|
||||
}
|
||||
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
|
||||
try:
|
||||
response = requests.post(graphql_url, json=json,headers=graphql_headers, cookies=graphql_cookies, timeout=20)
|
||||
except Exception as e:
|
||||
exit_plugin(err="[FATAL] Exception with GraphQL request. {}".format(e))
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get("error"):
|
||||
for error in result["error"]["errors"]:
|
||||
if raise_exception:
|
||||
raise Exception("GraphQL error: {}".format(error))
|
||||
else:
|
||||
log.LogError("GraphQL error: {}".format(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("GraphQL query failed: {} - {}".format(response.status_code, response.content))
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
studio {
|
||||
...SlimStudioData
|
||||
}
|
||||
movies {
|
||||
movie {
|
||||
...MovieData
|
||||
}
|
||||
scene_index
|
||||
}
|
||||
tags {
|
||||
...SlimTagData
|
||||
}
|
||||
performers {
|
||||
...PerformerData
|
||||
}
|
||||
}
|
||||
fragment SlimStudioData on Studio {
|
||||
id
|
||||
name
|
||||
parent_studio {
|
||||
id
|
||||
name
|
||||
}
|
||||
details
|
||||
rating
|
||||
aliases
|
||||
}
|
||||
fragment MovieData on Movie {
|
||||
id
|
||||
checksum
|
||||
name
|
||||
aliases
|
||||
date
|
||||
rating
|
||||
director
|
||||
studio {
|
||||
...SlimStudioData
|
||||
}
|
||||
synopsis
|
||||
url
|
||||
}
|
||||
fragment SlimTagData on Tag {
|
||||
id
|
||||
name
|
||||
aliases
|
||||
}
|
||||
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
|
||||
tags {
|
||||
...SlimTagData
|
||||
}
|
||||
rating
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
weight
|
||||
}
|
||||
"""
|
||||
variables = {
|
||||
"id": scene_id
|
||||
}
|
||||
result = callGraphQL(query, variables)
|
||||
return result.get('findScene')
|
||||
|
||||
|
||||
def graphql_getConfiguration():
|
||||
query = """
|
||||
query Configuration {
|
||||
configuration {
|
||||
general {
|
||||
databasePath
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
result = callGraphQL(query)
|
||||
return result.get('configuration')
|
||||
|
||||
|
||||
def graphql_findScene(perPage,direc="DESC"):
|
||||
query = """
|
||||
query FindScenes($filter: FindFilterType) {
|
||||
findScenes(filter: $filter) {
|
||||
count
|
||||
scenes {
|
||||
...SlimSceneData
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment SlimSceneData on Scene {
|
||||
id
|
||||
checksum
|
||||
oshash
|
||||
title
|
||||
details
|
||||
url
|
||||
date
|
||||
rating
|
||||
o_counter
|
||||
organized
|
||||
path
|
||||
phash
|
||||
interactive
|
||||
scene_markers {
|
||||
id
|
||||
title
|
||||
seconds
|
||||
}
|
||||
galleries {
|
||||
id
|
||||
path
|
||||
title
|
||||
}
|
||||
studio {
|
||||
id
|
||||
name
|
||||
}
|
||||
movies {
|
||||
movie {
|
||||
id
|
||||
name
|
||||
}
|
||||
scene_index
|
||||
}
|
||||
tags {
|
||||
id
|
||||
name
|
||||
}
|
||||
performers {
|
||||
id
|
||||
name
|
||||
gender
|
||||
favorite
|
||||
}
|
||||
}
|
||||
"""
|
||||
# ASC DESC
|
||||
variables = {'filter': {"direction": direc, "page": 1, "per_page": perPage, "sort": "updated_at"}}
|
||||
result = callGraphQL(query, variables)
|
||||
return result.get("findScenes")
|
||||
|
||||
|
||||
def makeFilename(scene_information, query):
|
||||
new_filename = str(query)
|
||||
for field in TEMPLATE_FIELD:
|
||||
field_name = field.replace("$","")
|
||||
if field in new_filename:
|
||||
if scene_information.get(field_name):
|
||||
if field == "$performer":
|
||||
if re.search(r"\$performer[-\s_]*\$title", new_filename) and scene_information.get('title') and PREVENT_TITLE_PERF:
|
||||
if re.search("^{}".format(scene_information["performer"]), scene_information["title"]):
|
||||
log.LogInfo("Ignoring the performer field because it's already in start of title")
|
||||
new_filename = re.sub('\$performer[-\s_]*', '', new_filename)
|
||||
continue
|
||||
new_filename = new_filename.replace(field, scene_information[field_name])
|
||||
else:
|
||||
new_filename = re.sub('\${}[-\s_]*'.format(field_name), '', new_filename)
|
||||
|
||||
# remove []
|
||||
new_filename = re.sub('\[\W*]', '', new_filename)
|
||||
# Remove multiple space/_ in row
|
||||
new_filename = re.sub('[\s_]{2,}', ' ', new_filename)
|
||||
# Remove multiple - in row
|
||||
new_filename = re.sub('(?:[\s_]-){2,}', ' -', new_filename)
|
||||
# Remove space at start/end
|
||||
new_filename = new_filename.strip(" -")
|
||||
return new_filename
|
||||
|
||||
|
||||
def find_diff_text(a, b):
|
||||
addi = minus = stay = ""
|
||||
minus_ = addi_ = 0
|
||||
for _, s in enumerate(difflib.ndiff(a, b)):
|
||||
if s[0] == ' ':
|
||||
stay += s[-1]
|
||||
minus += "*"
|
||||
addi += "*"
|
||||
elif s[0] == '-':
|
||||
minus += s[-1]
|
||||
minus_ += 1
|
||||
elif s[0] == '+':
|
||||
addi += s[-1]
|
||||
addi_ += 1
|
||||
if minus_ > 20 or addi_ > 20:
|
||||
log.LogDebug("Diff Checker: +{}; -{};".format(addi_,minus_))
|
||||
log.LogDebug("OLD: {}".format(a))
|
||||
log.LogDebug("NEW: {}".format(b))
|
||||
else:
|
||||
log.LogDebug("Original: {}\n- Charac: {}\n+ Charac: {}\n Result: {}".format(a, minus, addi, b))
|
||||
return
|
||||
|
||||
|
||||
def has_handle(fpath,all_result=False):
|
||||
lst = []
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
for item in proc.open_files():
|
||||
if fpath == item.path:
|
||||
if all_result:
|
||||
lst.append(proc)
|
||||
else:
|
||||
return proc
|
||||
except Exception:
|
||||
pass
|
||||
return lst
|
||||
|
||||
|
||||
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 renamer(scene_id):
|
||||
filename_template = None
|
||||
STASH_SCENE = graphql_getScene(scene_id)
|
||||
# ================================================================ #
|
||||
# RENAMER #
|
||||
# Tags > Studios > Default
|
||||
|
||||
# Default
|
||||
if config.use_default_template:
|
||||
filename_template = config.default_template
|
||||
|
||||
# Change by Studio
|
||||
if STASH_SCENE.get("studio") and config.studio_templates:
|
||||
if config.studio_templates.get(STASH_SCENE["studio"]["name"]):
|
||||
filename_template = config.studio_templates[STASH_SCENE["studio"]["name"]]
|
||||
# by Parent
|
||||
if STASH_SCENE["studio"].get("parent_studio"):
|
||||
if config.studio_templates.get(STASH_SCENE["studio"]["name"]):
|
||||
filename_template = config.studio_templates[STASH_SCENE["studio"]["name"]]
|
||||
|
||||
# Change by Tag
|
||||
if STASH_SCENE.get("tags") and config.tag_templates:
|
||||
for tag in STASH_SCENE["tags"]:
|
||||
if config.tag_templates.get(tag["name"]):
|
||||
filename_template = config.tag_templates[tag["name"]]
|
||||
break
|
||||
|
||||
# END #
|
||||
####################################################################
|
||||
|
||||
if config.only_organized and not STASH_SCENE["organized"]:
|
||||
return("Scene ignored (not organized)")
|
||||
|
||||
if not filename_template:
|
||||
return("No template for this scene.")
|
||||
|
||||
#log.LogDebug("Using this template: {}".format(filename_template))
|
||||
|
||||
current_path = STASH_SCENE["path"]
|
||||
# note: contain the dot (.mp4)
|
||||
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 STASH_SCENE.get("title"):
|
||||
# Removing extension if present in title
|
||||
scene_information["title"] = re.sub("{}$".format(file_extension), "", STASH_SCENE["title"])
|
||||
|
||||
# Grab Date
|
||||
scene_information["date"] = STASH_SCENE.get("date")
|
||||
|
||||
# Grab Performer
|
||||
if STASH_SCENE.get("performers"):
|
||||
perf_list = ""
|
||||
if len(STASH_SCENE["performers"]) > PERFORMER_LIMIT:
|
||||
log.LogInfo("More than {} performer(s). Ignoring $performer".format(PERFORMER_LIMIT))
|
||||
else:
|
||||
for perf in STASH_SCENE["performers"]:
|
||||
#log.LogDebug(performer)
|
||||
if PERFORMER_IGNORE_MALE:
|
||||
if perf["gender"] != "MALE":
|
||||
perf_list += perf["name"] + PERFORMER_SPLITCHAR
|
||||
else:
|
||||
perf_list += perf["name"] + PERFORMER_SPLITCHAR
|
||||
# Remove last character
|
||||
perf_list = perf_list[:-len(PERFORMER_SPLITCHAR)]
|
||||
scene_information["performer"] = perf_list
|
||||
|
||||
# Grab Studio name
|
||||
if STASH_SCENE.get("studio"):
|
||||
scene_information["studio"] = STASH_SCENE["studio"].get("name")
|
||||
scene_information["studio_family"] = scene_information["studio"]
|
||||
# Grab Parent name
|
||||
if STASH_SCENE["studio"].get("parent_studio"):
|
||||
scene_information["parent_studio"] = STASH_SCENE["studio"]["parent_studio"]["name"]
|
||||
scene_information["studio_family"] = scene_information["parent_studio"]
|
||||
|
||||
# Grab Height (720p,1080p,4k...)
|
||||
scene_information["resolution"] = 'SD'
|
||||
scene_information["height"] = "{}p".format(STASH_SCENE["file"]["height"])
|
||||
if STASH_SCENE["file"]["height"] >= 720:
|
||||
scene_information["resolution"] = 'HD'
|
||||
if STASH_SCENE["file"]["height"] >= 2160:
|
||||
scene_information["height"] = '4k'
|
||||
scene_information["resolution"] = 'UHD'
|
||||
if STASH_SCENE["file"]["height"] >= 4320:
|
||||
scene_information["height"] = '8k'
|
||||
# For Phone ?
|
||||
if STASH_SCENE["file"]["height"] > STASH_SCENE["file"]["width"]:
|
||||
scene_information["resolution"] = 'VERTICAL'
|
||||
|
||||
scene_information["video_codec"] = STASH_SCENE["file"]["video_codec"]
|
||||
scene_information["audio_codec"] = STASH_SCENE["file"]["audio_codec"]
|
||||
|
||||
log.LogDebug("[{}] Scene information: {}".format(scene_id,scene_information))
|
||||
|
||||
if scene_information.get("date"):
|
||||
scene_information["year"] = scene_information["date"][0:4]
|
||||
|
||||
|
||||
# Create the new filename
|
||||
new_filename = makeFilename(scene_information, filename_template) + file_extension
|
||||
|
||||
# Remove illegal character for Windows ('#' and ',' is not illegal you can remove it)
|
||||
new_filename = re.sub('[\\/:"*?<>|#,]+', '', new_filename)
|
||||
|
||||
# Trying to remove non standard character
|
||||
if MODULE_UNIDECODE and UNICODE_USE:
|
||||
new_filename = unidecode.unidecode(new_filename, errors='preserve')
|
||||
else:
|
||||
# Using typewriter for Apostrophe
|
||||
new_filename = re.sub("[’‘]+", "'", new_filename)
|
||||
|
||||
# Replace the old filename by the new in the filepath
|
||||
new_path = current_path.rstrip(current_filename) + new_filename
|
||||
|
||||
# Trying 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 ({})...".format(len(new_path)))
|
||||
for word in ORDER_SHORTFIELD:
|
||||
if word not in filename_template:
|
||||
continue
|
||||
filename_template = re.sub('\{}[-\s_]*'.format(word), '', filename_template).strip()
|
||||
log.LogDebug("Removing field: {}".format(word))
|
||||
new_filename = makeFilename(scene_information, filename_template) + file_extension
|
||||
new_path = current_path.rstrip(current_filename) + new_filename
|
||||
if len(new_path) < 240:
|
||||
log.LogInfo("Reduced filename to: {}".format(new_filename))
|
||||
break
|
||||
if len(new_path) > 240:
|
||||
return("Can't manage to 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):
|
||||
return("Filename already correct. ({})".format(current_filename))
|
||||
|
||||
if ALT_DIFF_DISPLAY:
|
||||
find_diff_text(current_filename,new_filename)
|
||||
else:
|
||||
log.LogDebug("[OLD] Filename: {}".format(current_filename))
|
||||
log.LogDebug("[NEW] Filename: {}".format(new_filename))
|
||||
if DRY_RUN:
|
||||
with open(FILE_DRYRUN_RESULT, 'a', encoding='utf-8') as f:
|
||||
f.write("{}|{}|{}\n".format(scene_id, current_filename, new_filename))
|
||||
return("[Dry-run] Writing in {}".format(FILE_DRYRUN_RESULT))
|
||||
|
||||
|
||||
# Connect to the DB
|
||||
try:
|
||||
sqliteConnection = sqlite3.connect(STASH_DATABASE)
|
||||
cursor = sqliteConnection.cursor()
|
||||
log.LogDebug("Python successfully connected to SQLite")
|
||||
except sqlite3.Error as error:
|
||||
return("FATAL SQLITE Error: {}".format(error))
|
||||
|
||||
# Looking for duplicate filename
|
||||
folder_name = os.path.basename(os.path.dirname(new_path))
|
||||
cursor.execute("SELECT id FROM scenes WHERE path LIKE ? AND NOT id=?;", ["%" + folder_name + "_" + new_filename, scene_id])
|
||||
dupl_check = cursor.fetchall()
|
||||
if len(dupl_check) > 0:
|
||||
for dupl_row in dupl_check:
|
||||
log.LogError("Same path: [{}]".format(dupl_row[0]))
|
||||
return("Duplicate path detected, check log!")
|
||||
|
||||
cursor.execute("SELECT id FROM scenes WHERE path LIKE ? AND NOT id=?;", ["%" + new_filename, scene_id])
|
||||
dupl_check = cursor.fetchall()
|
||||
if len(dupl_check) > 0:
|
||||
for dupl_row in dupl_check:
|
||||
log.LogInfo("Same filename: [{}]".format(dupl_row[0]))
|
||||
|
||||
# OS Rename
|
||||
if (os.path.isfile(current_path) == True):
|
||||
try:
|
||||
os.rename(current_path, new_path)
|
||||
except PermissionError as err:
|
||||
if "[WinError 32]" in str(err) and MODULE_PSUTIL:
|
||||
log.LogWarning("A process use this file, trying to find it (Probably FFMPEG)")
|
||||
# Find what process access the file, it's ffmpeg for sure...
|
||||
process_use = has_handle(current_path, PROCESS_ALLRESULT)
|
||||
if process_use:
|
||||
# Terminate the process then try again to rename
|
||||
log.LogDebug("Process that use this file: {}".format(process_use))
|
||||
if PROCESS_KILL:
|
||||
p = psutil.Process(process_use.pid)
|
||||
p.terminate()
|
||||
p.wait(10)
|
||||
# If we don't manage to close it, this will create a error again.
|
||||
os.rename(current_path, new_path)
|
||||
else:
|
||||
return("A process prevent editing the file.")
|
||||
else:
|
||||
log.LogError(err)
|
||||
return ""
|
||||
if (os.path.isfile(new_path) == True):
|
||||
log.LogInfo("[OS] File Renamed!")
|
||||
if LOGFILE:
|
||||
with open(LOGFILE, 'a', encoding='utf-8') as f:
|
||||
f.write("{}|{}|{}\n".format(scene_id, current_path, new_path))
|
||||
else:
|
||||
# I don't think it's possible.
|
||||
return("[OS] File failed to rename ? {}".format(new_path))
|
||||
else:
|
||||
return("[OS] File don't exist in your Disk/Drive ({})".format(current_path))
|
||||
|
||||
# Database rename
|
||||
cursor.execute("UPDATE scenes SET path=? WHERE id=?;", [new_path, scene_id])
|
||||
sqliteConnection.commit()
|
||||
# Close DB
|
||||
cursor.close()
|
||||
sqliteConnection.close()
|
||||
log.LogInfo("[SQLITE] Database updated and closed!")
|
||||
return ""
|
||||
|
||||
|
||||
# File that show what we will changed.
|
||||
FILE_DRYRUN_RESULT = os.path.join(PLUGIN_DIR, "renamer_scan.txt")
|
||||
|
||||
STASH_CONFIG = graphql_getConfiguration()
|
||||
STASH_DATABASE = STASH_CONFIG["general"]["databasePath"]
|
||||
TEMPLATE_FIELD = "$date $year $performer $title $height $resolution $studio $parent_studio $studio_family $video_codec $audio_codec".split(" ")
|
||||
|
||||
# READING CONFIG
|
||||
|
||||
LOGFILE = config.log_file
|
||||
PERFORMER_SPLITCHAR = config.performer_splitchar
|
||||
PERFORMER_LIMIT = config.performer_limit
|
||||
PERFORMER_IGNORE_MALE = config.performer_ignore_male
|
||||
PREVENT_TITLE_PERF = config.prevent_title_performer
|
||||
|
||||
PROCESS_KILL = config.process_kill_attach
|
||||
PROCESS_ALLRESULT = config.process_getall
|
||||
UNICODE_USE = config.use_ascii
|
||||
|
||||
ORDER_SHORTFIELD = config.order_field
|
||||
ALT_DIFF_DISPLAY = config.alt_diff_display
|
||||
|
||||
# Task
|
||||
scenes = None
|
||||
progress = 0
|
||||
start_time = time.time()
|
||||
|
||||
if PLUGIN_ARGS in ["Process_test","Process_full","Process_dry"]:
|
||||
DRY_RUN = False
|
||||
else:
|
||||
log.LogDebug("Dry-Run enable")
|
||||
DRY_RUN = True
|
||||
|
||||
if PLUGIN_ARGS in ["DRYRUN_test","Process_test"]:
|
||||
scenes = graphql_findScene(10, "DESC")
|
||||
if PLUGIN_ARGS in ["DRYRUN_full","Process_full"]:
|
||||
scenes = graphql_findScene(-1, "ASC")
|
||||
if PLUGIN_ARGS == "Process_dry":
|
||||
if os.path.exists(FILE_DRYRUN_RESULT):
|
||||
scenes = {"scenes":[]}
|
||||
with open(FILE_DRYRUN_RESULT, 'r') as f:
|
||||
for line in f:
|
||||
scene_id_file = line.split("|")[0]
|
||||
scenes["scenes"].append(scene_id_file)
|
||||
else:
|
||||
exit_plugin(err="Can't find the file from the dry-run ({}). Be sure to run a Dry-Run task before.".format(FILE_DRYRUN_RESULT))
|
||||
|
||||
if not scenes:
|
||||
exit_plugin(err="no scene")
|
||||
|
||||
log.LogDebug("Count scenes: {}".format(len(scenes["scenes"])))
|
||||
progress_step = 1 / len(scenes["scenes"])
|
||||
|
||||
for scene in scenes["scenes"]:
|
||||
msg = renamer(scene["id"])
|
||||
if msg:
|
||||
log.LogDebug(msg)
|
||||
progress += progress_step
|
||||
log.LogProgress(progress)
|
||||
|
||||
if PLUGIN_ARGS == "Process_dry":
|
||||
os.remove(FILE_DRYRUN_RESULT)
|
||||
|
||||
if DRY_RUN:
|
||||
num_lines = 0
|
||||
if os.path.exists(FILE_DRYRUN_RESULT):
|
||||
num_lines = sum(1 for _ in open(FILE_DRYRUN_RESULT))
|
||||
if num_lines > 0:
|
||||
log.LogInfo("[DRY-RUN] There wil be {} file(s) changed. Check {} for more details".format(num_lines, FILE_DRYRUN_RESULT))
|
||||
else:
|
||||
log.LogInfo("[DRY-RUN] No change to do.")
|
||||
|
||||
log.LogInfo("Took {} seconds".format(round(time.time() - start_time)))
|
||||
exit_plugin("Successful!")
|
||||
29
plugins/renamer/renamerTask.yml
Normal file
29
plugins/renamer/renamerTask.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
name: renamerTask
|
||||
description: Rename filename based to a template.
|
||||
url: https://github.com/stashapp/CommunityScripts
|
||||
version: 1.0
|
||||
exec:
|
||||
- python
|
||||
- "{pluginDir}/renamerTask.py"
|
||||
interface: raw
|
||||
tasks:
|
||||
- name: '[DRYRUN] Check 10 scenes'
|
||||
description: Only check 10 scenes. Just show in log and create a file with the possible change.
|
||||
defaultArgs:
|
||||
mode: DRYRUN_test
|
||||
- name: '[DRYRUN] Check all scenes'
|
||||
description: Check all scenes. Just show in log and create a file with the possible change.
|
||||
defaultArgs:
|
||||
mode: DRYRUN_full
|
||||
- name: 'Process scanned scene from Dry-Run task'
|
||||
description: Edit scenes listed on the textfile from the Dry-Run task. ! Don't do anything in Stash in same time !
|
||||
defaultArgs:
|
||||
mode: Process_dry
|
||||
- name: 'Process 10 scenes'
|
||||
description: Edit the filename (if needed) for 10 scenes. ! Don't do anything in Stash in same time !
|
||||
defaultArgs:
|
||||
mode: Process_test
|
||||
- name: 'Process all scenes'
|
||||
description: Edit the filename (if needed) for all scenes. ! Don't do anything in Stash in same time !
|
||||
defaultArgs:
|
||||
mode: Process_full
|
||||
Reference in New Issue
Block a user