How Do I Copy a Specific Number of Files

I have an old Subaru that accepts a USB drive to play music. The limitation is that it can only handle a maximum of 256 files per folder, with a maximum of 256 folder.

I want to copy a random selection of 256 mp3 files from a master folder, then create & name a folder and then copy 256 files to this new folder. It should continue creating new folders and copying files until all have been copied.

Any direction or help figuring this out would be greatly appreciated

Do you have any coding experience?

This could be done in KM but it would probably be easier to write it in a scripting language like Ruby or Python.

It would look something like this:

Input:
Source path (master folder)
Destination path (thumb drive)

Algorithm

  1. Use the Source path to put all mp3 files in the master folder into an array

  2. If there are files left, randomly select a file path and remove it from the array; otherwise stop execution

  3. Get the next destination folder location
    .
    --Get the highest folder number available.
    ----If no folder exists then create the '1' folder and return it as the next destination folder
    .
    --Get the mp3 count of the highest folder number
    ----If less than or equal to 255 then return that folder as the next destination folder
    ------If equal to 256 and there are less than or equal to 255 folders, the create a new folder (highest folder number + 1) and return it as the next destination folder
    ------If equal to 256 and there are 256 folders, stop execution

  4. Copy the randomly selected file (#2) to the next destination folder (#3)

  5. Repeat 2-4 until the script stops

@Potts_Jeff, a couple of questions...

Do you actually want a random selection, ie where a file could be copied over multiple times, or "randomly choose from the files that haven't yet been chosen" so any file is copied a maximum of 1 time?

Is your source folder a single container of only music files, or are there sub-folders and/or other files to take account of?

The source folder is flat, i.e. no subfolders. The idea is to only copy a file once and I assume that tagging it would exclude it from the next iteration of the selection process.

Does that follow ?

Each file can be paired, on each run of the script, with a random numeric value, and the list of pairs can be sorted by the numeric value in each.

My last coding experience is over 40 yrs old! That's why I love KM. I suspect that removing the random function would make this a lot easier.

Honestly the random factor is an easy part with built-in random functions. Keeping track of folder and file count is more challenging but doable

@ComplexPoint:

Each file can be paired, on each run of the script, with a random numeric value

I've been bitten before by assuming what someone meant by "random", so it's a bit of a reflex to check every time the word is used!

Given an unchanging folder, I believe an AppleScript 'tell Finder"' block will return the same file every time you "get item n of theFolder". Since @Potts_Jeff has a single folder of files, that suggests the following could work:

Generate an "index list", {1,2,3..number_of_items_in_folder}
Set a target folder
Repeat 250 times: Pick a random item of the index list, copy the contained number item from the source to target folders, remove the item from the index list.
New target folder and repeat.

So with each pick you are removing the source item from the index, ensuring it can't be copied again.

AppleScript can get a bit slow when cutting and joining large lists, so it might be better to do the "removal" by setting the chosen item to a "wrong" value, eg an empty string, so the list length remains the same but you keep generating random numbers until you get a valid one.

Thoughts?

Perhaps something like this:

Music files copied to up to 256 folders of 256 files each.kmmacros (15 KB)

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Music files in source folder randomly grouped into
    // at most N groups, each of a maximum size N,
    // and each group copied out to a separate sub-folder
    // in a target folder.

    // File paths specified in Keyboard Maestro variables:
    // - "musicFilesSourceFolder"
    // - "musicGroupsOutputFolder"

    // N.B.
    // Make sure that all the music file extensions you want to
    // copy are included, with their dot prefix, in the
    // `extensions` variable below.

    // Rob Trew @2022
    // Ver 0.02

    // main :: IO ()
    const main = () => {
        // Including dot prefix
        const extensions = [".mp3", ".aac"];

        const
            kme = Application("Keyboard Maestro Engine"),
            kmVar = kme.getvariable;

        const
            fpInFolder = filePath(
                kmVar("musicFilesSourceFolder")
            ),
            fpOutFolder = filePath(
                kmVar("musicGroupsOutputFolder")
            ),
            n = 256;

        return either(
            alert("Problem creating folders")
        )(
            report => report
        )(
            bindLR(
                uptoNRandomGroupsOfNFilesFromFolderLR(n)(
                    extensions
                )(fpInFolder)
            )(
                randomGroups => doesDirectoryExist(
                    fpOutFolder
                ) ? Left(
                    [
                        `Output folder already exists:\n\n\t${fpOutFolder}`,
                        "Specify a different output folder path ?"
                    ]
                    .join("\n\n")
                ) : bindLR(
                    createDirectoryIfMissingLR(true)(
                        fpOutFolder
                    )
                )(
                    fileGroupsCopiedToFolderLR(fpInFolder)(
                        randomGroups
                    )
                )
            )
        );
    };


    // --- GROUPING FILES INTO FOLDERS OF LIMITED SIZE ---

    // uptoNRandomGroupsOfNFilesFromFolderLR :: Int -> [String] ->
    // FilePath -> Either String [FileName]
    const uptoNRandomGroupsOfNFilesFromFolderLR = n =>
        // Either a message, if the source folder is not
        // found, or the names of up to n^2 files with the
        // given (dot-prefixed) extensions in that folder,
        // randomly selected and ordered, and  grouped into
        // at most N sub-lists, each of maximum length N.
        extensions => fpFolder => doesDirectoryExist(
            fpFolder
        ) ? Right(
            chunksOf(n)(
                take(n ** 2)(
                    sortOn(Math.random)(
                        listDirectory(fpFolder)
                        .filter(
                            s => extensions.includes(
                                takeExtension(s)
                            )
                        )
                    )
                )
            )
        ) : Left(`Folder not found: ${fpFolder}`);


    // fileGroupsCopiedToFolderLR ::
    const fileGroupsCopiedToFolderLR = fpSourceFolder =>
        randomGroups => fpOutFolder => {
            const
                nGroups = randomGroups.length,
                w = `${nGroups - 1}`.length,
                [problems, successes] = partitionEithers(
                    randomGroups.flatMap(
                        (fileGroup, i) => bindLR(
                            createDirectoryIfMissingLR(false)(
                                combine(fpOutFolder)(
                                    `group${i.toString().padStart(w, "0")}`
                                )
                            )
                        )(
                            fpGroupFolder => fileGroup.map(
                                fileName => copyFileLR(
                                    combine(fpSourceFolder)(
                                        fileName
                                    )
                                )(
                                    combine(fpGroupFolder)(
                                        fileName
                                    )
                                )
                            )
                        )
                    )
                ),
                nFiles = successes.length;

            return Boolean(problems.length) ? (
                Left(problems.join("\n"))
            ) : Right(
                `${nFiles} files copied to ${nGroups} folders.`
            );
        };

    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };


    // --------------------- GENERIC ---------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => m.Left ? (
            m
        ) : mf(m.Right);


    // copyFileLR :: FilePath -> FilePath -> Either String IO ()
    const copyFileLR = fpFrom =>
        fpTo => {
            const fpTargetFolder = takeDirectory(fpTo);

            return doesFileExist(fpFrom) ? (
                doesDirectoryExist(fpTargetFolder) ? (() => {
                    const
                        e = $(),
                        blnCopied = ObjC.unwrap(
                            $.NSFileManager.defaultManager
                            .copyItemAtPathToPathError(
                                $(fpFrom).stringByStandardizingPath,
                                $(fpTo).stringByStandardizingPath,
                                e
                            )
                        );

                    return blnCopied ? (
                        Right(fpTo)
                    ) : Left(ObjC.unwrap(e.localizedDescription));

                })() : Left(
                    `Target folder not found: ${fpTargetFolder}`
                )
            ) : Left(`Source file not found: ${fpFrom}`);
        };


    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && !ref[0];
    };


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // chunksOf :: Int -> [a] -> [[a]]
    const chunksOf = n => {
        // xs split into sublists of length n.
        // The last sublist will be short if n
        // does not evenly divide the length of xs .
        const go = xs => {
            const chunk = xs.slice(0, n);

            return Boolean(chunk.length) ? [
                chunk, ...go(xs.slice(n))
            ] : [];
        };

        return go;
    };


    // combine (</>) :: FilePath -> FilePath -> FilePath
    const combine = fp =>
        // Two paths combined with a path separator.
        // Just the second path if that starts with
        // a path separator.
        fp1 => Boolean(fp) && Boolean(fp1) ? (
            "/" === fp1.slice(0, 1) ? (
                fp1
            ) : "/" === fp.slice(-1) ? (
                fp + fp1
            ) : `${fp}/${fp1}`
        ) : fp + fp1;


    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        // The ordering of f(x) and f(y) as a value
        // drawn from {-1, 0, 1}, representing {LT, EQ, GT}.
        x => y => {
            const
                a = f(x),
                b = f(y);

            return a < b ? -1 : (a > b ? 1 : 0);
        };


    // createDirectoryIfMissingLR :: Bool -> FilePath
    // -> Either String FilePath
    const createDirectoryIfMissingLR = blnParents =>
        dirPath => {
            const fp = filePath(dirPath);

            return doesPathExist(fp) ? (
                Right(fp)
            ) : (() => {
                const
                    e = $(),
                    blnOK = $.NSFileManager
                    .defaultManager[
                        "createDirectoryAtPath" + (
                            "WithIntermediateDirectories"
                        ) + "AttributesError"
                    ](fp, blnParents, void 0, e);

                return blnOK ? (
                    Right(fp)
                ) : Left(e.localizedDescription);
            })();
        };


    // doesDirectoryExist :: FilePath -> IO Bool
    const doesDirectoryExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && ref[0];
    };


    // doesPathExist :: FilePath -> IO Bool
    const doesPathExist = fp =>
        $.NSFileManager.defaultManager
        .fileExistsAtPath(
            $(fp).stringByStandardizingPath
        );


    // filePath :: String -> FilePath
    const filePath = s =>
        // The given file path with any tilde expanded
        // to the full user directory path.
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);


    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
        // A simple function lifted to one which applies
        // to a tuple, transforming only its first item.
        ([x, y]) => [f(x), y];


    // last :: [a] -> a
    const last = xs =>
        // The last item of a list.
        Boolean(xs.length) ? (
            xs.slice(-1)[0]
        ) : null;


    // listDirectory :: FilePath -> [FilePath]
    const listDirectory = fp =>
        ObjC.unwrap(
            $.NSFileManager.defaultManager
            .contentsOfDirectoryAtPathError(
                ObjC.wrap(fp)
                .stringByStandardizingPath,
                null
            ))
        .map(ObjC.unwrap);


    // partitionEithers :: [Either a b] -> ([a],[b])
    const partitionEithers = xs =>
        xs.reduce(
            (a, x) => (
                "Left" in x ? (
                    first(ys => [...ys, x.Left])
                ) : second(ys => [...ys, x.Right])
            )(a),
            [
                [],
                []
            ]
        );


    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
        // A function over a simple value lifted
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        ([x, y]) => [x, f(y)];


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        // A copy of xs sorted by the comparator function f.
        xs => xs.slice()
        .sort((a, b) => f(a)(b));


    // sortOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortOn = f =>
        // Equivalent to sortBy(comparing(f)), but with f(x)
        // evaluated only once for each x in xs.
        // ('Schwartzian' decorate-sort-undecorate).
        xs => sortBy(
            comparing(x => x[0])
        )(
            xs.map(x => [f(x), x])
        )
        .map(x => x[1]);


    // take :: Int -> [a] -> [a]
    // take :: Int -> String -> String
    const take = n =>
        // The first n elements of a list,
        // string of characters, or stream.
        xs => xs.slice(0, n);


    // takeDirectory :: FilePath -> FilePath
    const takeDirectory = fp =>
        // The directory component of a filepath.
        "" !== fp ? (
            (xs => xs.length > 0 ? xs.join("/") : ".")(
                fp.split("/").slice(0, -1)
            )
        ) : ".";


    // takeExtension :: FilePath -> String
    const takeExtension = fp => {
        const fn = last(fp.split("/"));

        return fn.includes(".") ? (
            `.${last(fn.split("."))}`
        ) : "";
    };

    // MAIN
    return main();
})();
2 Likes

