I have a lot of 4k scenes and they take a lot of space. Since I’m totally fine with 1080p I want to downscale the said 4k scenes. To achieve that I thought I would batch rename all 4k scene files by adding “to_downscale” in the filename or something like that to make them easy to find and then downscale them.
But I have not found a way to achieve that. I saw that there are 3 plugins to rename files and I tested one of them but, afaik, none of them do batch renaming.
So is there a way to do batch rename of files? Or is there any other method of acheiving what I want?
Create a new tag in Stash to keep track of the scenes you want to downscale.
Run the script below, it’ll output all the file paths of the scenes you tagged above.
Once you’ve got the list of file paths you can do whatever you want with it, e.g. use it as input into a process that’ll rename the files for you or even loop through each line and run ffmpeg.
cat << EOF | sqlite3 /path/to/stash.db
SELECT
folders.path || "/" || files.basename AS path
FROM
scenes_tags st
LEFT JOIN tags ON st.tag_id = tags.id
LEFT JOIN scenes_files sf ON st.scene_id = sf.scene_id
LEFT JOIN files ON sf.file_id = files.id
LEFT JOIN folders ON files.parent_folder_id = folders.id
WHERE
tags.name = "to_downscale";
EOF
PPP, I took your lead and wrote a python script to move the files that have a certain tag.
The script takes 3 parameters:
the Stash SQLite db
the tag
the destination folder
import sqlite3
import argparse
import os
import shutil
def move_files_from_db(db_path, tag, destination_folder):
# Ensure destination folder exists
os.makedirs(destination_folder, exist_ok=True)
# Connect to the SQLite database
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# SQL query using parameterized input for tag
query = """
SELECT
folders.path || '/' || files.basename AS path
FROM
scenes_tags st
LEFT JOIN tags ON st.tag_id = tags.id
LEFT JOIN scenes_files sf ON st.scene_id = sf.scene_id
LEFT JOIN files ON sf.file_id = files.id
LEFT JOIN folders ON files.parent_folder_id = folders.id
WHERE
tags.name = ?
"""
try:
cursor.execute(query, (tag,))
rows = cursor.fetchall()
if not rows:
print(f"No files found for tag '{tag}'.")
return
moved_files = 0
for row in rows:
original_path = row[0]
# Replace leading '/data' with '/Volumes/Tank/Stash'
if original_path.startswith('/data'):
file_path = original_path.replace('/data', '/Volumes/Tank/Stash', 1)
else:
print(f"⚠️ Path does not start with '/data': {original_path}")
continue
if not os.path.isfile(file_path):
print(f"⚠️ File not found: {file_path}")
continue
dest_path = os.path.join(destination_folder, os.path.basename(file_path))
if os.path.exists(dest_path):
print(f"⚠️ Destination file already exists: {dest_path}")
continue
try:
shutil.move(file_path, dest_path)
print(f"✅ Moved: {file_path} → {dest_path}")
moved_files += 1
except Exception as e:
print(f"❌ Error moving {file_path}: {e}")
print(f"\n✅ Done. {moved_files} file(s) moved.")
finally:
conn.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Move files from database paths to a destination folder.")
parser.add_argument("database", help="Path to the SQLite .db file")
parser.add_argument("tag", help="Tag name to filter files")
parser.add_argument("destination", help="Destination folder for moved files")
args = parser.parse_args()
move_files_from_db(args.database, args.tag, args.destination)
Beware that the script has a hardcoded location relative to my setup.
If anyone wants to turn this into a plugin, feel free to do it.