Annotation of plm/pl, revision 1.3

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

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