Very nice! Does it control for no more than 256 folders as well?

Thanks – I think it should now (updated above to Ver 0.02), though I don't personally have that many sound files to test : -)

Starting with a core of:

sortOn(Math.random)(
    listDirectory(fpFolder)
    .filter(
        s => extensions.includes(
            takeExtension(s)
        )
    )
)

we take up to (N to the power of 2) files from the randomly sorted list, and then chunk:

chunksOf(n)(
    take(n ** 2)(
        sortOn(Math.random)(
            listDirectory(fpFolder)
            .filter(
                s => extensions.includes(
                    takeExtension(s)
                )
            )
        )
    )
)
Before and after

And here's my AppleScript version -- I won't pretend it's perfect, but it works! It can be run standalone from Script Editor, saved as an app and triggered by KM, or it wouldn't take much work to use it in a KM macro, feeding in the source and target folders.

It's a bit more generic in that it will duplicate all files in the source folder -- you could easily limit the selection to eg mp4 files using a "whose" clause at line 13. You can easily change max folders, max files per folder, and destination folder prefix in the properties. There's an ersatz "progress dialog" after every folder is filled -- you can keep those on screen for longer by increasing the "giving up after" value.

It does mute your output volume so you don't get spammed with a dozen or more "copy completed" Finder boings every second :slight_smile:

