Annotation of plm/pl, revision 1.4

1.1       snw         1: #!/usr/bin/env bash
                      2: 
1.2       snw         3: #
1.4     ! snw         4: # $Id: pl,v 1.3 2025/04/27 16:29:20 snw Exp $
1.2       snw         5: #  plm primary script
                      6: #
                      7: #  Copyright (C) 2025 Serena Willis
                      8: #
1.3       snw         9: #   $Log: pl,v $
1.4     ! snw        10: #   Revision 1.3  2025/04/27 16:29:20  snw
        !            11: #   Fix SPDX license identifiers for REUSE
        !            12: #
1.3       snw        13: #   Revision 1.2  2025/04/27 16:27:49  snw
                     14: #   Add CVS keywords
                     15: #
1.2       snw        16: #
                     17: # SPDX-FileCopyrightText: (C) 2025 Serena Willis
1.3       snw        18: # SPDX-License-Identifier: AGPL-3.0-or-later
1.1       snw        19: 
                     20: BLACK=$(tput setaf 0)
                     21: RED=$(tput setaf 1)
                     22: GREEN=$(tput setaf 2)
                     23: YELLOW=$(tput setaf 3)
                     24: LIME_YELLOW=$(tput setaf 190)
                     25: POWDER_BLUE=$(tput setaf 153)
                     26: BLUE=$(tput setaf 4)
                     27: MAGENTA=$(tput setaf 5)
                     28: CYAN=$(tput setaf 6)
                     29: WHITE=$(tput setaf 7)
                     30: BRIGHT=$(tput bold)
                     31: NORMAL=$(tput sgr0)
                     32: BLINK=$(tput blink)
                     33: REVERSE=$(tput smso)
                     34: UNDERLINE=$(tput smul)
                     35: 
1.4     ! snw        36: BASEDIR=""
1.1       snw        37: CHOSEN=""
                     38: REPL=""
                     39: SHUFFLE=0
                     40: REPEAT=0
                     41: 
1.4     ! snw        42: [[ -f /etc/default/pl ]] && . /etc/default/pl
        !            43: [[ -f ${HOME}/.plrc ]] && . ${HOME}/.plrc
        !            44: [[ -f ${HOME}/.config/plm/defaults ]] && . ${HOME}/.config/plm/defaults
        !            45: 
