From 6b6fa9153c495e47991ffa2344e4da884b9d93a0 Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Mon, 26 Jan 2026 00:34:19 +0000 Subject: [PATCH 01/16] feat(user/mp3s_to_m4b.sh):Adapt flacs_to_m4b for mp3 input - fix(user/htmlz_to_cbz.sh):Flatten jpg dir tree in output zip --- user/htmlz_to_cbz.sh | 4 +- user/mp3s_to_m4b.sh | 144 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100755 user/mp3s_to_m4b.sh diff --git a/user/htmlz_to_cbz.sh b/user/htmlz_to_cbz.sh index 8826e9e..6552563 100755 --- a/user/htmlz_to_cbz.sh +++ b/user/htmlz_to_cbz.sh @@ -1,6 +1,6 @@ #!/bin/bash # Desc: Collects .jpg/jpeg files from a Calibre .htmlz file into .cbz files -# Version: 0.0.2 +# Version: 0.0.3 for fin in ./*.htmlz; do ( @@ -26,7 +26,7 @@ for fin in ./*.htmlz; do if [[ -f "$faout" ]]; then rm "$fout"; fi; - zip -r output.cbz output; + zip -j output.cbz output/*; ) & done; wait && echo "STATUS:Finished." 1>&2; diff --git a/user/mp3s_to_m4b.sh b/user/mp3s_to_m4b.sh new file mode 100755 index 0000000..042200c --- /dev/null +++ b/user/mp3s_to_m4b.sh @@ -0,0 +1,144 @@ +#!/bin/bash +# Desc: Converts a directory of mp3s into a single m4b file with chapters +# Usage: mp3s_to_m4b.sh [DIR in] [DIR out] [BITRATE] +# Example: mp3s_to_m4b.sh ./dir_source ./dir_output 64k +# Depends: GNU Coreutils 8.32 (date), ffmpeg, ffprobe +# Ref/Attrib: [1] FFmpeg Formats Documentation https://ffmpeg.org/ffmpeg-formats.html#toc-Metadata-2 +# Version: 0.0.1 + +# plumbing +aac_bitrate="$3"; # e.g. "64k" +script_rundate="$(date +%s)"; +dir_tmp="/dev/shm"; +dir_in="$(readlink -f "$1")"; +dir_out="$(readlink -f "$2")"; +file_flist="$dir_tmp"/"$script_rundate"..flist.txt; +file_out_m4b="$dir_out"/output.m4b; +file_albumart="$dir_out"/albumart.png; +file_chapters="$dir_tmp"/"$script_rundate"..chapters.txt; + +yell() { echo "$0: $*" >&2; } # print script path and all args to stderr +die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status +must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails +show_usage() { + # Desc: Display script usage information + # Usage: showUsage + # Version 0.0.2 + # Input: none + # Output: stdout + # Depends: GNU-coreutils 8.30 (cat) + cat <<'EOF' + USAGE: + mp3s_to_m4b.sh [DIR in] [DIR out] [BITRATE] + + EXAMPLE: + mp3s_to_m4b.sh ./source_dir ./out_dir 64k +EOF +} # Display information on how to use this script. +check_depends() { + if ! command -v ffmpeg; then show_usage; die "FATAL:Missing ffmpeg."; fi; + if ! command -v ffprobe; then show_usage; die "FATAL:Missing ffprobe."; fi; +}; # check dependencies +check_plumbing() { + if [[ $# -ne 3 ]]; then show_usage; die "FATAL:Invalid arg count:$#"; fi; + if [[ ! -d "$dir_in" ]]; then show_usage; die "FATAL:Not a dir:$dir_in"; fi; + if [[ ! -d "$dir_out" ]]; then mkdir -p "$2"; fi; +}; # check arguments +get_media_length() { + # Use ffprobe to get media container length in seconds (float) + # Usage: get_media_length arg1 + # Input: arg1: path to file + # Output: stdout: seconds (float) + # Depends: ffprobe 4.1.8 + # BK-2020-03: die() + local file_in; + file_in="$1"; + if [[ ! -f $file_in ]]; then + die "ERROR:Not a file:$file_in"; + fi; + ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_in"; +} # Get media container length in seconds via stdout +build_filelist_and_chapters() { + # Depends: var dir_tmp temporary directory + # var dir_in input dir + # var file_flist path file list + # Output: file: $file_flist list of mp3 files for ffmpeg + # file: $file_chapters chapters file for ffmpeg + + # Change directory to input dir + pushd "$dir_in" || die "FATAL:Directory error:$(pwd)"; + + local chapter_start=0; + local chapter_end; + local duration; + + + # Initialize chapter ffmetadata file. See [1]. + { + echo ";FFMETADATA1"; + } >> "$file_chapters"; + + + find "$dir_in" -type f -iname "*.mp3" | sort | while read -r line; do + local filename="${line#./}"; + yell "$(printf "file '%s'\n" "$filename")"; + printf "file '%s'\n" "$filename" >> "$file_flist"; + + # Get duration of the current file + duration=$(get_media_length "$filename"); + chapter_end=$(echo "$chapter_start + $duration" | bc -l); + + # Write chapter info + { + echo "[CHAPTER]"; + echo "TIMEBASE=1/1000"; + echo "START=$(echo "scale=0; $chapter_start * 1000" | bc -l)"; + echo "END=$(echo "scale=0; $chapter_end * 1000" | bc -l)"; + echo "title=$(basename "$filename" .mp3)"; + } >> "$file_chapters"; + + chapter_start=$chapter_end; + done + + # Return to original dir + popd || die "FATAL:Directory error:$(pwd)"; +}; # build file list and chapters for ffmpeg +ffmpeg_convert() { + # Depends: var dir_tmp + # dir_in + # dir_out + # Input: file $file_flist list of mp3 files for ffmpeg + # file $file_chapters chapters file for ffmpeg + + # Change directory to input dir + pushd "$dir_in" || die "FATAL:Directory error:$(pwd)"; + + # Concatenate mp3 files into a single WAV file + # Convert WAV to aac m4b file + ffmpeg -nostdin -f concat -safe 0 -i "$file_flist" -c:a pcm_s24le -rf64 auto -f wav - | \ + ffmpeg -i - -i "$file_chapters" \ + -map_metadata 1 -map_chapters 1 \ + -map 0 -c:a aac -b:a "$aac_bitrate" \ + "$file_out_m4b"; + + # Return to original dir + popd || die "FATAL:Directory error:$(pwd)"; +}; # convert mp3s to aac m4b via ffmpeg +save_albumart() { + local file + file="$(find "$dir_in" -type f -iname "*.mp3" | sort | head -n1)"; + file="$(readlink -f "$file")"; + ffmpeg -nostdin -i "$file" -an -vcodec copy "$file_albumart"; +}; # save album art from an mp3 to output dir +main() { + check_depends && yell "DEBUG:check_depends OK"; + check_plumbing "$@" && yell "DEBUG:check_plumbing OK"; + build_filelist_and_chapters "$@" && yell "DEBUG:build_filelist_and_chapters OK"; + ffmpeg_convert "$@" && yell "DEBUG:ffmpeg_convert OK"; + save_albumart "$@" && yell "DEBUG:save_albumart OK"; +}; # main program + +main "$@"; + +# Author: Steven Baltakatei Sandoval +# License: GPLv3+ -- 2.39.5 From 213fc61ddc48fd181776af34a9c7bd536647c24a Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Mon, 2 Feb 2026 07:54:45 +0000 Subject: [PATCH 02/16] update(README.org):Update date --- README.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.org b/README.org index 644245f..d0ab90c 100644 --- a/README.org +++ b/README.org @@ -1,7 +1,7 @@ * Baltakatei Executables Development #+TITLE: Baltakatei Executables Development #+AUTHOR: Steven Baltakatei Sandoval -#+DATE:2023-03-19 +#+DATE:2026-02-02 #+EMAIL:baltakatei@gmail.com #+LANGUAGE: en #+OPTIONS: toc:nil -- 2.39.5 From 5b4c0cf8cf6ef073a0b34da844d28dee765d43bf Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Fri, 6 Feb 2026 16:19:15 +0000 Subject: [PATCH 03/16] feat(user/bkytpldl-generic):Reference external javascript solver - Note: external javascript solver required as of 2026 https://github.com/yt-dlp/yt-dlp/issues/14404 - feat(user/bkytpldl-generic):Add 24-hour timeout --- user/bkytpldl-generic | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/user/bkytpldl-generic b/user/bkytpldl-generic index 4f6689f..199ed40 100644 --- a/user/bkytpldl-generic +++ b/user/bkytpldl-generic @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Desc: Download YouTube videos # Usage: $ ./bkytpldl-generic -# Version: 4.1.2 +# Version: 4.2.0 declare -a args; # array for yt-dlp arguments declare -a urls urls_rand; # array for YouTube playlist URLs @@ -22,6 +22,10 @@ if ! command -v yt-dlp 1>/dev/random 2>&1; then die "FATAL:yt-dlp not found."; f # Donʼt run multiple yt-dlp instances if pgrep "^yt-dlp$" 1>/dev/random 2>&1; then die "FATAL:yt-dlp already running."; fi; +# Enable JavaScript solver via deno. See https://github.com/yt-dlp/yt-dlp/wiki/EJS#step-2-install-ejs-challenge-solver-scripts +args+=("--remote-components"); +args+=("ejs:npm"); + # Check directories if [[ ! -d $dir_out ]]; then mkdir -p "$dir_out"; fi; @@ -101,7 +105,7 @@ pushd "$dir_out" || die "FATAL:Failed to change pwd to:dir_out:$dir_out"; # == Download videos == #yell "DEBUG:args:$(declare -p args)"; # debug command -must yt-dlp "${args[@]}"; # execute command +timeout "$((1*24*3600))" yt-dlp "${args[@]}"; popd || die "FATAL:Failed to return from dir_out:$dir_out"; # Author: Steven Baltakatei Sandoval -- 2.39.5 From 9a556174846f83cc28db1277b36219504c277159 Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Thu, 5 Mar 2026 22:47:27 +0000 Subject: [PATCH 04/16] feat(user/bkfeh):Update options --- user/bkfeh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/user/bkfeh b/user/bkfeh index fba697c..7620e25 100755 --- a/user/bkfeh +++ b/user/bkfeh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Desc: Wrapper for feh that accepts directory paths via posargs or stdin lines. -# Version: 0.3.0 +# Version: 0.3.4 # Ref/Attrib: [1] Tange, Ole. GNU Parallel with Bash Array. 2019-03-24. https://unix.stackexchange.com/a/508365/411854 -# Depends: GNU Parallel, GNU Bash v5.1.16, feh 3.6.3, GNU Coreutils 8.32 (b2sum) +# Depends: GNU Parallel, GNU Bash v5.1.16, feh 3.6.3, GNU Coreutils 9.4 (b2sum, cp) #===Declare local functions=== yell() { echo "$0: $*" >&2; } # print script path and all args to stderr @@ -157,7 +157,7 @@ check_depends() { displayMissing; die "FATAL:Missing apps."; fi; - return 1; + return 0; }; # check dependencies checkInt() { # Desc: Checks if arg is integer @@ -257,6 +257,7 @@ save_sample() { local list_paths sample_count="100"; # max number of images to put in sample dir sample_max_space="10000000"; # max bytes to put in sample dir + n_fail_max="10"; # max failures to save sample file (e.g. due to space limits) # Load environment variables if set if [[ ! -v BKFEH_SAMPLE_DIR ]]; then return 0; fi; # return early if environment var not set. @@ -286,14 +287,16 @@ save_sample() { yell "STATUS:Saving random sample of size $sample_count to $BKFEH_SAMPLE_DIR..."; list_paths_sample="$(echo "$list_paths" | bkshuf "$sample_count" | head -n"$sample_count")"; n_samp=0; # init sample file counter + n_fail=0; # init failure counter sample_log="$BKFEH_SAMPLE_DIR"/paths.txt; printf "%s,%s,%s\n" "n_samp" "file_hash" "file_path" >> "$sample_log"; - while read -r line; do + while read -r line && [[ "$n_fail" -le "$n_fail_max" ]]; do if [[ -z "$line" ]]; then continue; fi; ### check size limit sample_act_space="$(du -bd1 "$BKFEH_SAMPLE_DIR" | cut -f1 )"; # actual used space cand_space="$(du -bd1 "$line" | cut -f1 )"; # size of candidate file to add sample_req_space="$((sample_act_space + cand_space))"; + if [[ ! "$sample_req_space" -lt "$sample_max_space" ]]; then ((n_fail++)); continue; fi; ### Customize file names n_samp_w="$(printf "%s" "$sample_count" | wc -c)"; @@ -307,10 +310,10 @@ save_sample() { file_name="${file_name%.*}"; file_shortname="${file_name:0:32}"; file_name_new="$n_samp_dd"_"$file_hash".."$file_shortname"."$file_ext"; - file_path_new="$BKFEH_SAMPLE_DIR"/"$file_name_new" + file_path_new="$BKFEH_SAMPLE_DIR"/"$file_name_new"; if [[ "$sample_req_space" -lt "$sample_max_space" ]]; then #### add file to sample dir - must cp -n "$file_path" "$file_path_new"; + must cp --update=none "$file_path" "$file_path_new"; #### note path in sample dir log printf "%s,%s,%s\n" "$n_samp_dd" "$file_hash" "$file_path" \ >> "$sample_log"; @@ -383,7 +386,7 @@ main() { fi; ## Call find_filelist() in parallel for stdin input if [[ "${#dirs_stdin[@]}" -gt 0 ]]; then - fdepth=1; export fdepth; # 1 for dirs from stdin + fdepth=1; export fdepth; # 1 ofr dirs from stdin paths_images+=("$( parallel find_flist {} "$fdepth" "$firegex" "$fsize" ::: "${dirs_stdin[@]}" )"); # See [1] fi; @@ -410,7 +413,7 @@ main() { yell "STATUS:Built file list in $SECONDS seconds."; # Run feh with filelist - feh --full-screen --auto-zoom --draw-filename --filelist "$list_paths_images_tmp" && \ + feh --full-screen --auto-zoom --draw-filename --filelist "$list_paths_images_tmp" -D 16 --stretch --scale-down && \ must rm "$list_paths_images_tmp" & # Save sample to path in env. var. BKFEH_SAMPLE_DIR if set @@ -422,6 +425,5 @@ export -f yell die must read_stdin read_psarg find_flist; main "$@"; - # Author: Steven Baltakatei Sandoval # License: GPLv3+ -- 2.39.5 From 71e0fc154695564592503078a950d6e57bb46d6d Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Thu, 5 Mar 2026 22:48:11 +0000 Subject: [PATCH 05/16] update(user/htmlz_to_cbz.sh): Use jdupes to deduplicate images --- user/htmlz_to_cbz.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/user/htmlz_to_cbz.sh b/user/htmlz_to_cbz.sh index 6552563..b1c450e 100755 --- a/user/htmlz_to_cbz.sh +++ b/user/htmlz_to_cbz.sh @@ -1,6 +1,7 @@ #!/bin/bash # Desc: Collects .jpg/jpeg files from a Calibre .htmlz file into .cbz files -# Version: 0.0.3 +# Version: 0.1.0 +# Depends: jdupes 1.27.3 for fin in ./*.htmlz; do ( @@ -19,14 +20,19 @@ for fin in ./*.htmlz; do cp "$path" "$fnew"; ((n++)); done; + # Add cover file if present if [[ -f cover.jpg ]]; then - cp -n cover.jpg ./output/000000.jpg; + cp -n cover.jpg "${dout}/000000.jpg"; + fi; + # Remove duplicate images + if command -v jdupes 1>/dev/random 2>&1; then + jdupes -dN "$dout"; fi; faout="output.cbz"; if [[ -f "$faout" ]]; then rm "$fout"; fi; - zip -j output.cbz output/*; + zip -j output.cbz "$dout"/*; ) & done; wait && echo "STATUS:Finished." 1>&2; -- 2.39.5 From 10992ec40e142ab5365624925c1dc95fcdfceca1 Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Fri, 24 Apr 2026 04:13:19 +0000 Subject: [PATCH 06/16] feat(user/htmlz_to_cbz.sh): Support png files - feat(user/cbz_dedup.sh): Use jdupes to dedupe CBZ files --- user/cbz_dedup.sh | 73 ++++++++++++++++++++++++++++++++++++++++++++ user/htmlz_to_cbz.sh | 20 ++++++------ 2 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 user/cbz_dedup.sh diff --git a/user/cbz_dedup.sh b/user/cbz_dedup.sh new file mode 100644 index 0000000..fbcb34b --- /dev/null +++ b/user/cbz_dedup.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Desc: Deduplicates files within a CBZ file +# Usage: cbz_dedup.sh [CBZ file] +# Example: cbz_dedup.sh input.cbz +# Version: 0.0.5 +# Depends: jdupes 1.27.3, unzip 6.00 by Debian + + +yell() { echo "$0: $*" >&2; } # print script path and all args to stderr +die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status +must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails +showUsage() { + # Desc: Display script usage information + # Usage: showUsage + # Version 0.0.2 + # Input: none + # Output: stdout + # Depends: GNU-coreutils 8.30 (cat) + cat <<'EOF' + USAGE: + cbz_dedup.sh [FILE] + + EXAMPLE: + cbz_dedup.sh input.cbz +EOF +}; # Display information on how to use this script. +main() { + fout="output.cbz"; + + # Check args + if [[ $# -ne 1 ]]; then showUsage; die "FATAL:Invalid arg count (should be 1):$#"; fi; + re1='\.cbz$'; + if [[ ! "$1" =~ $re1 ]]; then showUsage; die "FATAL:Not a .cbz file:$1"; fi; + if [[ ! -f "$1" ]]; then die "FATAL:Not a file:$1"; fi; + fin="$1"; + dir_tmp="$(mktemp -d)"; + if [[ ! -d "$dir_tmp" ]]; then die "FATAL:Could not make temporary directory:${dir_tmp}"; fi; + trap 'rm -rf "${dir_tmp}"' EXIT; + pout="${dir_tmp}/${fout}"; + + # Check depends + for _cmd in unzip zip jdupes; do + if ! command -v "$_cmd" 1>/dev/random 2>&1; then + die "FATAL:Missing app:${_cmd}"; + fi; + done; + + # Extract CBZ to temp dir + must unzip "$fin" -x / -d "$dir_tmp"; + + # Dedupe with jdupes + ## Check size before + tmp_size1="$(du -bd0 "$dir_tmp" | cut -f1; )"; + must jdupes -dN "${dir_tmp}"; + ## Check size after + tmp_size2="$(du -bd0 "$dir_tmp" | cut -f1; )"; + + # Replace CBZ if size changed + if [[ "$tmp_size1" -eq "$tmp_size2" ]]; then yell "STATUS:No deduplication detected."; return 0; fi; + + ## Recreate CBZ + must zip -j "$pout" "${dir_tmp}"/*; + + ## Preserve original CBZ + must mv -n "$fin" "${fin%.*}_original.cbz"; + + ## Move deduped CBZ + must mv -n "$pout" "$fin"; + + return 0; +}; + +main "$@"; diff --git a/user/htmlz_to_cbz.sh b/user/htmlz_to_cbz.sh index b1c450e..de313f0 100755 --- a/user/htmlz_to_cbz.sh +++ b/user/htmlz_to_cbz.sh @@ -1,6 +1,6 @@ #!/bin/bash # Desc: Collects .jpg/jpeg files from a Calibre .htmlz file into .cbz files -# Version: 0.1.0 +# Version: 0.1.2 # Depends: jdupes 1.27.3 for fin in ./*.htmlz; do @@ -8,31 +8,31 @@ for fin in ./*.htmlz; do dout="${fin%.*}"; unzip "$fin" -x / -d "$dout"; pushd "$dout"; - mapfile -t images < <(cat index.html | grep -E "(.jpg|.jpeg)" | sed -E -e 's#.+(images/[0-9]+.(jpeg|jpg)).+#\1#' | uniq; ); - dout="./output"; - if [[ -d "$dout" ]]; then - rm -r "$dout"; + mapfile -t images < <(cat index.html | grep -E "(.jpg|.jpeg|.png)" | sed -E -e 's#.+(images/[0-9]+.(jpeg|jpg|png)).+#\1#' | uniq; ); + dout2="./output"; + if [[ -d "$dout2" ]]; then + rm -r "$dout2"; fi; - mkdir "$dout"; + mkdir "$dout2"; n=1; for path in "${images[@]}"; do - fnew="${dout}/$(printf "%06d" "$n").jpg"; + fnew="${dout2}/$(printf "%06d" "$n").jpg"; cp "$path" "$fnew"; ((n++)); done; # Add cover file if present if [[ -f cover.jpg ]]; then - cp -n cover.jpg "${dout}/000000.jpg"; + cp -n cover.jpg "${dout2}/000000.jpg"; fi; # Remove duplicate images if command -v jdupes 1>/dev/random 2>&1; then - jdupes -dN "$dout"; + jdupes -dN "$dout2"; fi; faout="output.cbz"; if [[ -f "$faout" ]]; then rm "$fout"; fi; - zip -j output.cbz "$dout"/*; + zip -j output.cbz "$dout2"/*; ) & done; wait && echo "STATUS:Finished." 1>&2; -- 2.39.5 From 30d714712a8a9e3622b063c52a67b65fd7c51e0a Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Sun, 26 Apr 2026 20:13:25 +0000 Subject: [PATCH 07/16] fix(user/bkdatev):Use leap seconds via 'TZ=right/UTC' - Ref: See https://web.archive.org/web/20260423214657/https://www.ucolick.org/~sla/leapsecs/right+gps.html --- user/bkdatev | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user/bkdatev b/user/bkdatev index cea6883..808ab9e 100755 --- a/user/bkdatev +++ b/user/bkdatev @@ -2,11 +2,12 @@ # Desc: Baltakatei's verbose date command # Usage: bkdatev [args] # Example: bkdatev --date="2001-09-11T09:02:59-04" -# Version: 1.0.0 +# Version: 1.0.1 # Depends: GNU Coreutils 8.32, Bash 3.2.57 # Ref/Attrib: [1] "ISO 8601". Wikipedia. https://en.wikipedia.org/wiki/ISO_8601 # [2] "Changing the Locale in Wine" https://stackoverflow.com/a/16428951 # [3] "Shanghai vs Beijing" https://bugs.launchpad.net/ubuntu/+source/libgweather/+bug/228554 +# [4] “"right" tz database (zoneinfo) files and GPS-based NTP” https://www.ucolick.org/~sla/leapsecs/right+gps.html # Notes: * Check `ls -R /usr/share/zoneinfo` for time zone names. # * Check `cat /usr/share/i18n/SUPPORTED` for supported locales. # * For list of valid locales, see: https://manpages.ubuntu.com/manpages/bionic/man3/DateTime::Locale::Catalog.3pm.html @@ -143,7 +144,7 @@ main() { # UTC (pop. (2021): 7,837,000,000) ( - export TZ=UTC; + export TZ=right/UTC; # See [4] id="UTC"; fs_3="+%s seconds since 1970-01-01T00:00+00"; print_dateline "$@"; -- 2.39.5 From 55ec083a329ab2c8a53b665dbbe1168a5fcfcbad Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Sun, 26 Apr 2026 23:10:42 +0000 Subject: [PATCH 08/16] fix(user/bkdatev):Display UTC *and* right/UTC correctly - Note: UTC unix epoch is seconds since 1970-01-01 without leap seconds - Note: right/UTC unix epoch is seconds since 1970-01-01 *with* leap seconds. --- user/bkdatev | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/user/bkdatev b/user/bkdatev index 808ab9e..3ab8c3b 100755 --- a/user/bkdatev +++ b/user/bkdatev @@ -2,12 +2,12 @@ # Desc: Baltakatei's verbose date command # Usage: bkdatev [args] # Example: bkdatev --date="2001-09-11T09:02:59-04" -# Version: 1.0.1 +# Version: 1.0.2 # Depends: GNU Coreutils 8.32, Bash 3.2.57 # Ref/Attrib: [1] "ISO 8601". Wikipedia. https://en.wikipedia.org/wiki/ISO_8601 # [2] "Changing the Locale in Wine" https://stackoverflow.com/a/16428951 # [3] "Shanghai vs Beijing" https://bugs.launchpad.net/ubuntu/+source/libgweather/+bug/228554 -# [4] “"right" tz database (zoneinfo) files and GPS-based NTP” https://www.ucolick.org/~sla/leapsecs/right+gps.html +# [4] “Understanding the "right" time zone database” https://kenta.blogspot.com/2016/03/sqfzcxay-understanding-right-time-zone.html # Notes: * Check `ls -R /usr/share/zoneinfo` for time zone names. # * Check `cat /usr/share/i18n/SUPPORTED` for supported locales. # * For list of valid locales, see: https://manpages.ubuntu.com/manpages/bionic/man3/DateTime::Locale::Catalog.3pm.html @@ -144,9 +144,11 @@ main() { # UTC (pop. (2021): 7,837,000,000) ( - export TZ=right/UTC; # See [4] + export TZ=UTC; id="UTC"; - fs_3="+%s seconds since 1970-01-01T00:00+00"; + date_string="$(date -Is)"; + right_epoch="$(TZ=right/UTC date --date="$date_string" +%s;)"; # see [4] + fs_3="+%s seconds (${right_epoch} with leap seconds) since 1970-01-01T00:00+00"; print_dateline "$@"; ); line_sep; -- 2.39.5 From 10fef6ad7acae8f9e90ee710ad054ecfc063132d Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Sun, 26 Apr 2026 23:34:45 +0000 Subject: [PATCH 09/16] feat(unitproc/bkt-dateDuration):Count seconds between two dates --- unitproc/bkt-dateDuration | 41 +++++++++++++++++++++++++++++++++++++++ unitproc/bkt-timeDuration | 6 +++--- 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100755 unitproc/bkt-dateDuration diff --git a/unitproc/bkt-dateDuration b/unitproc/bkt-dateDuration new file mode 100755 index 0000000..0b09e66 --- /dev/null +++ b/unitproc/bkt-dateDuration @@ -0,0 +1,41 @@ +#!/bin/bash + +yell() { echo "$0: $*" >&2; } # print script path and all args to stderr +die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status +must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails +dateDuration() { + # Desc: Given two date strings, output seconds duration + # Example: dateDuration 2012-06-30 2012-07-01 # Yields 86401 because leap second + # Version 0.0.1 + # Depends: GNU Coreutils 9.4 (date, sed), GNU Bash 5.2.21 + # References: [1] “"right" tz database (zoneinfo) files and GPS-based NTP” https://www.ucolick.org/~sla/leapsecs/right+gps.html + + date1="$1"; + date2="$2"; + # Check args + if [[ -z "$date1" ]]; then die "FATAL:Invalid first date provided:${date1}"; fi; + if [[ -z "$date2" ]]; then die "FATAL:Invalid second date provided:${date2}"; fi; + if [[ $# -gt 2 ]]; then die "FATAL:Too many arguments."; fi; + ## Convert @-specified unix epoch into right unix epoch (rue) + re='@.*'; + if [[ "$date1" =~ $re ]] || [[ "$date2" =~ $re ]]; then + die "FATAL:@-specified unix epoch detected. Why are you using me?"; + fi; + + # Use right unix epoch to account for leap seconds. + export TZ=right/UTC; # See [1] + + # Convert date strings into Unix epoch + unixEpoch1="$(must date --date="$date1" +%s)"; + unixEpoch2="$(must date --date="$date2" +%s)"; + + # Calculate duration + duration="$((unixEpoch2 - unixEpoch1))"; + duration="$(sed -e 's/^-//' <<<"$duration"; )"; + + printf "%s\n" "$duration"; +}; + +# Examples +dateDuration 2026-06-30 2026-07-01; # 86400 seconds +dateDuration 2012-06-30 2012-07-01; # 86401 seconds (leap second) diff --git a/unitproc/bkt-timeDuration b/unitproc/bkt-timeDuration index 1db408b..7786723 100644 --- a/unitproc/bkt-timeDuration +++ b/unitproc/bkt-timeDuration @@ -2,9 +2,9 @@ # Desc: Template to indicate time duration in ISO-8601 format -yell() { echo "$0: $*" >&2; } # Yell, Die, Try Three-Fingered Claw technique; # Ref/Attrib: https://stackoverflow.com/a/25515370 -die() { yell "$*"; exit 111; } -try() { "$@" || die "cannot $*"; } +yell() { echo "$0: $*" >&2; } # print script path and all args to stderr +die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status +must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails timeDuration(){ # Desc: Given seconds, output ISO-8601 duration string # Ref/Attrib: ISO-8601:2004(E), §4.4.4.2 Representations of time intervals by duration and context information -- 2.39.5 From 40b8985588fb1975650b886c40465b83624937be Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Mon, 27 Apr 2026 00:21:49 +0000 Subject: [PATCH 10/16] chore(unitproc/bkt-dateDuration):Add warning for missing TZ file --- unitproc/bkt-dateDuration | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/unitproc/bkt-dateDuration b/unitproc/bkt-dateDuration index 0b09e66..10be78b 100755 --- a/unitproc/bkt-dateDuration +++ b/unitproc/bkt-dateDuration @@ -6,32 +6,34 @@ must() { "$@" || die "cannot $*"; } # runs args as command, reports args if comm dateDuration() { # Desc: Given two date strings, output seconds duration # Example: dateDuration 2012-06-30 2012-07-01 # Yields 86401 because leap second - # Version 0.0.1 + # Version 0.0.4 # Depends: GNU Coreutils 9.4 (date, sed), GNU Bash 5.2.21 - # References: [1] “"right" tz database (zoneinfo) files and GPS-based NTP” https://www.ucolick.org/~sla/leapsecs/right+gps.html + # References: [1] “Understanding the "right" time zone database” https://kenta.blogspot.com/2016/03/sqfzcxay-understanding-right-time-zone.html + local date1 date2 re unixEpoch1 unixEpoch2 duration; + + path_tz="/usr/share/zoneinfo/right/UTC"; + if [[ ! -f "$path_tz" ]]; then yell "WARNING:Time zone 'right/UTC' file not found at ${path_tz}. Try 'sudo apt install tzdata-legacy'."; fi; + if [[ $# -ne 2 ]]; then die "FATAL:Need two date arguments. Got $#"; fi; date1="$1"; date2="$2"; # Check args if [[ -z "$date1" ]]; then die "FATAL:Invalid first date provided:${date1}"; fi; if [[ -z "$date2" ]]; then die "FATAL:Invalid second date provided:${date2}"; fi; - if [[ $# -gt 2 ]]; then die "FATAL:Too many arguments."; fi; + ## Convert @-specified unix epoch into right unix epoch (rue) - re='@.*'; + re='^@.*'; if [[ "$date1" =~ $re ]] || [[ "$date2" =~ $re ]]; then die "FATAL:@-specified unix epoch detected. Why are you using me?"; fi; - # Use right unix epoch to account for leap seconds. - export TZ=right/UTC; # See [1] - - # Convert date strings into Unix epoch - unixEpoch1="$(must date --date="$date1" +%s)"; - unixEpoch2="$(must date --date="$date2" +%s)"; + # Convert date strings into Unix epoch. # See [1] + unixEpoch1="$(TZ=right/UTC date --date="$date1" +%s)" || die "Problem with $(declare -p date1)"; + unixEpoch2="$(TZ=right/UTC date --date="$date2" +%s)" || die "Problem with $(declare -p date2)"; # Calculate duration duration="$((unixEpoch2 - unixEpoch1))"; - duration="$(sed -e 's/^-//' <<<"$duration"; )"; + duration=${duration#-}; printf "%s\n" "$duration"; }; @@ -39,3 +41,5 @@ dateDuration() { # Examples dateDuration 2026-06-30 2026-07-01; # 86400 seconds dateDuration 2012-06-30 2012-07-01; # 86401 seconds (leap second) +dateDuration 2000-01-01 1999-12-31; # 86400 seconds +dateDuration 2012-06-30T23:59:59+00 2012-06-30T23:59:60; # 1 (leap second) -- 2.39.5 From beaee940e4acc696d3ba5450db737ea768b5452b Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Mon, 27 Apr 2026 00:26:56 +0000 Subject: [PATCH 11/16] fix(user/bkdatev):Make tzdata-legacy 'right/UTC' TZ optional --- user/bkdatev | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/user/bkdatev b/user/bkdatev index 3ab8c3b..fe60499 100755 --- a/user/bkdatev +++ b/user/bkdatev @@ -2,8 +2,8 @@ # Desc: Baltakatei's verbose date command # Usage: bkdatev [args] # Example: bkdatev --date="2001-09-11T09:02:59-04" -# Version: 1.0.2 -# Depends: GNU Coreutils 8.32, Bash 3.2.57 +# Version: 1.0.3 +# Depends: GNU Coreutils 8.32, Bash 3.2.57, tzdata-legacy # Ref/Attrib: [1] "ISO 8601". Wikipedia. https://en.wikipedia.org/wiki/ISO_8601 # [2] "Changing the Locale in Wine" https://stackoverflow.com/a/16428951 # [3] "Shanghai vs Beijing" https://bugs.launchpad.net/ubuntu/+source/libgweather/+bug/228554 @@ -146,9 +146,14 @@ main() { ( export TZ=UTC; id="UTC"; - date_string="$(date -Is)"; - right_epoch="$(TZ=right/UTC date --date="$date_string" +%s;)"; # see [4] - fs_3="+%s seconds (${right_epoch} with leap seconds) since 1970-01-01T00:00+00"; + path_right_tz="/usr/share/zoneinfo/right/UTC"; + if [[ -f "$path_right_tz" ]]; then + date_string="$(date -Is)"; + right_epoch="$(TZ=right/UTC date --date="$date_string" +%s;)"; # see [4] + fs_3="+%s POSIX seconds (${right_epoch} real) since 1970-01-01T00:00+00"; + else + fs_3="+%s POSIX seconds since 1970-01-01T00:00+00"; + fi; print_dateline "$@"; ); line_sep; -- 2.39.5 From 4a0d0e9acd5d9a1cc7612dbcdb87452f888e4423 Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Tue, 28 Apr 2026 08:25:50 +0000 Subject: [PATCH 12/16] fix(user/bkdatev):Fix --date='' functionality for right/UTC --- user/bkdatev | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/bkdatev b/user/bkdatev index fe60499..fb0b96c 100755 --- a/user/bkdatev +++ b/user/bkdatev @@ -2,7 +2,7 @@ # Desc: Baltakatei's verbose date command # Usage: bkdatev [args] # Example: bkdatev --date="2001-09-11T09:02:59-04" -# Version: 1.0.3 +# Version: 1.0.4 # Depends: GNU Coreutils 8.32, Bash 3.2.57, tzdata-legacy # Ref/Attrib: [1] "ISO 8601". Wikipedia. https://en.wikipedia.org/wiki/ISO_8601 # [2] "Changing the Locale in Wine" https://stackoverflow.com/a/16428951 @@ -148,7 +148,7 @@ main() { id="UTC"; path_right_tz="/usr/share/zoneinfo/right/UTC"; if [[ -f "$path_right_tz" ]]; then - date_string="$(date -Is)"; + date_string="$(date -Is "$@"; )"; right_epoch="$(TZ=right/UTC date --date="$date_string" +%s;)"; # see [4] fs_3="+%s POSIX seconds (${right_epoch} real) since 1970-01-01T00:00+00"; else -- 2.39.5 From fd31760b99dc5eb6af73beb12831544160b8cadb Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Sun, 31 May 2026 23:10:44 +0000 Subject: [PATCH 13/16] feat(user/bkots):v3.0.0: Batch upgrade and stamp with xargs --- user/bkots | 210 ++++++++++++++++++++++++++--------------------------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/user/bkots b/user/bkots index 8d6ecde..acea7f5 100755 --- a/user/bkots +++ b/user/bkots @@ -1,10 +1,11 @@ #!/usr/bin/env bash # Desc: Recursively timestamp files with OpenTimestamp # Usage: bkots [PATH] -# Version: 0.2.0 +# Version: 3.0.0 # Define variables declare -g max_job_count="2"; # default max job count +declare -g max_batch_size="500"; # default xargs batch size for upgrading and stamping declare -g age_threshold="60"; # min age to add file; seconds; declare -g stamp_throttle="0.1"; # seconds delay between stamps declare -g stamp_throttle_fail="1"; # seconds delay if stamp errors @@ -17,6 +18,7 @@ calendars+=("https://finney.calendar.eternitywall.com"); calendars+=("https://btc.calendar.catallaxy.com"); calendars+=("https://alice.btc.calendar.opentimestamps.org"); calendars+=("https://bob.btc.calendar.opentimestamps.org"); +export OTS_CALS; OTS_CALS="$(printf '%s\n' "${calendars[@]}")"; # convert calendars array into string list # Declare functions yell() { echo "$0: $*" >&2; } # print script path and all args to stderr @@ -205,8 +207,8 @@ showVersion() { vbm "DEBUG:showVersion function called." cat <<'EOF' -bkots 2.1.1 -Copyright (C) 2022 Steven Baltakatei Sandoval +bkots 3.0.0 +Copyright (C) 2026 Steven Baltakatei Sandoval License GPLv3: GNU GPL version 3 This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. @@ -246,8 +248,12 @@ showUsage() { included by default). -j, --jobs Specify simultaneous job count (default: 2) + -n, --batch-size + Specify file batch sizes for upgrade and stamp operations (default:500) -r, --recursive Consider files in dirs recursively. + --verify + Verify OTS files. --version Display script version. -v, --verbose @@ -344,9 +350,21 @@ processArgs() { showUsage; die "FATAL:Invalid job count:$2"; fi;; + -n | --batch-size) + if [[ -n "$2" ]] && [[ "$2" =~ ^[0-9]+$ ]]; then + max_batch_size="$2"; + vbm "STATUS:Max batch size set to:$max_batch_size"; + shift; + else + showUsage; + die "FATAL:Invalid xargs file batch size:$2"; + fi;; -r | --recursive) # Specify recursive option option_recursive="true"; - vbm "DEBUG:option enabled:include files in dirs recursively";; + vbm "DEBUG:Option enabled:include files in dirs recursively";; + --verify) + option_verify="true"; + vbm "DEBUG:Option enabled:verify OTS files";; --version) showVersion; exit 1;; # Show version -v | --verbose) opVerbose="true"; vbm "DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1]. --) # End of all options. See [2]. @@ -644,6 +662,7 @@ main() { ## Sort and prune file action arrays ### files to upgrade while read -r -d $'\0' line; do + if [[ -z $line ]]; then continue; fi; # Skip empty lines vbm "DEBUG:adding to files_to_upgrade_pruned:line:$line"; files_to_upgrade_pruned+=("$line"); done < <(printf "%s\0" "${files_to_upgrade[@]}" | sort -zu | shuf -z); # See [1] @@ -654,6 +673,7 @@ main() { ### files to verify while read -r -d $'\0' line; do + if [[ -z $line ]]; then continue; fi; # Skip empty lines vbm "DEBUG:adding to files_to_verify_pruned:line:$line"; files_to_verify_pruned+=("$line"); done < <(printf "%s\0" "${files_to_verify[@]}" | sort -zu | shuf -z); # See [1] @@ -664,6 +684,7 @@ main() { ### files to stamp while read -r -d $'\0' line; do + if [[ -z $line ]]; then continue; fi; # Skip empty lines vbm "DEBUG:adding to files_to_stamp_pruned:line:$line"; files_to_stamp_pruned+=("$line"); done < <(printf "%s\0" "${files_to_stamp[@]}" | sort -zu | shuf -z); # See [1] @@ -674,111 +695,90 @@ main() { # Act on files ## Assemble and execute upgrade file commands - for item in "${files_to_upgrade_pruned[@]}"; do - wait_for_jobslot && { - path_prf="$(cut -d $'\n' -f1 < <(echo "$item"))"; - if [[ -z "$path_prf" ]]; then - yell "ERROR:blank upgrade item encountered. Skipping:item:$item"; - return 1; # would have been `continue` were it not in a subshell - fi; - vbm "DEBUG:Attempting to upgrade proof file:path_prf:$path_prf"; - if [[ ! $option_dry_run == "true" ]]; then - ### Try upgrade with known calendars in random order - while read -r url; do - vbm "DEBUG:Upgrading with calendar:url:$url"; - - #### assemble command - local -a cmd_temp; - cmd_temp=("ots"); - if [[ "$opVerbose" = "true" ]]; then cmd_temp+=("-v"); fi; - cmd_temp+=("-l" "$url" "--no-default-whitelist"); - cmd_temp+=("upgrade" "$path_prf"); - if [[ "$opVerbose" = "true" ]]; then declare -p cmd_temp; fi; - - #### execute command - "${cmd_temp[@]}"; - unset cmd_temp; - break; - #ots -l "$url" --no-default-whitelist upgrade "$path_prf" && break; - done < <(printf "%s\n" "${calendars[@]}" | shuf); - else - yell "DEBUG:DRY RUN:Not running:\"ots upgrade $path_prf\""; - fi; - } & - done; - - ## Assemble and execute verify file commands - for item in "${files_to_verify_pruned[@]}"; do - wait_for_jobslot && { - path_src="$(cut -d $'\n' -f1 < <(echo "$item"))"; - path_prf="$(cut -d $'\n' -f2 < <(echo "$item"))"; - if [[ -z "$path_src" ]] || [[ -z "$path_prf" ]]; then - yell "ERROR:blank verify item encountered. Skipping:item:$item"; - return 1; # would have been `continue` were it not in a subshell - fi; - vbm "DEBUG:Attempting to verify source file:path_src:$path_src"; - vbm "DEBUG: against proof file: path_prf:$path_prf"; - if [[ ! $option_dry_run == "true" ]]; then - ### Try verify with known calendars in random order - while read -r url; do - vbm "DEBUG:Verifying with calendar:url:$url"; - - #### assemble command - local -a cmd_temp; - cmd_temp=("ots"); - if [[ "$opVerbose" = "true" ]]; then cmd_temp+=("-v"); fi; - cmd_temp+=("-l" "$url" "--no-default-whitelist"); - cmd_temp+=("verify" "-f" "$path_src" "$path_prf"); - if [[ "$opVerbose" = "true" ]]; then declare -p cmd_temp; fi; - - #### execute command - "${cmd_temp[@]}"; - unset cmd_temp; - break; - #ots -l "$url" --no-default-whitelist verify -f "$path_src" "$path_prf" && break; - done < <(printf "%s\n" "${calendars[@]}" | shuf); - else - yell "DEBUG:DRY RUN:Not running:\"ots verify -f $path_src $path_prf\""; - fi; - } & - done; + if [[ ${#files_to_upgrade_pruned[@]} -gt 0 ]]; then + if [[ $option_dry_run == "true" ]]; then + yell "DEBUG:DRY RUN:Not running:\"ots upgrade\""; + else + ### Pick random calendar + function get_calurl() { + # Desc: Returns random calendar url + # Input: string OTS_CALS newline-delimited list of calendars + # Output: stdout single calendar url + printf "%s" "$OTS_CALS" | shuf -n1; + }; # print random calendar url to stdout + export -f get_calurl; + + ### Perform upgrade command in xargs batches + printf '%s\0' "${files_to_upgrade_pruned[@]}" \ + | shuf -z \ + | xargs -0 -r -n "$max_batch_size" -P "$max_job_count" bash -c 'ots --no-default-whitelist -l "$(get_calurl)" upgrade "$@";' _ ; + fi; + fi; - ## Assemble and execute stamp file commands - for item in "${files_to_stamp_pruned[@]}"; do - wait_for_jobslot && { - path_src="$(cut -d $'\n' -f1 < <(echo "$item"))"; - if [[ -z "$path_src" ]]; then - yell "ERROR:blank stamp item encountered. Skipping:item:$item"; - return 1; # would have been `continue` were it not in a subshell - fi; - vbm "DEBUG:Attempting to stamp source file:path_src:$path_src"; - if [[ ! $option_dry_run == "true" ]]; then - - #### assemble command - local -a cmd_temp; - cmd_temp=("ots"); - if [[ "$opVerbose" = "true" ]]; then cmd_temp+=("-v"); fi; - cmd_temp+=("stamp" "$path_src"); - if [[ "$opVerbose" = "true" ]]; then declare -p cmd_temp; fi; - - #### execute command - if "${cmd_temp[@]}"; then - sleep "$stamp_throttle" || die "FATAL:Invalid stamp throttle."; + # ## Assemble and execute verify file commands + if [[ $option_verify == "true" ]]; then + for item in "${files_to_verify_pruned[@]}"; do + wait_for_jobslot && { + path_src="$(cut -d $'\n' -f1 < <(echo "$item"))"; + path_prf="$(cut -d $'\n' -f2 < <(echo "$item"))"; + if [[ -z "$path_src" ]] || [[ -z "$path_prf" ]]; then + yell "ERROR:blank verify item encountered. Skipping:item:$item"; + return 1; # would have been `continue` were it not in a subshell + fi; + vbm "DEBUG:Attempting to verify source file:path_src:$path_src"; + vbm "DEBUG: against proof file: path_prf:$path_prf"; + if [[ ! $option_dry_run == "true" ]]; then + ### Try verify with known calendars in random order + while read -r url; do + vbm "DEBUG:Verifying with calendar:url:$url"; + + #### assemble command + local -a cmd_temp; + cmd_temp=("ots"); + if [[ "$opVerbose" = "true" ]]; then cmd_temp+=("-v"); fi; + cmd_temp+=("-l" "$url" "--no-default-whitelist"); + cmd_temp+=("verify" "-f" "$path_src" "$path_prf"); + if [[ "$opVerbose" = "true" ]]; then declare -p cmd_temp; fi; + + #### execute command + "${cmd_temp[@]}"; + unset cmd_temp; + break; + #ots -l "$url" --no-default-whitelist verify -f "$path_src" "$path_prf" && break; + done < <(printf "%s\n" "${calendars[@]}" | shuf); else - yell "ERROR:Could not stamp file with command:$(declare -p cmd_temp)"; - sleep "$stamp_throttle_fail" || die "FATAL:Invalid stamp throttle."; + yell "DEBUG:DRY RUN:Not running:\"ots verify -f $path_src $path_prf\""; fi; - - unset cmd_temp; - #ots stamp "$path_src"; - else - yell "DEBUG:DRY RUN:Not running:\"ots stamp $path_src\""; - fi; - } & - done; + } & + done; + + ## Wait for jobs to finish. + wait; + else + vbm "DEBUG:Skipping operation:ots verify."; + fi; + + ## Assemble and execute stamp file commands + if [[ ${#files_to_stamp_pruned[@]} -gt 0 ]]; then + if [[ $option_dry_run == "true" ]]; then + yell "DEBUG:DRY RUN:Not running:\"ots stamp\""; + else + function get_calurl() { + # Desc: Returns random calendar url + # Input: string OTS_CALS newline-delimited list of calendars + # Output: stdout single calendar url + printf "%s" "$OTS_CALS" | shuf -n1; + }; # print random calendar url to stdout + export -f get_calurl; + + ### Perform stamp command in xargs batches + printf '%s\0' "${files_to_stamp_pruned[@]}" \ + | shuf -z \ + | xargs -0 -r -n "$max_batch_size" -P "$max_job_count" bash -c 'ots stamp "$@";' _ ; + fi; + fi; + - ## Wait for jobs to finish. - wait; }; # main program # Run program -- 2.39.5 From ef3df5c46b494872ad11cf69c8e75c046820c803 Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Mon, 1 Jun 2026 22:37:20 +0000 Subject: [PATCH 14/16] feat(user/bkotsgu):Add script to update OpenTimestamp files --- prvt | 2 +- user/bkotsgu | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100755 user/bkotsgu diff --git a/prvt b/prvt index 5d30a14..981e3b2 160000 --- a/prvt +++ b/prvt @@ -1 +1 @@ -Subproject commit 5d30a14c7710d2cacde755e16eead6be5079d999 +Subproject commit 981e3b23ed633b7c093b06947304e3b0b46e3183 diff --git a/user/bkotsgu b/user/bkotsgu new file mode 100755 index 0000000..348ed67 --- /dev/null +++ b/user/bkotsgu @@ -0,0 +1,33 @@ +#!/bin/bash +# Desc: Updates all ots files in $HOME and /mnt/ directories recursively. +# Depends: bash 5.1.16, GNU Findutils 4.8.0, GNU Coreutils 8.32 +# Version: 0.1.3 +# Depends: GNU Coreutils (nproc) + +main() { + # Specify find targets + declare -a find_targets; + + ## User home directory + find_targets+=("$HOME"); + + ## Subdirectories of /mnt/ + mapfile -t -O "${#find_targets[@]}" find_targets < <(find /mnt/ -mindepth 1 -maxdepth 1 -type d; ); + + # Specify options + find_depth=12; # find directory depth + cpu_count="$(nproc)"; # get logical CPU count + batch_size_xargs=100; # xargs batch size + cpu_duty="$((cpu_count / 4 + 1))"; # logical CPU cores to use + + # Remove OTS backup files + find "${find_targets[@]}" -maxdepth "$find_depth" -type f -name "*.ots.bak" -exec rm '{}' \; 2>/dev/random; + + # Upgrade OTS files + find "${find_targets[@]}" -maxdepth "$find_depth" -type f -name "*.ots" -print0 2>/dev/random | \ + sort -uz | \ + shuf -z | \ + xargs -0 -n "$batch_size_xargs" -P "$cpu_duty" ots u; +}; # main program + +time { main "$@"; }; -- 2.39.5 From 444527bd826b11e5132f49bb2ec55fe59369eaa0 Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Tue, 16 Jun 2026 21:18:58 +0000 Subject: [PATCH 15/16] feat(user/gthumb/mvall.org):Add gThumb command - Note: version 0.0.1 --- user/gthumb/mvall.org | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 user/gthumb/mvall.org diff --git a/user/gthumb/mvall.org b/user/gthumb/mvall.org new file mode 100644 index 0000000..af4319b --- /dev/null +++ b/user/gthumb/mvall.org @@ -0,0 +1,48 @@ +#+AUTHOR: Steven Baltakatei Sandoval +#+DATE: 2026-06-16 +#+TITLE: mvall +#+VERSION: 0.0.1 + +This command moves to trash a single selected file ~dir1/file123.ext~ +along with any adjacent files that share the same stem located within +the same directory (e.g. ~dir1/file123.*~). Also moved are matching +files in directories neighboring the originalʼs parent directory +(e.g. ~dir2/file123.*~). The destination trash directory is ~.trash~. + +Keyboard shortcut: ~Shift + T~ + +This is useful for pruning photos with multiple format extensions with +each extension type in a separate directory. For example, a ~JPG/~ +directory contains ~JPG/DSC01001.JPG~, ~JPG/DSC01001.JPG.ots~ and a +~ARW/~ directory contains ~ARW/DSC01001.ARW~. With ~JPG/DSC01001.JPG~ +selected and the ''rmall'' command executed within [[gThumb]], a ~.trash~ +directory is created next to ~JPG/~ and ~ARW/~. Then, the following +moves are performed: + +- ~JPG/DSC01001.JPG~ is moved to ~.trash/DSC01001.JPG~ +- ~JPG/DSC01001.JPG.ots~ is moved to ~.trash/DSC01001.JPG.ots~ +- ~ARW/DSC01001.ARW~ is moved to ~.trash/DSC01001.ARW~ + +Thus, by browsing ~JPG/~ in [[gThumb]] and performing *rmall* on undesired +photos, the unwanted related photos from not only ~JPG/~, ~ARW/~ and +other similar directories are prepared for disposal in ~.trash~. + +Line-by-line view: +#+BEGIN_EXAMPLE +# rmall v0.0.1 +dbf=/tmp/gtlog.txt; +f=%F; +fbase="$(basename "$f")"; +fne="$(printf "%s" "$fbase" | rev | cut -d'.' -f2- | rev; )"; +dppar="$(dirname %P)"; +dtrash="${dppar}/.trash"; +mkdir "$dtrash"; +printf "%s\n" "$dbf" "$f" "$fbase" "$fne" "$dppar" "$dtrash" 1>"$dbf" 2>&1; +mv -n -t "$dtrash" "${dppar}/"*"/${fne}".*; +sleep 1; +#+END_EXAMPLE + +Paste-ready view: +#+begin_example +dbf=/tmp/gtlog.txt; f=%F; fbase="$(basename "$f")"; fne="$(printf "%s" "$fbase" | rev | cut -d'.' -f2- | rev; )"; dppar="$(dirname %P)"; dtrash="${dppar}/.trash"; mkdir "$dtrash"; printf "%s\n" "$dbf" "$f" "$fbase" "$fne" "$dppar" "$dtrash" 1>"$dbf" 2>&1; mv -n -t "$dtrash" "${dppar}/"*"/${fne}".*; sleep 1; +#+end_example -- 2.39.5 From 40979a9da56b90894368a14145d848f4b216a7e5 Mon Sep 17 00:00:00 2001 From: Steven Baltakatei Sandoval Date: Tue, 16 Jun 2026 21:27:29 +0000 Subject: [PATCH 16/16] feat(user/gthumb/rmall.org):Rename and upgrade mvall to rmall - Note: Now preserves some directory structure when moving to .trash --- user/gthumb/mvall.org | 48 ----------------------------- user/gthumb/rmall.org | 72 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 48 deletions(-) delete mode 100644 user/gthumb/mvall.org create mode 100644 user/gthumb/rmall.org diff --git a/user/gthumb/mvall.org b/user/gthumb/mvall.org deleted file mode 100644 index af4319b..0000000 --- a/user/gthumb/mvall.org +++ /dev/null @@ -1,48 +0,0 @@ -#+AUTHOR: Steven Baltakatei Sandoval -#+DATE: 2026-06-16 -#+TITLE: mvall -#+VERSION: 0.0.1 - -This command moves to trash a single selected file ~dir1/file123.ext~ -along with any adjacent files that share the same stem located within -the same directory (e.g. ~dir1/file123.*~). Also moved are matching -files in directories neighboring the originalʼs parent directory -(e.g. ~dir2/file123.*~). The destination trash directory is ~.trash~. - -Keyboard shortcut: ~Shift + T~ - -This is useful for pruning photos with multiple format extensions with -each extension type in a separate directory. For example, a ~JPG/~ -directory contains ~JPG/DSC01001.JPG~, ~JPG/DSC01001.JPG.ots~ and a -~ARW/~ directory contains ~ARW/DSC01001.ARW~. With ~JPG/DSC01001.JPG~ -selected and the ''rmall'' command executed within [[gThumb]], a ~.trash~ -directory is created next to ~JPG/~ and ~ARW/~. Then, the following -moves are performed: - -- ~JPG/DSC01001.JPG~ is moved to ~.trash/DSC01001.JPG~ -- ~JPG/DSC01001.JPG.ots~ is moved to ~.trash/DSC01001.JPG.ots~ -- ~ARW/DSC01001.ARW~ is moved to ~.trash/DSC01001.ARW~ - -Thus, by browsing ~JPG/~ in [[gThumb]] and performing *rmall* on undesired -photos, the unwanted related photos from not only ~JPG/~, ~ARW/~ and -other similar directories are prepared for disposal in ~.trash~. - -Line-by-line view: -#+BEGIN_EXAMPLE -# rmall v0.0.1 -dbf=/tmp/gtlog.txt; -f=%F; -fbase="$(basename "$f")"; -fne="$(printf "%s" "$fbase" | rev | cut -d'.' -f2- | rev; )"; -dppar="$(dirname %P)"; -dtrash="${dppar}/.trash"; -mkdir "$dtrash"; -printf "%s\n" "$dbf" "$f" "$fbase" "$fne" "$dppar" "$dtrash" 1>"$dbf" 2>&1; -mv -n -t "$dtrash" "${dppar}/"*"/${fne}".*; -sleep 1; -#+END_EXAMPLE - -Paste-ready view: -#+begin_example -dbf=/tmp/gtlog.txt; f=%F; fbase="$(basename "$f")"; fne="$(printf "%s" "$fbase" | rev | cut -d'.' -f2- | rev; )"; dppar="$(dirname %P)"; dtrash="${dppar}/.trash"; mkdir "$dtrash"; printf "%s\n" "$dbf" "$f" "$fbase" "$fne" "$dppar" "$dtrash" 1>"$dbf" 2>&1; mv -n -t "$dtrash" "${dppar}/"*"/${fne}".*; sleep 1; -#+end_example diff --git a/user/gthumb/rmall.org b/user/gthumb/rmall.org new file mode 100644 index 0000000..d5e8986 --- /dev/null +++ b/user/gthumb/rmall.org @@ -0,0 +1,72 @@ +#+AUTHOR: Steven Baltakatei Sandoval +#+DATE: 2026-06-16 +#+TITLE: rmall + +** Summary +This command moves to trash a single selected file ~dir1/file123.ext~ +along with any adjacent files that share the same stem located within +the same directory (e.g. ~dir1/file123.*~). Also moved are matching +files in directories neighboring the originalʼs parent directory +(e.g. ~dir2/file123.*~). The destination trash directory is ~.trash~; +parent directories (e.g. ~dir1~, ~dir2~) are preserved within +~.trash/~. + +** Stats +Version: 0.0.3 +gThumb version: ~3.12.6~ +Suggested keyboard shortcut: ~Shift + T~ + +** Usage +*rmall* is useful for pruning photos with multiple format extensions +with each extension type in a separate directory. For example, letʼs +say a ~JPG/~ directory contains ~JPG/DSC01001.JPG~, +~JPG/DSC01001.JPG.ots~ and a ~ARW/~ directory contains +~ARW/DSC01001.ARW~. With ~JPG/DSC01001.JPG~ selected in gThumb, +~.trash~ directory is created next to ~JPG/~ and ~ARW/~. Then, the +following moves are performed: + +- ~JPG/DSC01001.JPG~ is moved to ~.trash/JPG/DSC01001.JPG~ +- ~JPG/DSC01001.JPG.ots~ is moved to ~.trash/JPG/DSC01001.JPG.ots~ +- ~ARW/DSC01001.ARW~ is moved to ~.trash/ARW/DSC01001.ARW~ + +Thus, by browsing ~JPG/~ in gThumb and performing *rmall* on undesired +photos, the unwanted related photos from not only ~JPG/~, ~ARW/~ and +other similar directories are prepared for disposal in ~.trash~. + +Then, it is upon the user to empty the contents of ~.trash~ to +permanently delete them. + +** Code +Line-by-line view: +#+BEGIN_EXAMPLE +# rmall v0.0.3 +dbf=/tmp/gtlog.txt; +f="%F"; +fbase="$(basename "$f")"; +fne="${fbase%.*}"; +dppar="$(dirname %P)"; +dtrash="${dppar}/.trash"; +printf "%s\n" "$dbf" "$f" "$fbase" "$fne" "$dppar" "$dtrash" 1>"$dbf" 2>&1; +for fsrc in "${dppar}/"*"/${fne}".*; do + if [ ! -e "$fsrc" ]; then continue; fi; + sdname="$(basename "$(dirname "$fsrc")")"; + ddest="${dtrash}/${sdname}"; + printf "fsrc:%s\n" "$fsrc"; + printf "sdname:%s\n" "$sdname"; + printf "ddest:%s\n" "$ddest"; + if [ ! -d "$ddest" ]; then + printf "DEBUG:ddest does not exist:%s" "$ddest"; + mkdir -p "$ddest"; + else + printf "DEBUG:ddest exists:%s" "$ddest"; + fi; + printf "STATUS: Moving %s → %s\n" "$fsrc" "$ddest/"; + mv -n "$fsrc" "$ddest/"; +done 1>>"$dbf" 2>&1; +sleep 1; +#+END_EXAMPLE + +Paste-ready view: +#+begin_example +dbf=/tmp/gtlog.txt; f="%F"; fbase="$(basename "$f")"; fne="${fbase%.*}"; dppar="$(dirname %P)"; dtrash="${dppar}/.trash"; printf "%s\n" "$dbf" "$f" "$fbase" "$fne" "$dppar" "$dtrash" 1>"$dbf" 2>&1; for fsrc in "${dppar}/"*"/${fne}".*; do if [ ! -e "$fsrc" ]; then continue; fi; sdname="$(basename "$(dirname "$fsrc")")"; ddest="${dtrash}/${sdname}"; printf "fsrc:%s\n" "$fsrc"; printf "sdname:%s\n" "$sdname"; printf "ddest:%s\n" "$ddest"; if [ ! -d "$ddest" ]; then printf "DEBUG:ddest does not exist:%s" "$ddest"; mkdir -p "$ddest"; else printf "DEBUG:ddest exists:%s" "$ddest"; fi; printf "STATUS: Moving %s → %s\n" "$fsrc" "$ddest/"; mv -n "$fsrc" "$ddest/"; done 1>>"$dbf" 2>&1; sleep 1; +#+end_example -- 2.39.5