Because it's generic there's a simple way to test it for a folder of files:

Make a new folder called "Source" on your Desktop
Make a new folder called "Transfer" on your Desktop
In Terminal, type cd ~/Desktop/Source;touch test file_{0001..9999}.txt
If you've got time to spare you can increase the "9999" to "66000" to test your max folders boundary
Note: I think the leading 0s in line 3 above are a zsh thing -- if you are using bash then miss them out

You can then run the script, selecting "Source" and "Transfer" as appropriate.

Summary

property maxFiles : 256
property maxFolders : 256
property targetFolderPrefix : "Transfer-"

set startTime to get current date
set targetList to {}

tell application "Finder"
-- pick the folders
set sourceFolder to (choose folder with prompt "Choose source folder")
set targetFolder to (choose folder with prompt "Choose or create target folder")

set sourceList to every file of sourceFolder
set numberOfFiles to length of sourceList
set numberOfFolders to round (numberOfFiles / maxFiles) rounding up

-- set up target structure
if numberOfFolders > maxFolders then set numberOfFolders to maxFolders
set theCount to 1
repeat numberOfFolders times
set newFolder to (make new folder at targetFolder with properties {name:(targetFolderPrefix & text -3 thru -1 of ("00" & theCount))})
copy {fileCount:0, targetFolder:newFolder} to end of targetList
set theCount to theCount + 1
end repeat