1.1       snw        46: function usage {
                     47:     echo "usage:"
1.4     ! snw        48:     echo "pl [OPTIONS...] --check|--repair|--play|--sync|--merge|--help --input=[playlist1,...] [--output=playlist]"
1.1       snw        49:     exit 1
                     50: }
                     51: 
                     52: function playsong {
                     53:     local SONG="$1"
                     54: 
                     55:     echo "Playing ${BRIGHT}${SONG}${NORMAL}..."
                     56:     ffplay -loglevel fatal -nodisp "${SONG}"
                     57: }
                     58: 
                     59: function rdl {
                     60:     local FILE=$1
                     61:     local CHOICE=""
                     62:     
                     63:     echo "Would you like to:"
                     64:     echo
                     65:     echo "  ${BRIGHT}r${NORMAL})eplace the missing file in the playlist"
                     66:     echo "  ${BRIGHT}d${NORMAL})elete the missing file from the playlist"
                     67:     echo "  ${BRIGHT}l${NORMAL})eave the missing file in the playlist"
                     68:     echo
                     69:     echo -n '> '
                     70:     read -n1 CHOICE < /dev/tty
                     71: 
                     72:     if [[ ${CHOICE} != "r" ]] && [[ ${CHOICE} != "d" ]] && [[ ${CHOICE} != "l" ]]
                     73:     then
                     74:         echo "pl: ${RED}must choose r, d, or l"
                     75:         CHOSEN=""
                     76:     else
                     77:         CHOSEN=$CHOICE
                     78:     fi
                     79: }
                     80: 
                     81: function replprompt {
                     82:     local FILE=$1
                     83:     
                     84:     CHOSEN=""
                     85:     REPL=""
                     86:     
                     87:     while [[ ${CHOSEN} == "" ]]
                     88:     do
                     89:         rdl "${FILE}"
                     90:     done
                     91: 
                     92:     case "${CHOSEN}" in
                     93:         r)
                     94:             NEWFILE=$(zenity --file-selection --title="Replace ${FILE}" --filename="${BASEDIR}/" 2>/dev/null)
                     95:             REPL="${NEWFILE:$BASEDIRLEN}"
                     96:             ;;
                     97:         d)
                     98:             REPL=""
                     99:             ;;
                    100:         l)
                    101:             REPL=$FILE
                    102:             ;;
                    103:     esac
                    104:    
                    105: }
                    106: 
                    107: VERBOSE=0
                    108: ACTION=""
                    109: INPUT=""
                    110: OUTPUT=""
                    111: OVERWRITE=0
                    112: COPY=0
                    113: SHUFFLE=0
                    114: REPEAT=0
                    115: SYNC=""
                    116: DELETE=""
                    117: 
                    118: 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' -- "$@")
                    119: eval set -- "$TEMP"
                    120: 
                    121: while true
                    122: do
                    123:     case "$1" in
                    124:         -h|--help)
                    125:             usage
                    126:             ;;
                    127:         -V|--overwrite)
                    128:             OVERWRITE=1
                    129:             shift
                    130:             ;;
                    131:         -v|--verbose)
                    132:             VERBOSE=1
                    133:             shift
                    134:             ;;
                    135:         -c|--check)
                    136:             ACTION="check"
                    137:             COPY=0
                    138:             shift
                    139:             ;;
                    140:         -r|--repair)
                    141:             ACTION="repair"
                    142:             COPY=1
                    143:             shift
                    144:             ;;
                    145:         -b|--basedir)
                    146:             BASEDIR=$2
                    147:             shift 2
                    148:             ;;
                    149:         -i|--input)
                    150:             INPUT="${INPUT} $(echo $2 | tr ',' ' ')"
                    151:             shift 2
                    152:             ;;
                    153:         -o|--output)
                    154:             OUTPUT="$2"
                    155:             shift 2
                    156:             ;;
                    157:         -m|--merge)
                    158:             ACTION="merge"
                    159:             COPY=1
                    160:             shift
                    161:             ;;
                    162:         -p|--play)
                    163:             ACTION="play"
                    164:             COPY=0
                    165:             shift
                    166:             ;;
                    167:         -s|--shuffle)
                    168:             SHUFFLE=1
                    169:             shift
                    170:             ;;
                    171:         -R|--repeat)
                    172:             REPEAT=1
                    173:             shift
                    174:             ;;
                    175:         -S|--sync)
                    176:             ACTION="sync"
                    177:             SYNC=$2
                    178:             shift 2
                    179:             ;;
                    180:         -d|--delete)
                    181:             DELETE=$2
                    182:             shift 2
                    183: 
                    184:             if [[ "${DELETE}" != "all" ]] && [[ "${DELETE}" != "update" ]]
                    185:             then
                    186:                 echo "pl: must specify --delete=all or --delete=update"
                    187:                 exit 13
                    188:             fi
                    189:             
                    190:             ;;
                    191:         --)
                    192:             shift
                    193:             break
                    194:             ;;
                    195:     esac
                    196: done
                    197: 
1.4     ! snw       198: if [[ "${BASEDIR}" == "" ]]
        !           199: then
        !           200:     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"
        !           201:     exit 1
        !           202: fi
        !           203: 
