#!/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