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>