4

I have a Music folder and the contents look like this:

Music
    -ArtistA
        -Song1.mp3
        -AlbumA
            -Song2.mp3
            -Song2.m4a
        -AlbumB
            -Song1.mp3
            -Song2.mp3
            -Song3.mp3
        -AlbumC
            -Song1.mp3
            -Song1.jpg
            -SomeFolder1
                -Song1.mp3
    -ArtistB
        -Song1.mp3
    -ArtistC
        -Song1.mp3
        -Song2.mp3
        -Song3.mp3
        -SomeFolder2
            -Song1.jpg
            -Song2.jpg
        -SomeFolder3


I am looking for output like this:

ArtistA/Song1.mp3
ArtistA/AlbumA/Song2.mp3
ArtistA/AlbumC/Song1.mp3
ArtistA/AlbumC/SomeFolder1/Song1.mp3
ArtistB/Song1.mp3



So, I don't want directories with only one file, I want directories with only one mp3 - regardless of subdirectories or other files. I also do not want empty directories, or directories which contain more than one mp3.

I am not opposed to a shell script for Linux. I don't want to compile a program on either OS, however.

I have tried: find . -type d -exec sh -c 'set -- "$0"/*.mp3; [ $# -le 1 ]' {} \; -print

but that gives me this output:

.
./ArtistB
./ArtistA
./ArtistA/AlbumA
./ArtistA/AlbumC
./ArtistA/AlbumC/SomeFolder1
./ArtistC/SomeFolder2
./ArtistC/SomeFolder3

which is close, but not exactly what I want. There are two empty folders and no filenames.

4 Answers 4

4

At first glance, the reason why your first command is reporting the empty folders (those with no *.mp3 files) is that you’re saying -le 1 rather than -eq 1.  This looks like it will report directories with one MP3 file or fewer.  But changing -le 1 to -eq 1 doesn’t work.  It turns out that the problem is, in a directory with ≥ one MP3 file, directory_name/*.mp3 gets you a list of the MP3 file names, but, in a directory with none, it remains, literally, as directory_name/*.mp3.  That can be fixed by telling the shell that wildcards that match no files should simply vanish from the command line:

find . -type d -exec
        sh -c 'shopt -s nullglob; set -- "$0"/*.mp3; [ $# -eq 1 ]' {} \; -print

(typed all as one line) yields

./ArtistA
./ArtistA/AlbumA
./ArtistA/AlbumC
./ArtistA/AlbumC/SomeFolder1
./ArtistB

But you’ve got the shell counting the files, and you’re asking find to do the printing – and find is looking only at directories.  Why not just let the shell print the filename that it sees?

find . -type d -exec
        sh -c 'shopt -s nullglob; set -- "$0"/*.mp3; [ $# -eq 1 ] && echo "$1"' {} \;

(i.e., if there is one file, echo the name of the first one ($1)) will give you what you want.

5

For Windows using PowerShell, assuming you're already in the folder C:\YOUR_PATH:

PS C:\YOUR_PATH> (Get-ChildItem -recurse | Where-Object {$_.name -match ".mp3"}) | Group-Object -Property Directory | Where-Object {$_.Count -eq 1} | ForEach-Object { (Get-ChildItem $_.Name -Filter "*.mp3").FullName}

Outputs:

C:\YOUR_PATH\ArtistA\Song1.mp3
C:\YOUR_PATH\ArtistA\AlbumA\Song2.mp3
C:\YOUR_PATH\ArtistA\AlbumC\Song1.mp3
C:\YOUR_PATH\ArtistA\AlbumC\SomeFolder1/Song1.mp3
C:\YOUR_PATH\ArtistB\Song1.mp3

Breaking down the piped command:

Get all files and folder underneath current directory

Get-ChildItem -recurse 

Match only the files based on your mask

Where-Object {$_.name -match ".mp3"})

Perform grouping based on Directory property

Group-Object -Property Directory 

Select only the directories with 1 item

Where-Object {$_.Count -eq 1} 

Extract the full name of the one file matching your filemask in each directory

ForEach-Object { (Get-ChildItem $_.Name -Filter "*.mp3").FullName} 
1

I'm sure someone has a better way of doing it, but this bash command will work on Linux.

for Folder in $(find . -name "*.mp3" -printf "%h\n")
do
    [[ $(ls -l $Folder/*.mp3 | wc -l) -eq "1" ]] && ls $Folder/*.mp3
done

This script performs a test on the folder of every single .mp3 file, repeating itself for each match on the same file. For instance, it checks Music/ArtistA/AlbumB 3 times. Depending on how many mp3 files you have, this could take a long time (you'd need a pretty huge library).

If so, you can make it slightly more complicated:

for Folder in $(find . -name "*.mp3" -printf "%h\n" | awk '!x[$0]++')
    do
        [[ $(ls -l $Folder/*.mp3 | wc -l) -eq "1" ]] && ls $Folder/*.mp3
    done

The result is the same, but it'll only check each folder once, which could save a lot of time. The awk command removes any duplicate Folder names.

Another note is that files are case sensitive on Linux, so this will totally ignore any .MP3 or .Mp3files. If you want it to match those, change all 3 instances of mp3 to [mM][pP]3

2
  • protip: to make find use case insensitive matches for filenames, you can use -iname <pattern> instead of -name <pattern>.
    – webmarc
    Oct 24, 2014 at 16:12
  • True, but it needs the regex in the ls commands anyway, so I thought I'd keep it consistent. Oct 24, 2014 at 16:13
0

Here's an answer:

find Music -name "*.mp3" -exec dirname {} \; |
    sort |
    uniq -c |
        while read count parent
        do
            if [[ $count -eq 1 ]]
            then
                echo "$parent"/*.mp3
            fi
        done
1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .