├── .gitignore ├── src ├── help │ ├── np-clear.txt │ ├── np-list.txt │ ├── np-now.txt │ ├── np-clean.txt │ ├── np-next.txt │ ├── np-start.txt │ ├── np-history.txt │ ├── np-stop.txt │ ├── np-doctor.txt │ ├── np-startstop.txt │ ├── np-index.txt │ ├── np-notify.txt │ ├── np-daemon.txt │ ├── np-add.txt │ └── np-help.txt ├── shared │ ├── mutexed.sh │ ├── functions │ │ ├── debug.sh │ │ ├── daemon.sh │ │ ├── config.sh │ │ ├── basic.sh │ │ ├── input-validation.sh │ │ ├── queue.sh │ │ ├── externalplayer.sh │ │ ├── display.sh │ │ ├── mode.sh │ │ ├── notify.sh │ │ ├── cross-platform.sh │ │ ├── filesystem.sh │ │ ├── details.sh │ │ └── processes.sh │ ├── man │ │ ├── README.md │ │ └── man1 │ │ │ └── np.1 │ ├── functions.sh │ ├── githooks │ │ └── pre-commit.sh │ └── functionality.sh ├── np-start.sh ├── np-stop.sh ├── np-now.sh ├── np-list.sh ├── np-history.sh ├── np-next.sh ├── np-startstop.sh ├── np-clear.sh ├── np-help.sh ├── np.sh ├── np-clean.sh ├── np-add.sh ├── np-daemon.sh ├── np-index.sh ├── np ├── np-doctor.sh └── np-notify.sh ├── INSTALL.md ├── README.md ├── CONTRIBUTE.md ├── USAGE.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/help/np-clear.txt: -------------------------------------------------------------------------------- 1 | Usage: np clear 2 | 3 | Empty the queue. 4 | -------------------------------------------------------------------------------- /src/help/np-list.txt: -------------------------------------------------------------------------------- 1 | Usage: np list 2 | 3 | See sounds currently in the queue. 4 | -------------------------------------------------------------------------------- /src/help/np-now.txt: -------------------------------------------------------------------------------- 1 | Usage: np now 2 | 3 | Shows the sound currently playing. 4 | -------------------------------------------------------------------------------- /src/help/np-clean.txt: -------------------------------------------------------------------------------- 1 | Usage: np clean 2 | 3 | Remove non-existant files from queue. 4 | -------------------------------------------------------------------------------- /src/help/np-next.txt: -------------------------------------------------------------------------------- 1 | Usage: np next 2 | 3 | Advance to the next sound in the queue. 4 | -------------------------------------------------------------------------------- /src/help/np-start.txt: -------------------------------------------------------------------------------- 1 | Usage: np start 2 | 3 | Let 'np daemon' consume the sound queue. 4 | -------------------------------------------------------------------------------- /src/help/np-history.txt: -------------------------------------------------------------------------------- 1 | Usage: np history 2 | 3 | Show the 999 most recently played sounds. 4 | -------------------------------------------------------------------------------- /src/help/np-stop.txt: -------------------------------------------------------------------------------- 1 | Usage: np stop 2 | 3 | Don't let 'np daemon' consume the sound queue. 4 | -------------------------------------------------------------------------------- /src/help/np-doctor.txt: -------------------------------------------------------------------------------- 1 | Usage: np doctor 2 | 3 | Display configuration, runtime and status values. 4 | -------------------------------------------------------------------------------- /src/help/np-startstop.txt: -------------------------------------------------------------------------------- 1 | Usage: np startstop 2 | 3 | Toggle playback between 'np start' and 'np stop'. 4 | -------------------------------------------------------------------------------- /src/shared/mutexed.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | exitIfAlreadyRunningOrCleanup "$configPidFile" "np" 5 | savePidButDeleteOnExit "np" "$$" "$configPidFile" 6 | -------------------------------------------------------------------------------- /src/np-start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | setModeStart 9 | -------------------------------------------------------------------------------- /src/np-stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | setModeStop 9 | -------------------------------------------------------------------------------- /src/np-now.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | highlight "$(getCurrentSound)" -------------------------------------------------------------------------------- /src/np-list.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | cat "$configQueueFile" | nullAsNewline numberLines | highlightAllWithLineNumbers -------------------------------------------------------------------------------- /src/np-history.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | cat "$configHistoryFile" | nullAsNewline tail -999 | nullAsNewline numberLinesReverse | highlightAllWithLineNumbers -------------------------------------------------------------------------------- /src/np-next.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | if isExternalPlayerRunning; 9 | then 10 | killExternalPlayerIfRunning 11 | else 12 | progressQueue 13 | fi 14 | -------------------------------------------------------------------------------- /src/np-startstop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | mode=$(getMode) 9 | 10 | if [[ "$mode" == "playing" ]]; 11 | then 12 | setModeStop 13 | else 14 | setModeStart 15 | fi 16 | -------------------------------------------------------------------------------- /src/help/np-index.txt: -------------------------------------------------------------------------------- 1 | Usage: np index [options...] 2 | 3 | Create a file with a cached list of all sounds in the current folder. 4 | 5 | Options: 6 | --force Recreate the index file even 7 | if it already exists. 8 | 9 | --clean Remove index files. 10 | 11 | --recursive Perform the action in subfolders. 12 | -------------------------------------------------------------------------------- /src/np-clear.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | echo -n '' > "$configQueueFile" 9 | 10 | # TODO: define a "next" function, as well as for other common user commands. 11 | killExternalPlayerIfRunning 12 | -------------------------------------------------------------------------------- /src/np-help.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | action="" 5 | 6 | (( "$#" > 0 )) && { action="$1"; shift; } 7 | 8 | [[ -z "$action" ]] && action="help" 9 | 10 | helpfile="${BASH_SOURCE%/*}/help/np-${action}.txt" 11 | 12 | if [[ -f $helpfile ]]; 13 | then 14 | cat $helpfile 15 | else 16 | echo -E "np: '${action}' is not a action." 1>&2 17 | exit 1 18 | fi 19 | 20 | -------------------------------------------------------------------------------- /src/shared/functions/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | isDebugEnabled() { 5 | [[ "$configDebug" == "true" ]] 6 | } 7 | 8 | debug() { 9 | if isDebugEnabled; 10 | then 11 | echo -ne "($$)\t" 1>&2 12 | echo -E "DEBUG: $@" 1>&2 13 | fi 14 | 15 | return 0 16 | } 17 | 18 | errorMessage() { 19 | echo -E "np:" "$@" 1>&2 20 | } 21 | 22 | die() { 23 | errorMessage "$@" 24 | exit 1 25 | return 1 26 | } 27 | -------------------------------------------------------------------------------- /src/shared/man/README.md: -------------------------------------------------------------------------------- 1 | # [npshell `np` -- command line music queue manager](https://github.com/joelpurra/npshell/) 2 | 3 | This directory contains the `man np` file. It has been autogenerated from `USAGE.md` in the repository root by a `git` `pre-commit` hook. Do not update manually. 4 | 5 | 6 | --- 7 | 8 | Copyright (c) 2014, 2015, 2016, 2017 [Joel Purra](https://joelpurra.com/). Released under [GNU General Public License version 3.0 (GPL-3.0)](https://www.gnu.org/licenses/gpl.html). 9 | -------------------------------------------------------------------------------- /src/shared/functions/daemon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | killDaemon() { 5 | if [[ -s "$configDaemonPidFile" ]]; 6 | then 7 | killPidFromFileAndWaitUntilDead "$configDaemonPidFile" 8 | fi 9 | 10 | return 0 11 | } 12 | 13 | isDaemonRunning() { 14 | if isValidPidFileAndRunningOrCleanup "$configDaemonPidFile"; 15 | then 16 | return 0 17 | else 18 | return 1 19 | fi 20 | } 21 | 22 | killDaemonIfRunning() { 23 | { isDaemonRunning && killDaemon; } || true &>/dev/null 24 | 25 | return 0 26 | } 27 | -------------------------------------------------------------------------------- /src/np.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | externalExecutableExists() { 5 | local executable="$1" 6 | 7 | if builtin type "$executable" &>/dev/null; 8 | then 9 | return 0 10 | else 11 | return 1 12 | fi 13 | } 14 | 15 | action="" 16 | 17 | (( "$#" > 0 )) && { action="$1"; shift; } 18 | 19 | [[ -z "$action" ]] && action="now" 20 | 21 | executable="np-${action}.sh" 22 | 23 | if ! externalExecutableExists "$executable"; 24 | then 25 | executable="${BASH_SOURCE%/*}/${executable}" 26 | 27 | if ! externalExecutableExists "$executable"; 28 | then 29 | echo -E "np: '${action}' is not a action." 1>&2; exit 1; 30 | fi 31 | fi 32 | 33 | exec "$executable" "$@" 34 | -------------------------------------------------------------------------------- /src/help/np-notify.txt: -------------------------------------------------------------------------------- 1 | Usage: np notify [options...] 2 | 3 | Show notifications when the track changes, playback is started/stopped or the queue is empty. 4 | If the sound has id3v2 tags, artist/album/title are shown. 5 | 6 | Options: 7 | --is-running Check if the daemon process has already started. 8 | Exits with '0' if it has, '1' otherwise. 9 | 10 | --stop Stop notification daemon execution. 11 | Can be used during system shutdown, 12 | but isn't part of everyday usage. 13 | 14 | Examples: 15 | # Start the notification daemon, let it run in the background. 16 | # Should be done at user login. 17 | np notify --is-running || ( np notify & ) 18 | -------------------------------------------------------------------------------- /src/help/np-daemon.txt: -------------------------------------------------------------------------------- 1 | Usage: np daemon [options...] 2 | 3 | Play sounds in queue as soon as there are any. Can be controlled with 'np start' and 'np stop', as well as the rest of the queue commands. 4 | 5 | Options: 6 | --is-running Check if the daemon process has already started. 7 | Exits with '0' if it has, '1' otherwise. 8 | 9 | --stop Stop daemon execution. Can be used during system 10 | shutdown, but isn't part of everyday usage. 11 | 12 | --verbose Output paths to the sounds as they play. 13 | 14 | Examples: 15 | # Start the daemon, let it run in the background. 16 | # Should be done at user login. 17 | np daemon --is-running || ( np daemon & ) 18 | -------------------------------------------------------------------------------- /src/np-clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | tmpQueueFile="${configQueueFile}.tmp~" 9 | 10 | skipNonexistantFiles() { 11 | sound="$1" 12 | 13 | debug "clean: checking '${sound}'." 14 | 15 | 16 | if [[ -s "${sound}" ]]; 17 | then 18 | debug "clean: kept existing sound '${sound}' in queue." 19 | echo -nE "${sound}" 20 | echo -ne "\0" 21 | else 22 | debug "clean: removed sound '${sound}' from queue." 23 | fi 24 | } 25 | 26 | cat "$configQueueFile" | nullDelimitedForEachWithEOF skipNonexistantFiles > "$tmpQueueFile" 27 | mv -f "$tmpQueueFile" "$configQueueFile" -------------------------------------------------------------------------------- /src/shared/functions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/functions/debug.sh" 5 | source "${BASH_SOURCE%/*}/functions/cross-platform.sh" 6 | source "${BASH_SOURCE%/*}/functions/basic.sh" 7 | source "${BASH_SOURCE%/*}/functions/processes.sh" 8 | source "${BASH_SOURCE%/*}/functions/config.sh" 9 | source "${BASH_SOURCE%/*}/functions/filesystem.sh" 10 | source "${BASH_SOURCE%/*}/functions/input-validation.sh" 11 | source "${BASH_SOURCE%/*}/functions/queue.sh" 12 | source "${BASH_SOURCE%/*}/functions/externalplayer.sh" 13 | source "${BASH_SOURCE%/*}/functions/display.sh" 14 | source "${BASH_SOURCE%/*}/functions/mode.sh" 15 | source "${BASH_SOURCE%/*}/functions/daemon.sh" 16 | source "${BASH_SOURCE%/*}/functions/details.sh" 17 | source "${BASH_SOURCE%/*}/functions/notify.sh" 18 | -------------------------------------------------------------------------------- /src/shared/functions/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ensureConfigFoldersAndFilesExist() { 5 | [[ -d "$configConfigFolder" ]] || mkdir -p "$configConfigFolder" 6 | [[ -e "$configConfigFile" ]] || touch "$configConfigFile" 7 | } 8 | 9 | ensureOtherFoldersAndFilesExist() { 10 | [[ -e "$configQueueFile" ]] || touch "$configQueueFile" 11 | [[ -e "$configHistoryFile" ]] || touch "$configHistoryFile" 12 | [[ -e "$configPlayingFile" ]] || touch "$configPlayingFile" 13 | 14 | # This makes the daemon start playing immidiately on first run, or if the mode file was deleted. 15 | [[ -e "$configModeFile" ]] || echo "playing" > "$configModeFile" 16 | } 17 | 18 | readConfig() { 19 | debug "Reading configuration file '${configConfigFile}'" 20 | [[ -e "$configConfigFile" ]] && source "$configConfigFile" 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/functions/basic.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | nullAsNewline() { 5 | tr '\n\0' '\0\n' | "$@" | tr '\0\n' '\n\0' 6 | } 7 | 8 | nullDelimitedForEachWithoutEOF() { 9 | while IFS= read -r -d '' item || true; 10 | do 11 | "$@" "$item" 12 | done 13 | } 14 | 15 | nullDelimitedForEachWithEOF() { 16 | while IFS= read -r -d '' item; 17 | do 18 | "$@" "$item" 19 | done 20 | } 21 | 22 | reverseLineOrderExec="$(getFirstExecutable "tac" "gtac" "/usr/local/bin/gtac")" 23 | 24 | reverseLineOrder() { 25 | "$reverseLineOrderExec" 26 | } 27 | 28 | keepDigitsOnly() { 29 | sedExtRegexp -e 's/[^[:digit:]]//g' -e '/^$/d' 30 | } 31 | 32 | trim () { 33 | # https://stackoverflow.com/questions/4422491/how-do-i-trim-lines-read-from-standard-input-on-bash 34 | local line; 35 | read -r line; 36 | echo "$line"; 37 | } 38 | -------------------------------------------------------------------------------- /src/np-add.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | if isValidStrictlyPositiveNumber "$1"; 9 | then 10 | configNumsounds="${1:-$configNumsounds}" 11 | shift 12 | elif isValidNumsoundsOverride "$1"; 13 | then 14 | configNumsounds=-1 15 | shift 16 | fi 17 | 18 | if isValidPlayOrder "$1"; 19 | then 20 | configOrder="${1:-$configOrder}" 21 | shift 22 | fi 23 | 24 | getLineCount() { 25 | wc -l | keepDigitsOnly 26 | } 27 | 28 | declare -i lineCount=$(getSounds "$@" | playOrder | limit | tee -a "$configQueueFile" | nullAsNewline getLineCount | trim) 29 | cat "$configQueueFile" | nullAsNewline numberLines | nullAsNewline tail -n "$lineCount" | highlightAllWithLineNumbers 30 | 31 | displayMessage "Added ${lineCount} sounds." 32 | -------------------------------------------------------------------------------- /src/np-daemon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | 7 | if [[ "$1" == "--is-running" ]]; 8 | then 9 | if isDaemonRunning; 10 | then 11 | exit 0 12 | else 13 | exit 1 14 | fi 15 | fi 16 | 17 | if [[ "$1" == "--stop" ]]; 18 | then 19 | killDaemonIfRunning 20 | killExternalPlayerIfRunning 21 | exit 0 22 | fi 23 | 24 | exitIfAlreadyRunningOrCleanup "$configDaemonPidFile" "daemon" 25 | savePidButDeleteOnExit "daemon" "$$" "$configDaemonPidFile" 26 | 27 | verboseOutput="false" 28 | if [[ "$1" == "--verbose" ]]; 29 | then 30 | shift || true 31 | verboseOutput="true" 32 | fi 33 | 34 | whenQueueOrModeIsChanged() { 35 | waitForFileChange "$configQueueFile" "$configModeFile" 36 | } 37 | 38 | daemonLoop() { 39 | while true; 40 | do 41 | checkMode || whenQueueOrModeIsChanged 42 | done 43 | } 44 | 45 | daemonLoop 46 | -------------------------------------------------------------------------------- /src/shared/functions/input-validation.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | isValidStrictlyPositiveNumber() { 5 | declare -r str="$1" 6 | 7 | if [[ "$str" =~ ^[[:digit:]]+$ ]]; 8 | then 9 | # Check if $1 has been interpreted to something else than it was before. 10 | # That is the strings "", "a", "abcdef" are all interpreted as zero. 11 | # Only afterwards can $n assumed to be zero, not "zero or any value that was not a number." 12 | # (Oh and the regex probably took care of it already, but wth.) 13 | declare -i -r n="$str" 14 | 15 | if [[ "$str" == "$n" ]] && (( n >= 0 )); 16 | then 17 | return 0 18 | else 19 | return 1 20 | fi 21 | else 22 | return 1 23 | fi 24 | } 25 | 26 | isValidNumsoundsOverride() { 27 | local override="$1" 28 | 29 | if [[ "$override" == "all" ]]; then 30 | return 0 31 | else 32 | return 1 33 | fi 34 | } 35 | 36 | isValidPlayOrder() { 37 | local order="$1" 38 | 39 | if [[ "$order" == "shuffle" || "$order" == "in-order" ]]; then 40 | return 0 41 | else 42 | return 1 43 | fi 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/functions/queue.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | shuffle() { 5 | # Kind of assuming GNU `shuf`; haven't tested the -z flag on anything else. 6 | "$externalShuffleExec" -z 7 | } 8 | 9 | playOrder() { 10 | if [[ "$configOrder" == "shuffle" ]]; then 11 | shuffle 12 | elif [[ "$configOrder" == "in-order" ]]; then 13 | cat - 14 | else 15 | die "Unknown order: ${configOrder}" 16 | fi 17 | } 18 | 19 | limit() { 20 | if (( configNumsounds > 0 )); then 21 | nullAsNewline head -n "$configNumsounds" 22 | else 23 | cat - 24 | fi 25 | } 26 | 27 | getNextSound() { 28 | { IFS= read -r -d '' sound || true; } < <(head -c 2048 "$configQueueFile") 29 | echo -nE "$sound" 30 | } 31 | 32 | getCurrentSound() { 33 | cat "$configPlayingFile" 34 | } 35 | 36 | pushSoundToHistory() { 37 | local sound=$(getNextSound) 38 | 39 | [[ -z "$sound" ]] && return 0 40 | 41 | echo -nE "$sound" >> "$configHistoryFile" 42 | echo -ne "\0" >> "$configHistoryFile" 43 | } 44 | 45 | progressQueue() { 46 | pushSoundToHistory 47 | <"$configQueueFile" nullAsNewline sed '1 d' > "${configQueueFile}.tmp~" 48 | mv "${configQueueFile}.tmp~" "$configQueueFile" 49 | } 50 | -------------------------------------------------------------------------------- /src/np-index.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | recursive=false 9 | clean=false 10 | force=false 11 | 12 | declare -i argumentsCount=0 13 | 14 | while (( argumentsCount != "$#" )); 15 | do 16 | argumentsCount="$#" 17 | 18 | if [[ "$1" == "--recursive" ]]; 19 | then 20 | recursive=true 21 | shift 22 | fi 23 | 24 | if [[ "$1" == "--clean" ]]; 25 | then 26 | clean=true 27 | shift 28 | fi 29 | 30 | if [[ "$1" == "--force" ]]; 31 | then 32 | force=true 33 | shift 34 | fi 35 | done 36 | 37 | if [[ "$force" == true && "$recursive" == true ]]; 38 | then 39 | die "can't --force and --recursive" 40 | fi 41 | 42 | if [[ "$force" == true && "$clean" == true ]]; 43 | then 44 | die "can't --force and --clean" 45 | fi 46 | 47 | if [[ "$clean" == true ]]; 48 | then 49 | if [[ "$recursive" == true ]]; 50 | then 51 | deleteSoundCacheRecursively 52 | else 53 | deleteSoundCache 54 | fi 55 | else 56 | if soundCacheExists && [[ "$force" != true ]]; 57 | then 58 | exit 0 59 | else 60 | generateSoundCache 61 | fi 62 | fi 63 | -------------------------------------------------------------------------------- /src/help/np-add.txt: -------------------------------------------------------------------------------- 1 | Usage: np add [LIMIT] [ORDER] [PATH...] 2 | 3 | Add some sounds to the queue. 4 | 5 | Arguments: 6 | LIMIT 7 | Values: number or "all" 8 | Default: 3 9 | 10 | ORDER 11 | Values: "in-order" or "shuffle" 12 | Default: "shuffle" 13 | 14 | PATH 15 | Values: Path to sound file, folders or "-" for piped values 16 | Default: "$PWD" 17 | 18 | Examples: 19 | # Add the default number of shuffled sounds from the current folder. 20 | np add 21 | 22 | # Add a single sound to the queue. 23 | np add path/to/sound.mp3 24 | 25 | # Add 25 shuffled sounds from the current folder. 26 | np add 25 27 | 28 | # Add the first 2 sounds from the current folder. 29 | np add 2 in-order 30 | 31 | # Add shuffled sounds from a folder, hierarchically. 32 | np add path/to/folder/with/sounds/ 33 | 34 | # Add an album. 35 | np add all in-order "Jazz/My Favorite Album/" 36 | 37 | # Add sounds by null-delimited paths from stdin. 38 | find . -iname '*best of*.mp3' -print0 | np add - 39 | 40 | # Add 10 shuffled sounds from the combined list of a single sound, stdin and a folder. 41 | find . -iname '*best of*.mp3' -print0 | np add 10 path/to/sound.mp3 - path/to/folder/with/sounds/ 42 | -------------------------------------------------------------------------------- /src/shared/functions/externalplayer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | externalPlayer() { 5 | # TODO: use dynamic index? 6 | local index=999 7 | exitIfAlreadyRunningOrCleanup "$configExternalPlayerPidFile" "externalplayer" 8 | "$externalPlayerExec" "$@" & 9 | local externalplayerPid="$!" 10 | savePidAtIndexButDeleteOnExit "$index" "externalplayer" "$externalplayerPid" "$configExternalPlayerPidFile" 11 | wait "$externalplayerPid" &>/dev/null 12 | killExternalPlayerIfRunningAndCleanup 13 | clearPidAtIndex "$index" 14 | } 15 | 16 | killExternalPlayer() { 17 | if [[ -s "$configExternalPlayerPidFile" ]]; 18 | then 19 | killPidFromFileAndWaitUntilDead "$configExternalPlayerPidFile" 20 | fi 21 | 22 | return 0 23 | } 24 | 25 | isExternalPlayerRunning() { 26 | if isValidPidFileAndRunningOrCleanup "$configExternalPlayerPidFile"; 27 | then 28 | return 0 29 | else 30 | return 1 31 | fi 32 | } 33 | 34 | killExternalPlayerIfRunning() { 35 | { isExternalPlayerRunning && killExternalPlayer; } || true &>/dev/null 36 | 37 | return 0 38 | } 39 | 40 | killExternalPlayerIfRunningAndCleanup() { 41 | killExternalPlayerIfRunning 42 | cleanupPidFile "$configExternalPlayerPidFile" 43 | 44 | return 0 45 | } 46 | 47 | playSound() { 48 | externalPlayer "$@" 49 | } 50 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | See [npshell `np` -- command line music queue manager](https://github.com/joelpurra/npshell/) 4 | 5 | ## Autostart 6 | 7 | The npshell daemon is designed to start upon login. 8 | 9 | 10 | ### On Mac with [Homebrew](https://brew.sh/) 11 | 12 | ```bash 13 | brew tap joelpurra/joelpurra 14 | brew install npshell 15 | brew services start npshell 16 | ``` 17 | 18 | ### On other systems 19 | 20 | - Clone the code then add a symlink to `npshell/src/np` to your path. 21 | - Add `np daemon --is-running || ( np daemon & )` to your `~/.bash_profile` or similar. 22 | - Requirements: 23 | - One of the music players `afplay`, `mplayer`, `mpg123`, `mpg321`, `play` in your `$PATH`. 24 | - [`fswatch`](https://github.com/emcrisostomo/fswatch), [`bash`](https://www.gnu.org/software/bash/) 4+, `shuf` (or `brew` prefixed `gshuf`), `tac` (or `brew` prefixed `gtac`) from [`coreutils`](https://www.gnu.org/software/coreutils/). 25 | 26 | 27 | 28 | ## Notifications 29 | 30 | The notification system is started separately. 31 | 32 | - Add `np notify --is-running || ( np notify & )` to your `~/.bash_profile` or similar. 33 | - Requirements 34 | - [`terminal-notifier`](https://github.com/alloy/terminal-notifier) or [`growlnotify`](https://growl.github.io/growl/). 35 | 36 | 37 | 38 | --- 39 | 40 | Copyright (c) 2014, 2015, 2016, 2017 [Joel Purra](https://joelpurra.com/). Released under [GNU General Public License version 3.0 (GPL-3.0)](https://www.gnu.org/licenses/gpl.html). 41 | -------------------------------------------------------------------------------- /src/shared/functions/display.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | displayMessage() { 5 | echo "$@" 6 | } 7 | 8 | highlight() { 9 | if (("$#" == 2 )); 10 | then 11 | local cwd="$1" 12 | local line="$2" 13 | elif (("$#" == 1 )); 14 | then 15 | local cwd=$(getCwd) 16 | local line="$1" 17 | else 18 | die "wrong number of highlight arguments" 19 | fi 20 | 21 | displayMessage -E "$line" | sed "s|^${cwd}/||" | grep --extended-regexp --color "/?[^/]+$" 22 | } 23 | 24 | highlightAll() { 25 | nullDelimitedForEachWithEOF highlight "$cwd" 26 | } 27 | 28 | highlightWithLineNumbers() { 29 | if (("$#" == 2 )); 30 | then 31 | local cwd="$1" 32 | local line="$2" 33 | elif (("$#" == 1 )); 34 | then 35 | local cwd=$(getCwd) 36 | local line="$1" 37 | else 38 | die "wrong number of highlight arguments" 39 | fi 40 | 41 | displayMessage -E "$line" | sedExtRegexp "s|^([[:space:]]*-?[[:digit:]]+[[:space:]]+)${cwd}/|\1|" | grep --extended-regexp --color "/?[^/]+$" 42 | } 43 | 44 | highlightAllWithLineNumbers() { 45 | local cwd=$(getCwd) 46 | 47 | nullDelimitedForEachWithEOF highlightWithLineNumbers "$cwd" 48 | } 49 | 50 | removeNumberingSpacesFromEachLine() { 51 | sed -e 's/^ //g' -e 's/^\( *[[:digit:]][[:digit:]]*\)\ /\1 /g' 52 | } 53 | 54 | addNegativeSignBeforeLineNumber() { 55 | sed 's/^\( *\)\([[:digit:]][[:digit:]]*\)/\1-\2/g' 56 | } 57 | 58 | numberLines() { 59 | cat -n | removeNumberingSpacesFromEachLine 60 | } 61 | 62 | numberLinesReverse() { 63 | reverseLineOrder | numberLines | addNegativeSignBeforeLineNumber | reverseLineOrder 64 | } 65 | -------------------------------------------------------------------------------- /src/shared/githooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | set -u 4 | 5 | if (( $(id -u) == 0 )); 6 | then 7 | echo -E "np git hook: cannot be executed with root privileges." 1>&2 8 | exit 1 9 | fi 10 | 11 | externalExecutableExists() { 12 | local executable="$1" 13 | 14 | if builtin type "$executable" &>/dev/null; 15 | then 16 | return 0 17 | else 18 | return 1 19 | fi 20 | } 21 | 22 | requireExternalExecutable() { 23 | local executable="$1" 24 | 25 | if externalExecutableExists "$executable"; 26 | then 27 | return 0 28 | else 29 | echo -E "np git hook: '${executable}' was not found on the \$PATH." 1>&2 30 | exit 1 31 | fi 32 | } 33 | 34 | wasFileModifiedAndStaged() { 35 | file="$1" 36 | 37 | fileStatus="$(git status --porcelain :/ | sed -n "/M. ${file}/ p")" 38 | 39 | if [[ -z "$fileStatus" ]]; 40 | then 41 | return 1 42 | else 43 | return 0 44 | fi 45 | } 46 | 47 | usageMdFile="USAGE.md" 48 | manBaseDir="src/shared/man" 49 | man1="${manBaseDir}/man1/np.1" 50 | 51 | if wasFileModifiedAndStaged "$usageMdFile"; 52 | then 53 | echo -E "np git hook: the file '${usageMdFile}' was modified, converting it to man (roff) format in '${man1}'." 1>&2 54 | 55 | # Requiring "md2man-roff" seems a bit rough -- showing a warning for now to encourage git hook usage. 56 | # requireExternalExecutable "md2man-roff" 57 | 58 | if externalExecutableExists "md2man-roff"; 59 | then 60 | md2man-roff "$usageMdFile" > "$man1"; 61 | 62 | git add "$man1" 63 | else 64 | echo -E "np git hook: the file '${usageMdFile}' was modified, but 'md2man-roff' wasn't found for the conversion. Skipping for now, but please let the np maintainers know." 1>&2 65 | fi 66 | fi 67 | -------------------------------------------------------------------------------- /src/np: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if (( $(id -u) == 0 )); 5 | then 6 | echo -E "np: cannot be executed with root privileges." 1>&2 7 | exit 1 8 | fi 9 | 10 | # Sometimes the $PWD gets pulled out from under the user (unmounted, deleted etcetera). 11 | # Bail before anything else happens because of actions (perhaps even deletions) based on $PWD. 12 | # Actually, specifically using `bash` as the hash-bang will print an error message already. 13 | # When using plain `sh` there seems to be none. 14 | if [[ ! -d "$(pwd 2>/dev/null)" ]]; 15 | then 16 | echo -E "np: the working directory is unavailable, cannot continue: '$PWD'" 1>&2 17 | exit 1 18 | fi 19 | 20 | function crossplatformReadlink { 21 | # NOTE: this function has been duplicated elsewhere in this project. 22 | # https://stackoverflow.com/questions/1055671/how-can-i-get-the-behavior-of-gnus-readlink-f-on-a-mac 23 | # https://stackoverflow.com/a/1116890 24 | TARGET_FILE="$1" 25 | 26 | cd "$(dirname "$TARGET_FILE")" 27 | TARGET_FILE="$(basename "$TARGET_FILE")" 28 | 29 | # Iterate down a (possible) chain of symlinks 30 | while [ -L "$TARGET_FILE" ] 31 | do 32 | TARGET_FILE="$(readlink "$TARGET_FILE")" 33 | cd "$(dirname "$TARGET_FILE")" 34 | TARGET_FILE="$(basename "$TARGET_FILE")" 35 | done 36 | 37 | # Compute the canonicalized name by finding the physical path 38 | # for the directory we're in and appending the target file. 39 | PHYS_DIR="$(pwd -P)" 40 | RESULT="${PHYS_DIR}/${TARGET_FILE}" 41 | echo "$RESULT" 42 | } 43 | 44 | # Must be executed in this file. 45 | # Used to recursively call np and to find files to `source`. 46 | NP_SOURCE="$(crossplatformReadlink "$BASH_SOURCE")" 47 | 48 | exec "${NP_SOURCE%/*}/np.sh" "$@" 49 | -------------------------------------------------------------------------------- /src/shared/functions/mode.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # This function is a mixture of displaying and process control. Is there a better place for it? 5 | playSoundInPlayer() { 6 | local sound=$(getNextSound) 7 | progressQueue 8 | local soundPath="$sound" 9 | 10 | # NOTE: resolve symlinks for the external player, in case it doesn't do it itself. 11 | if [[ "$configFollowSymlinks" == "true" && -L "$sound" ]]; 12 | then 13 | soundPath="$(crossplatformReadlink "$sound")" 14 | fi 15 | 16 | if [[ -s "$soundPath" ]]; 17 | then 18 | echo "$sound" > "$configPlayingFile" 19 | 20 | if [[ "$verboseOutput" == "true" ]]; 21 | then 22 | highlight "$(getCurrentSound)" 23 | echo -ne '\r' 24 | fi 25 | 26 | ( trap 'echo -n > "$configPlayingFile"' SIGINT EXIT; { playSound "$soundPath" || true; } ) 27 | else 28 | errorMessage "sound not found: '${sound}'." 29 | fi 30 | 31 | echo -n > "$configPlayingFile" 32 | } 33 | 34 | setModeStart() { 35 | echo "playing" > "$configModeFile" 36 | } 37 | 38 | setModeStop() { 39 | echo "stopped" > "$configModeFile" 40 | 41 | killExternalPlayerIfRunning 42 | } 43 | 44 | modePlaying() { 45 | # Don't start the player unless there's a song to play. 46 | sound=$(getNextSound) 47 | 48 | if [[ -z "$sound" ]]; 49 | then 50 | return 1 51 | else 52 | playSoundInPlayer 53 | fi 54 | } 55 | 56 | modeStopped() { 57 | return 1 58 | } 59 | 60 | modeUnknown() { 61 | die "unknown mode in file '$configModeFile'." 62 | } 63 | 64 | getMode() { 65 | cat "$configModeFile" 66 | } 67 | 68 | checkMode() { 69 | local mode=$(getMode) 70 | 71 | debug "checkMode: '$mode'" 72 | 73 | case "$mode" in 74 | 'playing') 75 | modePlaying 76 | ;; 77 | 'stopped') 78 | modeStopped 79 | ;; 80 | '') 81 | modeStopped 82 | ;; 83 | *) 84 | modeUnknown 85 | ;; 86 | esac 87 | } 88 | -------------------------------------------------------------------------------- /src/np-doctor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | source "${BASH_SOURCE%/*}/shared/mutexed.sh" 7 | 8 | displayMessage "Configuration: '$configConfigFolder'" 9 | 10 | 11 | displayMessage "External player: '$externalPlayerExec'" 12 | displayMessage "fswatch: '$fswatchExec'" 13 | displayMessage "Shuffler: '$externalShuffleExec'" 14 | displayMessage "Line reverser: '$reverseLineOrderExec'" 15 | displayMessage "Notifications: '$notifyExec'" 16 | displayMessage "Sound details: '$soundDetailsExec'" 17 | 18 | 19 | displayMessage "Mode: '$(cat "$configModeFile")'" 20 | 21 | 22 | displayMessage -n "Daemon: " 23 | if isValidPidFileAndRunningOrCleanup "$configDaemonPidFile"; 24 | then 25 | displayMessage "running (pid $(cat "$configDaemonPidFile" ))" 26 | else 27 | displayMessage "stopped" 28 | fi 29 | 30 | 31 | displayMessage -n "Notifications: " 32 | if isValidPidFileAndRunningOrCleanup "$configNotifyPidFile"; 33 | then 34 | displayMessage "running (pid $(cat "$configNotifyPidFile" ))" 35 | else 36 | displayMessage "stopped" 37 | fi 38 | 39 | 40 | displayMessage -n "External player: " 41 | if isExternalPlayerRunning; 42 | then 43 | displayMessage "running (pid $(cat "$configExternalPlayerPidFile" ))" 44 | 45 | displayMessage -n "Sound: " 46 | highlight "$(cat "$configPlayingFile")" || displayMessage 47 | else 48 | displayMessage "stopped" 49 | fi 50 | 51 | displayMessage -n "Sounds in queue: " 52 | if [[ -e "$configQueueFile" ]]; 53 | then 54 | cat "$configQueueFile" | nullAsNewline wc -l | keepDigitsOnly 55 | # NOTE: newline. 56 | echo "" 57 | else 58 | displayMessage "(queue file missing, expected: '${configQueueFile}')" 59 | fi 60 | 61 | displayMessage -n "Sounds in history: " 62 | if [[ -e "$configHistoryFile" ]]; 63 | then 64 | cat "$configHistoryFile" | nullAsNewline wc -l | keepDigitsOnly 65 | # NOTE: newline. 66 | echo "" 67 | else 68 | displayMessage "(history file missing, expected: '${configHistoryFile}')" 69 | fi 70 | -------------------------------------------------------------------------------- /src/shared/functions/notify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # TODO: add more notifiers. 5 | # TODO: add support for node-notifier when available. 6 | # https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md 7 | # https://github.com/mikaelbr/node-notifier/issues/40 8 | notifyExec="$(getFirstExecutable "terminal-notifier" "growlnotify")" 9 | 10 | killNotifications() { 11 | if [[ -s "$configNotifyPidFile" ]]; 12 | then 13 | killPidFromFileAndWaitUntilDead "$configNotifyPidFile" 14 | fi 15 | 16 | return 0 17 | } 18 | 19 | isNotificationsRunning() { 20 | if isValidPidFileAndRunningOrCleanup "$configNotifyPidFile"; 21 | then 22 | return 0 23 | else 24 | return 1 25 | fi 26 | } 27 | 28 | killNotificationsIfRunning() { 29 | { isNotificationsRunning && killNotifications; } || true &>/dev/null 30 | 31 | return 0 32 | } 33 | 34 | notify() { 35 | local title="$1" 36 | shift 37 | local subtitle="$1" 38 | shift 39 | local message="$1" 40 | shift 41 | 42 | case "$notifyExec" in 43 | 'terminal-notifier') 44 | # Currently not awaiting exit/output as it depends on the configurable timeout. 45 | ( 46 | # NOTE: forcefully close any previous notification, in case it wasn't already gone. 47 | terminal-notifier \ 48 | -remove "np" >/dev/null 49 | 50 | # NOTE: need to escaping the first character of title/subtitle/message; at least special characters such as square brackets '['. 51 | terminal-notifier \ 52 | -group "np" \ 53 | -title "\\${title}" \ 54 | -open "https://github.com/joelpurra/npshell" \ 55 | -subtitle "\\${subtitle}" \ 56 | -message "\\${message}" >/dev/null 57 | ) & 58 | ;; 59 | 'growlnotify') 60 | # https://growl.github.io/growl/ 61 | # Untested. 62 | # Does not seem to support a timeout argument. 63 | growlnotify \ 64 | --noteName "${title} - ${subtitle}" \ 65 | --identifier "np" \ 66 | --url "https://github.com/joelpurra/npshell" \ 67 | --message "$message" >/dev/null 68 | ;; 69 | *) 70 | die "no notifier executable found." 71 | ;; 72 | # TODO: add more notifiers. 73 | esac 74 | } 75 | -------------------------------------------------------------------------------- /src/help/np-help.txt: -------------------------------------------------------------------------------- 1 | npshell np -- command line music queue manager 2 | 3 | Usage: np [arguments...] 4 | np help 5 | 6 | Commands: 7 | 8 | Queue Commands: 9 | now Shows the sound currently playing 10 | list See sounds currently in the queue 11 | add Add some sounds to the queue 12 | clear Empty the queue 13 | clean Remove non-existant files from queue 14 | 15 | Playback Commands: 16 | next Play next sound in the queue 17 | start Start playback 18 | stop Stop playback 19 | startstop Toggle playback 20 | 21 | Other Commands: 22 | daemon Control the playback daemon 23 | notify Control the notification daemon 24 | history Show the 999 most recently played sounds 25 | index Save the list of sounds in the current folder (recursive) 26 | doctor Display configuration, runtime and status values 27 | help Display usage help 28 | 29 | Configuration: 30 | 31 | Settings are read from '~/.np/config.sh'. 32 | The format is one 'setting=value' per line. 33 | 34 | Settings: 35 | configNumsounds 36 | Number of sounds 'np add' adds 37 | 38 | Values: number ("all" not allowed) 39 | Default: 3 40 | 41 | configOrder 42 | Order of files 'np add' adds 43 | 44 | Values: "in-order" or "shuffle" 45 | Default: "shuffle" 46 | 47 | configDebug 48 | Enable debug output 49 | 50 | Values: "true" or "false" 51 | Default: "false" 52 | 53 | configUseCache 54 | Automatically generate index files 55 | for each loaded folder. See also 'np index' 56 | 57 | Values: "true" or "false" 58 | Default: "true" 59 | 60 | configFollowSymlinks 61 | Follow sound file symlinks before passing 62 | the path to the external player. 63 | 64 | Values: "true" or "false" 65 | Default: "false" 66 | 67 | configNotificationTimeout 68 | Time before the sound notifications close automatically. 69 | 70 | Values: a positive integer, or "0" to disable the timeout. 71 | Default: "3" 72 | 73 | See 'np help ' for more information on a specific command. 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [npshell `np` -- command line music queue manager](https://github.com/joelpurra/npshell/) 2 | 3 | Keep a daemon running in the background. Add sounds to a queue. Let the daemon play the sweet music for you. All from the comfort of your own shell. 4 | 5 | 6 |

