Get Random File From Folder, Including Subfolders

I found this, but can't get it to do what I want. For starters, I can only get it to return the names of subfolders and not actual files.

What I'm trying to do is create a simple "change desktop images for 2 monitors every x minutes" macro. The only thing missing from Big Sur's built-in randomizer is that it can't go into subfolders, and unfortunately, this is way too full-featured and too hard to figure out. , That's why I'm trying to roll my own.

As a proof of concept, this works if I hardcode a random file:
Screen Shot 2021-11-30 at 2.27.27 PM

So I just need a way to grab a random file from a selected folder and/or its subfolders and feed the path/file name into a variable and I think I'm set.

Can someone help me with this?

That's a clear request and should be easy, but I need a few minutes to remember the parameters for the "ls" command.

Okay I see the following approach: (using "find" instead of "ls" since that seemed easier to remember)

  1. We will need to use the Execute Shell Script action to execute a short Shell script and store the result into a variable. That will be your final result.
  2. We will generate the list of all files with their full paths using the "find /mypath -print" command
  3. We will choose a random item by sorting the results of the above into "sort -R"
  4. We will pick the first one of the above list by piping it into "head -n 1"

I think that should work fine. I'm not sure if you need help putting those pieces together.

Thank you! Not only does your solution work, but it's lightning fast!

You are very welcome.

It's fast for small subdirectories, but if you try it on large subdirectories like the root directory or even your home directory it could take a while.

I'm glad you were able to take my tips and run with it.

Hey Guys,

Just for fun I did this entirely with Keyboard Maestro native actions.

It was much tougher (for me) to get the job done without scripting, but it wasn't ridiculously hard.

-Chris


Return a Random File Image Path from a Directory Stack v1.00.kmmacros (9.0 KB)

1 Like

@ccstone Thanks! I tried your method too, but for some reason, it's really slow. For around 10k files, it takes well over a minute.

Shell scripts tend to be very fast. They regularly amaze me with their speed.

Zowie! That's atrociously slow...

Here's some AppleScript that'll do the job in less than a second on my old hardware.

--------------------------------------------------------
# Auth: Christopher Stone
# dCre: 2017/03/23 22:04
# dMod: 2021/12/04 20:26
# Appl: AppleScriptObjC
# Task: Recursively extract all file and folder paths from the target folder path.
#     : Return a random file path from the path list.
# Libs: None
# Osax: None
# Tags: @Applescript, @Script, @BBEdit, @Recursively, @Extract, @File, @Folder, @Paths, @Target, @Folder, @Random
--------------------------------------------------------
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
--------------------------------------------------------

### USER SETTING ###

# Get the POSIX Path of the Front Finder Window
# Replace with your preferred folder path.

tell application "Finder" to set folderToSearchPath to POSIX path of (insertion location as alias)

--------------------------------------------------------

# Convert Tilde-Path to full POSIX Path if needed.
set folderToSearchPath to ((current application's NSString's stringWithString:folderToSearchPath)'s ¬
   stringByExpandingTildeInPath) as text

# Classes, constants, and enums used
set curApp to current application
set NSDirectoryEnumerationSkipsHiddenFiles to a reference to 4
set NSFileManager to a reference to curApp's NSFileManager
set NSDirectoryEnumerationSkipsPackageDescendants to a reference to 2

# Create NSString from Path String
set nsPath to curApp's NSString's stringWithString:folderToSearchPath
# Expand Tilde & Symlink (If Any Exist)
set nsPath to nsPath's stringByResolvingSymlinksInPath()

# Gets the NSURL, Whether or Not the File/Folder Exists
set folderNSURL to curApp's |NSURL|'s fileURLWithPath:nsPath

# Create NSArray of File URLs
set theURLs to (NSFileManager's defaultManager()'s enumeratorAtURL:folderNSURL includingPropertiesForKeys:{} ¬
   options:(NSDirectoryEnumerationSkipsPackageDescendants + ¬
   (get NSDirectoryEnumerationSkipsHiddenFiles)) errorHandler:(missing value))'s allObjects()
