Removing inflammatory YouTube comments programmatically

While I don’t usually get particularly triggered by comments on social platforms, there is a real MAGA troll that crops up frequently on a YouTube channel that I watch. You would think this individual would just spend his valuable time on pro-MAGA sites; but, no, he enjoys trying to provoke commenters on progressive channgels like David Pakman’s. Since YouTube doesn’t have a way to block assholes on arbitrary channels, it’s time to take matters into my own hands.

This is the kind of unhelpful, inflammatory comment that I’m trying to block:

Here’s how I do it.

Violentmonkey

Our blocker runs as a userscript in Violentmonkey, which is a free, open-source userscript manager available for Chromium-based browsers and Firefox. It allows users to run small JavaScript programs, called userscripts, that modify the behavior or appearance of websites. These scripts can automate tasks, remove ads, add features, or change layouts on web pages. Violentmonkey is compatible with most scripts written for Greasemonkey and Tampermonkey.

There are similar extensions including a port that runs on Safari, but this is what I use; and I use it in Firefox.

The script

Since the user ID of the person we’re trying to block is this: @_ID_as_Non_Bidenary we will of course need to define let asshole = '@_ID_as_Non_Bidenary'; Using the Firefox developer tools, we can find the largest block that encloses this user’s comments.

// ==UserScript==
// @name        Remove Specific YouTube Comments by ID
// @namespace   Violentmonkey Scripts
// @match       https://www.youtube.com/*
// @grant       none
// @version     1.0
// @author      Ojisan Seiuchi - 2025-06-11
// @description Removes YouTube comment blocks where the author's ID (from aria-label)
//              matches a specific pattern.
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Finds and removes YouTube comment blocks based on the author's aria-label.
     * It specifically targets comments where the 'aria-label' attribute of the
     * author's thumbnail button starts with '@_ID_as_Non_Bidenary'.
     */
    function removeSpecificComments() {
        // Select all elements that represent a comment block.
        const commentViewModels = document.querySelectorAll('ytd-comment-view-model');

        commentViewModels.forEach(comment => {
            const authorThumbnailButton =
                comment.querySelector('button#author-thumbnail-button');

            if (authorThumbnailButton && authorThumbnailButton.ariaLabel) {
                if (authorThumbnailButton.ariaLabel.startsWith('@_ID_as_Non_Bidenary')) {
                    if (comment.parentNode) {
                        comment.parentNode.removeChild(comment);
                        console.log(
                            'Removed a comment block by:',
                            authorThumbnailButton.ariaLabel
                        );
                    }
                }
            }
        });
    }

    // --- Initial Run and Dynamic Content Handling ---

    // 1. Run once on script load
    removeSpecificComments();

    // 2. Observe DOM for dynamically loaded comments
    const observer = new MutationObserver((mutationsList, observer) => {
        for (const mutation of mutationsList) {
            if (
                mutation.type === 'childList' &&
                mutation.addedNodes.length > 0
            ) {
                removeSpecificComments();
            }
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();

Next steps

  • Since there is more than one MAGA asshole, we should extend the script to allows us to filter an entire list of them.
  • Because there are still comments that mention the asshole, if we’re being really thorough, we should remove those as well.

Creating Obsidian tables of content

When viewing longer Markdown notes in Obsidian, tables of content (TOC) help a lot with navigation. There is a handful of community plugins to help with TOC generation, but I have two issues with them:

  1. It creates a dependency on code whose developer may lose interest and eventually abandon the project. At least one dynamic TOC plugin has suffered this fate.
  2. All of the TOC plugins have the same visual result. When you navigate to a note, Obsidian places the focus at the top of the note, beneath the frontmatter. That’s fine unless the content starts with a TOC markup block, in which case it’s not the TOC itself that is displayed, but the markup for the TOC plugin itself as depicted in the image below.

For me the solution was to write a script that scans the vault looking for this pair of markers:

<!--ts-->
<!--te-->

that define the start and end for the TOC block. When these markers are found, the script inserts a TOC between the markers. The downside of course is that the markers, which are just raw HTML comments in the Markdown document are still visible when you’re in Edit mode. But it’s still better than not seeing the actual TOC at all when you navigate to a note.

Technical Details

Finding Files that Need TOC Updates

The script begins by using the find command to locate recently modified Markdown files in your Obsidian vault. It specifically looks for files modified within the last minute, making it efficient for running as a background process. This approach ensures that TOCs are updated only in notes you’ve recently worked on, rather than scanning your entire vault every time.

find_cmd="find \"$VAULT_DIR\" \
  -path \"*/$EXCLUDE_DIRS\" -prune \
  -o -type f \
  -name \"*.md\" \
  -newermt \"1 minute ago\" \
  -print"
  

For each file, the script:

  1. Checks if valid TOC markers exist outside of code blocks
  2. Counts the headings in the document
  3. Compares the current TOC entries against the document’s headings
  4. Updates the TOC if:
    • The TOC is empty but headings exist
    • The heading content has changed
    • The file was modified since the last run

This strategy minimizes unnecessary updates and ensures TOCs stay in sync with your content as you write and edit.

Handling Code Blocks and Marker Detection

One of the challenges with this approach is distinguishing between actual TOC markers and examples of markers included in code blocks. For instance, if you’re writing documentation about the TOC script itself, you might include the TOC markers in a code example.

The script solves this by carefully tracking when it’s inside or outside of code blocks:

# Check for code block start/end
if [[ "$line" =~ ^[[:space:]]*(\`\`\`|\~\~\~) ]]; then
    # Extract the fence type (``` or ~~~)
    fence_match="${BASH_REMATCH[1]}"
    
    if [[ "$in_code_block" == "false" ]]; then
        in_code_block=true
        code_fence="$fence_match"
    elif [[ "$line" == *"$code_fence"* ]]; then
        in_code_block=false
        code_fence=""
    fi
}

When analyzing a file, the script maintains state variables to track:

  • Whether we’re currently inside a code block
  • The type of code fence used (``` or ~~~)
  • Whether we’re in YAML frontmatter

This context-aware parsing ensures that TOC markers inside code blocks are completely ignored. This is crucial because it prevents the script from incorrectly modifying code examples or documentation that happens to contain the markers.

Usage

Basic Usage

The script can be run directly from the command line:

./obsidian_toc_monitor.sh /path/to/obsidian/vault

You can also specify which directories to exclude:

./obsidian_toc_monitor.sh --exclude ".obsidian,Templates,Attachments" /path/to/vault

Automating TOC Updates

There are several ways to automate the script depending on your operating system:

On macOS:

  1. Using launchd - Create a launch agent to run the script periodically:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.obsidian.tocmonitor</string>
    <key>ProgramArguments</key>
    <array>
        <string>/path/to/obsidian_toc_monitor.sh</string>
        <string>/path/to/obsidian/vault</string>
    </array>
    <key>StartInterval</key>
    <integer>60</integer>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

Save this to ~/Library/LaunchAgents/com.user.obsidian.tocmonitor.plist and load it with:

launchctl load ~/Library/LaunchAgents/com.user.obsidian.tocmonitor.plist
  1. Using cron - Add an entry to your crontab:
* * * * * /path/to/obsidian_toc_monitor.sh /path/to/obsidian/vault

On Linux:

  1. Using systemd - Create a user service:
[Unit]
Description=Obsidian TOC Monitor

[Service]
Type=simple
ExecStart=/path/to/obsidian_toc_monitor.sh /path/to/obsidian/vault
Restart=always
RestartSec=60

[Install]
WantedBy=default.target

Save this to ~/.config/systemd/user/obsidian-toc.service and enable it:

systemctl --user enable --now obsidian-toc.service
  1. Using cron (same as macOS)

Adding TOC Markers to Your Notes

To prepare a note for TOC generation, simply add these markers where you want the TOC to appear:

# My Note Title

<!--ts-->
<!--te-->

## First Section
Content here...


The script will detect these markers and insert a formatted TOC.

You get the benefits of a dynamic TOC without visual issues.

Script code

#!/bin/bash

# Default configuration
LOG_FILE="$HOME/Library/Logs/obsidian_toc_monitor.log"
LAST_RUN_FILE="/tmp/obsidian_toc_monitor_lastrun"
EXCLUDE_DIRS=".obsidian pdf Templates Attachments Meta"
DEBUG="${DEBUG:-false}"
VERBOSE="${VERBOSE:-true}"  # Set to true to show terminal output

# Global counters
FILE_COUNT=0
MODIFIED_COUNT=0

# Color codes for better terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
RESET='\033[0m'

# Usage information
show_usage() {
    echo "Usage: $(basename "$0") [OPTIONS] VAULT_DIRECTORY"
    echo ""
    echo "Options:"
    echo "  -h, --help            Show this help message and exit"
    echo "  -q, --quiet           Run in quiet mode (no terminal output)"
    echo "  -d, --debug           Enable debug logging"
    echo "  -e, --exclude DIRS    Comma-separated list of directories to exclude (default: $EXCLUDE_DIRS)"
    echo "  -l, --log FILE        Path to log file (default: $LOG_FILE)"
    echo ""
    echo "Example:"
    echo "  $(basename "$0") /path/to/obsidian/vault"
    echo "  $(basename "$0") --exclude '.obsidian,pdf,Templates' /path/to/vault"
}

# Parse command line arguments
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            show_usage
            exit 0
            ;;
        -q|--quiet)
            VERBOSE=false
            shift
            ;;
        -d|--debug)
            DEBUG=true
            shift
            ;;
        -e|--exclude)
            EXCLUDE_DIRS="$2"
            shift 2
            ;;
        -l|--log)
            LOG_FILE="$2"
            shift 2
            ;;
        -*|--*)
            echo "Error: Unknown option $1"
            show_usage
            exit 1
            ;;
        *)
            POSITIONAL_ARGS+=("$1")
            shift
            ;;
    esac
done
set -- "${POSITIONAL_ARGS[@]}"

# Check if vault directory was provided
if [ $# -lt 1 ]; then
    echo "Error: No vault directory specified"
    show_usage
    exit 1
fi

# Set the vault directory
VAULT_DIR="$1"

# Check if vault directory exists
if [ ! -d "$VAULT_DIR" ]; then
    echo "Error: Vault directory does not exist: $VAULT_DIR"
    exit 1
fi

# Ensure log directory exists
mkdir -p "$(dirname "$LOG_FILE")"

# Touch the last run file if it doesn't exist
[ ! -f "$LAST_RUN_FILE" ] && touch "$LAST_RUN_FILE"

# Logging functions
log_info() {
    echo "[INFO] $1" >> "$LOG_FILE"
    if [[ "$VERBOSE" == "true" ]]; then
        echo -e "${GREEN}[INFO]${RESET} $1"
    fi
}

log_error() {
    echo "[ERROR] $1" >> "$LOG_FILE"
    if [[ "$VERBOSE" == "true" ]]; then
        echo -e "${RED}[ERROR]${RESET} $1"
    fi
}

log_debug() {
    if [[ "$DEBUG" == "true" ]]; then
        echo "[DEBUG] $1" >> "$LOG_FILE"
        echo -e "${BLUE}[DEBUG]${RESET} $1"
    fi
}

generate_toc() {
    local FILE_PATH="$1"
    local TEMP_FILE="$2"
    
    # First add the TOC heading
    echo "## Table of contents" > "$TEMP_FILE"
    echo "" >> "$TEMP_FILE"  # Add blank line after the heading
    
    # Find headings and generate TOC
    local min_level=6
    declare -a heading_lines
    local in_code_block=false
    local code_fence=""
    local in_frontmatter=false
    
    while IFS= read -r line; do
        # Handle YAML frontmatter
        if [[ "$line" == "---" ]]; then
            if [[ "$in_frontmatter" == "false" ]]; then
                in_frontmatter=true
            else
                in_frontmatter=false
            fi
            continue
        fi
        
        # Skip processing if in frontmatter
        if [[ "$in_frontmatter" == "true" ]]; then
            continue
        fi
        
        # Skip processing the TOC heading itself
        if [[ "$line" == "## Table of contents" ]]; then
            continue
        fi
        
        # Check for code block start/end
        if [[ "$line" =~ ^[[:space:]]*(\`\`\`|\~\~\~) ]]; then
            # Extract the fence type (``` or ~~~)
            fence_match="${BASH_REMATCH[1]}"
            
            if [[ "$in_code_block" == "false" ]]; then
                in_code_block=true
                code_fence="$fence_match"
            elif [[ "$line" == *"$code_fence"* ]]; then
                in_code_block=false
                code_fence=""
            fi
            continue
        fi
        
        # Only process headings if we're not inside a code block
        if [[ "$in_code_block" == "false" ]]; then
            if [[ "$line" =~ ^[[:space:]]*(#{1,6})[[:space:]]+(.*[^[:space:]]) ]]; then
                # The heading marker is captured in BASH_REMATCH[1]
                local marker="${BASH_REMATCH[1]}"
                local level=${#marker}
                # The heading text is captured in BASH_REMATCH[2]
                local title="${BASH_REMATCH[2]}"
                
                # Update min level if needed
                if [[ $level -lt $min_level ]]; then
                    min_level=$level
                fi
                
                # Store heading info for later processing
                heading_lines+=("$level|$title")
                
                if [[ "$VERBOSE" == "true" ]]; then
                    echo -e "    ${CYAN}→ Level $level:${RESET} $title"
                fi
            fi
        fi
    done < "$FILE_PATH"
    
    # Generate TOC entries
    for entry in "${heading_lines[@]}"; do
        IFS='|' read -r level title <<< "$entry"
        
        # Clean markdown formatting
        clean_title="$title"
        clean_title="${clean_title//\*\*/}"  # Remove bold
        clean_title="${clean_title//\*/}"    # Remove italic
        clean_title="${clean_title//\`/}"    # Remove code
        
        # Calculate relative indent level
        rel_level=$((level - min_level))
        
        if [[ $rel_level -eq 0 ]]; then
            echo "* [[#$clean_title]]" >> "$TEMP_FILE"
        else
            indent=$(printf "%$((rel_level * 4))s" "")
            echo "${indent}- [[#$clean_title]]" >> "$TEMP_FILE"
        fi
    done
    
    # Return the number of headings found
    echo ${#heading_lines[@]}
}

update_toc() {
    local FILE_PATH="$1"
    local START_LINE="$2"
    local END_LINE="$3"
    local was_updated=false
    
    log_debug "Updating TOC for $FILE_PATH (markers at lines $START_LINE and $END_LINE)"
    
    # Create temporary files
    local TEMP_TOC=$(mktemp)
    local TEMP_FILE=$(mktemp)
    
    # Generate the TOC
    if [[ "$VERBOSE" == "true" ]]; then
        echo -e "  ${YELLOW}Parsing headings...${RESET}"
    fi
    
    local heading_count=$(generate_toc "$FILE_PATH" "$TEMP_TOC")
    
    if [[ "$VERBOSE" == "true" ]]; then
        echo -e "  ${YELLOW}Generating TOC with $heading_count headings...${RESET}"
    fi
    
    # Extract current TOC content for comparison
    local current_toc=$(sed -n "${START_LINE},${END_LINE}p" "$FILE_PATH")
    
    # Create updated file with new TOC
    {
        # Copy lines before TOC
        sed -n "1,$((START_LINE-1))p" "$FILE_PATH"
        
        # Copy start marker line
        sed -n "${START_LINE}p" "$FILE_PATH"
        
        # Insert new TOC content
        cat "$TEMP_TOC"
        
        # Copy end marker line
        sed -n "${END_LINE}p" "$FILE_PATH"
        
        # Copy lines after TOC
        sed -n "$((END_LINE+1)),\$p" "$FILE_PATH"
    } > "$TEMP_FILE"
    
    # Extract new TOC content for comparison
    local new_toc=$(sed -n "${START_LINE},$((START_LINE+1+$(wc -l < "$TEMP_TOC")+1))p" "$TEMP_FILE")
    
    # Compare TOCs to see if an update is needed
    if ! diff -q <(echo "$current_toc") <(echo "$new_toc") >/dev/null; then
        # TOC content has changed - update the file
        if [[ "$VERBOSE" == "true" ]]; then
            echo -e "  ${GREEN}✓ Updating TOC with $heading_count entries${RESET}"
        fi
        
        # Move updated file into place
        mv "$TEMP_FILE" "$FILE_PATH"
        
        # Add a small delay and touch the file to ensure changes are detected
        sleep 0.1
        touch "$FILE_PATH"
        
        log_info "TOC updated for: $FILE_PATH"
        was_updated=true
        
        # Increment global counter 
        ((MODIFIED_COUNT++))
    else
        if [[ "$VERBOSE" == "true" ]]; then
            echo -e "  ${GREEN}✓ TOC already up to date${RESET}"
        fi
        log_debug "No TOC changes needed for: $FILE_PATH"
    fi
    
    # Clean up temporary files
    rm -f "$TEMP_TOC" "$TEMP_FILE" 2>/dev/null
    
    return $([ "$was_updated" = true ] && echo 0 || echo 1)
}

find_valid_toc_markers() {
    local FILE_PATH="$1"
    local START_LINE_VAR="$2"
    local END_LINE_VAR="$3"
    
    # Use grep to find potential marker line numbers
    local potential_start_markers=$(grep -n "<!-- *ts *-->" "$FILE_PATH" | cut -d':' -f1)
    local potential_end_markers=$(grep -n "<!-- *te *-->" "$FILE_PATH" | cut -d':' -f1)
    
    # If no potential markers found at all, return false
    if [[ -z "$potential_start_markers" || -z "$potential_end_markers" ]]; then
        return 1
    fi
    
    # Parse the file line by line to verify markers are outside code blocks
    local line_num=0
    local in_code_block=false
    local code_fence=""
    local in_frontmatter=false
    local valid_start_line=""
    local valid_end_line=""
    
    while IFS= read -r line; do
        ((line_num++))
        
        # Handle YAML frontmatter
        if [[ "$line" == "---" ]]; then
            if [[ "$in_frontmatter" == "false" ]]; then
                in_frontmatter=true
            else
                in_frontmatter=false
            fi
            continue
        fi
        
        # Skip processing if in frontmatter
        if [[ "$in_frontmatter" == "true" ]]; then
            continue
        fi
        
        # Check for code block start/end
        if [[ "$line" =~ ^[[:space:]]*(\`\`\`|\~\~\~) ]]; then
            # Extract the fence type (``` or ~~~)
            fence_match="${BASH_REMATCH[1]}"
            
            if [[ "$in_code_block" == "false" ]]; then
                in_code_block=true
                code_fence="$fence_match"
                log_debug "Line $line_num: Code block starts"
            elif [[ "$line" == *"$code_fence"* ]]; then
                in_code_block=false
                code_fence=""
                log_debug "Line $line_num: Code block ends"
            fi
            continue
        fi
        
        # Only check for TOC markers if we're not inside a code block
        if [[ "$in_code_block" == "false" ]]; then
            # Check if current line is in our potential marker list
            if echo "$potential_start_markers" | grep -q "^$line_num$"; then
                valid_start_line=$line_num
                log_debug "Line $line_num: Valid start marker found"
            fi
            
            if echo "$potential_end_markers" | grep -q "^$line_num$"; then
                valid_end_line=$line_num
                log_debug "Line $line_num: Valid end marker found"
            fi
        fi
    done < "$FILE_PATH"
    
    # Only succeed if we found valid markers outside code blocks
    if [[ -z "$valid_start_line" || -z "$valid_end_line" ]]; then
        return 1
    fi
    
    # Set the output variables
    eval "$START_LINE_VAR=$valid_start_line"
    eval "$END_LINE_VAR=$valid_end_line"
    return 0
}

process_file() {
    local FILE_PATH="$1"
    local update_needed=false
    
    # Skip if file doesn't exist or isn't a regular file
    if [[ ! -f "$FILE_PATH" ]]; then
        return 1
    fi
    
    # Skip non-markdown files
    if [[ ! "$FILE_PATH" =~ \.(md|markdown)$ ]]; then
        return 1
    fi
    
    # Increment global counter
    ((FILE_COUNT++))
    
    log_debug "Checking file: $FILE_PATH"
    if [[ "$VERBOSE" == "true" ]]; then
        filename=$(basename "$FILE_PATH")
        echo -e "${YELLOW}Checking:${RESET} $filename"
    fi
    
    # Find valid TOC markers (outside code blocks)
    local start_line=""
    local end_line=""
    if ! find_valid_toc_markers "$FILE_PATH" start_line end_line; then
        log_debug "No valid TOC markers found in file - $FILE_PATH"
        if [[ "$VERBOSE" == "true" ]]; then
            echo -e "  ${YELLOW}→ No valid TOC markers found${RESET}"
        fi
        return 1
    fi
    
    log_debug "Found valid TOC markers at lines $start_line and $end_line"
    
    # Count the number of headings in the file, excluding those in code blocks
    heading_count=0
    in_code=false
    code_fence=""
    in_front=false
    declare -a heading_lines
    
    while IFS= read -r line; do
        # Handle YAML frontmatter
        if [[ "$line" == "---" ]]; then
            if [[ "$in_front" == "false" ]]; then
                in_front=true
            else
                in_front=false
            fi
            continue
        fi
        
        if [[ "$in_front" == "true" ]]; then
            continue
        fi
        
        # Check for code block start/end
        if [[ "$line" =~ ^[[:space:]]*(\`\`\`|\~\~\~) ]]; then
            fence="${BASH_REMATCH[1]}"
            if [[ "$in_code" == "false" ]]; then
                in_code=true
                code_fence="$fence"
            elif [[ "$line" == *"$code_fence"* ]]; then
                in_code=false
                code_fence=""
            fi
            continue
        fi
        
        # Only count headings outside code blocks
        if [[ "$in_code" == "false" && "$line" =~ ^[[:space:]]*(#{1,6})[[:space:]]+(.*[^[:space:]]) ]]; then
            # Skip TOC heading
            if [[ "$line" =~ "Table of contents" ]]; then
                continue
            fi
            
            # Extract heading level and text
            level=${#BASH_REMATCH[1]}
            title="${BASH_REMATCH[2]}"
            
            # Store heading info for later comparison
            heading_lines+=("$title")
            ((heading_count++))
        fi
    done < "$FILE_PATH"
    
    log_debug "Found $heading_count headings in file (excluding code blocks)"
    
    # Extract TOC content (between markers)
    local toc_content=$(sed -n "$((start_line+1)),$((end_line-1))p" "$FILE_PATH")
    
    # Extract TOC entry texts
    declare -a toc_texts
    while IFS= read -r line; do
        if [[ "$line" =~ \[\[#(.*)\]\] ]]; then
            toc_text="${BASH_REMATCH[1]}"
            toc_texts+=("$toc_text")
            log_debug "TOC entry: $toc_text"
        fi
    done < <(echo "$toc_content")
    
    # Count TOC entries
    toc_entries=${#toc_texts[@]}
    log_debug "TOC has $toc_entries entries"
    
    # Check if any heading text has changed
    headings_changed=false
    if [[ ${#toc_texts[@]} -eq ${#heading_lines[@]} ]]; then
        for ((i=0; i<${#heading_lines[@]}; i++)); do
            # Clean heading text for comparison
            clean_heading="${heading_lines[$i]}"
            clean_heading="${clean_heading//\*\*/}"
            clean_heading="${clean_heading//\*/}"
            clean_heading="${clean_heading//\`/}"
            
            if [[ "${toc_texts[$i]}" != "$clean_heading" ]]; then
                log_debug "Heading text changed: '${toc_texts[$i]}' → '$clean_heading'"
                headings_changed=true
                break
            fi
        done
    else
        log_debug "Different number of headings (TOC: ${#toc_texts[@]}, Doc: ${#heading_lines[@]})"
        headings_changed=true
    fi
    
    # Get the last modified time of the file
    file_mtime=$(stat -f "%m" "$FILE_PATH" 2>/dev/null || stat -c "%Y" "$FILE_PATH")
    last_run_time=$(stat -f "%m" "$LAST_RUN_FILE" 2>/dev/null || stat -c "%Y" "$LAST_RUN_FILE")
    
    # Decide if update is needed
    if [[ "$toc_entries" -eq 0 || "$headings_changed" == "true" || "$file_mtime" -lt "$last_run_time" ]]; then
        reason=""
        if [[ "$toc_entries" -eq 0 ]]; then
            reason="TOC has no entries"
        elif [[ "$headings_changed" == "true" ]]; then
            reason="Heading content has changed"
        else
            reason="File was modified by user"
        fi
        
        log_info "Processing file: $FILE_PATH ($reason)"
        update_toc "$FILE_PATH" "$start_line" "$end_line"
        return $?
    else
        log_debug "TOC is up to date (entries: $toc_entries, headings: $heading_count)"
        if [[ "$VERBOSE" == "true" ]]; then
            echo -e "  ${GREEN}✓ TOC already up to date${RESET}"
        fi
        return 1
    fi
}

# Main script starts here
log_info "Starting TOC monitor run on vault: $VAULT_DIR"

# Convert exclude dirs from comma-separated to space-separated if needed
EXCLUDE_DIRS="${EXCLUDE_DIRS//,/ }"

# Build the find command with exclude directories
find_cmd="find \"$VAULT_DIR\""
for dir in $EXCLUDE_DIRS; do
    find_cmd="$find_cmd -path \"*/$dir\" -prune -o"
done
find_cmd="$find_cmd -type f -name \"*.md\" -newermt \"1 minute ago\" -print"

log_debug "Find command: $find_cmd"

if [[ "$VERBOSE" == "true" ]]; then
    echo -e "\n${MAGENTA}====================================================${RESET}"
    echo -e "${MAGENTA}  OBSIDIAN TOC MONITOR - STARTING RUN $(date '+%H:%M:%S')${RESET}"
    echo -e "${MAGENTA}====================================================${RESET}\n"
    echo -e "${CYAN}Checking for recently modified Markdown files in:${RESET}"
    echo -e "${CYAN}$VAULT_DIR${RESET}\n"
    
    if [[ -n "$EXCLUDE_DIRS" ]]; then
        echo -e "${CYAN}Excluding directories:${RESET} $EXCLUDE_DIRS\n"
    fi
fi

# Get the list of files to process
FILE_LIST=$(eval "$find_cmd")

# Process each file
if [[ -n "$FILE_LIST" ]]; then
    while IFS= read -r file; do
        process_file "$file"
    done <<< "$FILE_LIST"
else
    log_info "No recently modified files found"
    if [[ "$VERBOSE" == "true" ]]; then
        echo -e "${YELLOW}No recently modified files found${RESET}"
    fi
fi

# After processing all files, update the last run timestamp
touch "$LAST_RUN_FILE"

# Show summary
if [[ "$VERBOSE" == "true" ]]; then
    echo -e "\n${MAGENTA}====================================================${RESET}"
    if [[ $MODIFIED_COUNT -gt 0 ]]; then
        echo -e "${GREEN}  ✓ Updated $MODIFIED_COUNT TOCs out of $FILE_COUNT files checked${RESET}"
    else
        echo -e "${GREEN}  ✓ No updates needed. Checked $FILE_COUNT files.${RESET}"
    fi
    echo -e "${MAGENTA}====================================================${RESET}\n"
fi

log_info "TOC monitor run completed. Updated $MODIFIED_COUNT files."
exit 0

How I rid my life of social media

If social media is working for you and you don’t care about the moral implications of using social media, then this post isn’t for you.

On the other hand, if the MAGA shift of social media, the love fest between Zuck, Musk, and Tr*mp and their slimey ilk makes you feel a little cringey. Or if you realize that you’re wasting countless minutes of your one wild and precious life, then this may be for you. Fair warning, it gets pretty technical; so stop wherever you want. It takes little more than a decision and a healthy dose of willpower. But if you want to block social media and cast it into the fires of Mt. Doom, here’s how.

When will I get to 1,000,000 Anki reviews?

Recently I’ve been wondering how long it would take me to get to 1,000,000 reviews. Right now I’m sitting at between 800,000 and 900,000 reviews and for no other reason than pure ridiculous curiosity I was curious whether I could get SQLite to compute it directly for me. Turns out the answer is “yes, you can.”

        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
        
    </div>
    <div class="callout-title-inner">CAUTION</div>
</div>
<div class="callout-content" >
    Before you run anything that accesses the Anki database outside of the Anki application itself, you absolutely should backup your database first. You have been warned.
</div>

Here’s the query in its gory detail and then I’ll walk through how it works:

Why I'm quitting Facebook (again)

This isn’t the first time, but I hope it will be the last.

Facebook, for me has long been a source of enjoyment and connection. But it also leaves me feeling cringey. So what changed?

What changed is that Facebook has gone full-on MAGA and I’m not OK with that:

  • “Meta CEO Mark Zuckerberg met with President-elect Donald Trump on Friday [January 10, 2025] at Mar-a-Lago, two sources familiar tell CNN. Meta declined to comment on the meeting between Zuckerberg and Trump.” - Source
  • Meta said today [January 7, 2025] it will end its fact-checking program in exchange for X-style community notes as part of a slate of changes targeting ‘censorship’ and embracing ‘free expression’. - Source,
    • We all know how this has gone at “X”, where self-proclaimed “free speech absolutist” has actively shaped pro-Republican messaging on the platform.
  • “Joel Kaplan, a prominent Republican, replaced Meta’s policy chief Nick Clegg last week. (He said Meta’s third-party fact-checkers have demonstrated ’too much political bias’ in a Fox News interview this morning [January 7, 2025.)” - Source
  • “CEO Mark Zuckerberg dined at Mar-a-Lago on Thanksgiving eve. [November 27, 2024]” - Source
  • “The company [Meta/Facebook] pledged a $1 million donation to Trump’s inauguration.” - Source
  • “On Monday, it [Meta] added three people to its board, including close Trump ally Dana White.” - Source
    • I didn’t know who Dana White was but he appears to be the president and CEO of Ultimate Fighting Championship (UFC) and the owner of Power Slap, which is a “slap fighting” promotion, whatever that is. The bottom line is that he sounds like he’s rich and into violence, just the type of person that would appeal to Tr*mp.

So thanks for the memories, Facebook. But for me this is the end of the road.

Registering a custom collation to query Anki database

While working on a project that requires querying the Anki database directly outside of the Anki desktop application, I encountered an interesting issue with sqlite3 collations. This is is just a short post about how I went about registering a collation in order to execute SQL queries against the Anki db.

        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
        
    </div>
    <div class="callout-title-inner">CAUTION</div>
</div>
<div class="callout-content" >
    Before you run anything that accesses the Anki database, you absolutely should backup your database first.
</div>

The problem

Let’s try a simple query. Open the Anki database:

Fix your Anki streak - the script edition

Like many Anki users, I keep track of my streaks because it motivates me to do my reviews each day. But since life gets in the way sometimes, I may miss my reviews in one or more decks. It has been years since I’ve neglected to do all of my reviews; but sometimes I will forget to come back later in the day to finish up some of my decks. Since I like to have a clean review heatmap, I will “fix” my streak in a skipped deck.

An API (sort of) for adding links to ArchiveBox

I use ArchiveBox extensively to save web content that might change or disappear. While a REST API is apparently coming eventually, it doesn’t appear to have been merged into the main fork. So I cobbled together a little application to archive links via a POST request. It takes advantage of the archivebox command line interface. If you are impatient, you can skip to the full source code. Otherwise I’ll describe my setup to provide some context.

A Keyboard Maestro action to save bookmarks to Espial

So this is a little esoteric, but it meets a need I encountered; and it may meet yours if you use Espial, Keyboard Maestro and are on macOS.

For several years I’ve been using Espial a bookmark manager that looks and feels like Pinboard, but is both self-hosted and drama-free1. Espial is easy to setup, stores its data in a comprehensible sqlite database and has an API, which comes in handy when it came to solving the problem I encountered.

Louisiana and the Ten Commandments

Recently, the governor of Louisiana signed a bill requiring all public school classrooms in the state to display a poster-sized copy of the Ten Commandments. In the “Beforetimes” (before the current partisan Supreme Court took shape), this would have been struck down immediately as a violation of the Establishment Clause of the First Amendment. This bill is a clear violation of that clause. I imagine that the justices will dance around the cultural and historical significance of the document without stopping to consider the state’s motives in passing this law. While the proponents of the Ten Commandments aren’t wrong about its historical significance, the U.S. Constitution and its Amendments arguably hold more importance from the secular perspective that one must adopt in a public school.