Funscript Matching/Renaming/Sorting application

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.


  • Install python
  • Copy the script below into a new file called “”
  • 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” from the folder you put the file (:bangbang: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

        "folder": "C:/Users/bounce/Downloads/",
        "action": "move"
        "folder": "G:/library/PMV/",
        "action": "symlink"
        "folder": "G:/library/VR/",
        "action": "symlink"

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)

    # 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"]
    ## Perform delete action, reset states
    if st.session_state["delete_confirm_btn"]: 
        st.session_state["delete_btn"] = not st.session_state["delete_btn"]
        st.session_state["delete_confirm_btn"] = not st.session_state["delete_confirm_btn"]

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"):
    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):
            os.remove( funscript_file)
        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):
    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)))
    if mp4_source_folder['action'] == 'symlink':
        renamed_funscript_path = selected_funscript_path.replace(
        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))) 
if __name__ == "__main__":

moved to Software

1 Like

Nice idea

1 Like