display dialog "Target folders created" giving up after 2

-- do the copying
-- you'll get Finder copy-done noise every file if you don't mute sound output!
set volume with output muted
set copyStartTime to get current date

set folderCount to 1
set targetFolderBase to (targetFolder as text)
repeat while length of sourceList > maxFiles and folderCount ≤ maxFolders
set destinationFolder to (targetFolderBase & targetFolderPrefix & text -3 thru -1 of ("00" & folderCount) as alias)
repeat maxFiles times
set i to get random number from 1 to length of sourceList
duplicate item i of sourceList to destinationFolder
set sourceList to removeFromList(i, sourceList) of me
end repeat
-- create and display progress report after each complete folder
set timeLeft to round ((((current date) - copyStartTime) / (folderCount * maxFiles)) * (length of sourceList) / 60) rounding up
display dialog "" & maxFiles * folderCount & " files copied out of " & numberOfFiles & "

" & timeLeft & " minutes remaining" giving up after 2
set folderCount to folderCount + 1
end repeat

-- and finish off the remainder
set destinationFolder to (targetFolderBase & targetFolderPrefix & text -3 thru -1 of ("00" & folderCount) as alias)
repeat with eachFile in sourceList
duplicate eachFile to destinationFolder
end repeat

-- reset sound and send alert
set volume without output muted
beep
set timeTaken to (get (current date) - startTime)
set {takenMinutes, takenSeconds} to {timeTaken div 60, timeTaken mod 60}
display dialog "Done. Took " & takenMinutes & " minutes and " & takenSeconds & " seconds"
end tell