1.1       snw       204: BASEDIRLEN=$(echo ${BASEDIR} | wc -c)
                    205: 
                    206: if [[ ${ACTION} == "" ]]
                    207: then
                    208:     usage
                    209: fi
                    210: 
                    211: if [[ ${ACTION} != "check" ]] && [[ ${ACTION} != "merge" ]] && [[ ${ACTION} != "repair" ]] && [[ ${ACTION} != "play" ]] && [[ ${ACTION} != "sync" ]]
                    212: then
                    213:     echo "pl: must supply --check, --repair, --play, --sync, or --merge"
                    214:     usage
                    215: fi
                    216: 
                    217: if [[ ${ACTION} != "sync" ]] && [[ ${DELETE} != "" ]]
                    218: then
                    219:     echo "pl: --delete only allowed with --sync"
                    220:     exit 14
                    221: fi
                    222: 
                    223: if [[ "${INPUT}" == "" ]]
                    224: then
                    225:     echo "pl: must supply --input"
                    226:     exit 3
                    227: fi
                    228: 
                    229: if [[ ${ACTION} == "merge" ]] && [[ "${OUTPUT}" == "" ]]
                    230: then
                    231:     echo "pl: must supply --output with --merge"
                    232:     exit 4
                    233: fi
                    234: 
                    235: if [[ ${ACTION} == "repair" ]] && [[ "${OUTPUT}" == "" ]]
                    236: then
                    237:     echo "pl: must supply --output with --repair"
                    238:     exit 10
                    239: fi
                    240: 
                    241: if [[ ${ACTION} == "check" ]] && [[ "${OUTPUT}" != "" ]]
                    242: then
                    243:     echo "pl: --output not supported with --check"
                    244:     exit 5
                    245: fi
                    246: 
                    247: if [[ -f "${OUTPUT}" ]] && [[ ${OVERWRITE} != 1 ]]
                    248: then
                    249:     echo "pl: output file exists; pass --overwrite to overwrite"
                    250:     exit 11
                    251: fi
                    252: 
                    253: if [[ ${ACTION} == "sync" ]] && [[ ! -d "${SYNC}" ]]
                    254: then
                    255:     echo "pl: sync destination ${SYNC} does not exist or is not a directory"
                    256:     exit 12
                    257: fi
                    258: 
                    259: for FILE in ${INPUT}
                    260: do
                    261:     if [[ ! -f "${FILE}" ]]
                    262:     then
                    263:         echo "pl: input file ${FILE} does not exist"
                    264:         exit 6
                    265:     fi
                    266: 
                    267:     FIRSTLINE=$(cat "${FILE}" | head -1)
                    268:     if [[ "${FIRSTLINE}" != "#EXTM3U" ]]
                    269:     then
                    270:         echo "pl: input file ${FILE} is not a valid m3u playlist"
                    271:         exit 7
                    272:     fi    
                    273: done
                    274: 
                    275: TOTOK=0
                    276: TOTMISSING=0
                    277: TOTFILES=0
                    278: TOTLISTS=0
                    279: 
                    280: [[ $COPY == 1 ]] && echo "#EXTM3U" > "${OUTPUT}"
                    281: 
                    282: if [[ ${ACTION} == "play" ]]
                    283: then
                    284:     PLAYFILE=$(mktemp)
                    285: fi
                    286: 
                    287: if [[ ${ACTION} == "sync" ]]
                    288: then
                    289:     echo "pl: initializing sync operation to ${SYNC}"
                    290:     
                    291:     # $SYNC == sync destination
                    292:     # $DELETE == all means to delete everything in that location
                    293:     # $DELETE == update means to only delete what was removed from the playlists
                    294: 
                    295:     SYNCFILE="${SYNC}/pl.sync"
                    296: 
                    297:     if [[ ${DELETE} == "update" ]]
                    298:     then
                    299:         
                    300:         if [[ -f "${SYNCFILE}" ]]
                    301:         then
                    302:             echo "pl: sync file found at ${SYNCFILE}"
                    303:         else
                    304:             echo "pl: no sync file found at ${SYNCFILE} - cannot --delete=update"
                    305:             exit 20
                    306:         fi
                    307: 
                    308:     fi
                    309: 
                    310:     if [[ ${DELETE} == "all" ]]
                    311:     then
                    312:         echo "${RED}${BRIGHT}CAUTION: THIS WILL PERMANENTLY DELETE EVERYTHING IN ${SYNC}!!!!${NORMAL}"
                    313:         read -p "Type CONFIRM to confirm: " CONFIRM
                    314: 
                    315:         if [[ ${CONFIRM} != "CONFIRM" ]]
                    316:         then
                    317:             echo "pl: sync cancelled on user request"
                    318:             exit 21
                    319:         fi
                    320: 
                    321:         read -n1 -p "Are you sure? (y/n) " CONFIRM
                    322:         echo
                    323: 
                    324:         if [[ $CONFIRM != "y" ]] && [[ $CONFIRM != "Y" ]]
                    325:         then
                    326:             echo "pl: sync cancelled on user request"
                    327:             exit 21
                    328:         fi
                    329: 
                    330:         rm -rfv "${SYNC}/*"
                    331:         echo "DATE: $(date +%Y%m%d%H%M%S)" > "${SYNCFILE}"
                    332:         echo "PLAYLISTS: ${INPUT}" >> "${SYNCFILE}"
                    333:         echo "DELETE: ${DELETE}" >> "${SYNCFILE}"
                    334:         echo "SYNC: ${SYNC}" >> "${SYNCFILE}"
                    335:     fi
                    336:     
                    337:     
                    338: fi
                    339: 
                    340: for FILE in ${INPUT}
                    341: do
                    342:     if [[ ${ACTION} == "sync" ]]
                    343:     then
                    344:         echo "pl: copying ${FILE} to ${SYNC}"
                    345:         cp "${FILE}" "${SYNC}/"
                    346:     fi
                    347: 
                    348:     if [[ ${VERBOSE} == 1 ]]
                    349:     then
                    350:         echo "${BRIGHT}Playlist:${NORMAL}${MAGENTA}     ${FILE}${NORMAL}"
                    351:         echo
                    352:         printf "%-8s %-50s %-80s %-10s\n" "LENGTH" "TITLE" "PATH" "STATUS"
                    353:         printf "%-8s %-50s %-80s %-10s\n" "======" "=====" "====" "======"
                    354:     fi
                    355:     
                    356:     TOTLISTS=$(($TOTLISTS+1))
                    357:     FILOK=0
                    358:     FILMISSING=0
                    359:     FILFILES=0
                    360:     while read LINE
                    361:     do
                    362:         if [[ ${LINE:0:1} == "#" ]]
                    363:         then
                    364:             
                    365:             if [[ ${LINE:0:7} == "#EXTINF" ]]
                    366:             then
                    367:                 INF=${LINE}
                    368:                 TAGPART=$(echo ${LINE} | cut -d: -f2)
                    369:                 SECS=$(echo ${TAGPART} | cut -d, -f1)
                    370:                 NAME=$(echo ${TAGPART} | cut -d, -f2)
                    371:             fi
                    372:             
                    373:         else
                    374:             
                    375:             FILFILES=$(($FILFILES+1))
                    376:             TOTFILES=$(($TOTFILES+1))
                    377:             AUPATH=$LINE
                    378: 
                    379:             if [[ ! -f "${AUPATH}" ]]
                    380:             then
                    381: 
                    382:                 LSTAT="[${BRIGHT}${RED}MISSING${NORMAL}]"
                    383:                 FILMISSING=$(($FILMISSING+1))
                    384:                 TOTMISSING=$(($TOTMISSING+1))
                    385:                 MISSING=1
                    386:                 
                    387:                 if [[ ${COPY} == 1 ]] && [[ ${ACTION} == "repair" ]]
                    388:                 then
                    389:                     replprompt "${AUPATH}"
                    390:                     
                    391:                     if [[ "${REPL}" != "" ]]
                    392:                     then
                    393:                         echo "${INF}" >> "${OUTPUT}"
                    394:                         echo "${REPL}" >> "${OUTPUT}"
                    395:                     fi
                    396:                 fi                
                    397:                 
                    398:             else
                    399: 
                    400:                 if [[ ${COPY} == 1 ]]
                    401:                 then
                    402:                     echo "${INF}" >> "${OUTPUT}"
                    403:                     echo "${AUPATH}" >> "${OUTPUT}"
                    404:                 fi
                    405: 
                    406:                 if [[ ${ACTION} == "play" ]]
                    407:                 then
                    408:                     echo "${BASEDIR}/${AUPATH}" >> "${PLAYFILE}"
                    409:                 fi
                    410: 
                    411:                 if [[ ${ACTION} == "sync" ]]
                    412:                 then
                    413:                     CPSRC="${BASEDIR}/${AUPATH}"
                    414:                     CPDST="${SYNC}/${AUPATH}"
                    415:                     DSTDIR="${CPDST%/*}"
                    416:                     echo "${AUPATH}" >> "${SYNCFILE}"
                    417:                     echo "pl: creating ${DSTDIR}"
                    418:                     mkdir -p "${DSTDIR}"
                    419:                     echo "pl: copying ${CPSRC} to ${CPDST}"
                    420:                     cp -r "${CPSRC}" "${CPDST}"
                    421:                 fi
                    422:                 
                    423:                 FILOK=$(($FILOK+1))
                    424:                 TOTOK=$(($TOTOK+1))
                    425:                 LSTAT="[${BRIGHT}${GREEN}OK${NORMAL}]"
                    426:                 MISSING=0
                    427:                 
                    428:             fi
                    429: 
                    430:             if [[ $VERBOSE == 1 ]]
                    431:             then
                    432:                 printf "%-8s %-50s %-80s %-10s\n" "${SECS}" "${NAME:0:48}" "${AUPATH:0:78}" "${LSTAT}"
                    433:             else
                    434:                 if [[ ${MISSING} == 1 ]]
                    435:                 then                    
                    436:                     [[ ${REPAIR} == 1 ]] && echo
                    437:                     echo "${GREEN}${NAME}${NORMAL} is ${RED}missing${NORMAL}"
                    438:                 fi
                    439:             fi
                    440:             
                    441:         fi
                    442:     done < "${FILE}"
                    443:     echo "Missing ${RED}${FILMISSING}/${FILFILES}${NORMAL} files in ${BRIGHT}${FILE}${NORMAL}" 
                    444: done
                    445: 
                    446: case $ACTION in
                    447:     check|repair)
                    448:         echo "Processed ${GREEN}${TOTLISTS} playlists${NORMAL}, and found ${RED}${TOTMISSING} files missing${NORMAL} out of ${GREEN}${TOTFILES} referenced files${NORMAL}."
                    449:         ;;
                    450:     merge)
                    451:         echo "Merged ${GREEN}${TOTLISTS} playlists (${INPUT:1})${NORMAL} into playlist ${GREEN}${OUTPUT}${NORMAL}."
                    452:         ;;
                    453:     play)
                    454:         echo "Playing $(cat ${PLAYFILE} | wc -l) files in ${PLAYFILE}"
                    455:         ;;
                    456: esac
                    457: 
                    458: if [[ ${ACTION} == "play" ]]
                    459: then
                    460:     declare -a PLAYLIST
                    461: 
                    462:     while read F
                    463:     do
                    464:         PLAYLIST+=("${F}")
                    465:     done < "${PLAYFILE}"
                    466: 
                    467:     rm -f "${PLAYFILE}"
                    468:     
                    469:     if [[ ${SHUFFLE} == 0 ]]
                    470:     then
                    471: 
                    472:         if [[ ${REPEAT} == 0 ]]
                    473:         then
                    474:             for SONG in "${PLAYLIST[@]}"
                    475:             do
                    476:                 playsong "${SONG}"
                    477:             done
                    478:         else
                    479:             while true
                    480:             do
                    481:                 for SONG in "${PLAYLIST[@]}"
                    482:                 do
                    483:                     playsong "${SONG}"
                    484:                 done
                    485:             done
                    486:         fi                
                    487:        
                    488:     else
                    489: 
                    490:         while true
                    491:         do
                    492:             SONG=${PLAYLIST[$RANDOM % ${#PLAYLIST[@]}]}
                    493:             playsong "${SONG}"
                    494:         done
                    495:         
                    496:     fi
                    497: fi

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>