7 | npshell in action 8 |

9 | 10 | 11 | - Strictly command line based for extra nerd credits. 12 | - Adds sounds from deep folder structures by default. 13 | - Control music playback daemon from any terminal window. 14 | - Displays song paths relative to `$PWD`. 15 | - Optional system notifications displaying the path or id3v2 tags. 16 | - Creates cached/index files to handle large sound libraries, useful on slow disks and network drives. 17 | - Works on Mac OS X, Linux, and similar systems. 18 | 19 | 20 | 21 | ## Installation 22 | 23 | See [INSTALL.md for Linux, other systems, and manual steps](INSTALL.md) as well as how to enable notifications. 24 | 25 | On Mac with [Homebrew](https://brew.sh/): 26 | 27 | ```bash 28 | brew tap joelpurra/joelpurra 29 | brew install npshell 30 | brew services start npshell 31 | ``` 32 | 33 | 34 | 35 | ## Get started 36 | 37 | See [USAGE.md for the full list of command and configuration](USAGE.md) with examples. 38 | 39 | Everyday usage, assuming `np daemon` has been started elsewhere: 40 | 41 | ```bash 42 | cd Music/ # Go to a folder with some sounds. 43 | np add 10 # Add 10 shuffled sounds from current folder hierarchy. 44 | np next # Play next sound. 45 | np # Display the currently playing sound. 46 | np list # List sounds in queue. 47 | # Add an album by folder path. 48 | np add all in-order "Jazz/My Favorite Album/" 49 | ``` 50 | 51 | 52 |
53 | Convenient aliases 54 | 55 | Save a keystroke or two, at least until tab completion is... completed. Add to your `~/.bash_profile` or similar autoexecuted file of your choice. 56 | 57 | ```bash 58 | alias npa='np add' 59 | alias npn='np next' 60 | alias npl='np list' 61 | ``` 62 | 63 |
64 | 65 | --- 66 | 67 | Copyright (c) 2014, 2015, 2016, 2017 [Joel Purra](https://joelpurra.com/). Released under [GNU General Public License version 3.0 (GPL-3.0)](https://www.gnu.org/licenses/gpl.html). 68 | -------------------------------------------------------------------------------- /src/shared/functions/cross-platform.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | externalExecutableExists() { 5 | local executable="$1" 6 | 7 | if builtin type "$executable" &>/dev/null; 8 | then 9 | return 0 10 | else 11 | return 1 12 | fi 13 | } 14 | 15 | getFirstExecutable() { 16 | debug "Finding first executable in \$PATH out of '$@'" 17 | 18 | local path="" 19 | 20 | for executable in "$@"; 21 | do 22 | if externalExecutableExists "$executable"; 23 | then 24 | path="$executable" 25 | break 26 | fi 27 | done 28 | 29 | debug "First executable is '${path}'" 30 | 31 | echo -nE "$path" 32 | } 33 | 34 | # External player detection. 35 | # TODO: confirm and expand list. 36 | externalPlayerExec="$(getFirstExecutable "afplay" "mplayer" "mpg123" "mpg321" "play")" 37 | 38 | # External player detection. 39 | # TODO: confirm and expand list. 40 | externalShuffleExec="$(getFirstExecutable "shuf" "gshuf")" 41 | 42 | # From https://github.com/EtiennePerot/parcimonie.sh/blob/master/parcimonie.sh 43 | # Test for GNU `sed`, or use a `sed` fallback in sedExtRegexp 44 | sedExec=(sed) 45 | if [ "$(echo 'abc' | sed -r 's/abc/def/' 2> /dev/null || true)" == 'def' ]; then 46 | # GNU Linux sed 47 | sedExec+=(-r) 48 | else 49 | # Mac OS X sed 50 | sedExec+=(-E) 51 | fi 52 | 53 | sedExtRegexp() { 54 | "${sedExec[@]}" "$@" 55 | } 56 | 57 | fswatchExec="$(getFirstExecutable "fswatch" "/usr/local/bin/fswatch")" 58 | 59 | waitForFileChange() { 60 | # https://github.com/emcrisostomo/fswatch 61 | # Should cover all systems 62 | [[ -z "$fswatchExec" ]] && die "could not find 'fswatch'" 63 | 64 | debug "Waiting for change in '$@': '$watched'" 65 | 66 | local watched=$("$fswatchExec" --one-event "$@") 67 | 68 | debug "Detected change in '$@': '$watched'" 69 | } 70 | 71 | # https://stackoverflow.com/questions/17878684/best-way-to-get-file-modified-time-in-seconds 72 | # https://stackoverflow.com/a/17907126/ 73 | if stat -c %Y . >/dev/null 2>&1; then 74 | get_modified_time() { stat -c %Y "$1" 2>/dev/null; } 75 | elif stat -f %m . >/dev/null 2>&1; then 76 | get_modified_time() { stat -f %m "$1" 2>/dev/null; } 77 | elif date -r . +%s >/dev/null 2>&1; then 78 | get_modified_time() { stat -r "$1" +%s 2>/dev/null; } 79 | else 80 | echo 'get_modified_time() is unsupported' >&2 81 | get_modified_time() { printf '%s' 0; } 82 | fi 83 | 84 | getLastFileModifiedTime() { 85 | echo $(get_modified_time "$1") 86 | } 87 | -------------------------------------------------------------------------------- /src/shared/functionality.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Debugging configuration defaults. 5 | readonly configDefaultDebug=false 6 | configDebug="$configDefaultDebug" 7 | 8 | 9 | # Fundamental configuration defaults. 10 | readonly configDefaultConfigFolder="${HOME}/.np" 11 | configConfigFolder="$configDefaultConfigFolder" 12 | 13 | configDefaultConfigFile="${configConfigFolder}/config.sh" 14 | configConfigFile="$configDefaultConfigFile" 15 | 16 | ensureConfigFoldersAndFilesExist 17 | 18 | 19 | # Keep track of lock/pid files. 20 | declare -a pidFilesCreatedByThisInstance 21 | declare -a pidsCreatedByThisInstance 22 | declare -a pidMessagesCreatedByThisInstance 23 | 24 | { trap 'onExit' EXIT; } 25 | 26 | 27 | # Lock/pid files defaults. 28 | readonly configDefaultPidFile="${configConfigFolder}/.pidfile~" 29 | configPidFile="$configDefaultPidFile" 30 | 31 | readonly configDefaultDaemonPidFile="${configConfigFolder}/.daemonpidfile~" 32 | configDaemonPidFile="$configDefaultDaemonPidFile" 33 | 34 | readonly configDefaultExternalPlayerPidFile="${configConfigFolder}/.externalplayerpidfile~" 35 | configExternalPlayerPidFile="$configDefaultExternalPlayerPidFile" 36 | 37 | readonly configDefaultNotifyPidFile="${configConfigFolder}/.notifypidfile~" 38 | configNotifyPidFile="$configDefaultNotifyPidFile" 39 | 40 | 41 | # File configuration defaults. 42 | configDefaultQueueFile="${configConfigFolder}/queue.pls" 43 | configQueueFile="$configDefaultQueueFile" 44 | 45 | configDefaultHistoryFile="${configConfigFolder}/history.pls" 46 | configHistoryFile="$configDefaultHistoryFile" 47 | 48 | configDefaultModeFile="${configConfigFolder}/.mode" 49 | configModeFile="$configDefaultModeFile" 50 | 51 | configDefaultPlayingFile="${configConfigFolder}/.playing" 52 | configPlayingFile="$configDefaultPlayingFile" 53 | 54 | ensureOtherFoldersAndFilesExist 55 | 56 | 57 | # Advanced configuration defaults. 58 | readonly configDefaultCacheFileName=".np.cache~" 59 | configCacheFileName="$configDefaultCacheFileName" 60 | 61 | 62 | # Normal configuration defaults. 63 | configDefaultUseCache=true 64 | configUseCache="$configDefaultUseCache" 65 | 66 | configDefaultNumsounds=10 67 | configNumsounds="$configDefaultNumsounds" 68 | 69 | configDefaultOrder="shuffle" 70 | configOrder="$configDefaultOrder" 71 | 72 | configDefaultFollowSymlinks="false" 73 | configFollowSymlinks="$configDefaultFollowSymlinks" 74 | 75 | configDefaultNotificationTimeout="3" 76 | configNotificationTimeout="$configDefaultNotificationTimeout" 77 | 78 | 79 | # Allow $configConfigFile to override above configuration. 80 | readConfig 81 | -------------------------------------------------------------------------------- /src/np-notify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source "${BASH_SOURCE%/*}/shared/functions.sh" 5 | source "${BASH_SOURCE%/*}/shared/functionality.sh" 6 | 7 | if [[ "$1" == "--is-running" ]]; 8 | then 9 | if isNotificationsRunning; 10 | then 11 | exit 0 12 | else 13 | exit 1 14 | fi 15 | fi 16 | 17 | if [[ "$1" == "--stop" ]]; 18 | then 19 | killNotificationsIfRunning 20 | exit 0 21 | fi 22 | 23 | exitIfAlreadyRunningOrCleanup "$configNotifyPidFile" "notify" 24 | savePidButDeleteOnExit "notify" "$$" "$configNotifyPidFile" 25 | 26 | 27 | getNotificationTitleFromSound() { 28 | local sound="$1" 29 | 30 | # Fallback value. 31 | local detail="np" 32 | 33 | local title="$(getSoundDetailArtist "$sound")" 34 | 35 | if [[ ! -z "$title" ]]; 36 | then 37 | detail="$title" 38 | fi 39 | 40 | echo -E "$detail" 41 | } 42 | 43 | getNotificationSubtitleFromSound() { 44 | local sound="$1" 45 | 46 | # Fallback value. 47 | local detail="playing" 48 | 49 | local album="$(getSoundDetailAlbum "$sound")" 50 | 51 | if [[ ! -z "$album" ]]; 52 | then 53 | detail="$album" 54 | fi 55 | 56 | echo -E "$detail" 57 | } 58 | 59 | getNotificationMessageFromSound() { 60 | local sound="$1" 61 | 62 | # Fallback value. 63 | local detail="$sound" 64 | 65 | local title="$(getSoundDetailTitle "$sound")" 66 | 67 | if [[ ! -z "$title" ]]; 68 | then 69 | detail="$title" 70 | fi 71 | 72 | echo -E "$detail" 73 | } 74 | 75 | 76 | checkNotify() { 77 | local mode=$(getMode) 78 | local sound=$(getCurrentSound) 79 | local title 80 | local subtitle 81 | local message 82 | 83 | notificationCount+=1 84 | 85 | # Don't display upon startup if np is stopped. 86 | if (( notificationCount == 1 )) && [[ "$mode" == "stopped" ]]; 87 | then 88 | return 0 89 | fi 90 | 91 | if [[ "$mode" == "playing" && -z "$sound" ]]; 92 | then 93 | # Don't display upon startup if queue is empty. 94 | if (( notificationCount == 1 )); 95 | then 96 | return 0 97 | fi 98 | 99 | title="np" 100 | subtitle="Reached end of queue." 101 | # TODO: encourage user to click on the notification bubble, and add more songs from the default directory if they do? 102 | message="" 103 | else 104 | title="$(getNotificationTitleFromSound "$sound")" 105 | subtitle="$(getNotificationSubtitleFromSound "$sound")" 106 | message="$(getNotificationMessageFromSound "$sound")" 107 | fi 108 | 109 | notify "$title" "$subtitle" "$message" 110 | } 111 | 112 | whenCurrentSoundIsChanged() { 113 | waitForFileChange "$configPlayingFile" 114 | 115 | # NOTE: when changing track there might be several fast changes; sleep a bit to avoid some. 116 | sleep 0.25 117 | } 118 | 119 | notifyLoop() { 120 | while true; 121 | do 122 | checkNotify 123 | whenCurrentSoundIsChanged 124 | done 125 | } 126 | 127 | declare -i notificationCount=0 128 | 129 | notifyLoop 130 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contribute to [npshell](https://github.com/joelpurra/npshell) 2 | 3 | 4 | 5 | This is experimental software, so feedback is most appreciated! 6 | 7 | - You can [open an issue for your problem or suggestion](https://github.com/joelpurra/npshell/issues). 8 | 9 | 10 | 11 | ## Development 12 | 13 | Please install the `git` `pre-commit` hook to get automatic `man np` page generation from `USAGE.md`. This requires [md2man](https://github.com/sunaku/md2man) (`md2man-roff`) to be installed. 14 | 15 | ```bash 16 | pushd .git/hooks 17 | ln -s ../../src/shared/githooks/pre-commit.sh pre-commit 18 | popd 19 | ``` 20 | 21 | 22 | 23 | ## TODO 24 | 25 | - Test cross-platform support, add common music players. 26 | - Installer in different package managers: 27 | - ~~[brew](https://brew.sh/)~~ 28 | - Command tab completion. 29 | - Search the `$PWD` folder tree backwards for `.npconfig` local configuration files? 30 | - Queue: 31 | - ~~Add sounds by piping paths into `np add`: `find sounds | np add all in-order`.~~ 32 | - Add a limit to `np list` (screen size?) or use `less`? 33 | - Improve `np history` limit. 34 | - ~~`np clean` - remove non-existent files from the queue.~~ 35 | - ~~Fix `np add all in-order` so it doesn't add folder paths to the queue.~~ 36 | - ~~Print sound queue number.~~ 37 | - `np prev` (`np add --from-history -1`) or similar to add most recently played sound? 38 | - `np remove #` to remove sound number # in the queue? 39 | - `np next #` (`np skip #`) to skip a number of sounds in the queue. 40 | - `np first` - like `np add`, but add sounds to top of queue. 41 | - ~~Number currently playing sound 0 instead of 1?~~ 42 | - `np default ` mode which reads sounds from when the queue is empty. 43 | - External player: 44 | - ~~Fix unstable/race condition `wait`/`sleep` usage.~~ 45 | - Set up a music player interface to switch out `afplay`. 46 | - Don't `kill -9 afplay`, use another signal? 47 | - Find a signal to pause playback? 48 | - `np daemon`: 49 | - ~~Create a system service wrapper.~~ 50 | - ~~`np daemon --pause` to try to pause/sleep the audio player, make daemon idle?~~ 51 | - ~~`np daemon --resume` to resume an idle daemon?~~ 52 | - Save sound playback position upon pause? 53 | - ~~Maintain np/pause status between `np daemon` executions, so it will resume playing (or not) upon reboot?~~ 54 | - Index/cache files: 55 | - If not using an index and an action takes too long, display a warning message "Using indexes could speed up `np add`"? 56 | - Use `.np.cache~` indexes hierachically when building parent indexes? 57 | - ~~`np index` to index the current folder.~~ 58 | - ~~`np index --force` to reindex the current folder.~~ 59 | - ~~`np index --clean` to remove `.np.cache~`.~~ 60 | - ~~`np index [--force|--clean] --recursive` to perfom action recursively.~~ 61 | - Consider using another language for the entire project -- or at least the process control parts. 62 | -------------------------------------------------------------------------------- /src/shared/functions/filesystem.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | function crossplatformReadlink { 5 | # NOTE: this function has been duplicated elsewhere in this project. 6 | # https://stackoverflow.com/questions/1055671/how-can-i-get-the-behavior-of-gnus-readlink-f-on-a-mac 7 | # https://stackoverflow.com/a/1116890 8 | TARGET_FILE="$1" 9 | 10 | cd "$(dirname "$TARGET_FILE")" 11 | TARGET_FILE="$(basename "$TARGET_FILE")" 12 | 13 | # Iterate down a (possible) chain of symlinks 14 | while [ -L "$TARGET_FILE" ] 15 | do 16 | TARGET_FILE="$(readlink "$TARGET_FILE")" 17 | cd "$(dirname "$TARGET_FILE")" 18 | TARGET_FILE="$(basename "$TARGET_FILE")" 19 | done 20 | 21 | # Compute the canonicalized name by finding the physical path 22 | # for the directory we're in and appending the target file. 23 | PHYS_DIR="$(pwd -P)" 24 | RESULT="${PHYS_DIR}/${TARGET_FILE}" 25 | echo "$RESULT" 26 | } 27 | 28 | resolveDirectory() { 29 | (cd -- "$1"; echo -nE "$PWD") 30 | } 31 | 32 | # Isn't there a better way to do this? String wise ./ and ../ squisher that doesn't re-parse directories/links? 33 | resolvePath() { 34 | if [[ -d "$1" ]]; 35 | then 36 | resolveDirectory "$1" 37 | else 38 | resolveDirectory "$(dirname "$1")/$(basename -a "$1")" 39 | fi 40 | } 41 | 42 | getCwd() { 43 | resolveDirectory "$PWD" 44 | } 45 | 46 | allsounds() { 47 | find . \( -not -path '/.*' \) -name '*.mp3' -print0 48 | } 49 | 50 | generateSoundCache() { 51 | allsounds >"$configCacheFileName" 52 | } 53 | 54 | soundCacheExists() { 55 | if [[ -e "$configCacheFileName" ]]; 56 | then 57 | return 0 58 | else 59 | return 1 60 | fi 61 | } 62 | 63 | getOrGenerateSoundCache() { 64 | if [[ "$configUseCache" == "false" ]]; 65 | then 66 | allsounds 67 | else 68 | soundCacheExists || generateSoundCache 69 | 70 | cat "$configCacheFileName" 71 | fi 72 | } 73 | 74 | deleteSoundCache() { 75 | rm -- "$configCacheFileName" 76 | } 77 | 78 | deleteSoundCacheRecursively() { 79 | find . -name "$configCacheFileName" -delete 80 | } 81 | 82 | absoluteSoundPath() { 83 | local base="$1" 84 | local sound="$2" 85 | 86 | if [[ "${sound:0:1}" == "/" ]]; 87 | then 88 | echo -nE "${sound/#.\/}" 89 | else 90 | echo -nE "${base}/${sound/#.\/}" 91 | fi 92 | 93 | echo -ne "\0" 94 | } 95 | 96 | absoluteSoundPaths() { 97 | local cwd="$(getCwd)" 98 | 99 | # Using read seems to be about 7 times slower than sed - why? 100 | # NOTE: The sed version might be suceptible to improperly escaped characters. 101 | # TODO: Can be written as a sed replace using \0 instead of nullAsNewline? 102 | # nullAsNewline sed -e 's|^./||' -e "s|^|$(echo -ne "${cwd/&/\\&}")/|" 103 | 104 | nullDelimitedForEachWithEOF absoluteSoundPath "$cwd" 105 | } 106 | 107 | getSoundsInFolder() { 108 | getOrGenerateSoundCache | absoluteSoundPaths 109 | } 110 | 111 | getSoundFromFile() { 112 | local cwd="$(getCwd)" 113 | 114 | absoluteSoundPath "$cwd" "$soundPath" 115 | } 116 | 117 | getSoundsFromFolderOrFile() { 118 | local soundPath="$1" 119 | 120 | if [[ -d "$soundPath" ]]; 121 | then 122 | pushd "$soundPath" >/dev/null 123 | getSoundsInFolder 124 | popd >/dev/null 125 | elif [[ -s "$soundPath" ]]; 126 | then 127 | getSoundFromFile "$soundPath" 128 | else 129 | if [[ -L "$soundPath" ]]; 130 | then 131 | errorMessage "could not add the symlink path '${soundPath}' to playlist." 132 | else 133 | errorMessage "could not add the path '${soundPath}' to playlist." 134 | fi 135 | fi 136 | } 137 | 138 | getSounds() { 139 | if (( $# == 0 )); 140 | then 141 | getSoundsInFolder 142 | else 143 | for soundPath in "$@"; 144 | do 145 | if [[ "$soundPath" == "-" ]]; 146 | then 147 | nullDelimitedForEachWithEOF getSoundsFromFolderOrFile 148 | else 149 | getSoundsFromFolderOrFile "$soundPath" 150 | fi 151 | done 152 | fi 153 | } 154 | -------------------------------------------------------------------------------- /src/shared/functions/details.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # TODO: add more sound info tools. 5 | # TODO: use fallback tags -- both id3 v1/v2 and tag names. 6 | # TODO: add other file formats. 7 | soundDetailsExec="$(getFirstExecutable "id3v2")" 8 | 9 | executeId3v2List() { 10 | local sound="$1" 11 | local soundPath="$sound" 12 | 13 | # NOTE: it's unclear why, but apparently id3v2 sometimes can't seem to find the file upon first attempt. 14 | # As subsequent calls work fine, this first call will "warm up the cache" so to speak. 15 | set +e 16 | id3v2 --list "$soundPath" >/dev/null 17 | set -e 18 | 19 | id3v2 --list "$soundPath" 20 | } 21 | 22 | getId3v1Frame() { 23 | local frame="$1" 24 | 25 | sed -E -n -e "/^.*${frame} *: */ { 26 | s/^.*${frame} *: *(.+) .*$/\1/ 27 | s/ *.*// 28 | p 29 | }" | trim 30 | } 31 | 32 | getId3v2Frame() { 33 | local frame="$1" 34 | 35 | sed -E -n -e "/^${frame} / s/^[^:]+: (.*)\$/\\1/ p" | trim 36 | } 37 | 38 | getSoundDetailArtist() { 39 | local sound="$1" 40 | 41 | # Fallback value. 42 | local detail="" 43 | 44 | case "$soundDetailsExec" in 45 | 'id3v2') 46 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TPE1") 47 | 48 | if [[ -z "$detail" ]]; 49 | then 50 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TPE2") 51 | fi 52 | 53 | if [[ -z "$detail" ]]; 54 | then 55 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TPE3") 56 | fi 57 | 58 | if [[ -z "$detail" ]]; 59 | then 60 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TPE4") 61 | fi 62 | 63 | if [[ -z "$detail" ]]; 64 | then 65 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TOPE") 66 | fi 67 | 68 | # NOTE: variations of frames found while testing. 69 | if [[ -z "$detail" ]]; 70 | then 71 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TP1") 72 | fi 73 | 74 | if [[ -z "$detail" ]]; 75 | then 76 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TP2") 77 | fi 78 | 79 | if [[ -z "$detail" ]]; 80 | then 81 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TP3") 82 | fi 83 | 84 | if [[ -z "$detail" ]]; 85 | then 86 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TP4") 87 | fi 88 | 89 | # NOTE: parsing id3v1. 90 | if [[ -z "$detail" ]]; 91 | then 92 | detail=$(executeId3v2List "$sound" | getId3v1Frame "Artist") 93 | fi 94 | ;; 95 | *) 96 | # Do nothing. 97 | ;; 98 | # TODO: add more sound info tools. 99 | esac 100 | 101 | echo -E "$detail" 102 | } 103 | 104 | getSoundDetailAlbum() { 105 | local sound="$1" 106 | 107 | # Fallback value. 108 | local detail="" 109 | 110 | case "$soundDetailsExec" in 111 | 'id3v2') 112 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TALB") 113 | 114 | if [[ -z "$detail" ]]; 115 | then 116 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TOAL") 117 | fi 118 | 119 | if [[ -z "$detail" ]]; 120 | then 121 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TAL") 122 | fi 123 | 124 | # NOTE: parsing id3v1. 125 | if [[ -z "$detail" ]]; 126 | then 127 | detail=$(executeId3v2List "$sound" | getId3v1Frame "Album") 128 | fi 129 | ;; 130 | *) 131 | # Do nothing. 132 | ;; 133 | # TODO: add more sound info tools. 134 | esac 135 | 136 | echo -E "$detail" 137 | } 138 | 139 | getSoundDetailTitle() { 140 | local sound="$1" 141 | 142 | # Fallback value. 143 | local detail="" 144 | 145 | case "$soundDetailsExec" in 146 | 'id3v2') 147 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TIT1") 148 | 149 | if [[ -z "$detail" ]]; 150 | then 151 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TIT2") 152 | fi 153 | 154 | if [[ -z "$detail" ]]; 155 | then 156 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TIT3") 157 | fi 158 | 159 | # NOTE: variations of frames found while testing. 160 | if [[ -z "$detail" ]]; 161 | then 162 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TT1") 163 | fi 164 | 165 | if [[ -z "$detail" ]]; 166 | then 167 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TT2") 168 | fi 169 | 170 | if [[ -z "$detail" ]]; 171 | then 172 | detail=$(executeId3v2List "$sound" | getId3v2Frame "TT3") 173 | fi 174 | 175 | # NOTE: parsing id3v1. 176 | if [[ -z "$detail" ]]; 177 | then 178 | detail=$(executeId3v2List "$sound" | getId3v1Frame "Title") 179 | fi 180 | ;; 181 | *) 182 | # Do nothing. 183 | ;; 184 | # TODO: add more sound info tools. 185 | esac 186 | 187 | echo -E "$detail" 188 | } 189 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # np 1 2017-10-22 1.4.0 npshell Usage 2 | 3 | 4 | 5 | ## Name 6 | 7 | `np` - Command line music queue manager 8 | 9 | 10 | 11 | ## Description 12 | 13 | Keep a daemon running in the background. Add sounds to a queue. Let the daemon play the sweet music for you. All from the comfort of your own shell. 14 | 15 | [npshell repository](https://github.com/joelpurra/npshell/) 16 | 17 | 18 | 19 | ## Commands 20 | 21 | 22 | ### `np daemon [--is-running|--stop|--verbose]` 23 | 24 | Play sounds in queue as soon as there are any. Can be controlled with `np start` and `np stop`, as well as the rest of the queue commands. 25 | 26 | - `--is-running` 27 | - Check if the daemon process has already started. Exits with `0` if it has, `1` otherwise. 28 | - `--stop` 29 | - Stop daemon execution. Can be used during system shutdown, but isn't part of everyday usage. 30 | - `--verbose` 31 | - Output paths to the sounds as they play. 32 | 33 | 34 | **Examples** 35 | 36 | ```bash 37 | # Start the daemon, let it run in the background. 38 | # Should be done at user login. 39 | np daemon --is-running || ( np daemon & ) 40 | ``` 41 | 42 | 43 | 44 | ### `np notify [--is-running|--stop]` 45 | 46 | Show notifications when the track changes, playback is started/stopped or the queue is empty. 47 | If the sound has id3v2 tags, artist/album/title are shown. 48 | 49 | - `--is-running` 50 | - Check if the daemon process has already started. Exits with `0` if it has, `1` otherwise. 51 | - `--stop` 52 | - Stop notification daemon execution. Can be used during system shutdown, but isn't part of everyday usage. 53 | 54 | 55 | **Examples** 56 | 57 | ```bash 58 | # Start the notification daemon, let it run in the background. 59 | # Should be done at user login. 60 | np notify --is-running || ( np notify & ) 61 | ``` 62 | 63 | 64 | 65 | ### `np`, `np now`. 66 | 67 | Shows the sound currently playing. 68 | 69 | 70 | 71 | ### `np list` 72 | 73 | See sounds currently in the queue. 74 | 75 | 76 | 77 | ### `np next` 78 | 79 | Advance to the next sound in the queue. 80 | 81 | 82 | 83 | ### `np add [limit] [order] [path ...]` 84 | 85 | Add some sounds to the queue. 86 | 87 | - Limit 88 | - Default is 3. 89 | - You can also use "all". 90 | 91 | 92 | - Order 93 | - Default is "shuffle". 94 | - You can also use "in-order". 95 | - Path 96 | - Default is `$PWD`. 97 | - You can use paths 98 | - to sounds 99 | - to folders 100 | - or "-" to read null-delimited paths from stdin. 101 | 102 | 103 | **Examples** 104 | 105 | ```bash 106 | # Add the default number of shuffled sounds from the current folder. 107 | np add 108 | 109 | # Add a single sound to the queue. 110 | np add path/to/sound.mp3 111 | 112 | # Add 25 shuffled sounds from the current folder. 113 | np add 25 114 | 115 | # Add the first 2 sounds from the current folder. 116 | np add 2 in-order 117 | 118 | # Add shuffled sounds from a folder, hierarchically. 119 | np add path/to/folder/with/sounds/ 120 | 121 | # Add an album. 122 | np add all in-order "Jazz/My Favorite Album/" 123 | 124 | # Add sounds by null-delimited paths from stdin. 125 | find . -iname '*best of*.mp3' -print0 | np add - 126 | 127 | # Add 10 shuffled sounds from the combined list of a single sound, stdin and a folder. 128 | find . -iname '*best of*.mp3' -print0 | np add 10 path/to/sound.mp3 - path/to/folder/with/sounds/ 129 | ``` 130 | 131 | 132 | 133 | ## More commands 134 | 135 | 136 | 137 | ### `np help [command]` 138 | 139 | Show general help for `np` usage. 140 | 141 | - Command 142 | - Show help about a specific command. 143 | 144 | 145 | 146 | 147 | ### `np start` 148 | 149 | Let `np daemon` consume the sound queue. 150 | 151 | 152 | 153 | ### `np stop` 154 | 155 | Don't let `np daemon` consume the sound queue. 156 | 157 | 158 | 159 | ### `np startstop` 160 | 161 | Toggle playback by alternating between `np start` and `np stop`. 162 | 163 | 164 | 165 | ### `np clear` 166 | 167 | Empty the queue. 168 | 169 | 170 | 171 | ### `np clean` 172 | 173 | Remove non-existant files from queue. 174 | 175 | 176 | 177 | ### `np history` 178 | 179 | Show the 999 most recently played sounds. 180 | 181 | 182 | 183 | ### `np index [--force|--clean[ --recursive]]` 184 | 185 | Create a file with a cached list of all sounds in the current folder, including subfolders. 186 | 187 | - `--force` 188 | - Recreate the index file even if it already exists. 189 | - `--clean` 190 | - Remove index files. 191 | - `--recursive` 192 | - Perform the action in subfolders. 193 | 194 | 195 | 196 | ### `np doctor` 197 | 198 | Display configuration, runtime and status values. 199 | 200 | 201 | 202 | ## Configuration 203 | 204 | Settings are read from `~/.np/config.sh`. The format is one `setting=value` per line. 205 | 206 | 207 | ### `configNumsounds` 208 | 209 | - Default is 3. 210 | - Set the number of sounds `np add` adds unless overridden. 211 | 212 | 213 | ### `configOrder` 214 | 215 | - Default is "shuffle". 216 | - The order `np add` adds files in. 217 | - Can also be "in-order". 218 | 219 | 220 | ### `configDebug` 221 | 222 | - Default is "false". 223 | - Enable debug output. 224 | 225 | 226 | ### `configUseCache` 227 | 228 | - Default is "true". 229 | - Automatically generate index files per folder sounds are loaded from. See `np index`. 230 | 231 | 232 | ### `configFollowSymlinks` 233 | 234 | - Default is "false". 235 | - Follow sound file symlinks before passing the path to the external player. 236 | 237 | 238 | ### `configNotificationTimeout` 239 | 240 | - Default is "3". 241 | - Set to a positive integer, or "0" to manually close the notification. 242 | - Time before the sound notifications close automatically. 243 | -------------------------------------------------------------------------------- /src/shared/man/man1/np.1: -------------------------------------------------------------------------------- 1 | .TH np 1 2017\-10\-22 1.4.0 npshell Usage 2 | .SH Name 3 | .PP 4 | \fB\fCnp\fR \- Command line music queue manager 5 | .SH Description 6 | .PP 7 | Keep a daemon running in the background. Add sounds to a queue. Let the daemon play the sweet music for you. All from the comfort of your own shell. 8 | .PP 9 | npshell repository 10 | \[la]https://github.com/joelpurra/npshell/\[ra] 11 | .SH Commands 12 | .SS \fB\fCnp daemon [\-\-is\-running|\-\-stop|\-\-verbose]\fR 13 | .PP 14 | Play sounds in queue as soon as there are any. Can be controlled with \fB\fCnp start\fR and \fB\fCnp stop\fR, as well as the rest of the queue commands. 15 | .RS 16 | .IP \(bu 2 17 | \fB\fC\-\-is\-running\fR 18 | .RS 19 | .IP \(bu 2 20 | Check if the daemon process has already started. Exits with \fB\fC0\fR if it has, \fB\fC1\fR otherwise. 21 | .RE 22 | .IP \(bu 2 23 | \fB\fC\-\-stop\fR 24 | .RS 25 | .IP \(bu 2 26 | Stop daemon execution. Can be used during system shutdown, but isn't part of everyday usage. 27 | .RE 28 | .IP \(bu 2 29 | \fB\fC\-\-verbose\fR 30 | .RS 31 | .IP \(bu 2 32 | Output paths to the sounds as they play. 33 | .RE 34 | .RE 35 | .PP 36 | \fBExamples\fP 37 | .PP 38 | .RS 39 | .nf 40 | # Start the daemon, let it run in the background. 41 | # Should be done at user login. 42 | np daemon \-\-is\-running || ( np daemon & ) 43 | .fi 44 | .RE 45 | .SS \fB\fCnp notify [\-\-is\-running|\-\-stop]\fR 46 | .PP 47 | Show notifications when the track changes, playback is started/stopped or the queue is empty. 48 | If the sound has id3v2 tags, artist/album/title are shown. 49 | .RS 50 | .IP \(bu 2 51 | \fB\fC\-\-is\-running\fR 52 | .RS 53 | .IP \(bu 2 54 | Check if the daemon process has already started. Exits with \fB\fC0\fR if it has, \fB\fC1\fR otherwise. 55 | .RE 56 | .IP \(bu 2 57 | \fB\fC\-\-stop\fR 58 | .RS 59 | .IP \(bu 2 60 | Stop notification daemon execution. Can be used during system shutdown, but isn't part of everyday usage. 61 | .RE 62 | .RE 63 | .PP 64 | \fBExamples\fP 65 | .PP 66 | .RS 67 | .nf 68 | # Start the notification daemon, let it run in the background. 69 | # Should be done at user login. 70 | np notify \-\-is\-running || ( np notify & ) 71 | .fi 72 | .RE 73 | .SS \fB\fCnp\fR, \fB\fCnp now\fR\&. 74 | .PP 75 | Shows the sound currently playing. 76 | .SS \fB\fCnp list\fR 77 | .PP 78 | See sounds currently in the queue. 79 | .SS \fB\fCnp next\fR 80 | .PP 81 | Advance to the next sound in the queue. 82 | .SS \fB\fCnp add [limit] [order] [path ...]\fR 83 | .PP 84 | Add some sounds to the queue. 85 | .RS 86 | .IP \(bu 2 87 | Limit 88 | .RS 89 | .IP \(bu 2 90 | Default is 3. 91 | .IP \(bu 2 92 | You can also use "all". 93 | .RE 94 | .IP \(bu 2 95 | Order 96 | .RS 97 | .IP \(bu 2 98 | Default is "shuffle". 99 | .IP \(bu 2 100 | You can also use "in\-order". 101 | .RE 102 | .IP \(bu 2 103 | Path 104 | .RS 105 | .IP \(bu 2 106 | Default is \fB\fC$PWD\fR\&. 107 | .IP \(bu 2 108 | You can use paths 109 | .IP \(bu 2 110 | to sounds 111 | .IP \(bu 2 112 | to folders 113 | .IP \(bu 2 114 | or "\-" to read null\-delimited paths from stdin. 115 | .RE 116 | .RE 117 | .PP 118 | \fBExamples\fP 119 | .PP 120 | .RS 121 | .nf 122 | # Add the default number of shuffled sounds from the current folder. 123 | np add 124 | # Add a single sound to the queue. 125 | np add path/to/sound.mp3 126 | # Add 25 shuffled sounds from the current folder. 127 | np add 25 128 | # Add the first 2 sounds from the current folder. 129 | np add 2 in\-order 130 | # Add shuffled sounds from a folder, hierarchically. 131 | np add path/to/folder/with/sounds/ 132 | # Add an album. 133 | np add all in\-order "Jazz/My Favorite Album/" 134 | # Add sounds by null\-delimited paths from stdin. 135 | find . \-iname '*best of*.mp3' \-print0 | np add \- 136 | # Add 10 shuffled sounds from the combined list of a single sound, stdin and a folder. 137 | find . \-iname '*best of*.mp3' \-print0 | np add 10 path/to/sound.mp3 \- path/to/folder/with/sounds/ 138 | .fi 139 | .RE 140 | .SH More commands 141 | .SS \fB\fCnp help [command]\fR 142 | .PP 143 | Show general help for \fB\fCnp\fR usage. 144 | .RS 145 | .IP \(bu 2 146 | Command 147 | .RS 148 | .IP \(bu 2 149 | Show help about a specific command. 150 | .RE 151 | .RE 152 | .SS \fB\fCnp start\fR 153 | .PP 154 | Let \fB\fCnp daemon\fR consume the sound queue. 155 | .SS \fB\fCnp stop\fR 156 | .PP 157 | Don't let \fB\fCnp daemon\fR consume the sound queue. 158 | .SS \fB\fCnp startstop\fR 159 | .PP 160 | Toggle playback by alternating between \fB\fCnp start\fR and \fB\fCnp stop\fR\&. 161 | .SS \fB\fCnp clear\fR 162 | .PP 163 | Empty the queue. 164 | .SS \fB\fCnp clean\fR 165 | .PP 166 | Remove non\-existant files from queue. 167 | .SS \fB\fCnp history\fR 168 | .PP 169 | Show the 999 most recently played sounds. 170 | .SS \fB\fCnp index [\-\-force|\-\-clean[ \-\-recursive]]\fR 171 | .PP 172 | Create a file with a cached list of all sounds in the current folder, including subfolders. 173 | .RS 174 | .IP \(bu 2 175 | \fB\fC\-\-force\fR 176 | .RS 177 | .IP \(bu 2 178 | Recreate the index file even if it already exists. 179 | .RE 180 | .IP \(bu 2 181 | \fB\fC\-\-clean\fR 182 | .RS 183 | .IP \(bu 2 184 | Remove index files. 185 | .RE 186 | .IP \(bu 2 187 | \fB\fC\-\-recursive\fR 188 | .RS 189 | .IP \(bu 2 190 | Perform the action in subfolders. 191 | .RE 192 | .RE 193 | .SS \fB\fCnp doctor\fR 194 | .PP 195 | Display configuration, runtime and status values. 196 | .SH Configuration 197 | .PP 198 | Settings are read from \fB\fC~/.np/config.sh\fR\&. The format is one \fB\fCsetting=value\fR per line. 199 | .SS \fB\fCconfigNumsounds\fR 200 | .RS 201 | .IP \(bu 2 202 | Default is 3. 203 | .IP \(bu 2 204 | Set the number of sounds \fB\fCnp add\fR adds unless overridden. 205 | .RE 206 | .SS \fB\fCconfigOrder\fR 207 | .RS 208 | .IP \(bu 2 209 | Default is "shuffle". 210 | .IP \(bu 2 211 | The order \fB\fCnp add\fR adds files in. 212 | .IP \(bu 2 213 | Can also be "in\-order". 214 | .RE 215 | .SS \fB\fCconfigDebug\fR 216 | .RS 217 | .IP \(bu 2 218 | Default is "false". 219 | .IP \(bu 2 220 | Enable debug output. 221 | .RE 222 | .SS \fB\fCconfigUseCache\fR 223 | .RS 224 | .IP \(bu 2 225 | Default is "true". 226 | .IP \(bu 2 227 | Automatically generate index files per folder sounds are loaded from. See \fB\fCnp index\fR\&. 228 | .RE 229 | .SS \fB\fCconfigFollowSymlinks\fR 230 | .RS 231 | .IP \(bu 2 232 | Default is "false". 233 | .IP \(bu 2 234 | Follow sound file symlinks before passing the path to the external player. 235 | .RE 236 | .SS \fB\fCconfigNotificationTimeout\fR 237 | .RS 238 | .IP \(bu 2 239 | Default is "3". 240 | .IP \(bu 2 241 | Set to a positive integer, or "0" to manually close the notification. 242 | .IP \(bu 2 243 | Time before the sound notifications close automatically. 244 | .RE 245 | -------------------------------------------------------------------------------- /src/shared/functions/processes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | onExit() { 5 | local pidFileContents="(they are all empty)" 6 | (( "${#pidFilesCreatedByThisInstance[@]}" > 0 )) && { local pidFileContents=$(paste -d ' ' "${pidFilesCreatedByThisInstance[@]}"); } 7 | 8 | # Kill own child processes 9 | #killPidChildren "$$" 10 | 11 | debug "EXIT: trapped ${pidMessagesCreatedByThisInstance[@]}" 12 | debug "EXIT: pid files contents: ${pidFileContents}" 13 | (( "${#pidFilesCreatedByThisInstance[@]}" > 0 )) && rm "${pidFilesCreatedByThisInstance[@]}" 14 | debug "EXIT: deleted ${pidFilesCreatedByThisInstance[@]}" 15 | 16 | exit 17 | } 18 | 19 | isPidRunning() { 20 | local pid="$1" 21 | local psInfo=$(ps -o pid= -p "${pid}" | awk '{ print $1 }') 22 | 23 | if (( psInfo == pid )); 24 | then 25 | return 0 26 | else 27 | return 1 28 | fi 29 | } 30 | 31 | pidFromFile() { 32 | local pidFile="$1" 33 | 34 | debug "Getting PID from file '${pidFile}'" 35 | 36 | if [[ -e "$pidFile" ]]; 37 | then 38 | cat "$pidFile" 39 | else 40 | # die "could not get pid from non-existant file '${pidFile}'" 41 | echo -n "" 42 | fi 43 | } 44 | 45 | isValidPidFile() { 46 | local pidFile="$1" 47 | 48 | if [[ -s "$pidFile" ]]; 49 | then 50 | local pid=$(pidFromFile "$pidFile") 51 | 52 | if (( pid > 0 )); 53 | then 54 | return 0 55 | else 56 | return 1 57 | fi 58 | else 59 | return 2 60 | fi 61 | } 62 | 63 | isPidRunningFromFile() { 64 | local pidFile="$1" 65 | local pid=$(pidFromFile "$pidFile") 66 | 67 | [[ -z "$pid" ]] && die "could not get the pid to check if it's running." 68 | 69 | isPidRunning "$pid" 70 | } 71 | 72 | isValidPidFileAndRunning() { 73 | local pidFile="$1" 74 | 75 | if isValidPidFile "$pidFile" && isPidRunningFromFile "$pidFile"; 76 | then 77 | return 0 78 | else 79 | return 1 80 | fi 81 | } 82 | 83 | cleanupPidFile() { 84 | local pidFile="$1" 85 | 86 | rm -f -- "$pidFile" 87 | 88 | if [[ -e "$pidFile" ]]; 89 | then 90 | die "could not remove pid file '$pidFile'" 91 | fi 92 | 93 | return 0 94 | } 95 | 96 | isValidPidFileAndRunningOrCleanup() { 97 | local pidFile="$1" 98 | 99 | if isValidPidFileAndRunning "$pidFile"; 100 | then 101 | return 0 102 | else 103 | if [[ -e "$pidFile" ]]; 104 | then 105 | debug "removing dead pid file '$pidFile'" 106 | 107 | cleanupPidFile "$pidFile" 108 | fi 109 | 110 | return 1 111 | fi 112 | } 113 | 114 | exitIfAlreadyRunning() { 115 | local pidFile="$1" 116 | local pidDescriptor="$2" 117 | 118 | isValidPidFileAndRunning "$pidFile" && die "'${pidDescriptor}' is already running with pid '$(cat "$pidFile")' according to '${pidFile}'." 119 | 120 | return 0 121 | } 122 | 123 | # exitIfAlreadyRunningUnlessParent() { 124 | # local pidFile="$1" 125 | # local pidDescriptor="$2" 126 | # [[ -e "$pidFile" ]] && [[ -z "$PPID" || "$PPID" != $(cat "$pidFile") ]] && exitIfAlreadyRunning "$pidFile" "$pidDescriptor" 127 | # return 0 128 | # } 129 | 130 | exitIfAlreadyRunningOrCleanup() { 131 | local pidFile="$1" 132 | local pidDescriptor="$2" 133 | 134 | isValidPidFileAndRunningOrCleanup "$pidFile" && die "'${pidDescriptor}' is already running with pid '$(cat "$pidFile")' according to '${pidFile}'." 135 | 136 | return 0 137 | } 138 | 139 | savePidAtIndexButDeleteOnExit() { 140 | local index="$1" 141 | local name="$2" 142 | local pid="$3" 143 | local pidFile="$4" 144 | [[ -e "$pidFile" ]] && die "${pid} tried to save ${pidFile} but it already exists and contains $(cat "${pidFile}")" 145 | debug "creating ${name} ${pid} ${pidFile}" 146 | echo -nE "$pid" >"$pidFile" 147 | debug "created ${name} ${pid} ${pidFile} ($(cat "${pidFile}"))" 148 | 149 | pidFilesCreatedByThisInstance[index]="$pidFile" 150 | pidsCreatedByThisInstance[index]="$pid" 151 | pidMessagesCreatedByThisInstance[index]="(${name} ${pid} ${pidFile})" 152 | debug "${#pidFilesCreatedByThisInstance[@]} pidFilesCreatedByThisInstance: ${pidFilesCreatedByThisInstance[@]}" 153 | debug "${#pidsCreatedByThisInstance[@]} pidsCreatedByThisInstance: ${pidsCreatedByThisInstance[@]}" 154 | debug "${#pidMessagesCreatedByThisInstance[@]} pidMessagesCreatedByThisInstance: ${pidMessagesCreatedByThisInstance[@]}" 155 | 156 | return 0 157 | } 158 | 159 | clearPidAtIndex() { 160 | local index="$1" 161 | 162 | unset pidFilesCreatedByThisInstance[index] 163 | unset pidsCreatedByThisInstance[index] 164 | unset pidMessagesCreatedByThisInstance[index] 165 | debug "${#pidFilesCreatedByThisInstance[@]} pidFilesCreatedByThisInstance: ${pidFilesCreatedByThisInstance[@]}" 166 | debug "${#pidsCreatedByThisInstance[@]} pidsCreatedByThisInstance: ${pidsCreatedByThisInstance[@]}" 167 | debug "${#pidMessagesCreatedByThisInstance[@]} pidMessagesCreatedByThisInstance: ${pidMessagesCreatedByThisInstance[@]}" 168 | 169 | return 0 170 | } 171 | 172 | savePidButDeleteOnExit() { 173 | local index="${#pidFilesCreatedByThisInstance[@]}" 174 | savePidAtIndexButDeleteOnExit "$index" "$1" "$2" "$3" 175 | } 176 | 177 | killPid() { 178 | local pid="$1" 179 | 180 | debug "about to kill '${pid}'" 181 | kill "${pid}" 182 | debug "killed '${pid}'" 183 | } 184 | 185 | killPidAndWaitUntilDead() { 186 | local pid="$1" 187 | 188 | killPid "${pid}" 189 | debug "waiting for '${pid}' to die" 190 | wait "$pid" &>/dev/null 191 | } 192 | 193 | killPidChildren() { 194 | # TODO: use an existing killtree kind of command to avoid orphans/zombies. 195 | local pid="$1" 196 | # debug killing "${pid}"-s children 197 | # ps -fg "${pid}" >&2 198 | read -a children < <(ps -fg "${pid}" | sed -e '1 d' | awk '{ print $2 " " $3 }' | sed "/^${pid}/ d" | awk '{ print $1 }') 199 | 200 | debug "about to kill '${pid}' children '${children[@]}'" 201 | for child in "${children[@]}"; 202 | do 203 | killPid "${child}" 204 | done 205 | debug "killed '${pid}' children '${children[@]}'" 206 | } 207 | 208 | killPidFromFile() { 209 | local pidFile="$1" 210 | local pid=$(pidFromFile "$pidFile") 211 | 212 | [[ -z "${pid}" ]] && die "could not get the pid to kill." 213 | 214 | debug "about to kill '${pid}' from '${pidFile}'" 215 | killPid "$pid" 216 | debug "killed '${pid}' from '${pidFile}'" 217 | } 218 | 219 | killPidFromFileAndWaitUntilDead() { 220 | local pidFile="$1" 221 | local pid=$(pidFromFile "$pidFile") 222 | 223 | [[ -z "${pid}" ]] && die "could not get the pid to kill and wait until dead." 224 | 225 | debug "about to kill '${pid}' from '${pidFile}' and wait until dead" 226 | killPidAndWaitUntilDead "$pid" 227 | debug "killed '${pid}' from '${pidFile}' and waited until dead" 228 | } 229 | 230 | killChildrenFromFile() { 231 | local pidFile="$1" 232 | local pid=$(pidFromFile "$pidFile") 233 | 234 | [[ -z "${pid}" ]] && die "could not get the pid to kill child process from." 235 | 236 | killPidChildren "${pid}" 237 | } 238 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------