# Map NSURLs to their Path Strings and return as a list
set itemPathList to ((theURLs's valueForKey:"path") as list)

set itemPathCount to length of itemPathList
set randomFilePath to item (random number from 1 to itemPathCount) of itemPathList

--------------------------------------------------------

@peternlewis...

I ran my macro from post #5 on a folder with about 12,500 files in it.

It took about 3.5 minutes.

[Edit: The Streamlined version below only takes about 25 seconds.]

On testing it seems the bottleneck is iterating through the path list.

If I do this in AppleScript and then iterate through the list of paths it only takes 0.5 seconds to return the list and about 7 seconds to iterate through them and copy them to another list.

Is it reasonable for Keyboard Maestro to take 3.5 minutes – or have we uncovered a problem?

-Chris

1 Like

Hey Guys,

Okay, I streamlined the macro as much as I could without scripting and got the time down to ~ 26 seconds on a folder with 12284 items in it.

I used local variables to prevent cruft and/or accidental data accumulation.

I'm disappointed with the speed, but it's waaay better than 3.5 minutes...

-Chris


Return a Random File Image Path from a Directory Stack v1.00.kmmacros (13 KB)

1 Like

Keyboard Maestro runs actions at 1000 actions per second. So to scan a folder with 10,000 items using a For Each action will take at a minimum 10 seconds per action within the For Each action.

Keyboard Maestro is not designed for high speed use, if you want to process a large number of files, use a script. If you want to scan a folder and find a specific set of subfiles, perhaps use a script that uses the find command. Similarly for lines, use the grep command.

Once you have whittled down the list to a manageable number, then use Keyboard Maestro actions to process them.

2 Likes

That seems like a very round number. So I tried it manually on my M1 Mini with the following macro and it reported 1002 actions/sec. That's so close to 1000 we might have to change its name to Kilohertz Maestro.

image

2 Likes

FWIW, I stuck with Sleepy's original shell script because of the combination of speed and simplicity. Plus I have it running just once a day because the file listing is saved to a variable and I don't add new images to the folder/subfolders that often, so I can randomize it throughout the day to get new desktop images. Everything's working beautifully.

1 Like

Would you mind sharing the script for those of us still working on a similar problem in 2024? :thinking::sweat_smile: tyvm

What are you trying to do? Perhaps this'll get you started -- an easy way to get the path to a random PNG, starting on the Desktop and including all subfolders (and sub-sub-folders, etc).

set theScript to "#!/bin/zsh
	ls ~/Desktop/**/*.png"
tell application "Finder"
	set theResult to every paragraph of (do shell script theScript)
	return item (random number from 1 to count of theResult) of theResult
end tell
1 Like

I split this up into two macros:

  1. On reboot, this KM runs this shell script and saves the results to variable desktopPicsFolderList

find (path to folder) -type f -print

  1. I have another macro that runs every 15 mins, and executes another Shell Script. It takes the variable desktopPicsFolderList and runs...

sort -R | head -n 1

...to randomize the list and pick the top item. The Execute Shell Script also saves the results to another variable local_desktopPicToShow1

Then that macro just Sets the Desktop Image to that local variable.

That's it!

(Actually, my Macro is a bit more complex because it's figuring out how many monitors I'm attached to, etc., but this is the gist)

1 Like

My end goal is to run a macro and pull a random file from my "Weird Library" full of every strange PDF i've found or newsletters I've printed to skim later, etc. For instance, I'd like to supply the path "/Volumes/Trove/weird library/" which has any number of subfolders, and return the path of a random file from any depth.

So something like this, which gets the listing via the shell but pulls a random element using pseudo-array techniques:

Open Random PDF.kmmacros (2.5 KB)

Thinking about it -- even that is one action too many!

Open Random PDF v2.kmmacros (2.2 KB)

Yeah, you could get down to one action by doing it all in a shell script or AppleScript action -- but that would be cheating :wink:

2 Likes

This is perfect, tyvm! I am working on adapting it to other file types and directories by variable, but I am having a blast pushing the button over and over and getting something new. tyvm.

1 Like