on removeFromList(index, aList)
if index = 1 then
return rest of aList
else if index = length of aList then
return items 1 thru -2 of aList
else
return (items 1 thru (index - 1) of aList) & (items (index + 1) thru -1 of aList)
end if
return aList
end removeFromList

1 Like

For completeness -- this variant completes roughly twice as fast as the previous because rather than copy one file at a time it collects (up to) 256 file references, uses those to create a Finder selection, and duplicates that selection of files to the appropriate target folder.

I tend to shy away from "selection-based" operations, especially in long-running scripts, in case I clumsily knock the mouse or try to wake the screen to see if the script had completed and so change the selection at exactly the wrong time. In this case the selection is probably only important for a second or two per repeat, so you may find the risk worth it.

Summary

property maxFiles : 256
property maxFolders : 256
property targetFolderPrefix : "Transfer-"

set startTime to get current date
set targetList to {}

tell application "Finder"
-- pick the folders
set sourceFolder to (choose folder with prompt "Choose source folder")
set targetFolder to (choose folder with prompt "Choose or create target folder")

set sourceList to every file of sourceFolder
set numberOfFiles to length of sourceList
set numberOfFolders to round (numberOfFiles / maxFiles) rounding up

-- set up target structure
if numberOfFolders > maxFolders then set numberOfFolders to maxFolders
set theCount to 1
repeat numberOfFolders times
set newFolder to (make new folder at targetFolder with properties {name:(targetFolderPrefix & text -3 thru -1 of ("00" & theCount))})
copy {fileCount:0, targetFolder:newFolder} to end of targetList
set theCount to theCount + 1
end repeat

display dialog "Target folders created" giving up after 2

-- do the copying
-- you'll get Finder copy-done noise every file if you don't mute sound output!
set volume with output muted
set copyStartTime to get current date

set folderCount to 1
set targetFolderBase to (targetFolder as text)
set sourceList to (every file of sourceFolder)
repeat while length of sourceList > maxFiles and folderCount ≤ maxFolders
set destinationFolder to (targetFolderBase & targetFolderPrefix & text -3 thru -1 of ("00" & folderCount) as alias)
set theSelection to {}
repeat maxFiles times
set i to get random number from 1 to length of sourceList
copy item i of sourceList to end of theSelection
set sourceList to removeFromList(i, sourceList) of me
end repeat
select every item in theSelection
duplicate the selection to destinationFolder
-- create and display progress report after each complete folder
set timeLeft to round ((((current date) - copyStartTime) / (folderCount * maxFiles)) * (length of sourceList) / 60) rounding up
display dialog "" & maxFiles * folderCount & " files copied out of " & numberOfFiles & "

" & timeLeft & " minutes remaining" giving up after 2
set folderCount to folderCount + 1
end repeat

-- and finish of the remainder
set destinationFolder to (targetFolderBase & targetFolderPrefix & text -3 thru -1 of ("00" & folderCount) as alias)
select every item in sourceList
duplicate the selection to destinationFolder

-- reset sound and send alert
set volume without output muted
beep
set timeTaken to (get (current date) - startTime)
set {takenMinutes, takenSeconds} to {timeTaken div 60, timeTaken mod 60}
display dialog "Done. Took " & takenMinutes & " minutes and " & takenSeconds & " seconds"
end tell

on removeFromList(index, aList)
if index = 1 then
return rest of aList
else if index = length of aList then
return items 1 thru -2 of aList
else
return (items 1 thru (index - 1) of aList) & (items (index + 1) thru -1 of aList)
end if
return aList
end removeFromList

Nige_S I send you a big load of gratitude for this solution. I would never have figured it out on my own, that's for sure. Once again thank you for such a fine solution.