Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base fork: ttencate/playnext
base: c88809a6a9
...
head fork: ttencate/playnext
compare: 0b0233c90c
  • 11 commits
  • 12 files changed
  • 0 commit comments
  • 1 contributor
22 LICENSE
View
@@ -0,0 +1,22 @@
+Copyright (c) 2013, Thomas ten Cate
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 README.md
View
@@ -2,11 +2,19 @@
`playnext` is a small shell script that helps you watch series, listen to podcasts, or basically do anything to a series of files sequentially.
-The basic operation is as follows: `cd` to a directory that contains some media files, and type:
+The basic operation is as follows: if `/some/directory` contains media files, type
- mplayer "`playnext`"
+ playnext /some/directory
-Instead of `mplayer`, you can of course use any media player or other tool of your choice. The `playnext` script will have remembered the filename of the last episode you played (if any), and output the next one. The backticks make the shell substitute this filename in that place.
+and it will play the first media file in the directory and its subdirectories, using `mplayer`. The next time you run it, it plays the second one, and so on.
+
+After you've once typed `/some/directory` in full, you can afterwards get by with just a substring of the final directory component, e.g. `directory`, `dir` or even `d`:
+
+ playnext dir
+
+As long as this uniquely identifies a previously used directory, it will work.
+
+Instead of `mplayer`, you can of course use any media player or other tool of your choice, using the `-c` option.
Caveats:
312 playnext
View
@@ -1,193 +1,261 @@
#!/bin/bash
-script_name=$(basename $0)
-progress_file=$HOME/.playnextrc
-previous=0
-advance=1
-list=0
-media_dir=.
-verbose=0
-current_episode=
+SCRIPT_NAME=$(basename $0)
+if [[ -t 1 ]]; then
+ DEFAULT_COMMAND=mplayer
+else
+ DEFAULT_COMMAND=echo
+fi
+DEFAULT_PROGRESS_FILE=.playnextrc
function print_usage() {
- echo "Usage: $script_name [OPTION]..."
+ # 12345678901234567890123456789012345678901234567890123456789012345678901234567890
+ echo "Usage: $SCRIPT_NAME [OPTION]... DIR"
+ echo
+ echo "Plays the next episode in the given directory."
+ echo
+ echo "If you previously played something from a particular directory, you can"
+ echo "supply any unique substring of that directory as the DIR argument, e.g.:"
echo
- echo "Outputs the file name of the next episode in the current directory, recursively."
- echo "Typically, you'd want to run this as:"
+ echo " $SCRIPT_NAME \"$HOME/My Wonderful Podcast\""
+ echo " $SCRIPT_NAME Wonder"
echo
- echo " cd /some/directory/with/media/files"
- echo " mplayer \`$script_name\`"
+ echo "If not connected to a terminal, prints the filename instead of playing it."
+ echo "This allows you to do things like:"
+ echo
+ echo " cp \"\`$SCRIPT_NAME\`\" /mnt/ipod"
+ echo
+ echo "History is tracked in the 'progress file' which is a plain text file listing one"
+ echo "absolute path name per line for each previously played episode."
echo
echo "Options:"
- echo " -d directory print files from given directory; defaults to current directory"
- echo " -e filename output given episode and remember this"
- echo " -f filename configuration file name; defaults to $progress_file"
- echo " -h show this help"
- echo " -l print the list of last episodes played"
- echo " -n don't advance; next run will print same output"
- echo " -p print previously outputted episode"
- echo " -v verbose mode; useful for debugging"
+ echo " -c, --command=command command to use to play file (default: mplayer"
+ echo " if connected to a tty, echo otherwise)"
+ echo " -e, --episode=filename play given episode and remember this"
+ echo " -f, --progress-file=filename progress file name (default: ~/$DEFAULT_PROGRESS_FILE)"
+ echo " -h, --help show this help"
+ echo " -l, --list print the list of last episodes played"
+ echo " -n, --next skip episode (can be repeated)"
+ echo " -p, --previous play previous episode (can be repeated)"
+ echo " -v, --verbose verbose mode; useful for debugging"
}
function print_verbose() {
if (( verbose )); then
- echo "$script_name: $@" >&2
+ echo "$SCRIPT_NAME: $@" >&2
fi
}
function print_error() {
- echo "$script_name: $@" >&2
+ echo "$SCRIPT_NAME: $@" >&2
+}
+
+function exit_error() {
+ print_error "$@"
+ exit 1
}
function read_progress_file() {
print_verbose "Reading progress file ${progress_file}..."
- while read; do
- if [[ $REPLY == $media_dir* ]]; then
+ IFS=$'\n' progress=( $(< "$progress_file") )
+}
+
+function find_media_dir() {
+ local matches=( )
+ for episode in "${progress[@]}"; do
+ if [[ $episode == *$media_dir* ]]; then
+ matches+=( $episode )
+ fi
+ done
+
+ if (( ${#matches[@]} == 0 )); then
+ exit_error "$media_dir is neither an existing directory nor a substring of a previously used directory"
+ fi
+ if (( ${#matches[@]} > 1 )); then
+ local message="'$media_dir' is not unique; did you mean any of the following?"
+ for match in "${matches[@]}"; do message="$message"$'\n'" $match"; done
+ exit_error "$message"
+ fi
+
+ local new_media_dir="${matches[0]}"
+ while [[ ${new_media_dir%/*} == *$media_dir* ]]; do
+ new_media_dir="${new_media_dir%/*}"
+ done
+
+ media_dir="$new_media_dir"
+}
+
+function find_previous_episode() {
+ for episode in "${progress[@]}"; do
+ if [[ $episode == $media_dir* ]]; then
if [[ -z $previous_episode ]]; then
- previous_episode="$REPLY"
- print_verbose "Previous episode: ${previous_episode#$media_dir/}"
+ previous_episode="$episode"
+ print_verbose "Previous episode: $previous_episode"
else
- print_error "Multiple previous episodes remembered for $media_dir: at least $previous_episode and $REPLY. Use -e to specify episode"
- return 1
+ exit_error "Multiple previous episodes remembered for $media_dir: at least $previous_episode and $REPLY. Use -e to specify episode"
fi
fi
- done < "$progress_file"
- echo "$previous_episode"
+ done
}
function write_progress_file() {
local current_episode="$1"
- local progress_file_tmp="$(mktemp --tmpdir="$(dirname "$progress_file")" playnextrc.XXXXX)"
- print_verbose "Writing new progress file to $progress_file_tmp..."
- while read; do
- if [[ $REPLY == $media_dir* ]]; then
- echo "$REPLY"
+ print_verbose "Writing new progress file to $progress_file..."
+ (
+ for episode in "${progress[@]}"; do
+ if [[ $episode != $media_dir* ]]; then
+ echo "$episode"
+ fi
+ done
+ if [[ ! -z $current_episode ]]; then
+ echo "$current_episode"
fi
- done < "$progress_file" > "$progress_file_tmp"
- if [[ ! -z $current_episode ]]; then
- echo "$current_episode" > "$progress_file_tmp"
- fi
- print_verbose "Moving $progress_file_tmp into place..."
- mv "$progress_file_tmp" "$progress_file"
+ ) > "$progress_file"
}
function find_next_episode() {
- local previous_episode="$1"
- local use_next=0
- local first=1
- while read; do
- if (( first )); then
- first=0
- first_episode="$REPLY"
- fi
- if (( use_next )); then
- use_next=0
- current_episode="$REPLY"
- break
- fi
- if [[ $REPLY == $previous_episode ]]; then
- if (( previous )); then
- current_episode="$REPLY"
+ IFS=$'\n' episodes=( $(find "$media_dir" -type f ! -path "*/.*" | sort -f -d) )
+ num_episodes=${#episodes[@]}
+ print_verbose "Found $num_episodes episodes in $media_dir"
+
+ if (( $num_episodes == 0 )); then
+ exit_error "No episodes found in $media_dir"
+ fi
+
+ local previous_index
+ if [[ -z $previous_episode ]]; then
+ print_verbose "No previous episode found; starting from first"
+ previous_index=-1
+ else
+ for (( i = 0; i < ${#episodes[@]}; i++ )); do
+ if [[ ${episodes[$i]} == $previous_episode ]]; then
+ previous_index=$i
break
- else
- use_next=1
+ return
fi
- fi
- done < <(find "$media_dir" -type f ! -path "*/.*" | sort -f -d)
- if (( use_next )); then
- print_error "No more episodes after ${previous_episode#$media_dir/}"
- return 1
+ done
fi
- if [[ -z $first_episode ]]; then
- print_error "No episodes found in ${media_dir}"
- return 1
+
+ if [[ -z previous_index ]]; then
+ exit_error "Previous episode ${previous_episode#$media_dir/} not found; use -e to specify episode"
fi
- if [[ -z $current_episode ]]; then
- if [[ -z $previous_episode ]]; then
- print_verbose "No previous episode found; starting from first"
- echo "$first_episode"
- else
- print_error "Previous episode ${previous_episode#$media_dir/} not found; use -e to specify episode"
- return 1
- fi
- else
- echo "$current_episode"
+
+ local current_index=$(( previous_index + offset ))
+ if (( current_index < 0 )); then
+ exit_error "No more episodes before ${episodes[0]#$media_dir/}"
fi
+ if (( current_index >= $num_episodes )); then
+ exit_error "No more episodes after ${episodes[$(( num_episodes - 1 ))]#$media_dir/}"
+ fi
+
+ current_episode="${episodes[$current_index]}"
+}
+
+function usage_error() {
+ print_usage
+ print_error "$1"
+ exit 1
+}
+
+function require_argument() {
+ [[ -z ${2+x} ]] && print_usage && print_error "Option $1 requires an argument" && exit 1
}
-while getopts ":d:e:hf:lnpv" opt; do
- case $opt in
- d)
- media_dir="$OPTARG"
+while (( $# > 0 )); do
+ case $1 in
+ -c | --command)
+ require_argument "$@"
+ command="$2"
+ shift
+ ;;
+ -d | --directory)
+ require_argument "$@"
+ media_dir="$2"
+ shift
;;
- e)
- current_episode="$OPTARG"
+ -e | --episode)
+ require_argument "$@"
+ current_episode="$2"
+ shift
;;
- f)
- progress_file="$OPTARG"
+ -f | --progress-file)
+ require_argument "$@"
+ progress_file="$2"
+ shift
;;
- h)
+ -h | --help)
print_usage
exit 0
;;
- l)
+ -l | --list)
list=1
;;
- n)
- advance=0
+ -n | --next)
+ offset=$(( offset + 1 ))
;;
- p)
- previous=1
+ -p | --previous)
+ offset=$(( offset - 1 ))
;;
- v)
+ -v | --verbose)
verbose=1
;;
- \?)
- print_usage
- print_error "Invalid option: -$OPTARG"
- exit 1
+ -*)
+ usage_error "Invalid option: $1"
;;
- :)
- print_usage
- print_error "Option -$OPTARG requires an argument"
+ *)
+ [[ ! -z $media_dir ]] && usage_error "Multiple directories given"
+ media_dir="$1"
;;
esac
+ shift
done
-media_dir="$(readlink -f "$media_dir")"
-print_verbose "Using directory $media_dir"
-current_episode="$(readlink -f "$current_episode")"
-print_verbose "Using manually specified episode $current_episode"
+progress_file=${progress_file-$HOME/$DEFAULT_PROGRESS_FILE}
+command=${command-$DEFAULT_COMMAND}
+offset=$(( offset + 1 ))
touch "$progress_file"
+read_progress_file
if (( list )); then
- cat "$progress_file"
+ for episode in "${progress[@]}"; do
+ echo "$episode"
+ done
exit 0
fi
+[[ -z $media_dir ]] && usage_error "No directory given"
+
+if [[ -d $media_dir ]]; then
+ print_verbose "Media directory found"
+elif [[ -f $media_dir ]]; then
+ print_verbose "Media directory $media_dir is a file; treating as current episode"
+ current_episode="$(readlink -f "$media_dir")"
+ media_dir="$(dirname "$current_episode")"
+else
+ print_verbose "Media directory does not exist; trying substring match"
+ find_media_dir
+fi
+media_dir="$(readlink -f "$media_dir")"
+print_verbose "Using directory $media_dir"
+cd "$media_dir"
+
+current_episode="$(readlink -f "$current_episode")"
+print_verbose "Canonical current episode is $current_episode"
if [[ ! -z $current_episode ]]; then
- if [[ $current_episode == $media_dir* ]]; then
- print_verbose "Using current episode ${current_episode#$media_dir/}"
- else
- print_error "The given file $current_episode does not reside in $media_dir"
- exit 1
+ if [[ $current_episode != $media_dir* ]]; then
+ exit_error "The given file $current_episode does not reside in $media_dir"
fi
+ print_verbose "Using current episode $current_episode"
else
- previous_episode="$(read_progress_file)"
- if (( $? )); then
- print_verbose "No matching previous episode found in config"
- exit 1
- fi
-
- current_episode="$(find_next_episode "$previous_episode")"
- if (( $? )); then
- exit 1
+ find_previous_episode
+ if (( previous )); then
+ current_episode="$previous_episode"
+ else
+ find_next_episode
fi
fi
-echo "${current_episode}"
-
-if (( advance )); then
+if $command "${current_episode}"; then
write_progress_file "$current_episode"
fi
221 test.sh
View
@@ -1,17 +1,9 @@
#!/bin/bash
playnext=$(dirname $(readlink -f $0))/playnext
-media_dir=$(mktemp -d)
-progress_file=$(mktemp)
-
-args="-f $progress_file"
-if [[ $1 == "-v" ]]; then
- args="$args -v"
-fi
function fail() {
- echo "FAIL: $@"
- echo "Last command: $last_command"
+ echo "$@"
echo "Backtrace:"
i=0
while caller $(( i++ )); do true; done
@@ -19,8 +11,19 @@ function fail() {
}
function playnext() {
- last_command="$playnext $args $@"
- $playnext $args "$@"
+ (
+ echo "Running command:"
+ echo -n "$playnext -f $progress_file -v"
+ for arg in "$@"; do
+ if [[ $arg =~ "^[a-zA-Z0-9]*$" ]]; then
+ echo -n " $arg"
+ else
+ echo -n " '$arg'"
+ fi
+ done
+ echo
+ ) >&2
+ $playnext -f $progress_file "$@"
}
function assert_output() {
@@ -29,88 +32,130 @@ function assert_output() {
local actual="$(playnext "$@")"
if (( $? )); then
fail "Nonzero exit code"
- exit 1
fi
if [[ $expected != $actual ]]; then
- fail "Expected: $expected, actual: $actual"
- exit 1
+ fail "Expected: $expected"$'\n'"Actual: $actual"
fi
}
function assert_fail() {
message="$1"
shift
- last_command="$@"
- ! playnext "$@" 2>/dev/null || fail "$message"
-}
-
-function reset_progress() {
- rm $progress_file
-}
-
-cd $media_dir
-
-# Some files that should be ignored
-touch ".dotfile"
-mkdir ".dotdir"
-touch ".dotdir/file"
-
-# Test with empty dir
-assert_fail "Did not fail with empty dir"
-
-# Test with some files
-mkdir "Dir 1"
-touch "Dir 1/File 1"
-touch "Dir 1/file 2"
-touch "Dir 1/File 3"
-mkdir "Dir 2"
-mkdir "Dir 3"
-touch "Dir 3/File 1"
-
-assert_output "$media_dir/Dir 1/File 1"
-assert_output "$media_dir/Dir 1/file 2"
-assert_output "$media_dir/Dir 1/File 3"
-assert_output "$media_dir/Dir 3/File 1"
-assert_fail "Did not run out of files"
-
-# Test multiple episodes in progress
-reset_progress
-playnext -d "Dir 1" -e "Dir 1/File 1" > /dev/null
-playnext -d "Dir 3" -e "Dir 3/File 1" > /dev/null
-assert_fail "Did not warn about multiple episodes in progress"
-
-# Test -d
-reset_progress
-pushd /tmp > /dev/null
-assert_output "$media_dir/Dir 1/File 1" -d $media_dir
-popd > /dev/null
-
-# Test -e
-assert_output "$media_dir/Dir 1/file 2" -e "Dir 1/file 2"
-assert_output "$media_dir/Dir 1/File 3"
-assert_fail "Invalid file for -e was accepted" -e /dev/null
-
-# Test -h
-playnext -h | grep -q "Usage" || fail "Did not print usage"
-
-# Test -l
-reset_progress
-file=$(playnext)
-assert_output "$file" -l
-
-# Test -n
-reset_progress
-assert_output "$media_dir/Dir 1/File 1"
-assert_output "$media_dir/Dir 1/file 2" -n
-assert_output "$media_dir/Dir 1/file 2" -n
-
-# Test -p
-reset_progress
-assert_output "$media_dir/Dir 1/File 1"
-assert_output "$media_dir/Dir 1/File 1" -p
-assert_output "$media_dir/Dir 1/File 1" -p
-
-rm -r "$media_dir"
-rm -r "$progress_file"
-
-echo "PASS"
+ ! playnext "$@" > /dev/null 2>&1 || fail "$message"
+}
+
+function set_up_fixture() {
+ cd "$(dirname $0)/testdata"
+ media_dir_1=$(readlink -f t1)
+ media_dir_2=$(readlink -f t2)
+ media_dir_3=$(readlink -f t3)
+}
+
+function tear_down_fixture() {
+ true
+}
+
+function set_up() {
+ progress_file=$(mktemp)
+}
+
+function tear_down() {
+ rm -f $progress_file
+}
+
+function test_with_empty_dir() {
+ assert_fail "Did not fail with empty dir" $media_dir_3
+}
+
+function test_with_some_files() {
+ assert_output "$media_dir_1/Dir 1/File 1" $media_dir_1
+ assert_output "$media_dir_1/Dir 1/file 2" $media_dir_1
+ assert_output "$media_dir_1/Dir 1/File 3" $media_dir_1
+ assert_output "$media_dir_1/Dir 3/File 1" $media_dir_1
+ assert_fail "Did not run out of files"
+}
+
+function test_multiple_episodes_in_progress() {
+ playnext "$media_dir_1/Dir 1" -e "Dir 1/File 1" > /dev/null
+ playnext "$media_dir_1/Dir 3" -e "Dir 3/File 1" > /dev/null
+ assert_fail "Did not warn about multiple episodes in progress"
+}
+
+function test_multiple_media_dirs() {
+ playnext $media_dir_1 > /dev/null
+ playnext $media_dir_2 > /dev/null
+ assert_output "$(echo -e "$media_dir_1/Dir 1/File 1\n$media_dir_2/Dir 4/File 1")" -l
+}
+
+function test_directory_argument() {
+ assert_output "$media_dir_1/Dir 1/File 1" $media_dir_1
+ assert_fail "Did not fail without directory argument"
+ assert_fail "Did not fail with multiple directories" $media_dir_1 $media_dir_2
+}
+
+function test_directory_substring() {
+ playnext $media_dir_1 > /dev/null
+ playnext $media_dir_2 > /dev/null
+ assert_output "$media_dir_1/Dir 1/file 2" "r 1"
+ assert_fail "Did not complain about multiple substring matches" 1
+}
+
+function test_filename_argument() {
+ cd $media_dir_1
+ assert_output "$media_dir_1/Dir 1/file 2" "Dir 1/file 2"
+ assert_output "$media_dir_1/Dir 1/File 3" $media_dir_1
+}
+
+function test_episode_option() {
+ assert_output "$media_dir_1/Dir 1/file 2" $media_dir_1 -e "Dir 1/file 2"
+ assert_output "$media_dir_1/Dir 1/File 3" $media_dir_1
+ assert_fail "Invalid file for -e was accepted" $media_dir_1 -e /dev/null
+}
+
+function test_help_option() {
+ playnext -h | grep -q "Usage" || fail "Did not print usage"
+}
+
+function test_list_option() {
+ playnext $media_dir_1 > /dev/null
+ playnext $media_dir_2 > /dev/null
+ file=$(cat $progress_file)
+ assert_output "$file" -l
+}
+
+function test_next_option() {
+ assert_output "$media_dir_1/Dir 1/file 2" $media_dir_1 -n
+ assert_output "$media_dir_1/Dir 3/File 1" $media_dir_1 -n
+}
+
+function test_previous_option() {
+ playnext $media_dir_1 > /dev/null
+ assert_output "$media_dir_1/Dir 1/File 1" $media_dir_1 -p
+ assert_output "$media_dir_1/Dir 1/file 2" $media_dir_1
+ assert_output "$media_dir_1/Dir 1/File 1" $media_dir_1 -p -p
+}
+
+function test_command_option() {
+ assert_output "" $media_dir_1 -c cat
+ assert_output "$media_dir_1/Dir 1/file 2" $media_dir_1 -c echo
+}
+
+pass_count=0
+fail_count=0
+set_up_fixture
+trap tear_down_fixture EXIT
+while read function; do
+ [[ ! $function = test* ]] && continue
+ set_up
+ output=$($function 2>&1)
+ if (( $? )); then
+ echo "FAIL: $function"
+ echo "$output"
+ (( fail_count++ ))
+ else
+ echo "PASS: $function"
+ (( pass_count++ ))
+ fi
+ tear_down
+done < <(declare -F | cut -d' ' -f3)
+echo "$pass_count passes, $fail_count failures"
0  testdata/t1/.dotdir/file
View
No changes.
0  testdata/t1/.dotfile
View
No changes.
0  testdata/t1/Dir 1/File 1
View
No changes.
0  testdata/t1/Dir 1/File 3
View
No changes.
0  testdata/t1/Dir 1/file 2
View
No changes.
0  testdata/t1/Dir 3/File 1
View
No changes.
0  testdata/t2/Dir 4/File 1
View
No changes.
0  testdata/t2/File 2
View
No changes.

No commit comments for this range

Something went wrong with that request. Please try again.