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>