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>