From a20d23ad5eba4dde34eb6461c6ead01ec03aae33 Mon Sep 17 00:00:00 2001 From: yage Date: Wed, 19 Feb 2025 21:58:15 -0700 Subject: [PATCH] media scwrypts v5 refactor --- scwrypts/media/.config/env.yaml | 8 + scwrypts/media/README.md | 5 + scwrypts/media/audio/audio.module.zsh | 5 + scwrypts/media/audio/play-sfx | 14 ++ scwrypts/media/audio/play-sfx.module.zsh | 79 ++++++++++ .../audio/pulseaudio/pulseaudio.module.zsh | 42 +++++ scwrypts/media/audio/pulseaudio/volume | 12 ++ .../media/audio/pulseaudio/volume.module.zsh | 93 +++++++++++ scwrypts/media/cloud/cloud.module.zsh | 14 ++ scwrypts/media/cloud/synchronize | 58 +++++++ .../media/cloud/synchronize-target.module.zsh | 88 +++++++++++ scwrypts/media/cloud/synchronize.module.zsh | 43 ++++++ .../media/cloud/zshparse/actions.module.zsh | 37 +++++ .../media/cloud/zshparse/zshparse.module.zsh | 5 + scwrypts/media/ffmpeg/ffmpeg.module.zsh | 9 ++ .../get-audio-clip-from-video.module.zsh | 145 ++++++++++++++++++ .../get-video-length-seconds.module.zsh | 20 +++ scwrypts/media/media.scwrypts.zsh | 2 + scwrypts/media/youtube/download | 39 +++++ scwrypts/media/youtube/download.module.zsh | 63 ++++++++ scwrypts/media/youtube/get-audio-clip | 38 +++++ .../youtube/get-download-path.module.zsh | 7 + .../media/youtube/get-filename.module.zsh | 12 ++ scwrypts/media/youtube/youtube.module.zsh | 15 ++ scwrypts/media/youtube/yt-dlp.module.zsh | 15 ++ 25 files changed, 868 insertions(+) create mode 100644 scwrypts/media/.config/env.yaml create mode 100644 scwrypts/media/README.md create mode 100644 scwrypts/media/audio/audio.module.zsh create mode 100755 scwrypts/media/audio/play-sfx create mode 100644 scwrypts/media/audio/play-sfx.module.zsh create mode 100644 scwrypts/media/audio/pulseaudio/pulseaudio.module.zsh create mode 100755 scwrypts/media/audio/pulseaudio/volume create mode 100644 scwrypts/media/audio/pulseaudio/volume.module.zsh create mode 100644 scwrypts/media/cloud/cloud.module.zsh create mode 100755 scwrypts/media/cloud/synchronize create mode 100644 scwrypts/media/cloud/synchronize-target.module.zsh create mode 100644 scwrypts/media/cloud/synchronize.module.zsh create mode 100644 scwrypts/media/cloud/zshparse/actions.module.zsh create mode 100644 scwrypts/media/cloud/zshparse/zshparse.module.zsh create mode 100644 scwrypts/media/ffmpeg/ffmpeg.module.zsh create mode 100644 scwrypts/media/ffmpeg/get-audio-clip-from-video.module.zsh create mode 100644 scwrypts/media/ffmpeg/get-video-length-seconds.module.zsh create mode 100644 scwrypts/media/media.scwrypts.zsh create mode 100755 scwrypts/media/youtube/download create mode 100644 scwrypts/media/youtube/download.module.zsh create mode 100755 scwrypts/media/youtube/get-audio-clip create mode 100644 scwrypts/media/youtube/get-download-path.module.zsh create mode 100644 scwrypts/media/youtube/get-filename.module.zsh create mode 100644 scwrypts/media/youtube/youtube.module.zsh create mode 100644 scwrypts/media/youtube/yt-dlp.module.zsh diff --git a/scwrypts/media/.config/env.yaml b/scwrypts/media/.config/env.yaml new file mode 100644 index 0000000..024b499 --- /dev/null +++ b/scwrypts/media/.config/env.yaml @@ -0,0 +1,8 @@ +--- +media-sync: + .DESCRIPTION: >- + s3 bucket name and filesystem targets for media backups + s3-bucket: + .ENVIRONMENT: MEDIA_SYNC__S3_BUCKET + targets: + .ENVIRONMENT: MEDIA_SYNC__TARGETS diff --git a/scwrypts/media/README.md b/scwrypts/media/README.md new file mode 100644 index 0000000..1bf7b5d --- /dev/null +++ b/scwrypts/media/README.md @@ -0,0 +1,5 @@ +# ZSH Scwrypts +[![Generic Badge](https://img.shields.io/badge/ytdl--org-youtube--dl-informational.svg)](https://github.com/ytdl-org/youtube-dl) +
+ +Quick wrappers for downloading and trimming YouTube videos. diff --git a/scwrypts/media/audio/audio.module.zsh b/scwrypts/media/audio/audio.module.zsh new file mode 100644 index 0000000..3b02dcd --- /dev/null +++ b/scwrypts/media/audio/audio.module.zsh @@ -0,0 +1,5 @@ +# +# +# + +# diff --git a/scwrypts/media/audio/play-sfx b/scwrypts/media/audio/play-sfx new file mode 100755 index 0000000..090c433 --- /dev/null +++ b/scwrypts/media/audio/play-sfx @@ -0,0 +1,14 @@ +#!/usr/bin/env zsh +##################################################################### + +use audio/play-sfx --group media + +##################################################################### + +media.audio.play-sfx.parse.usage + +##################################################################### + +MAIN() { + media.audio.play-sfx $@ +} diff --git a/scwrypts/media/audio/play-sfx.module.zsh b/scwrypts/media/audio/play-sfx.module.zsh new file mode 100644 index 0000000..2b382b5 --- /dev/null +++ b/scwrypts/media/audio/play-sfx.module.zsh @@ -0,0 +1,79 @@ +##################################################################### + +use notify +use scwrypts/get-realpath + +DEPENDENCIES+=(canberra-gtk-play) +REQUIRED_ENV+=(DESKTOP__SFX_PATH) + +##################################################################### + +${scwryptsmodule}() { + local SCWRYPTS_NOTIFICATION_ENGINES=(echo notify.desktop) + + eval "$(utils.parse.autosetup)" + + ########################################## + + echo.status 'starting playback' + canberra-gtk-play -f "${SFX_FILE}" \ + && echo.success "finished output of '${SFX_FILE}'" \ + || notify.error "something went wrong playing file '${SFX_FILE}'" \ + || return 1 +} + +##################################################################### + +${scwryptsmodule}.parse() { + [[ ${POSITIONAL_ARGS} -eq 0 ]] || return 0 + + ((POSITIONAL_ARGS+=1)) + case $1 in + ( backlight ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-audio-volume-change.oga" ;; + ( gamedock ) SFX_FILE="${DESKTOP__SFX_PATH}/gamedock.oga" ;; + ( homedock ) SFX_FILE="${DESKTOP__SFX_PATH}/homedock.oga" ;; + ( login ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-desktop-login.oga" ;; + ( logout ) SFX_FILE="${DESKTOP__SFX_PATH}/smooth-desktop-login.oga" ;; + ( mute ) SFX_FILE="${DESKTOP__SFX_PATH}/smooth-dialog-warning.oga" ;; + ( notify ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-complete.oga" ;; + ( undock ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-desktop-login.oga" ;; + ( volume ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-message.oga" ;; + + ( * ) + SFX_FILE="$1" + ;; + esac + + return 1 +} + +${scwryptsmodule}.parse.locals() { + local SFX_FILE +} + +${scwryptsmodule}.parse.usage() { + USAGE__description=' + play the indicated sound effect by mapped name or filename + + mapped names : + backlight notify + login logout + mute volume + gamedock homedock undock + ' + + USAGE__args=' + \$1 mapped name or filename + ' +} + +${scwryptsmodule}.parse.validate() { + [ -f "$(scwrypts.get-realpath "${SFX_FILE}")" ] \ + && SFX_FILE="$(scwrypts.get-realpath "${SFX_FILE}")" \ + || SFX_FILE="${DESKTOP__SFX_PATH}/${SFX_FILE}" \ + ; + + [ -f "${SFX_FILE}" ] \ + || notify.error "unable to locate sfx file '$1'" \ + || return 1 +} diff --git a/scwrypts/media/audio/pulseaudio/pulseaudio.module.zsh b/scwrypts/media/audio/pulseaudio/pulseaudio.module.zsh new file mode 100644 index 0000000..0392b64 --- /dev/null +++ b/scwrypts/media/audio/pulseaudio/pulseaudio.module.zsh @@ -0,0 +1,42 @@ +##################################################################### + +DEPENDENCIES+=(canberra-gtk-play) +REQUIRED_ENV+=() + +use notify + +##################################################################### + +media.audio.play-sfx() { + local SCWRYPTS_NOTIFICATION_ENGINES=(echo notify.desktop) + local SFX_FILE + case $1 in + ( volume ) SFX_FILE=$DESKTOP__SFX_PATH/yaru-message.oga ;; + ( mute ) SFX_FILE=$DESKTOP__SFX_PATH/smooth-dialog-warning.oga ;; + ( backlight ) SFX_FILE=$DESKTOP__SFX_PATH/yaru-audio-volume-change.oga ;; + ( login ) SFX_FILE=$DESKTOP__SFX_PATH/yaru-desktop-login.oga ;; + ( logout ) SFX_FILE=$DESKTOP__SFX_PATH/smooth-desktop-login.oga ;; + ( notify ) SFX_FILE=$DESKTOP__SFX_PATH/yaru-complete.oga ;; + ( undock ) SFX_FILE=$DESKTOP__SFX_PATH/yaru-desktop-login.oga ;; + ( homedock ) SFX_FILE=$DESKTOP__SFX_PATH/homedock.oga ;; + ( gamedock ) SFX_FILE=$DESKTOP__SFX_PATH/gamedock.oga ;; + + * ) SFX_FILE="$1" + ;; + esac + + [ ! -f $SFX_FILE ] && SFX_FILE="$DESKTOP__SFX_PATH/$SFX_FILE" + + [ -f $SFX_FILE ] \ + && echo.status "detected file '$SFX_FILE'" \ + || notify.error "unable to locate sfx file '$1'" \ + || return 1 \ + ; + + echo.status 'starting playback' + canberra-gtk-play -f "$SFX_FILE" \ + && echo.success "finished output of '$SFX_FILE'" \ + || notify.error "something went wrong playing file '$SFX_FILE'" \ + || return 1 \ + ; +} diff --git a/scwrypts/media/audio/pulseaudio/volume b/scwrypts/media/audio/pulseaudio/volume new file mode 100755 index 0000000..823037a --- /dev/null +++ b/scwrypts/media/audio/pulseaudio/volume @@ -0,0 +1,12 @@ +#!/bin/zsh +##################################################################### + +use audio/pulseaudio/volume --group media + +##################################################################### + +media.audio.pulseaudio.volume.parse.usage + +##################################################################### + +MAIN() { media.audio.pulseaudio.volume $@; } diff --git a/scwrypts/media/audio/pulseaudio/volume.module.zsh b/scwrypts/media/audio/pulseaudio/volume.module.zsh new file mode 100644 index 0000000..852f39c --- /dev/null +++ b/scwrypts/media/audio/pulseaudio/volume.module.zsh @@ -0,0 +1,93 @@ +#!/bin/zsh +##################################################################### + +use notify +use audio/play-sfx --group media + +DEPENDENCIES+=(pactl) + +##################################################################### + +${scwryptsmodule}() { + local SCWRYPTS_NOTIFICATION_ENGINES=(echo notify.desktop) + eval "$(utils.parse.autosetup)" + ########################################## + + case ${COMMAND} in + ( up ) + pactl set-${DEVICE}-volume ${PACTL_DEVICE} +10% \ + || notify.error "pactl error with set-${DEVICE}-volume" + + media.audio.play-sfx volume + ;; + + ( down ) + pactl set-${DEVICE}-volume ${PACTL_DEVICE} -10% \ + || notify.error "pactl error with set-${DEVICE}-volume" + + media.audio.play-sfx volume + ;; + + ( mute ) + pactl set-${DEVICE}-mute ${PACTL_DEVICE} toggle \ + && notify.success "default ${DEVICE}" "$(amixer sget ${AMIXER_DEVICE} | grep -q '\[on\]' && echo unmuted || echo muted)" \ + || notify.error "pactl error with set-${DEVICE}-mute" + + media.audio.play-sfx mute + ;; + esac + + return ${ERRORS} +} + +##################################################################### + +${scwryptsmodule}.parse() { return 0; } +${scwryptsmodule}.parse.locals() { + local ARGS=() + + local DEVICE + local AMIXER_DEVICE + local PACTL_DEVICE + local COMMAND +} + +${scwryptsmodule}.parse.usage() { + USAGE__description=' + simplified pactl for volume up/down/mute on default sink and source + ' + + USAGE__args=' + \$1 target device : one of (sink source) + \$2 volume command : one of (up down mute) + ' +} + +${scwryptsmodule}.parse.validate() { + DEVICE="${ARGS[1]}" + [ "${DEVICE}" ] \ + || notify.error 'missing device' + + COMMAND="${ARGS[2]}" + [ "${COMMAND}" ] \ + || notify.error 'missing command' + + [ "${DEVICE}" ] && [ "${COMMAND}" ] || return + + case ${DEVICE} in + ( sink ) AMIXER_DEVICE=Master ;; + ( source ) AMIXER_DEVICE=Capture ;; + ( * ) + notify.error "unsupported device '${DEVICE}'" + ;; + esac + + case ${COMMAND} in + ( up | down | mute ) ;; + ( * ) + notify.error "unsupported command '${COMMAND}'" + ;; + esac + + PACTL_DEVICE="@DEFAULT_$(echo ${DEVICE} | tr '[:lower:]' '[:upper:]')@" +} diff --git a/scwrypts/media/cloud/cloud.module.zsh b/scwrypts/media/cloud/cloud.module.zsh new file mode 100644 index 0000000..752776d --- /dev/null +++ b/scwrypts/media/cloud/cloud.module.zsh @@ -0,0 +1,14 @@ +# +# cloud media synchronization tools +# + + +# synchronize cloud media with configured targets +use cloud/synchronize --group media + +# synchronize cloud media with a specific target +use cloud/synchronize-target --group media + + +# common parsers +use cloud/zshparse --group media diff --git a/scwrypts/media/cloud/synchronize b/scwrypts/media/cloud/synchronize new file mode 100755 index 0000000..c87a519 --- /dev/null +++ b/scwrypts/media/cloud/synchronize @@ -0,0 +1,58 @@ +#!/usr/bin/env zsh +##################################################################### + +use cloud --group media + +##################################################################### + +media.cloud.zshparse.actions.usage + +USAGE__options+=' + --target local/remote target to synchronize (optional) +' + +USAGE__description=' + synchronize local media with an S3 bucket; *-synchronize actions + will perform a pull/push back-to-back in the indicated order +' + +##################################################################### + +MAIN() { + local \ + TARGET \ + ACTION \ + PARSERS=( + media.cloud.zshparse.actions + ) + + eval "$ZSHPARSEARGS" + + ########################################## + + case ${TARGET} in + ( '' ) + media.cloud.synchronize --action "${ACTION}" + ;; + ( * ) + media.cloud.synchronize-target --action "${ACTION}" --target "${TARGET}" + ;; + esac +} + +##################################################################### + +MAIN.parse() { + # local TARGET + local PARSED=0 + + case $1 in + ( --target ) + PARSED=2 + TARGET="$2" + ;; + esac + + return $PARSED +} + diff --git a/scwrypts/media/cloud/synchronize-target.module.zsh b/scwrypts/media/cloud/synchronize-target.module.zsh new file mode 100644 index 0000000..9976cd8 --- /dev/null +++ b/scwrypts/media/cloud/synchronize-target.module.zsh @@ -0,0 +1,88 @@ +##################################################################### + +REQUIRED_ENV+=(MEDIA_SYNC__S3_BUCKET) + +use cloud/aws +use cloud/zshparse/actions --group media + +##################################################################### + +${scwryptsmodule}() { + local \ + TARGET TARGET_CLOUD TARGET_LOCAL \ + ACTION \ + PARSERS=( + media.cloud.zshparse.actions + ) + + eval "$ZSHPARSEARGS" + + ########################################## + + local A B + case $ACTION in + ( push ) A="$TARGET_LOCAL"; B="$TARGET_CLOUD" ;; + ( pull ) A="$TARGET_CLOUD"; B="$TARGET_LOCAL" ;; + + ( pull-first-synchronize ) + : \ + && media.cloud.synchronize-target \ + --target "${TARGET}" \ + --action pull \ + && media.cloud.synchronize-target \ + --target "${TARGET}" \ + --action push \ + && return 0 \ + || return 1 \ + ; + ;; + + ( push-first-synchronize ) + : \ + && media.cloud.synchronize-target \ + --target "${TARGET}" \ + --action push \ + && media.cloud.synchronize-target \ + --target "${TARGET}" \ + --action pull \ + && return 0 \ + || return 1 \ + ; + ;; + esac + + echo.status "${ACTION}ing ${TARGET}" + cloud.aws s3 sync $A $B \ + && echo.success "${TARGET} up-to-date" \ + || { ERROR "unable to sync ${TARGET} (see above)"; return 1; } +} + +##################################################################### + +${scwryptsmodule}.parse() { + # local TARGET TARGET_CLOUD TARGET_LOCAL + local PARSED=0 + + case $1 in + ( --target ) + PARSED=2 + TARGET="$2" + ;; + esac + + return $PARSED +} + +${scwryptsmodule}.parse.usage() { + USAGE__options+=" + --target local/remote target to synchronize + " +} + +${scwryptsmodule}.parse.validate() { + [ "${TARGET}" ] \ + || ERROR 'must specify a target' + + TARGET_LOCAL="${HOME}/${TARGET}" + TARGET_CLOUD="s3://${MEDIA_SYNC__S3_BUCKET}/${TARGET}" +} diff --git a/scwrypts/media/cloud/synchronize.module.zsh b/scwrypts/media/cloud/synchronize.module.zsh new file mode 100644 index 0000000..d582839 --- /dev/null +++ b/scwrypts/media/cloud/synchronize.module.zsh @@ -0,0 +1,43 @@ +##################################################################### + +REQUIRED_ENV+=(MEDIA_SYNC__TARGETS) + +use cloud/synchronize-target --group media +use cloud/zshparse/actions --group media + +##################################################################### + +${scwryptsmodule}() { + eval "$(USAGE.reset)" + + local \ + ACTION \ + PARSERS=( + media.cloud.zshparse.actions + ) + + eval "$ZSHPARSEARGS" + + ########################################## + + local TARGET_COUNT=${#MEDIA_SYNC__TARGETS[@]} + local FAILED_COUNT=0 + + echo.status "starting media ${ACTION}" + + local TARGET + for TARGET in ${MEDIA_SYNC__TARGETS[@]} + do + media.cloud.synchronize-target \ + --action "${ACTION}" \ + --target "${TARGET}" \ + || ((FAILED_COUNT+=1)) + done + + [[ $FAILED_COUNT -eq 0 ]] \ + && echo.success "successfully completed ${ACTION} for ${TARGET_COUNT} / ${TARGET_COUNT} target(s)" \ + || ERROR "failed ${ACTION} for ${FAILED_COUNT} / ${TARGET_COUNT} target(s)" \ + ; +} + +##################################################################### diff --git a/scwrypts/media/cloud/zshparse/actions.module.zsh b/scwrypts/media/cloud/zshparse/actions.module.zsh new file mode 100644 index 0000000..7c167d6 --- /dev/null +++ b/scwrypts/media/cloud/zshparse/actions.module.zsh @@ -0,0 +1,37 @@ +${scwryptsmodule}() { + # local ACTION + local PARSED=0 + + case $1 in + ( --action ) + PARSED=2 + ACTION="$2" + ;; + esac + + return $PARSED +} + +##################################################################### + +${scwryptsmodule}.usage() { + USAGE__options+=" + --action a media sync action: + push + pull + push-first-synchronize + pull-first-synchronize + " +} + +${scwryptsmodule}.validate() { + case "${ACTION}" in + ( push | pull | pull-first-synchronize | push-first-synchronize ) ;; + ( '' ) + ERROR 'must specify a media sync action' + ;; + ( * ) + ERROR "invalid media sync action '${ACTION}'" + ;; + esac +} diff --git a/scwrypts/media/cloud/zshparse/zshparse.module.zsh b/scwrypts/media/cloud/zshparse/zshparse.module.zsh new file mode 100644 index 0000000..f65d08c --- /dev/null +++ b/scwrypts/media/cloud/zshparse/zshparse.module.zsh @@ -0,0 +1,5 @@ +# +# common parsers for cloud media synchronization +# + +use cloud/zshparse/actions --group media diff --git a/scwrypts/media/ffmpeg/ffmpeg.module.zsh b/scwrypts/media/ffmpeg/ffmpeg.module.zsh new file mode 100644 index 0000000..3d6767a --- /dev/null +++ b/scwrypts/media/ffmpeg/ffmpeg.module.zsh @@ -0,0 +1,9 @@ +# +# personal ffmpeg utility since I don't use ffmpeg much and don't +# want to read the man every time +# + +DEPENDENCIES+=(ffmpeg) + +use ffmpeg/get-audio-clip-from-video.module.zsh --group media +use ffmpeg/get-video-length-seconds.module.zsh --group media diff --git a/scwrypts/media/ffmpeg/get-audio-clip-from-video.module.zsh b/scwrypts/media/ffmpeg/get-audio-clip-from-video.module.zsh new file mode 100644 index 0000000..958683e --- /dev/null +++ b/scwrypts/media/ffmpeg/get-audio-clip-from-video.module.zsh @@ -0,0 +1,145 @@ +##################################################################### + +DEPENDENCIES+=(ffmpeg) + +use ffmpeg/get-video-length-seconds --group media + +##################################################################### + +${scwryptsmodule}() { + eval "$(USAGE.reset)" + local USAGE__description=' + converts a video into an audio clip (mp3) + + if only --start is used, the audio clip will be from + the specified start point to the end of the video + + if only --end is used, the audio clip will be from + the start of the video to the specified end point + + if --start, --end, and --use-whole-video are all omitted, + start and end times will be prompted interactively + ' + + local \ + INPUT_FILENAME OUTPUT_FILENAME \ + USE_WHOLE_VIDEO=false \ + START_TIME_SECONDS END_TIME_SECONDS \ + PARSERS=() + + eval "$ZSHPARSEARGS" + + ########################################## + + echo.status "converting video to audio + video input : ${INPUT_FILENAME} + audio output : ${OUTPUT_FILENAME} + start time : ${START_TIME_SECONDS} + end time : ${END_TIME_SECONDS} + " + + ffmpeg -i "${INPUT_FILENAME}" -q:a 0 -map a \ + -ss ${START_TIME_SECONDS} -t $((${END_TIME_SECONDS} - ${START_TIME_SECONDS}))\ + "${OUTPUT_FILENAME}" \ + && echo.success "created clip '${OUTPUT_FILENAME}'" \ + || ERROR "error creating clip '$(basename -- "${OUTPUT_FILENAME}")' (see above)" +} + +##################################################################### + +${scwryptsmodule}.parse() { + # local INPUT_FILENAME OUTPUT_FILENAME + # local USE_WHOLE_VIDEO=false + # local START_TIME_SECONDS END_TIME_SECONDS optional + + local PARSED=0 + + case $1 in + -i | --input-filename ) PARSED=2; INPUT_FILENAME="$2" ;; + -o | --output-filename ) PARSED=2; OUTPUT_FILENAME="$2" ;; + + --start ) PARSED=2; START_TIME_SECONDS="$2" ;; + --end ) PARSED=2; END_TIME_SECONDS="$2" ;; + + --use-whole-video ) PARSED=1; USE_WHOLE_VIDEO=true ;; + esac + + return $PARSED +} + +${scwryptsmodule}.parse.usage() { + USAGE__options=' + -i, --input-filename fully-qualified path to input file + -o, --output-filename fully-qualified path to output file + + --start start time of the clip + --end end time of the clip + + --use-whole-video convert whole video instead of just a portion + ' +} + +${scwryptsmodule}.parse.validate() { + : \ + && [ "${INPUT_FILENAME}" ] \ + && [ -f "${INPUT_FILENAME}" ] \ + && [ "${OUTPUT_FILENAME}" ] \ + || ERROR "must provide a valid input and output filename\ninput : '${INPUT_FILENAME}\noutput : '${OUTPUT_FILENAME}'" \ + || return + + [[ ${OUTPUT_FILENAME} =~ .mp3$ ]] || OUTPUT_FILENAME="${OUTPUT_FILENAME}.mp3" + + local VIDEO_LENGTH_SECONDS=$(media.ffmpeg.get-video-length-seconds "${INPUT_FILENAME}") + [ "${VIDEO_LENGTH_SECONDS}" ] && [[ "${VIDEO_LENGTH_SECONDS}" -gt 0 ]] \ + || ERROR "unable to determine video length; is '${INPUT_FILENAME}' a video?" \ + || return + + local CLIP_METHOD=start-time-to-end-time + case ${USE_WHOLE_VIDEO} in + true ) + [ ! "${START_TIME_SECONDS}" ] \ + || ERROR "conflicting arguments '--start' and '--use-whole-video'" + + [ ! "${END_TIME_SECONDS}" ] \ + || ERROR "conflicting arguments '--end' and '--use-whole-video'" + ;; + false ) + [ ! "${START_TIME_SECONDS}" ] && [ ! "${END_TIME_SECONDS}" ] \ + && CLIP_METHOD=interactive + ;; + esac + + case ${CLIP_METHOD} in + start-time-to-end-time ) + [ "${START_TIME_SECONDS}" ] || START_TIME_SECONDS=0 + [ "${END_TIME_SECONDS}" ] || END_TIME_SECONDS="${VIDEO_LENGTH_SECONDS}" + ;; + + interactive ) + START_TIME_SECONDS=$(echo 0 | utils.fzf.user-input "enter start time (0 ≤ t < ${VIDEO_LENGTH_SECONDS})") + [ "${START_TIME_SECONDS}" ] \ + || ERROR 'interactive user abort' \ + || return + + END_TIME_SECONDS=$(echo ${VIDEO_LENGTH_SECONDS} | utils.fzf.user-input "enter end time (${START_TIME_SECONDS} > t ≥ $VIDEO_LENGTH_SECONDS)") + [ "${END_TIME_SECONDS}" ] \ + || ERROR 'interactive user abort' \ + || return + ;; + esac + + [[ "${START_TIME_SECONDS}" -ge 0 ]] \ + || ERROR "cannot use negative start time (start time = ${START_TIME_SECONDS})" + + [[ "${END_TIME_SECONDS}" -gt 0 ]] \ + || ERROR "end time must be after the video starts (end time = ${END_TIME_SECONDS})" + + [[ "${START_TIME_SECONDS}" -lt "${VIDEO_LENGTH_SECONDS}" ]] \ + || ERROR "start time must be before video ends (start time = ${START_TIME_SECONDS}; video length = ${VIDEO_LENGTH_SECONDS})" + + [[ "${END_TIME_SECONDS}" -le "${VIDEO_LENGTH_SECONDS}" ]] \ + || ERROR "end time cannot go beyond video end (end time = ${END_TIME_SECONDS}; video length = ${VIDEO_LENGTH_SECONDS})" + + [[ "${START_TIME_SECONDS}" -lt "${END_TIME_SECONDS}" ]] \ + || ERROR "start time must come before end time (start time = ${START_TIME_SECONDS}; end time = ${END_TIME_SECONDS})" +} diff --git a/scwrypts/media/ffmpeg/get-video-length-seconds.module.zsh b/scwrypts/media/ffmpeg/get-video-length-seconds.module.zsh new file mode 100644 index 0000000..ab8c148 --- /dev/null +++ b/scwrypts/media/ffmpeg/get-video-length-seconds.module.zsh @@ -0,0 +1,20 @@ +##################################################################### + +DEPENDENCIES+=(ffprobe) + +##################################################################### + +${scwryptsmodule}() { + local FILENAME="$1" + + [ "${FILENAME}" ] && [ -f "${FILENAME}" ] \ + || ERROR "invalid or missing file '${FILENAME}'" \ + || return 1 + + ffprobe \ + -v quiet \ + -show_entries format=duration \ + -of default=noprint_wrappers=1:nokey=1 \ + -i "${FILENAME}" \ + ; +} diff --git a/scwrypts/media/media.scwrypts.zsh b/scwrypts/media/media.scwrypts.zsh new file mode 100644 index 0000000..1575be5 --- /dev/null +++ b/scwrypts/media/media.scwrypts.zsh @@ -0,0 +1,2 @@ +readonly ${scwryptsgroup}__type=zsh +readonly ${scwryptsgroup}__color=$(utils.colors.magenta) diff --git a/scwrypts/media/youtube/download b/scwrypts/media/youtube/download new file mode 100755 index 0000000..e912d42 --- /dev/null +++ b/scwrypts/media/youtube/download @@ -0,0 +1,39 @@ +#!/bin/zsh +##################################################################### + +use youtube --group media + +##################################################################### + +USAGE__description=" + download videos from youtube +" + +USAGE__args=' + $@ any number of URLS to download (becomes interactive if omitted) +' + +##################################################################### + +MAIN() { + local URLS=($@) + local ARGS=() + + local DOWNLOAD_ERRORS=0 + + [[ $# -eq 0 ]] && { + URLS=($(echo '' | utils.fzf.user-input 'download URL')) + [[ ${#URLS[@]} -gt 0 ]] || ABORT + + ARGS+=(--interactive) + } + + local URL FILENAME + for URL in ${URLS[@]} + do + media.youtube.download ${ARGS[@]} --url "${URL}" \ + || ((DOWNLOAD_ERRORS+=1)) + done + + return ${DOWNLOAD_ERRORS} +} diff --git a/scwrypts/media/youtube/download.module.zsh b/scwrypts/media/youtube/download.module.zsh new file mode 100644 index 0000000..01184c4 --- /dev/null +++ b/scwrypts/media/youtube/download.module.zsh @@ -0,0 +1,63 @@ +##################################################################### + +use youtube/yt-dlp --group media +use youtube/get-filename --group media +use youtube/get-download-path --group media + +##################################################################### + +${scwryptsmodule}() { + eval "$(USAGE.reset)" + + local \ + URL INTERACTIVE=false \ + PARSERS=() + + eval "$ZSHPARSEARGS" + + ########################################## + + local FILENAME="$(media.youtube.get-filename "${URL}")" + + [ "${FILENAME}" ] \ + || ERROR "could not find metadata; cannot proceed with download\n${URL}" \ + || return 1 + + media.youtube.yt-dlp "${URL}" \ + --format 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4' \ + && echo.success "finished download of ${URL}\n$(media.youtube.get-download-path)/${FILENAME}" \ + || ERROR "failed to download '${FILENAME}' (${URL})" + ; +} + +##################################################################### + +${scwryptsmodule}.parse() { + # local URL INTERACTIVE=false + local PARSED=0 + + case $1 in + --url ) + PARSED=2 + URL="$2" + ;; + + --interactive ) + PARSED=1 + INTERACTIVE=true + ;; + esac + + return $PARSED +} + +${scwryptsmodule}.parse.usage() { + USAGE__options+=" + --url the URL of the target video + " +} + +${scwryptsmodule}.parse.validate() { + [ "$URL" ] \ + || ERROR "must provide download URL" +} diff --git a/scwrypts/media/youtube/get-audio-clip b/scwrypts/media/youtube/get-audio-clip new file mode 100755 index 0000000..259e4c8 --- /dev/null +++ b/scwrypts/media/youtube/get-audio-clip @@ -0,0 +1,38 @@ +#!/bin/zsh + +use ffmpeg/get-audio-clip-from-video --group media +use youtube/get-download-path --group media + +##################################################################### + + +##################################################################### + +MAIN() { + local DOWNLOAD_PATH="$(media.youtube.get-download-path)" + + local INPUT_FILENAME="$( + cd -- "${DOWNLOAD_PATH}" + find . -type f -name \*.mp4 \ + | sed 's|^./||' \ + | utils.fzf 'select a video' \ + | sed 's/\.mp4$//' \ + )" + [ "${INPUT_FILENAME}" ] || ABORT + + local OUTPUT_FILENAME="$(\ + basename -- "${INPUT_FILENAME}" \ + | sed 's/\.[^.]*$//' \ + | utils.fzf.user-input 'what should I call this clip? (.mp3)' \ + | sed 's/\(\.mp3\)*$//' \ + )" + [ "${OUTPUT_FILENAME}" ] || ABORT + + INPUT_FILENAME="${DOWNLOAD_PATH}/${INPUT_FILENAME}.mp4" + OUTPUT_FILENAME="${DOWNLOAD_PATH}/${OUTPUT_FILENAME}.mp3" + + media.ffmpeg.get-audio-clip-from-video \ + --input-filename "${INPUT_FILENAME}" \ + --output-filename "${OUTPUT_FILENAME}" \ + ; +} diff --git a/scwrypts/media/youtube/get-download-path.module.zsh b/scwrypts/media/youtube/get-download-path.module.zsh new file mode 100644 index 0000000..7e33ff2 --- /dev/null +++ b/scwrypts/media/youtube/get-download-path.module.zsh @@ -0,0 +1,7 @@ +${scwryptsmodule}() { + local DOWNLOAD_PATH="${SCWRYPTS_DATA_PATH}/youtube" + + mkdir -p -- "${DOWNLOAD_PATH}" &>/dev/null + + echo "${DOWNLOAD_PATH}" +} diff --git a/scwrypts/media/youtube/get-filename.module.zsh b/scwrypts/media/youtube/get-filename.module.zsh new file mode 100644 index 0000000..ed89c20 --- /dev/null +++ b/scwrypts/media/youtube/get-filename.module.zsh @@ -0,0 +1,12 @@ +##################################################################### + +use youtube/yt-dlp --group media + +##################################################################### + +${scwryptsmodule}() { + media.youtube.yt-dlp --dump-json $@ \ + | jq -r '._filename' \ + | sed 's/\.[^.]*$/\.mp4/' \ + ; +} diff --git a/scwrypts/media/youtube/youtube.module.zsh b/scwrypts/media/youtube/youtube.module.zsh new file mode 100644 index 0000000..274b08c --- /dev/null +++ b/scwrypts/media/youtube/youtube.module.zsh @@ -0,0 +1,15 @@ +# +# interact with youtube +# + + +# download a youtube video by URL +use youtube/download --group media + + +# show fully-qualified path to downloads +use youtube/get-download-path --group media + + +# interact with yt-dlp directly +use youtube/yt-dlp --group media diff --git a/scwrypts/media/youtube/yt-dlp.module.zsh b/scwrypts/media/youtube/yt-dlp.module.zsh new file mode 100644 index 0000000..a8dafda --- /dev/null +++ b/scwrypts/media/youtube/yt-dlp.module.zsh @@ -0,0 +1,15 @@ +##################################################################### + +use youtube/get-download-path --group media + +##################################################################### + +DEPENDENCIES+=(yt-dlp) +${scwryptsmodule}() { + ( + cd -- "$(media.youtube.get-download-path)" + yt-dlp \ + --restrict-filenames \ + $@ + ) +}