Loading Your MP3 Player

Problem

You have a collection of MP3 files that you would like to put in your MP3 player. But you have more music than can fit on your MP3 player. How can you load your player with music without having to baby-sit it by dragging and dropping files until it is full?

Solution

Use a shell script to keep track of the available space as it copies files onto the MP3 player, quitting when it is full.

 1 #!/usr/bin/env bash
 2 # cookbook filename: load_mp3
 3 # Fill up my mp3 player with as many songs as will fit.
 4 # N.B.: This assumes that the mp3 player is mounted on /media/mp3
 5 #
 6
 7 #
 8 # determine the size of a file
 9 #
10 function FILESIZE ()
11 {
12     FN=${1:-/dev/null}
13     if [[ -e $FN ]]
14     then
15         # FZ=$(ls -s $FN | cut -d ' ' -f 1)
16         set -- $(ls -s "$FN")
17         FZ=$1
18     fi
19 }
20
21 #
22 # compute the freespace on the mp3 player
23 #
24 function FREESPACE
25 {
26     # FREE=$(df /media/mp3 | awk '/^/dev/ {print $4}')
27     set -- $(df /media/mp3 | grep '^/dev/')
28     FREE=$4
29 }
30
31 # subtract the (given) filesize from the (global) freespace
32 function REDUCE ()
33 (( FREE-=${1:-0}))
34
35 #
36 # main:
37 #
38 let SUM=0
39 let COUNT=0
40 export FZ
41 export FREE
42 FREESPACE
43 find . -name '*.mp3' -print | 
44 (while read PATHNM
45 do
46     FILESIZE "$PATHNM"
47     if ((FZ <= FREE))
48     then
49         echo loading $PATHNM
50         cp "$PATHNM" /media/mp3
51         if (( $? == 0 ))
52         then
53             let SUM+=FZ
54             let COUNT++
55             REDUCE $FZ
56         else
57             echo "bad copy of $PATHNM to /media/mp3"
58             rm -f /media/mp3/$(basename "$PATHNM")
59             # recompute because we don't know how far it got
60             FREESPACE
61         fi
62         # any reason to go on?
63         if (( FREE <= 0 ))
64         then
65             break
66         fi
67     else
68         echo skipping $PATHNM
69     fi
70 done
71 printf "loaded %d songs (%d blocks)" $COUNT $SUM
72 printf " onto /media/mp3 (%d blocks free)
" $FREE
73 )
74 # end of script

Discussion

Invoke this script and it will copy any MP3 file that it finds from the current directory on down (toward the leaf nodes of the tree) onto an MP3 player (or other device) mounted on /media/mp3. The script will try to determine the freespace on the device before it begins its copying, and then it will subtract the disk size of copied items so as to know when to quit (i.e., when the device is full, or as full as we can get it).

The script is simple to invoke:

$ fillmp3

and then you can watch as it copies files, or you can go grab a cup of coffee—it depends on how fast your disk and your MP3 memory writes go.

Let’s look at some bash features used in this script, referencing them by line number.

Let’s start at line 35, after the opening comments and the function definitions. (We’ll return to the function definitions later.) The main body of the shell script starts by initializing some variables (lines 38–39) and exporting some variables so they will be available globally. At line 42 we call the FREESPACE function to determine how much free space is available on the MP3 player before we begin copying files.

Line 43 has the find command that will locate all the MP3 files (actually only those files whose names end in “.mp3”). This information is piped into a while loop that begins on line 44.

Why is the while loop wrapped inside of parentheses? The parentheses mean that the statements inside it will be run inside of a subshell. But what we’re concerned about here is that we group the while statement with the printf statements that follow (lines 71 and 72). Since each statement in a pipeline is run in its own subshell, and since the find pipes its output into the while loop, then none of the counting that we do inside the while loop will be available outside of that loop. By putting the while and the printfs inside of a subshell, they are now both executing in the same shell environment and can share variables.

Let’s look inside the while loop and see what it’s doing:

46 FILESIZE "$PATHNM"
47      if ((FZ <= FREE))
48      then
49          echo loading $PATHNM
50          cp "$PATHNM" /media/mp3
51          if (( $? == 0 ))
52          then

