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()