#!/usr/bin/env bash
#
# $Id: pl,v 1.4 2025/04/27 18:53:36 snw Exp $
# plm primary script
#
# Copyright (C) 2025 Serena Willis
#
# $Log: pl,v $
# Revision 1.4 2025/04/27 18:53:36 snw
# Add pl man page
#
# Revision 1.3 2025/04/27 16:29:20 snw
# Fix SPDX license identifiers for REUSE
#
# Revision 1.2 2025/04/27 16:27:49 snw
# Add CVS keywords
#
#
# SPDX-FileCopyrightText: (C) 2025 Serena Willis
# SPDX-License-Identifier: AGPL-3.0-or-later
BLACK=$(tput setaf 0)
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
LIME_YELLOW=$(tput setaf 190)
POWDER_BLUE=$(tput setaf 153)
BLUE=$(tput setaf 4)
MAGENTA=$(tput setaf 5)
CYAN=$(tput setaf 6)
WHITE=$(tput setaf 7)
BRIGHT=$(tput bold)
NORMAL=$(tput sgr0)
BLINK=$(tput blink)
REVERSE=$(tput smso)
UNDERLINE=$(tput smul)
BASEDIR=""
CHOSEN=""
REPL=""
SHUFFLE=0
REPEAT=0
[[ -f /etc/default/pl ]] && . /etc/default/pl
[[ -f ${HOME}/.plrc ]] && . ${HOME}/.plrc
[[ -f ${HOME}/.config/plm/defaults ]] && . ${HOME}/.config/plm/defaults
function usage {
echo "usage:"
echo "pl [OPTIONS...] --check|--repair|--play|--sync|--merge|--help --input=[playlist1,...] [--output=playlist]"
exit 1
}
function playsong {
local SONG="$1"
echo "Playing ${BRIGHT}${SONG}${NORMAL}..."
ffplay -loglevel fatal -nodisp "${SONG}"
}
function rdl {
local FILE=$1
local CHOICE=""
echo "Would you like to:"
echo
echo " ${BRIGHT}r${NORMAL})eplace the missing file in the playlist"
echo " ${BRIGHT}d${NORMAL})elete the missing file from the playlist"
echo " ${BRIGHT}l${NORMAL})eave the missing file in the playlist"
echo
echo -n '> '
read -n1 CHOICE < /dev/tty
if [[ ${CHOICE} != "r" ]] && [[ ${CHOICE} != "d" ]] && [[ ${CHOICE} != "l" ]]
then
echo "pl: ${RED}must choose r, d, or l"
CHOSEN=""
else
CHOSEN=$CHOICE
fi
}
function replprompt {
local FILE=$1
CHOSEN=""
REPL=""
while [[ ${CHOSEN} == "" ]]
do
rdl "${FILE}"
done
case "${CHOSEN}" in
r)
NEWFILE=$(zenity --file-selection --title="Replace ${FILE}" --filename="${BASEDIR}/" 2>/dev/null)
REPL="${NEWFILE:$BASEDIRLEN}"
;;
d)
REPL=""
;;
l)
REPL=$FILE
;;
esac
}
VERBOSE=0
ACTION=""
INPUT=""
OUTPUT=""
OVERWRITE=0
COPY=0
SHUFFLE=0
REPEAT=0
SYNC=""
DELETE=""
TEMP=$(getopt -o hvci:o:mb:rVpsRS:d: --long help,verbose,check,input:,output:,merge,basedir:,repair,overwrite,play,shuffle,repeat,sync:,delete: -n 'pl' -- "$@")
eval set -- "$TEMP"
while true
do
case "$1" in
-h|--help)
usage
;;
-V|--overwrite)
OVERWRITE=1
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-c|--check)
ACTION="check"
COPY=0
shift
;;
-r|--repair)
ACTION="repair"
COPY=1
shift
;;
-b|--basedir)
BASEDIR=$2
shift 2
;;
-i|--input)
INPUT="${INPUT} $(echo $2 | tr ',' ' ')"
shift 2
;;
-o|--output)
OUTPUT="$2"
shift 2
;;
-m|--merge)
ACTION="merge"
COPY=1
shift
;;
-p|--play)
ACTION="play"
COPY=0
shift
;;
-s|--shuffle)
SHUFFLE=1
shift
;;
-R|--repeat)
REPEAT=1
shift
;;
-S|--sync)
ACTION="sync"
SYNC=$2
shift 2
;;
-d|--delete)
DELETE=$2
shift 2
if [[ "${DELETE}" != "all" ]] && [[ "${DELETE}" != "update" ]]
then
echo "pl: must specify --delete=all or --delete=update"
exit 13
fi
;;
--)
shift
break
;;
esac
done
if [[ "${BASEDIR}" == "" ]]
then
echo "pl: must supply a configuration in /etc/default/pl, $HOME/.plrc, $HOME/.config/pl/defaults, define the \$PL_BASEDIR environment variable, or specify the --basedir option"
exit 1
fi
BASEDIRLEN=$(echo ${BASEDIR} | wc -c)
if [[ ${ACTION} == "" ]]
then
usage
fi
if [[ ${ACTION} != "check" ]] && [[ ${ACTION} != "merge" ]] && [[ ${ACTION} != "repair" ]] && [[ ${ACTION} != "play" ]] && [[ ${ACTION} != "sync" ]]
then
echo "pl: must supply --check, --repair, --play, --sync, or --merge"
usage
fi
if [[ ${ACTION} != "sync" ]] && [[ ${DELETE} != "" ]]
then
echo "pl: --delete only allowed with --sync"
exit 14
fi
if [[ "${INPUT}" == "" ]]
then
echo "pl: must supply --input"
exit 3
fi
if [[ ${ACTION} == "merge" ]] && [[ "${OUTPUT}" == "" ]]
then
echo "pl: must supply --output with --merge"
exit 4
fi
if [[ ${ACTION} == "repair" ]] && [[ "${OUTPUT}" == "" ]]
then
echo "pl: must supply --output with --repair"
exit 10
fi
if [[ ${ACTION} == "check" ]] && [[ "${OUTPUT}" != "" ]]
then
echo "pl: --output not supported with --check"
exit 5
fi
if [[ -f "${OUTPUT}" ]] && [[ ${OVERWRITE} != 1 ]]
then
echo "pl: output file exists; pass --overwrite to overwrite"
exit 11
fi
if [[ ${ACTION} == "sync" ]] && [[ ! -d "${SYNC}" ]]
then
echo "pl: sync destination ${SYNC} does not exist or is not a directory"
exit 12
fi
for FILE in ${INPUT}
do
if [[ ! -f "${FILE}" ]]
then
echo "pl: input file ${FILE} does not exist"
exit 6
fi
FIRSTLINE=$(cat "${FILE}" | head -1)
if [[ "${FIRSTLINE}" != "#EXTM3U" ]]
then
echo "pl: input file ${FILE} is not a valid m3u playlist"
exit 7
fi
done
TOTOK=0
TOTMISSING=0
TOTFILES=0
TOTLISTS=0
[[ $COPY == 1 ]] && echo "#EXTM3U" > "${OUTPUT}"
if [[ ${ACTION} == "play" ]]
then
PLAYFILE=$(mktemp)
fi
if [[ ${ACTION} == "sync" ]]
then
echo "pl: initializing sync operation to ${SYNC}"
# $SYNC == sync destination
# $DELETE == all means to delete everything in that location
# $DELETE == update means to only delete what was removed from the playlists
SYNCFILE="${SYNC}/pl.sync"
if [[ ${DELETE} == "update" ]]
then
if [[ -f "${SYNCFILE}" ]]
then
echo "pl: sync file found at ${SYNCFILE}"
else
echo "pl: no sync file found at ${SYNCFILE} - cannot --delete=update"
exit 20
fi
fi
if [[ ${DELETE} == "all" ]]
then
echo "${RED}${BRIGHT}CAUTION: THIS WILL PERMANENTLY DELETE EVERYTHING IN ${SYNC}!!!!${NORMAL}"
read -p "Type CONFIRM to confirm: " CONFIRM
if [[ ${CONFIRM} != "CONFIRM" ]]
then
echo "pl: sync cancelled on user request"
exit 21
fi
read -n1 -p "Are you sure? (y/n) " CONFIRM
echo
if [[ $CONFIRM != "y" ]] && [[ $CONFIRM != "Y" ]]
then
echo "pl: sync cancelled on user request"
exit 21
fi
rm -rfv "${SYNC}/*"
echo "DATE: $(date +%Y%m%d%H%M%S)" > "${SYNCFILE}"
echo "PLAYLISTS: ${INPUT}" >> "${SYNCFILE}"
echo "DELETE: ${DELETE}" >> "${SYNCFILE}"
echo "SYNC: ${SYNC}" >> "${SYNCFILE}"
fi
fi
for FILE in ${INPUT}
do
if [[ ${ACTION} == "sync" ]]
then
echo "pl: copying ${FILE} to ${SYNC}"
cp "${FILE}" "${SYNC}/"
fi
if [[ ${VERBOSE} == 1 ]]
then
echo "${BRIGHT}Playlist:${NORMAL}${MAGENTA} ${FILE}${NORMAL}"
echo
printf "%-8s %-50s %-80s %-10s\n" "LENGTH" "TITLE" "PATH" "STATUS"
printf "%-8s %-50s %-80s %-10s\n" "======" "=====" "====" "======"
fi
TOTLISTS=$(($TOTLISTS+1))
FILOK=0
FILMISSING=0
FILFILES=0
while read LINE
do
if [[ ${LINE:0:1} == "#" ]]
then
if [[ ${LINE:0:7} == "#EXTINF" ]]
then
INF=${LINE}
TAGPART=$(echo ${LINE} | cut -d: -f2)
SECS=$(echo ${TAGPART} | cut -d, -f1)
NAME=$(echo ${TAGPART} | cut -d, -f2)
fi
else
FILFILES=$(($FILFILES+1))
TOTFILES=$(($TOTFILES+1))
AUPATH=$LINE
if [[ ! -f "${AUPATH}" ]]
then
LSTAT="[${BRIGHT}${RED}MISSING${NORMAL}]"
FILMISSING=$(($FILMISSING+1))
TOTMISSING=$(($TOTMISSING+1))
MISSING=1
if [[ ${COPY} == 1 ]] && [[ ${ACTION} == "repair" ]]
then
replprompt "${AUPATH}"
if [[ "${REPL}" != "" ]]
then
echo "${INF}" >> "${OUTPUT}"
echo "${REPL}" >> "${OUTPUT}"
fi
fi
else
if [[ ${COPY} == 1 ]]
then
echo "${INF}" >> "${OUTPUT}"
echo "${AUPATH}" >> "${OUTPUT}"
fi
if [[ ${ACTION} == "play" ]]
then
echo "${BASEDIR}/${AUPATH}" >> "${PLAYFILE}"
fi
if [[ ${ACTION} == "sync" ]]
then
CPSRC="${BASEDIR}/${AUPATH}"
CPDST="${SYNC}/${AUPATH}"
DSTDIR="${CPDST%/*}"
echo "${AUPATH}" >> "${SYNCFILE}"
echo "pl: creating ${DSTDIR}"
mkdir -p "${DSTDIR}"
echo "pl: copying ${CPSRC} to ${CPDST}"
cp -r "${CPSRC}" "${CPDST}"
fi
FILOK=$(($FILOK+1))
TOTOK=$(($TOTOK+1))
LSTAT="[${BRIGHT}${GREEN}OK${NORMAL}]"
MISSING=0
fi
if [[ $VERBOSE == 1 ]]
then
printf "%-8s %-50s %-80s %-10s\n" "${SECS}" "${NAME:0:48}" "${AUPATH:0:78}" "${LSTAT}"
else
if [[ ${MISSING} == 1 ]]
then
[[ ${REPAIR} == 1 ]] && echo
echo "${GREEN}${NAME}${NORMAL} is ${RED}missing${NORMAL}"
fi
fi
fi
done < "${FILE}"
echo "Missing ${RED}${FILMISSING}/${FILFILES}${NORMAL} files in ${BRIGHT}${FILE}${NORMAL}"
done
case $ACTION in
check|repair)
echo "Processed ${GREEN}${TOTLISTS} playlists${NORMAL}, and found ${RED}${TOTMISSING} files missing${NORMAL} out of ${GREEN}${TOTFILES} referenced files${NORMAL}."
;;
merge)
echo "Merged ${GREEN}${TOTLISTS} playlists (${INPUT:1})${NORMAL} into playlist ${GREEN}${OUTPUT}${NORMAL}."
;;
play)
echo "Playing $(cat ${PLAYFILE} | wc -l) files in ${PLAYFILE}"
;;
esac
if [[ ${ACTION} == "play" ]]
then
declare -a PLAYLIST
while read F
do
PLAYLIST+=("${F}")
done < "${PLAYFILE}"
rm -f "${PLAYFILE}"
if [[ ${SHUFFLE} == 0 ]]
then
if [[ ${REPEAT} == 0 ]]
then
for SONG in "${PLAYLIST[@]}"
do
playsong "${SONG}"
done
else
while true
do
for SONG in "${PLAYLIST[@]}"
do
playsong "${SONG}"
done
done
fi
else
while true
do
SONG=${PLAYLIST[$RANDOM % ${#PLAYLIST[@]}]}
playsong "${SONG}"
done
fi
fi
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>