Annotation of plm/pl, revision 1.2

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

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