File:  [Coherent Logic Development] / plm / pl
Revision 1.4: download - view: text, annotated - select for diffs
Sun Apr 27 18:53:36 2025 UTC (5 weeks, 1 day ago) by snw
Branches: MAIN
CVS tags: HEAD
Add pl man page

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