For each filename that it reads (from the find command’s output) it will use the FILESIZE function to determine the size of that file (see below for a discussion of that function). Then it checks (line 47) to see if the file is smaller than the remaining disk space, i.e., whether there is room for this file. If so, it will echo the filename so we can see what it’s doing and then it will copy (line 50) the file onto the MP3 player.

It’s important to check and see if the copy command completed successfully (line 51). The $? is the result of the previous command, so it represents the result of the the cp command. If the copy is successful, then we can deduct its size from the space available on the MP3 player. But if the copy failed, then we need to try to remove the copy (since, if it is there at all, it will be incomplete). We use the -f option on rm so as to avoid error messages if the file never got created. Then we recalculate the free space to be sure that we have the count right. (After all, the copy might have failed because somehow our estimate was wrong and we really are out of space.)

In the main part of the script, all three of our if statements (lines 47, 51, and 63) use the double parentheses around the expression. All three are numerical if statements, and we wanted to use the familiar operators (vis. <= and ==). These same if conditions could have been checked using the square bracket ([) form of the if statement, but then the operators would be -le and -eq. We do use a different form of the if statement in line 13, in the FILESIZE function. There we need to check the existence of the file (whose name is in the variable $FN). That is simple to write with the -e operator, but that is not available to the arithmetic-style if statement (i.e., when using parentheses instead of square brackets).

Speaking of arithmetic expressions, lets take a look at the REDUCE function and see what’s going on there:

32 function REDUCE ( )
33 (( FREE-=${1:-0}))

Most people write functions using curly braces to delimit the body of the function. However, in bash, any compound statement will work. In this case we chose the double parentheses of arithmetic evaluation, since that is all we need the function to do. Whatever value is supplied on the command line that invokes REDUCE will be the first (positional) parameter (i.e., $1). We simply subtract that value from $FREE to get the new value for $FREE. That is why we used the arithmetic expression syntax—so that we can use the -= operator.

While we are looking at the functions, let’s look at two lines in the FILESIZE function. Take a close look at these lines:

16    set -- $(ls -s "$FN")
17    FZ=$1

There is a lot going on in those few characters. First, the ls command is run inside of a subshell (the $() construct). The -s option on ls gives us the size, in blocks, of the file along with the file name. The output of the command is returned as words on the command line for the set command. The purpose of the set command here is to parse the words of the ls output. Now there are lots of ways we could do that, but this approach is a useful technique to remember.

The set -- will take the remaining words on the command line and make them the new positional parameters. If you write set --this is a test, then $1 is this and $3 is a. The previous values for $1, $2, etc are lost, but in line 12 we saved into $FN the only parameter that gets passed in to this function. Having done so, we are free to reuse the positional parameters, and we use them by having the shell do the parsing for us. We can then get at the file size as $1, as you see in line 17. (By the way, in this case, since this is inside a function, it is only the function’s positional parameters that are changed, not those from the invoking of the script.)

We use this technique of having the shell do our parsing for us, again on line 27 in the other function:

27       set -- $(df /media/mp3 | grep '^/dev/')
28       FREE=$4

The output of the df command will report on the size, in blocks, available on the device. We pipe the output through grep, since we only want the one line with our device’s information and we don’t want the heading line that df produces. Once bash has set our arguments, we can grab the free space on the device as $4.

The comment on line 26 shows an alternative way to parse the output of the df command. We could just pipe the output into awk and let it parse the output from df for us:

26       # FREE=$(df /media/mp3 | awk '/^/dev/ {print $4}')

By using the expression in slashes, we tell awk to pay attention only to lines with a leading /dev. (The caret anchors the search to the beginning of the line and the back-slash escapes the meaning of the slash, so as not to end the search expression at that point and to include a slash as the first character to find.)

So which approach to use? They both involve invoking an external program, in one case grep and in the other awk. There are usually several ways to accomplish the same thing (in bash as in life), so the choice is yours. In our experience, it usually comes down to which one you think of first.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.23.101.60