1

I want to copy mp3 files from a directory to a flash drive. They are stored in a directory structure such as this: Artist/Album/Track.mp3. Since my car's mp3 player sucks, I want to have all my mp3 files in the root directory of the flash drive, with the filename in the format Artist-Album-Track.mp3. How can I copy files from the directory to the flash drive, whilst adding the path into the filename?

user394
  • 14,404
  • 21
  • 67
  • 93

4 Answers4

2

A simple for loop could do it, with parameter expansions to extract the various elements:

for file in */*/*.mp3;
do
  artist=${file%%/*}
  rest=${file#*/}
  album=${rest%%/*}
  track=${rest#*/*}
  cp -- "$file" /flash/drive/"${artist}-${album}-${track}"
done

The first expansion strips everything from the end of the file up to and through the first forward-slash -- generating the artist.

The second expansion strips the leading characters up to and through the first forward-slash -- stripping the artist off of the path.

The third expansion is like the first, stripping the filename off of the remaining path, leaving the album.

The fourth expansion is like the second, stripping the album off, leaving just the filename.

Then we piece it all back together with dashes and cp it to the desired /flash/drive path.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
1

I believe it is a bad practice to manipulate multiple strings with shell parameter expansions and variable assignments.

root_dir="~/songs"
for fn in */*/*.mp3  
do 
   cp -v "$fn" "${root_dir}/${fn////-}"    # replace all slashes by dashes
done

Output:

~/songs/Artist-Album-Track.mp3
1

Using find and bash to copy the files out of $mp3dir to $destdir:

mp3dir="$HOME/my_music"
destdir="/mnt/my_mp3_player"

find "$mp3dir" -type f -name '*.mp3' -exec bash -c '
    mp3dir=$1; destdir=$2; shift 2
    for pathname do
        dest=${pathname#$mp3dir/}
        dest="$destdir/${dest//\//-}"
        if [ -f "$dest" ]; then
            printf "%s exist, skipping %s\n" "$dest" "$pathname" >&2
        else
            cp "$pathname" "$dest"
        fi
    done' bash "$mp3dir" "$destdir" {} +

This looks for any regular file in or below $mp3dir whose names end with .mp3. For batches of these, a short script is executed. The couple of parameters to the script is $mp3dir and $destdir and the rest are pathnames of MP3 files.

The script picks out two directory names from the first two command line arguments and then loops over the rest of the arguments, constructing the destination pathname as $dest for each MP3 file. This is done using a bash parameter substitution that replaces all slashes in the pathname with dashes after removing the initial $mp3dir bit of the pathname.

If $dest already exists, a message about this is printed, otherwise the file is copied.

Related:

Kusalananda
  • 333,661
0

Background

I was curious if I could make a solution that achieves two goals:

  • easier to read
  • easier to debug

Building off of @kusalananda's wonderful answer to this other U&L Q&A titled: Understanding the -exec option of find, I think I've come up with something that solves your issue.

To start here's my sample directory structure that mimics yours.

$ mkdir -p Artist{1..5}/Album{1..5}
$ touch Artist{1..5}/Album{1..5}/Track{1..5}.mp3

This results in the following:

$ find Artist* -type f | head
Artist1/Album1/Track1.mp3
Artist1/Album1/Track2.mp3
Artist1/Album1/Track3.mp3
Artist1/Album1/Track4.mp3
Artist1/Album1/Track5.mp3
Artist1/Album2/Track1.mp3
Artist1/Album2/Track2.mp3
Artist1/Album2/Track3.mp3
Artist1/Album2/Track4.mp3
Artist1/Album2/Track5.mp3

Now to copy your mp3 files into ./targetDir:

$ find . -type f -name '*.mp3' -exec sh -c \
    'cp {} targetDir/$(echo "{}" | sed "s#\./##g;s#/#-#g")' \;

Which results in this:

$ ls targetDir/ | head
Artist1-Album1-Track1.mp3
Artist1-Album1-Track2.mp3
Artist1-Album1-Track3.mp3
Artist1-Album1-Track4.mp3
Artist1-Album1-Track5.mp3
Artist1-Album2-Track1.mp3
Artist1-Album2-Track2.mp3
Artist1-Album2-Track3.mp3
Artist1-Album2-Track4.mp3
Artist1-Album2-Track5.mp3

I like this approach, because I can wrap the cp ... command in a echo first so I can verify the output before committing to doing the work:

$ find . -type f -name '*.mp3' -exec sh -c \
    'echo "cp {} targetDir/$(echo "{}" | sed "s#\./##g;s#/#-#g")"' \;
cp ./Artist1/Album1/Track1.mp3 targetDir/Artist1-Album1-Track1.mp3
cp ./Artist1/Album1/Track2.mp3 targetDir/Artist1-Album1-Track2.mp3
cp ./Artist1/Album1/Track3.mp3 targetDir/Artist1-Album1-Track3.mp3
cp ./Artist1/Album1/Track4.mp3 targetDir/Artist1-Album1-Track4.mp3
cp ./Artist1/Album1/Track5.mp3 targetDir/Artist1-Album1-Track5.mp3
cp ./Artist1/Album2/Track1.mp3 targetDir/Artist1-Album2-Track1.mp3
cp ./Artist1/Album2/Track2.mp3 targetDir/Artist1-Album2-Track2.mp3
cp ./Artist1/Album2/Track3.mp3 targetDir/Artist1-Album2-Track3.mp3
cp ./Artist1/Album2/Track4.mp3 targetDir/Artist1-Album2-Track4.mp3
...

How it works

This solution takes the output of find and then runs the following shell command:

cp {} targetDir/$(echo "{}" | sed "s#\./##g;s#/#-#g")'

This will cp the file from Artist../Album../Track.. to targetDir/.. and reformulate the name so that it has dashes (-) where ever there's a forward slash (/).

NOTE: I added 2 operations to the sed. The first strips any prefix ./ that may exist if you use find . ... instead of find Artist* ...

Alternatives?

I think doing this using find and cp is still not the ideal solution. I too maintain directories of MP3 files and I think doing something like this with rsync and through lists that you provide rsync might be a more useful implementation, longer term since you can use it to update, rather than re-copy each time you run it.

$ man rsync
...
        --files-from=FILE       read list of source-file names from FILE

Just a thought.

slm
  • 369,824