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

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

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