Hi! Managing, renaming and categorizing funscripts and videos can be a bit tedious. I have written a small streamlit (python) based application that streamlines the process. The interface is available in the browser.
What it does:
- Find funscript files and mp4 files in a list of potential source folders (i.e. your downloads folder)
- It matches the .funscript files to similarly named .mp4 files in the source folders
- It allows you to select a destination folder and the application will rename and move/symlink the files so they can be used by your application of choice.
installation:
- Install python
- Copy the script below into a new file called “app.py”
- install dependencies:
pip install streamlit
pip install fuzzywuzzy
pip install levenshtein
pip install glob
- edit the configuration at the top of the script to configure your source folders, destination folders and actions to be performed (either move or symlink).
- run the application using “streamlit run app.py” from the folder you put the app.py file (
if you want to use symlinks you need to run the app from an elevated terminal)
- your browser will open and you can use the app from there
import streamlit as st
from fuzzywuzzy import fuzz
import os
import shutil
import glob
SOURCE_FOLDERS = [
{
"folder": "C:/Users/bounce/Downloads/",
"action": "move"
},
{
"folder": "G:/library/PMV/",
"action": "symlink"
},
{
"folder": "G:/library/VR/",
"action": "symlink"
}
]
DESTINATION_FOLDERS = [
"G:/xtplayerlib/audio",
"G:/xtplayerlib/action",
"G:/xtplayerlib/vr"
]
MATCHING_PERCENTAGE_THRESHOLD = 60
def main():
st.title("Funscript Sorting App")
# Get the list of .funscript files from the downloads folder
funscripts = get_funscripts()
# Select a funscript from the list
selected_funscript = st.selectbox("Select a .funscript file", funscripts)
# Find matching mp4 files
matched_mp4_files = match_mp4_files(selected_funscript)
ordered_mp4_files = order_mp4_files(matched_mp4_files)
# Select one of the matched .mp4 files
selected_mp4_file = st.selectbox("Select a matched .mp4 file", ordered_mp4_files)
# Select destination folder
selected_folder = st.selectbox("Select a destination folder", DESTINATION_FOLDERS)
# Process button control flow
if st.button("Process Files"):
#process files
process_files(selected_funscript, selected_mp4_file, selected_folder)
st.experimental_rerun()
# Delete button control flow
## Set initial session state keys
if "delete_btn" not in st.session_state:
st.session_state["delete_btn"] = False
if "delete_confirm_btn" not in st.session_state:
st.session_state["delete_confirm_btn"] = False
## Delete funscript button clicked
if st.button("Delete .funscript"):
st.session_state["delete_btn"] = not st.session_state["delete_btn"]
## Initiate confirmation flow
if st.session_state["delete_btn"]:
st.text(f"Delete {selected_funscript}?")
if st.button("Confirm Deletion"):
st.session_state["delete_confirm_btn"] = not st.session_state["delete_confirm_btn"]
if st.button("Cancel"):
st.session_state["delete_btn"] = not st.session_state["delete_btn"]
st.experimental_rerun()
## Perform delete action, reset states
if st.session_state["delete_confirm_btn"]:
delete_funscript(selected_funscript)
st.session_state["delete_btn"] = not st.session_state["delete_btn"]
st.session_state["delete_confirm_btn"] = not st.session_state["delete_confirm_btn"]
st.experimental_rerun()
def get_funscripts():
# Get the list of .funscript files from source folders
funscripts = []
for folder in SOURCE_FOLDERS:
list_of_files = filter( os.path.isfile, glob.glob(folder['folder'] + '*') )
list_of_files = sorted( list_of_files, key = os.path.getmtime, reverse= True)
for filename in list_of_files:
if filename.endswith(".funscript"):
funscripts.append(filename)
return funscripts
def match_mp4_files(selected_funscript):
# Use fuzzywuzzy to match .mp4 files in source folders
matched_mp4_files = []
for folder in SOURCE_FOLDERS:
for filename in os.listdir(folder['folder']):
if filename.endswith(".mp4"):
matching_percentage = fuzz.ratio(selected_funscript, filename)
if matching_percentage>MATCHING_PERCENTAGE_THRESHOLD:
matched_mp4_files.append((os.path.join(folder['folder'],filename), matching_percentage))
return matched_mp4_files
def order_mp4_files(matched_mp4_files):
# Order the matched .mp4 files based on matching percentage
ordered_mp4_files = sorted(matched_mp4_files, key=lambda x: x[1], reverse=True)
return [mp4_file[0] for mp4_file in ordered_mp4_files]
def delete_funscript(funscript_file):
if (funscript_file):
try:
os.remove( funscript_file)
#st.experimental_rerun()
except OSError as e:
st.error(f"Failed with: {e.strerror} Error code: {e.code}")
def process_files(selected_funscript_path, selected_mp4_path, selected_destination_folder):
if(not selected_funscript_path or not selected_mp4_path or not selected_destination_folder):
return
mp4_source_folder = [folder for folder in SOURCE_FOLDERS if os.path.dirname(folder["folder"]) in os.path.dirname(selected_mp4_path)][0]
if mp4_source_folder['action'] == 'move':
renamed_mp4_path = selected_funscript_path.replace(".funscript", ".mp4")
os.rename(selected_mp4_path, renamed_mp4_path)
shutil.move(renamed_mp4_path, os.path.join(selected_destination_folder,os.path.basename(renamed_mp4_path)))
shutil.move(selected_funscript_path, os.path.join(selected_destination_folder,os.path.basename(selected_funscript_path)))
return
if mp4_source_folder['action'] == 'symlink':
renamed_funscript_path = selected_funscript_path.replace(
".".join(os.path.basename(selected_funscript_path).split('.')[:-1]),
".".join(os.path.basename(selected_mp4_path).split('.')[:-1])
)
os.rename(selected_funscript_path, renamed_funscript_path)
shutil.move(renamed_funscript_path, os.path.join(selected_destination_folder,os.path.basename(renamed_funscript_path)))
os.symlink(selected_mp4_path, os.path.join(selected_destination_folder, os.path.basename(selected_mp4_path)))
return
if __name__ == "__main__":
main()