diff --git a/bat/batch_compress.md b/bat/batch_compress.md new file mode 100644 index 0000000..5e9e245 --- /dev/null +++ b/bat/batch_compress.md @@ -0,0 +1,110 @@ +# Batch Video Compression Scripts + +This repository contains two PowerShell scripts designed to recursively scan your directories, find large video files, and compress them into highly efficient H.265 (HEVC) formats. + +Choose the script that best matches your system hardware and compression goals: +1. **`batch_compress.ps1` (GPU Accelerated)**: Best for raw speed and keeping CPU usage at 0%. +2. **`batch_compress_cpu.ps1` (CPU Optimized)**: Best for achieving the absolute smallest file sizes and maximum storage savings using high-quality CRF encoding. + +--- + +## 🚀 Key Features Comparison + +| Feature | `batch_compress.ps1` (GPU) | `batch_compress_cpu.ps1` (CPU) | +| :--- | :--- | :--- | +| **Engine** | NVIDIA NVENC (`hevc_nvenc`) | Software x265 (`libx265`) | +| **Hardware Reqs** | NVIDIA Graphics Card | Modern Multi-core CPU | +| **Encoding Speed** | **Extremely Fast** (Hardware matrix blocks) | **Slower** (Deep software calculations) | +| **Compression Efficiency** | Great size reduction | **Max Space Savings** (20%-40% smaller than GPU) | +| **Encoding Mode** | Adaptive Target Bitrate | Constant Rate Factor (CRF) Quality Engine | +| **Resolution Downscale** | Hardware-native (`scale_cuda`) | Software-native (`scale`) | + +### Shared Intelligent Logic: +* **Anti-Bloat Bitrate Clamping**: Both scripts evaluate the original file's true bitrate via `ffprobe`. If your target settings calculate a bitrate higher than the original file, the script **automatically clamps the bitrate down** to prevent up-sampling file bloating. +* **Auto-Downscaling**: Both scripts automatically identify videos larger than 720p (like 1080p or 4K) and scale them down to 720p. Files already at 720p or lower are processed at their native resolution. +* **Smart JSON Metadata Parsing**: Uses robust `ffprobe -of json` mappings to cleanly pull resolutions and codecs without failing on unusual file names or system language barriers. +* **Session Auditing**: Tracks individual processing stopwatches per file alongside a running session runtime counter. + +--- + +## ðŸ› ï¸ Prerequisites + +* **Operating System**: Windows 10 or 11 with PowerShell. +* **Dependencies**: `ffmpeg` and `ffprobe` must be installed on your machine and added to your system environment `PATH` variable. +* **For GPU Script Only**: An NVIDIA graphics card supporting HEVC hardware encoding. +* **PowerShell Execution Policy**: By default, Windows blocks script execution. **Before running the scripts**, you must allow script execution for your current session by running: +```powershell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process +``` +--- + +## 💻 Configuration & Usage Guide + +### Script Parameters + +| Parameter | Position | Data Type | Default Value | Description | +| :--- | :---: | :---: | :---: | :--- | +| `MinSize` | 0 | String | `"2GB"` | File size threshold. Smaller files are skipped. (e.g., `"500M"`, `"1G"`, `"2GB"`) | +| `MBPerMinute` | 1 | Integer | `12` | Target storage allowed per minute of video. Acts as the target bitrate for GPU, or the absolute maximum cap ceiling for CPU. | +| `CRF` *(CPU Only)* | 2 | Integer | `26` | Constant Rate Factor. Lower = better quality/larger file. Standard range is `24`-`28`. | + +### Practical Examples + +Open **PowerShell**, navigate (`cd`) to your video library root path, and execute your chosen script format: + +#### Option A: Running the GPU Version (`batch_compress.ps1`) + +**Default Execution (Files > 2GB at 12MB/min target):** +```powershell +.\batch_compress.ps1 + +``` + +**Targeting smaller files with higher quality margins (Files > 1GB at 25MB/min):** + +```powershell +.\batch_compress.ps1 -MinSize "1GB" -MBPerMinute 25 + +``` + +#### Option B: Running the CPU Version (`batch_compress_cpu.ps1`) + +**Default Execution (CRF 26 balanced profile, 12MB/min hard cap ceiling):** + +```powershell +.\batch_compress_cpu.ps1 + +``` + +**Aggressive Compression Mode (CRF 28 for extremely tiny file sizes):** + +```powershell +.\batch_compress_cpu.ps1 -MinSize "1GB" -MBPerMinute 10 -CRF 28 + +``` + +**High-Fidelity Archival Mode (CRF 23 for crisp details, lifting the cap ceiling to 20MB/min):** + +```powershell +.\batch_compress_cpu.ps1 "2GB" 20 23 + +``` + +--- + +## 📊 Technical Processing Pipeline + +When a file enters the compression pipeline, it undergoes the following automated stages: + +1. **Scan**: Discovers `.mp4`, `.mkv`, `.avi`, and `.ts` files inside the target tree matching your `MinSize`. +2. **Deduplication Check**: Instantly skips any files with `_x265` in the title or items where the output file already exists. +3. **Inspection**: Extracts stream tracks, height profiles, and container bitrates natively in clean JSON. +4. **Safety Verification**: Compares target values against source bitrates and applies safety clamping if required. +5. **Transcode Execution**: Spawns your chosen encoder context: +* **GPU**: `Source File ──> CUDA Decode ──> scale_cuda ──> hevc_nvenc ──> Output` +* **CPU**: `Source File ──> Software Decode ──> scale ──> libx265 (CRF) ──> Output` + + +6. **Reporting**: Computes exact Megabytes saved, prints session timers, and cleans up the thread pipeline for the next file. + + diff --git a/bat/batch_compress.ps1 b/bat/batch_compress.ps1 new file mode 100644 index 0000000..f2a3592 --- /dev/null +++ b/bat/batch_compress.ps1 @@ -0,0 +1,193 @@ +param ( + # Parameter 1: Threshold for original files (Integer only, e.g., 2GB, 2G, 500MB, 500M) + [Parameter(Mandatory=$false, Position=0)] + [string]$MinSize = "2GB", + + # Parameter 2: Target MB per minute of video length (Default: 12MB/min) + [Parameter(Mandatory=$false, Position=1)] + [int]$MBPerMinute = 12 +) + +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process + +# --- 1. Native Integer Size Parsing --- +try { + $minSizeBytes = Invoke-Expression $MinSize + if ($minSizeBytes -isnot [long] -and $minSizeBytes -isnot [int]) { throw "Invalid MinSize" } +} catch { + Write-Error "[Args Error] Cannot parse size format. Use integer syntax like: 2G, 500M, 2GB" + exit 1 +} + +# --- 2. Bitrate Calculations --- +$totalKbps = [math]::Round(($MBPerMinute * 1048576 * 8) / 60 / 1000) +$audioKbps = 96 +$videoKbps = $totalKbps - $audioKbps + +if ($videoKbps -lt 100) { + Write-Error "[Config Error] The requested MB/min ($MBPerMinute MB) is too low to sustain video and audio." + exit 1 +} + +$currentDir = Get-Location +$displayMinSizeGB = [math]::Round($minSizeBytes / 1GB, 2) + +Write-Host "Scanning root directory: $currentDir" -ForegroundColor Cyan +Write-Host " -> Processing files larger than: $MinSize ($displayMinSizeGB GB)" -ForegroundColor Gray +Write-Host " -> Target Size Metric: $MBPerMinute MB per minute of video length (Calculated Target: ${videoKbps}k)" -ForegroundColor Yellow +Write-Host " -> Auto-Downscale: Yes (If > 720P -> Downscale to 720P)" -ForegroundColor Magenta +Write-Host "--------------------------------------------------------" + +$targetFiles = Get-ChildItem -Path $currentDir -Recurse -File -Include "*.mp4","*.mkv","*.avi","*.ts" | Where-Object { + $_.Length -gt $minSizeBytes -and $_.Name -notlike "*_x265*" +} + +if ($targetFiles.Count -eq 0) { + Write-Host "No files found matching the filter criteria." -ForegroundColor Green + exit 0 +} + +# START TOTAL BATCH TIMER +$totalScriptTimer = [System.Diagnostics.Stopwatch]::StartNew() + +foreach ($file in $targetFiles) { + $OutputFile = Join-Path -Path $file.DirectoryName -ChildPath "$($file.BaseName)_x265$($file.Extension)" + + if (Test-Path -Path $OutputFile -PathType Leaf) { + Write-Host "`n[SKIP] Already processed: $($file.Name)" -ForegroundColor Yellow + continue + } + + $currentSizeGB = [math]::Round($file.Length / 1GB, 2) + Write-Host "`n[Task] Processing: $($file.Name) ($currentSizeGB GB)" -ForegroundColor Cyan + Write-Host " -> Encoding started at: $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor Gray + + # --- 3. Robust Metadata Tracking via ffprobe (JSON Style) --- + $width = 0 + $height = 0 + $vCodec = "unknown" + $aCodec = "unknown" + $sourceBitrateKbps = 0 + $ffprobeError = $null + try { + $ffprobeArgs = @("-v", "error", "-show_entries", "stream=codec_type,codec_name,width,height", "-show_entries", "format=bit_rate", "-of", "json", $file.FullName) + $ffprobeOut = & ffprobe $ffprobeArgs 2>&1 + + if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrEmpty($ffprobeOut)) { + $metadata = $ffprobeOut | ConvertFrom-Json + + # Isolate primary video and audio streams respectively + $vStream = $metadata.streams | Where-Object { $_.codec_type -eq "video" } | Select-Object -First 1 + $aStream = $metadata.streams | Where-Object { $_.codec_type -eq "audio" } | Select-Object -First 1 + + if ($vStream) { + $vCodec = if ($vStream.codec_name) { $vStream.codec_name } else { "unknown" } + $width = if ($vStream.width) { [int]$vStream.width } else { 0 } + $height = if ($vStream.height) { [int]$vStream.height } else { 0 } + } + if ($aStream) { + $aCodec = if ($aStream.codec_name) { $aStream.codec_name } else { "unknown" } + } + + # FIXED: Handle string-enclosed json number format safely using explicit conversions + if ($metadata.format -and $metadata.format.bit_rate) { + $rawBitrate = $metadata.format.bit_rate.ToString().Trim() + if ($rawBitrate -match '^\d+$') { + $sourceBitrateKbps = [math]::Round(([long]$rawBitrate) / 1000) + } + } + + $resolutionDisplay = if ($width -gt 0 -and $height -gt 0) { "${width}x${height}" } else { "unknown" } + $bitrateDisplay = if ($sourceBitrateKbps -gt 0) { "${sourceBitrateKbps}k" } else { "unknown" } + + Write-Host " -> Source Properties: Resolution [$resolutionDisplay] | Video [$vCodec] | Audio [$aCodec] | Total Bitrate: [$bitrateDisplay]" -ForegroundColor Gray + } else { + $ffprobeError = $ffprobeOut + throw "ffprobe failed" + } + } catch { + Write-Host " -> [Warning] Failed to detect stream metadata automatically." -ForegroundColor Yellow + if ($ffprobeError) { + Write-Host " Reason: $ffprobeError" -ForegroundColor Gray + } + Write-Host " Defaulting to safe mode: Processing without forced downscaling or bitrate capping." -ForegroundColor Gray + } + + # --- 4. Dynamic Bitrate Capping Logic --- + $activeVideoKbps = $videoKbps + if ($sourceBitrateKbps -gt 0) { + # Calculate original video portion estimate (Total original bitrate minus allocated transcode audio bitrate) + $sourceVideoKbps = $sourceBitrateKbps - $audioKbps + if ($sourceVideoKbps -lt 100) { $sourceVideoKbps = 100 } + + if ($videoKbps -gt $sourceVideoKbps) { + $activeVideoKbps = $sourceVideoKbps + Write-Host " -> [Notice] Target bitrate (${videoKbps}k) exceeds original source video bitrate (${sourceVideoKbps}k)." -ForegroundColor Yellow + Write-Host " Capping encoding target to match source: ${activeVideoKbps}k" -ForegroundColor Yellow + } + } + + $maxKbps = [math]::Round($activeVideoKbps * 1.35) + $bufKbps = $activeVideoKbps * 2 + + # --- 5. Dynamic Scale Arguments Construction --- + $vfParam = @() + if ($height -gt 720) { + Write-Host " -> Detected Resolution: ${height}P (> 720P). Adding downscale filter." -ForegroundColor Magenta + $vfParam = @("-vf", "scale_cuda=-2:720") + } elseif ($height -gt 0) { + Write-Host " -> Detected Resolution: ${height}P (<= 720P). Keeping original resolution." -ForegroundColor Gray + } + + Write-Host " -> Encoding with NVIDIA GPU acceleration..." -ForegroundColor Green + + # START INDIVIDUAL VIDEO TIMER + $videoTimer = [System.Diagnostics.Stopwatch]::StartNew() + + # Execute Transcode Pipeline using final calculated active bitrates + ffmpeg -loglevel warning -hwaccel cuda -hwaccel_device 0 -hwaccel_output_format cuda -extra_hw_frames 8 -threads 1 -i $file.FullName $vfParam -c:v hevc_nvenc -b:v "${activeVideoKbps}k" -maxrate "${maxKbps}k" -bufsize "${bufKbps}k" -preset p5 -c:a aac -b:a "${audioKbps}k" -y $OutputFile + + # STOP INDIVIDUAL VIDEO TIMER + $videoTimer.Stop() + $elapsedVideo = $videoTimer.Elapsed + + if ($LASTEXITCODE -eq 0) { + $newSize = (Get-Item $OutputFile).Length + $savedBytes = $file.Length - $newSize + $savedMB = [math]::Round($savedBytes / 1MB, 2) + + # Format the time nicely into mm:ss or hh:mm:ss + $timeString = "{0:00}m {1:00}s" -f $elapsedVideo.Minutes, $elapsedVideo.Seconds + if ($elapsedVideo.Hours -gt 0) { $timeString = "{0}h " -f $elapsedVideo.Hours + $timeString } + + if ($savedBytes -gt 0) { + Write-Host "[SUCCESS] Done in $timeString! Reduced file size by ${savedMB} MB." -ForegroundColor Green + } else { + Write-Host "[NOTICE] Complete in $timeString, but file size didn't shrink." -ForegroundColor Yellow + } + } else { + Write-Host "[FAILED] FFmpeg execution crashed after processing for $($elapsedVideo.Minutes)m $($elapsedVideo.Seconds)s." -ForegroundColor Red + } + + # Display running total of how long the whole script session has been active + $currentTotalElapsed = $totalScriptTimer.Elapsed + + $hours = [math]::Truncate($currentTotalElapsed.TotalHours).ToString("00") + $minutes = $currentTotalElapsed.Minutes.ToString("00") + $seconds = $currentTotalElapsed.Seconds.ToString("00") + + Write-Host " -> Total session run time so far: ${hours}h ${minutes}m ${seconds}s" -ForegroundColor Gray + Write-Host "--------------------------------------------------------" +} + +# STOP TOTAL BATCH TIMER +$totalScriptTimer.Stop() +$finalTotalElapsed = $totalScriptTimer.Elapsed + +$fHours = [math]::Truncate($finalTotalElapsed.TotalHours).ToString("00") +$fMinutes = $finalTotalElapsed.Minutes.ToString("00") +$fSeconds = $finalTotalElapsed.Seconds.ToString("00") +$finalTimeString = "${fHours}h ${fMinutes}m ${fSeconds}s" + +Write-Host "`nAll batch process targets complete!" -ForegroundColor Green +Write-Host "Total Processing Duration: $finalTimeString" -ForegroundColor Cyan \ No newline at end of file diff --git a/bat/batch_compress_cpu.ps1 b/bat/batch_compress_cpu.ps1 new file mode 100644 index 0000000..40e7f49 --- /dev/null +++ b/bat/batch_compress_cpu.ps1 @@ -0,0 +1,200 @@ +param ( + # Parameter 1: Threshold for original files (Integer only, e.g., 2GB, 2G, 500MB, 500M) + [Parameter(Mandatory=$false, Position=0)] + [string]$MinSize = "2GB", + + # Parameter 2: Target MAX MB per minute cap of video length (Default: 12MB/min) + [Parameter(Mandatory=$false, Position=1)] + [int]$MBPerMinute = 12, + + # Parameter 3: CRF Quality Value (Lower = Better Quality / Larger Size. 28 is standard for x265, 24-26 is high-quality) + [Parameter(Mandatory=$false, Position=2)] + [int]$CRF = 26 +) + +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process + +# --- 1. Native Integer Size Parsing --- +try { + $minSizeBytes = Invoke-Expression $MinSize + if ($minSizeBytes -isnot [long] -and $minSizeBytes -isnot [int]) { throw "Invalid MinSize" } +} catch { + Write-Error "[Args Error] Cannot parse size format. Use integer syntax like: 2G, 500M, 2GB" + exit 1 +} + +# --- 2. Bitrate Cap Calculations --- +$totalKbps = [math]::Round(($MBPerMinute * 1048576 * 8) / 60 / 1000) +$audioKbps = 96 +$videoCapKbps = $totalKbps - $audioKbps + +if ($videoCapKbps -lt 100) { + Write-Error "[Config Error] The requested MB/min ($MBPerMinute MB) is too low to sustain video and audio." + exit 1 +} + +$currentDir = Get-Location +$displayMinSizeGB = [math]::Round($minSizeBytes / 1GB, 2) + +Write-Host "Scanning root directory: $currentDir" -ForegroundColor Cyan +Write-Host " -> Processing files larger than: $MinSize ($displayMinSizeGB GB)" -ForegroundColor Gray +Write-Host " -> Target Quality: CRF $CRF (Lower means better quality)" -ForegroundColor Yellow +Write-Host " -> Target Size Limit: Max cap of $MBPerMinute MB per minute (${videoCapKbps}k max video)" -ForegroundColor Yellow +Write-Host " -> Auto-Downscale: Yes (If > 720P -> Downscale to 720P via CPU)" -ForegroundColor Magenta +Write-Host "--------------------------------------------------------" + +$targetFiles = Get-ChildItem -Path $currentDir -Recurse -File -Include "*.mp4","*.mkv","*.avi","*.ts" | Where-Object { + $_.Length -gt $minSizeBytes -and $_.Name -notlike "*_x265*" +} + +if ($targetFiles.Count -eq 0) { + Write-Host "No files found matching the filter criteria." -ForegroundColor Green + exit 0 +} + +# START TOTAL BATCH TIMER +$totalScriptTimer = [System.Diagnostics.Stopwatch]::StartNew() + +foreach ($file in $targetFiles) { + $OutputFile = Join-Path -Path $file.DirectoryName -ChildPath "$($file.BaseName)_x265$($file.Extension)" + + if (Test-Path -Path $OutputFile -PathType Leaf) { + Write-Host "`n[SKIP] Already processed: $($file.Name)" -ForegroundColor Yellow + continue + } + + $currentSizeGB = [math]::Round($file.Length / 1GB, 2) + Write-Host "`n[Task] Processing: $($file.Name) ($currentSizeGB GB)" -ForegroundColor Cyan + Write-Host " -> Encoding started at: $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor Gray + + # --- 3. Robust Metadata Tracking via ffprobe (JSON Style) --- + $width = 0 + $height = 0 + $vCodec = "unknown" + $aCodec = "unknown" + $sourceBitrateKbps = 0 + $ffprobeError = $null + try { + $ffprobeArgs = @("-v", "error", "-show_entries", "stream=codec_type,codec_name,width,height", "-show_entries", "format=bit_rate", "-of", "json", $file.FullName) + $ffprobeOut = & ffprobe $ffprobeArgs 2>&1 + + if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrEmpty($ffprobeOut)) { + $metadata = $ffprobeOut | ConvertFrom-Json + + $vStream = $metadata.streams | Where-Object { $_.codec_type -eq "video" } | Select-Object -First 1 + $aStream = $metadata.streams | Where-Object { $_.codec_type -eq "audio" } | Select-Object -First 1 + + if ($vStream) { + $vCodec = if ($vStream.codec_name) { $vStream.codec_name } else { "unknown" } + $width = if ($vStream.width) { [int]$vStream.width } else { 0 } + $height = if ($vStream.height) { [int]$vStream.height } else { 0 } + } + if ($aStream) { + $aCodec = if ($aStream.codec_name) { $aStream.codec_name } else { "unknown" } + } + + if ($metadata.format -and $metadata.format.bit_rate) { + $rawBitrate = $metadata.format.bit_rate.ToString().Trim() + if ($rawBitrate -match '^\d+$') { + $sourceBitrateKbps = [math]::Round(([long]$rawBitrate) / 1000) + } + } + + $resolutionDisplay = if ($width -gt 0 -and $height -gt 0) { "${width}x${height}" } else { "unknown" } + $bitrateDisplay = if ($sourceBitrateKbps -gt 0) { "${sourceBitrateKbps}k" } else { "unknown" } + + Write-Host " -> Source Properties: Resolution [$resolutionDisplay] | Video [$vCodec] | Audio [$aCodec] | Total Bitrate: [$bitrateDisplay]" -ForegroundColor Gray + } else { + $ffprobeError = $ffprobeOut + throw "ffprobe failed" + } + } catch { + Write-Host " -> [Warning] Failed to detect stream metadata automatically." -ForegroundColor Yellow + if ($ffprobeError) { + Write-Host " Reason: $ffprobeError" -ForegroundColor Gray + } + Write-Host " Defaulting to safe mode: Processing without forced downscaling or bitrate capping." -ForegroundColor Gray + } + + # --- 4. Dynamic Bitrate Capping Logic --- + $activeMaxVideoKbps = $videoCapKbps + if ($sourceBitrateKbps -gt 0) { + $sourceVideoKbps = $sourceBitrateKbps - $audioKbps + if ($sourceVideoKbps -lt 100) { $sourceVideoKbps = 100 } + + # If user target cap is bigger than original file bitrate, lower the cap to match original file + if ($videoCapKbps -gt $sourceVideoKbps) { + $activeMaxVideoKbps = $sourceVideoKbps + Write-Host " -> [Notice] Target cap (${videoCapKbps}k) exceeds original video bitrate (${sourceVideoKbps}k)." -ForegroundColor Yellow + Write-Host " Lowering maximum cap ceiling to match source: ${activeMaxVideoKbps}k" -ForegroundColor Yellow + } + } + + # Calculate VBR buffer sizes relative to our active maximum cap + $bufKbps = $activeMaxVideoKbps * 2 + + # --- 5. CPU Scaling Filter Selection --- + $vfParam = @() + if ($height -gt 720) { + Write-Host " -> Detected Resolution: ${height}P (> 720P). Adding CPU software downscale filter." -ForegroundColor Magenta + # Using standard CPU scale filter since we aren't using hardware decode pipelines + $vfParam = @("-vf", "scale=-2:720") + } elseif ($height -gt 0) { + Write-Host " -> Detected Resolution: ${height}P (<= 720P). Keeping original resolution." -ForegroundColor Gray + } + + Write-Host " -> Encoding with libx265 on CPU (CRF Mode)..." -ForegroundColor Green + + # START INDIVIDUAL VIDEO TIMER + $videoTimer = [System.Diagnostics.Stopwatch]::StartNew() + + # Execute CPU Transcode Pipeline + # -c:v libx265 : standard high-efficiency CPU encoder + # -crf $CRF : constant rate factor quality engine + # -preset fast : balance point between compression speed and maximum file savings on CPU + ffmpeg -loglevel warning -i $file.FullName $vfParam -c:v libx265 -crf $CRF -preset fast -maxrate "${activeMaxVideoKbps}k" -bufsize "${bufKbps}k" -c:a aac -b:a "${audioKbps}k" -y $OutputFile + + # STOP INDIVIDUAL VIDEO TIMER + $videoTimer.Stop() + $elapsedVideo = $videoTimer.Elapsed + + if ($LASTEXITCODE -eq 0) { + $newSize = (Get-Item $OutputFile).Length + $savedBytes = $file.Length - $newSize + $savedMB = [math]::Round($savedBytes / 1MB, 2) + + # Format the time nicely into mm:ss or hh:mm:ss + $timeString = "{0:00}m {1:00}s" -f $elapsedVideo.Minutes, $elapsedVideo.Seconds + if ($elapsedVideo.Hours -gt 0) { $timeString = "{0}h " -f $elapsedVideo.Hours + $timeString } + + if ($savedBytes -gt 0) { + Write-Host "[SUCCESS] Done in $timeString! Reduced file size by ${savedMB} MB." -ForegroundColor Green + } else { + Write-Host "[NOTICE] Complete in $timeString, but file size didn't shrink." -ForegroundColor Yellow + } + } else { + Write-Host "[FAILED] FFmpeg execution crashed after processing for $($elapsedVideo.Minutes)m $($elapsedVideo.Seconds)s." -ForegroundColor Red + } + + # Display running total of how long the whole script session has been active + $currentTotalElapsed = $totalScriptTimer.Elapsed + + $hours = [math]::Truncate($currentTotalElapsed.TotalHours).ToString("00") + $minutes = $currentTotalElapsed.Minutes.ToString("00") + $seconds = $currentTotalElapsed.Seconds.ToString("00") + + Write-Host " -> Total session run time so far: ${hours}h ${minutes}m ${seconds}s" -ForegroundColor Gray + Write-Host "--------------------------------------------------------" +} + +# STOP TOTAL BATCH TIMER +$totalScriptTimer.Stop() +$finalTotalElapsed = $totalScriptTimer.Elapsed + +$fHours = [math]::Truncate($finalTotalElapsed.TotalHours).ToString("00") +$fMinutes = $finalTotalElapsed.Minutes.ToString("00") +$fSeconds = $finalTotalElapsed.Seconds.ToString("00") +$finalTimeString = "${fHours}h ${fMinutes}m ${fSeconds}s" + +Write-Host "`nAll batch process targets complete!" -ForegroundColor Green +Write-Host "Total Processing Duration: $finalTimeString" -ForegroundColor Cyan \ No newline at end of file diff --git a/bat/convertMusic2Flac.bat b/bat/convertMusic2Flac.bat new file mode 100644 index 0000000..5975dc1 --- /dev/null +++ b/bat/convertMusic2Flac.bat @@ -0,0 +1,8 @@ +:: ʹÓÃffmpeg½«ÒôÀÖÎļþת»»Îªflac¸ñʽ +:: ^((.*)\\(.*)\.ape)$ +:: C:\Software\jellyfin_10.8.10\ffmpeg -i "$1" -c:a flac "$2\\$3.flac" +:: set file to ansi encoding + +C:\Software\jellyfin_10.8.10\ffmpeg -i "D:\Music\han\Beyond - ¹â»ÔËêÔÂ.ape" -c:a flac "D:\Music\han\Beyond - ¹â»ÔËêÔÂ.flac" +C:\Software\jellyfin_10.8.10\ffmpeg -i "D:\Music\Instrumental\Bandari - Childhoood Memory.ape" -c:a flac "D:\Music\Instrumental\Bandari - Childhoood Memory.flac" + diff --git a/bat/win10Hibernate.bat b/bat/win10Hibernate.bat new file mode 100644 index 0000000..b60df93 --- /dev/null +++ b/bat/win10Hibernate.bat @@ -0,0 +1,9 @@ +:: https://stackoverflow.com/questions/7977743/how-do-i-make-a-hibernation-batch-file +:: https://answers.microsoft.com/en-us/windows/forum/all/anyone-know-a-batchscriptprogramexe-that-can-put/741c42d6-41a4-47b2-9c64-fddbf1605637 +:: https://superuser.com/questions/42039/change-windows-sound-volume-via-the-command-line +C:\Software\nircmd-x64\nircmd.exe changesysvolume -50000 +C:\Software\nircmd-x64\nircmd.exe mutesysvolume 1 +C:\Software\nircmd-x64\nircmd.exe setbrightness 30 + +cd c:\ +shutdown /h \ No newline at end of file diff --git a/bat/win10offwork.bat b/bat/win10offwork.bat new file mode 100644 index 0000000..242b3cd --- /dev/null +++ b/bat/win10offwork.bat @@ -0,0 +1,3 @@ +C:\Software\nircmd-x64\nircmd.exe mutesysvolume 0 +C:\Software\nircmd-x64\nircmd.exe setsysvolume 15000 +C:\Software\nircmd-x64\nircmd.exe setbrightness 100 diff --git a/bat/win10sleep.bat b/bat/win10sleep.bat new file mode 100644 index 0000000..527ede1 --- /dev/null +++ b/bat/win10sleep.bat @@ -0,0 +1,9 @@ +:: https://www.addictivetips.com/windows-tips/schedule-sleep-on-windows-10/ +@echo off &mode 32,2 &color cf &title Power Sleep +set "s1=$m='[DllImport ("Powrprof.dll", SetLastError = true)]" +set "s2=static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);" +set "s3=public static void PowerSleep(){ SetSuspendState(false, false, false); }';" +set "s4=add-type -name Import -member $m -namespace Dll; [Dll.Import]::PowerSleep();" +set "ps_powersleep=%s1%%s2%%s3%%s4%" +call powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass -Command "%ps_powersleep:"=\"%" +exit \ No newline at end of file diff --git a/bookmarklet/demo.js b/bookmarklet/demo.js new file mode 100644 index 0000000..9b7fbbb --- /dev/null +++ b/bookmarklet/demo.js @@ -0,0 +1,18 @@ +// http://www.ruanyifeng.com/blog/2011/06/a_guide_for_writing_bookmarklet.html +if (!window.jQuery) { + + script = document.createElement('script'); + + script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js'; + + script.onload = foo; + + document.body.appendChild(script); + +} else { + foo(); +} + +function foo() { + alert($('a')[0].innerHTML); +} \ No newline at end of file diff --git a/jsscript/pricemonitor/AGENTS.md b/jsscript/pricemonitor/AGENTS.md new file mode 100644 index 0000000..39e1011 --- /dev/null +++ b/jsscript/pricemonitor/AGENTS.md @@ -0,0 +1,31 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +This repository contains standalone browser automation scripts at the root. `goldfresh.js` is the original price monitor; `goldfresh_v2.js` is the active enhanced monitor with minimum/maximum thresholds, alerts, and panel controls. `test.html` is a local manual harness: it supplies `#now_price`, simulates prices, and loads `goldfresh_v2.js`. Keep related scripts at the root unless a descriptive subdirectory becomes necessary. There are no shared modules or automated test directories. + +## Build, Test, and Development Commands + +There is no package manifest or build system. Before committing, run: + +```powershell +node --check goldfresh.js +node --check goldfresh_v2.js +git diff --check +``` + +The Node commands validate syntax; the Git command finds whitespace errors. For manual testing, open `test.html` in a browser. Its simulation crosses the default 900 and 945 limits. Start the monitor, optionally select an audio file, and use the page controls to test custom prices. + +## Coding Style & Naming Conventions + +Use four-space indentation, semicolons, double quotes, and lower camelCase names (`targetPrice`, `stopMonitoring`). Wrap browser scripts in an IIFE. Use descriptive `price_` DOM IDs such as `price_start`, `price_reset`, and `price_status_icon`. + +Check DOM queries before use. Pair every created interval, timeout, `Audio`, or `AudioContext` with cleanup. When adding a notification path, ensure Reset and manual Stop can cancel it without changing unrelated monitoring state. + +## Testing Guidelines + +For monitor changes, manually test missing or invalid prices, invalid thresholds, both threshold crossings, stop/restart behavior, and selected-file versus fallback audio. Confirm alerts automatically stop monitoring, the indicator and Start/Stop controls update, Reset silences audio only, and the default fallback chime repeats five times but can be cancelled. Document manual steps in the pull request. If automated tests are added, put them in `test/` and name them after the script (for example, `test/goldfresh_v2.test.js`). + +## Commit & Pull Request Guidelines + +Use short imperative commit subjects, consistent with history (for example, `add price alert reset`). In pull requests, name the affected script, target page selectors, testing performed, and any visible panel changes. Include a screenshot or short recording for control-panel or browser-behavior updates. \ No newline at end of file diff --git a/jsscript/pricemonitor/README.md b/jsscript/pricemonitor/README.md new file mode 100644 index 0000000..e8d42c0 --- /dev/null +++ b/jsscript/pricemonitor/README.md @@ -0,0 +1,37 @@ +# 黄金价格监控脚本 + +`goldfresh_v2.js` æ˜¯å½“å‰æŽ¨èçš„æµè§ˆå™¨ä»·æ ¼ç›‘控脚本。它读å–页é¢ä¸­çš„ `#now_price`,当价格到达设置的最低或最高阈值时,播放æé†’å¹¶è‡ªåŠ¨åœæ­¢ç›‘控。 + +## 文件说明 + +- `goldfresh.js`:原始价格监控版本。 +- `goldfresh_v2.js`ï¼šå½“å‰æŽ¨è版本,支æŒä¸Šä¸‹é™æé†’ã€çŠ¶æ€æŒ‡ç¤ºå’ŒéŸ³é¢‘控制。 +- `test.html`:本地测试页,æä¾›æ¨¡æ‹Ÿä»·æ ¼å’Œæ‰‹åŠ¨è°ƒä»·åŠŸèƒ½ã€‚ + +## 快速开始 + +1. 在æµè§ˆå™¨ä¸­æ‰“å¼€ `test.html`。 +2. 设置最低和最高价格(默认分别为 `900` å’Œ `945`)。 +3. å¯é€‰æ‹©æœ¬åœ°éŸ³é¢‘文件,或使用默认æç¤ºéŸ³ã€‚ +4. 点击 **Start** 开始监控。 + +测试页会自动以æ¯ç§’ 5 的幅度在 `880` 到 `965` 间往返å˜åŒ–,å¯è¦†ç›–é»˜è®¤é˜ˆå€¼ã€‚åœæ­¢è‡ªåŠ¨æ¨¡æ‹ŸåŽï¼Œä¹Ÿå¯ä»¥è¾“入手动价格测试。 + +## æŽ§åˆ¶é¢æ¿ + +- **æé†’声音**:å¯é€‰æœ¬åœ°éŸ³é¢‘文件;未选择时使用默认åŒéŸ³æç¤ºã€‚ +- **最低价格æé†’ / 最高价格æé†’**:设置价格下é™å’Œä¸Šé™ã€‚ +- **Start / Stop**ï¼šå¼€å§‹æˆ–æ‰‹åŠ¨åœæ­¢ç›‘控;状æ€åœ†ç‚¹æ˜¾ç¤ºå½“剿˜¯å¦æ­£åœ¨ç›‘控。 +- **Reset**ï¼šä»…åœæ­¢æé†’å£°éŸ³ï¼Œä¸æ”¹å˜ç›‘控状æ€ã€‚ + +价格达到任一阈值åŽï¼Œç›‘æŽ§ä¼šè‡ªåŠ¨åœæ­¢ã€‚已选音频优先播放;未选择时默认åŒéŸ³æç¤ºè¿žç»­æ’­æ”¾äº”次。Reset 和手动 Stop 都å¯ç«‹å³åœæ­¢å£°éŸ³ã€‚ + +## éªŒè¯ + +```powershell +node --check goldfresh.js +node --check goldfresh_v2.js +git diff --check +``` + +æµè§ˆå™¨å¯èƒ½éœ€è¦ç”¨æˆ·å…ˆç‚¹å‡»é¡µé¢æˆ– Start 按钮æ‰èƒ½æ’­æ”¾éŸ³é¢‘。 \ No newline at end of file diff --git a/jsscript/pricemonitor/goldfresh.js b/jsscript/pricemonitor/goldfresh.js new file mode 100644 index 0000000..2751ef2 --- /dev/null +++ b/jsscript/pricemonitor/goldfresh.js @@ -0,0 +1,187 @@ +// setInterval(refreshNameAndCode, 1000); + +(function () { + let timer = null; + let targetPrice = 945; + let minPrice = 900; + let above = false; + let below = false; + + // 声音 + function beep(times = 5) { + let count = 0; + + function playOnce() { + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + + function playTone(freq, start, duration) { + const oscillator = ctx.createOscillator(); + const gain = ctx.createGain(); + + oscillator.type = "sine"; + oscillator.frequency.value = freq; + + gain.gain.setValueAtTime(0, ctx.currentTime + start); + gain.gain.linearRampToValueAtTime( + 0.25, + ctx.currentTime + start + 0.05 + ); + gain.gain.exponentialRampToValueAtTime( + 0.001, + ctx.currentTime + start + duration + ); + + oscillator.connect(gain); + gain.connect(ctx.destination); + + oscillator.start(ctx.currentTime + start); + oscillator.stop(ctx.currentTime + start + duration); + } + + // å®å’š + // playTone(880, 0, 0.25); + // playTone(660, 0.25, 0.4); + playTone(587, 0, 0.25); + playTone(660, 0.25, 0.4); + + setTimeout(() => ctx.close(), 1000); + + count++; + + if (count < times) { + setTimeout(playOnce, 1000); + } + } + + playOnce(); +} + + + // 检查价格 + function checkPrice() { + const el = document.getElementById("now_price"); + if (!el) return; + + const price = parseFloat(el.innerText); + + if (isNaN(price)) return; + + console.log("当å‰ä»·æ ¼:", price, "最低:", minPrice, "最高:", targetPrice); + + if (price >= targetPrice && !above) { + above = true; + beep(5); + console.log("🔔 达到目标价格:", price); + } + + // 跌回去åŽé‡æ–°å…许æé†’ + if (price < targetPrice) { + above = false; + } + + if (price <= minPrice && !below) { + below = true; + beep(5); + console.log("低于最低价格:", price); + } + + // 涨回去åŽé‡æ–°å…è®¸ä¸‹é™æé†’ + if (price > minPrice) { + below = false; + } + } + + + // åˆ›å»ºæŽ§åˆ¶é¢æ¿ + const panel = document.createElement("div"); + + panel.style = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 999999; + background: white; + border: 2px solid #333; + padding: 12px; + border-radius: 8px; + font-size: 14px; + box-shadow: 0 0 10px #999; + `; + + panel.innerHTML = ` +
+ æé†’声音: + +
+
+ 最低价格æé†’: + +
+
+ 最高价格æé†’: + +
+
+ + +
+
+ 未å¯åЍ +
+ `; + + document.body.appendChild(panel); + + + // 开始 + document.getElementById("price_start").onclick = function () { + + targetPrice = parseFloat( + document.getElementById("price_target_input").value + ); + minPrice = parseFloat( + document.getElementById("price_min_input").value + ); + + if (isNaN(targetPrice) || isNaN(minPrice)) { + return; + } + + if (!timer) { + timer = setInterval(checkPrice, 1000); + } + + above = false; + below = false; + + document.getElementById("price_status").innerText = + "监控中,最低: " + minPrice + ",最高: " + targetPrice; + + console.log("监控å¯åŠ¨ï¼Œæœ€ä½Ž:", minPrice, "最高:", targetPrice); + }; + + + // æš‚åœ + document.getElementById("price_stop").onclick = function () { + + if (timer) { + clearInterval(timer); + timer = null; + } + + document.getElementById("price_status").innerText = + "已暂åœ"; + + console.log("ç›‘æŽ§åœæ­¢"); + }; + + + console.log("ä»·æ ¼æé†’控件加载完æˆ"); +})(); diff --git a/jsscript/pricemonitor/goldfresh_v2.js b/jsscript/pricemonitor/goldfresh_v2.js new file mode 100644 index 0000000..9d45814 --- /dev/null +++ b/jsscript/pricemonitor/goldfresh_v2.js @@ -0,0 +1,330 @@ +(function () { + let timer = null; + let targetPrice = 945; + let minPrice = 900; + let above = false; + let below = false; + let notifyAudio = null; + let audioTimers = []; + let fallbackAudioContexts = []; + let fallbackAudioTimers = []; + + + function playDefaultBeep() { + const AudioContextClass = window.AudioContext || window.webkitAudioContext; + + if (!AudioContextClass) { + console.log("Web Audio is unavailable; default alert sound could not play"); + return; + } + + let context; + + try { + context = new AudioContextClass(); + } catch (error) { + console.log("Default alert sound could not start:", error); + return; + } + + fallbackAudioContexts.push(context); + context.resume().catch(error => { + console.log("Default alert sound could not resume:", error); + }); + + function playTone(frequency, start, duration) { + const oscillator = context.createOscillator(); + const gain = context.createGain(); + + oscillator.type = "sine"; + oscillator.frequency.value = frequency; + oscillator.detune.value = 300; + + gain.gain.setValueAtTime(0, context.currentTime + start); + gain.gain.linearRampToValueAtTime( + 0.25, + context.currentTime + start + 0.05 + ); + gain.gain.exponentialRampToValueAtTime( + 0.001, + context.currentTime + start + duration + ); + + oscillator.connect(gain); + gain.connect(context.destination); + + oscillator.start(context.currentTime + start); + oscillator.stop(context.currentTime + start + duration); + } + + const fallbackChimeCount = 5; + const fallbackChimeInterval = 1; + + for (let index = 0; index < fallbackChimeCount; index++) { + const start = index * fallbackChimeInterval; + playTone(440, start, 0.25); + playTone(660, start + 0.25, 0.4); + } + + const cleanupTimer = setTimeout(() => { + context.close().catch(() => {}); + fallbackAudioContexts = fallbackAudioContexts.filter(item => item !== context); + fallbackAudioTimers = fallbackAudioTimers.filter(item => item !== cleanupTimer); + }, fallbackChimeCount * fallbackChimeInterval * 1000); + + fallbackAudioTimers.push(cleanupTimer); + } + + // 声音 + function beep(times = 3) { + + if (!notifyAudio) { + playDefaultBeep(); + return; + } + + let count = 0; + + function play() { + + // å¦‚æžœå·²ç»æš‚åœï¼Œä¸å†æ’­æ”¾ + if (!timer) { + return; + } + + notifyAudio.currentTime = 0; + + notifyAudio.play().catch(err => { + console.log("声音播放失败:", err); + }); + + count++; + + if (count < times) { + const timeout = setTimeout(play, 1500); + audioTimers.push(timeout); + } + } + + play(); + } + + + // åœæ­¢å£°éŸ³ + function stopAudio() { + + // å–æ¶ˆæ‰€æœ‰ç­‰å¾…中的播放 + audioTimers.forEach(timeout => { + clearTimeout(timeout); + }); + + audioTimers = []; + + fallbackAudioTimers.forEach(timeout => { + clearTimeout(timeout); + }); + + fallbackAudioTimers = []; + fallbackAudioContexts.forEach(context => { + context.close().catch(() => {}); + }); + + fallbackAudioContexts = []; + + // åœæ­¢å½“剿’­æ”¾ + if (notifyAudio) { + notifyAudio.pause(); + notifyAudio.currentTime = 0; + } + } + + function stopMonitoring(stopSound, statusText) { + if (timer) { + clearInterval(timer); + timer = null; + } + + if (stopSound) { + stopAudio(); + } + + document.getElementById("price_status_icon").style.color = "gray"; + document.getElementById("price_status_icon").setAttribute("aria-label", "Stopped"); + document.getElementById("price_status_text").innerText = statusText; + document.getElementById("price_start").disabled = false; + document.getElementById("price_stop").disabled = true; + } + + + // 检查价格 + function checkPrice() { + const el = document.getElementById("now_price"); + if (!el) return; + + const price = parseFloat(el.innerText); + + if (isNaN(price)) return; + + console.log("Current price:", price, "minimum:", minPrice, "maximum:", targetPrice); + + if (price >= targetPrice && !above) { + above = true; + + beep(1); + stopMonitoring(false, "Stopped: maximum alert at " + price); + + console.log("Maximum price reached:", price); + return; + } + + // 跌回去åŽé‡æ–°å…许æé†’ + if (price < targetPrice) { + above = false; + } + + if (price <= minPrice && !below) { + below = true; + + beep(1); + stopMonitoring(false, "Stopped: minimum alert at " + price); + + console.log("Minimum price reached:", price); + return; + } + + if (price > minPrice) { + below = false; + } + } + + + // åˆ›å»ºæŽ§åˆ¶é¢æ¿ + const panel = document.createElement("div"); + + panel.style = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 999999; + background: white; + border: 2px solid #333; + padding: 12px; + border-radius: 8px; + font-size: 14px; + box-shadow: 0 0 10px #999; + `; + + panel.innerHTML = ` +
+ æé†’声音: + +
+
+
+ 最低价格提醒: + +
+
+ 最高价格æé†’: + +
+
+ + + + + +
+ +
+ + Stopped +
+ `; + + document.body.appendChild(panel); + + + // 开始 + document.getElementById("price_start").onclick = function () { + + targetPrice = parseFloat( + document.getElementById("price_target_input").value + ); + minPrice = parseFloat( + document.getElementById("price_min_input").value + ); + + if (isNaN(targetPrice) || isNaN(minPrice)) { + return; + } + + if (!timer) { + timer = setInterval(checkPrice, 1000); + } + + above = false; + below = false; + + document.getElementById("price_status_icon").style.color = "green"; + document.getElementById("price_status_icon").setAttribute("aria-label", "Monitoring"); + document.getElementById("price_status_text").innerText = + "Monitoring, minimum: " + minPrice + ", maximum: " + targetPrice; + document.getElementById("price_start").disabled = true; + document.getElementById("price_stop").disabled = false; + + console.log("Monitoring started, minimum:", minPrice, "maximum:", targetPrice); + }; + + + // æš‚åœ + document.getElementById("price_stop").onclick = function () { + stopMonitoring(true, "Stopped"); + + console.log("Monitoring stopped and audio cancelled"); + }; + + document.getElementById("price_reset").onclick = function () { + stopAudio(); + console.log("Notification sound reset"); + }; + + + // 选择声音文件 + document.getElementById("notify_sound").onchange = function (e) { + + const file = e.target.files[0]; + + if (file) { + + // å¦‚æžœä¹‹å‰æœ‰å£°éŸ³ï¼Œå…ˆåœæ­¢ + stopAudio(); + + notifyAudio = new Audio( + URL.createObjectURL(file) + ); + + console.log("声音加载完æˆ:", file.name); + } + }; + + + console.log("ä»·æ ¼æé†’控件加载完æˆ"); + +})(); \ No newline at end of file diff --git a/jsscript/pricemonitor/test.html b/jsscript/pricemonitor/test.html new file mode 100644 index 0000000..2005cdb --- /dev/null +++ b/jsscript/pricemonitor/test.html @@ -0,0 +1,133 @@ + + + + + + Gold Price Alert Test + + + +
+

Gold Price Alert Test

+

Use the alert panel on the right to set price thresholds and start monitoring.

+

Current price

+
920
+

Auto simulation running

+ +
+ + + + + +
+
+ + + + + diff --git a/npp_macro/shortcuts.xml b/npp_macro/shortcuts.xml new file mode 100644 index 0000000..61d042e --- /dev/null +++ b/npp_macro/shortcuts.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + firefox "$(FULL_CURRENT_PATH)" + iexplore "$(FULL_CURRENT_PATH)" + chrome "$(FULL_CURRENT_PATH)" + safari "$(FULL_CURRENT_PATH)" + http://www.php.net/$(CURRENT_WORD) + https://en.wikipedia.org/wiki/Special:Search?search=$(CURRENT_WORD) + $(NPP_FULL_FILE_PATH) $(CURRENT_WORD) -nosession -multiInst + outlook /a "$(FULL_CURRENT_PATH)" + + + + + + + diff --git a/pdf621/pdf-nup-merger.html b/pdf621/pdf-nup-merger.html new file mode 100644 index 0000000..b69b198 --- /dev/null +++ b/pdf621/pdf-nup-merger.html @@ -0,0 +1,550 @@ + + + + + + PDF N-Up Merger + + + + +
+

PDF N-Up Merger

+

Merge PDF files with N-up tiling

+ +
+
📄
+
+ Click to upload or drag & drop PDF files +
+ +
+ +
+ +
+ + +
+ + + +
+
+
+ +
+ +
+ How it works: Upload multiple PDF files, specify how many pages (x) + should be combined onto a single sheet, and the app will create a merged PDF with N-up layout + on standard A4 pages. Default is 6 pages per sheet (2x3 grid). + Aspect ratio is preserved with center-scaling. +
+
+ + + + diff --git a/sakura_editor_macro/escape_regex.mac b/sakura_editor_macro/escape_regex.mac new file mode 100644 index 0000000..b790ba1 --- /dev/null +++ b/sakura_editor_macro/escape_regex.mac @@ -0,0 +1,5 @@ +//キーボードマクロã®ãƒ•ァイル +S_ReplaceAll('([\\.\\^\\$\\*\\+\\?\\(\\)\\[\\{\\\\\\|])', '\\\\$1', 22); // ã™ã¹ã¦ç½®æ› +S_ReDraw(0); // å†æç”» +S_ReplaceAll('\\r\\n', '\\\\r\\\\n', 22); // ã™ã¹ã¦ç½®æ› +S_ReDraw(0); // å†æç”» diff --git a/sakura_editor_macro/grep_blog.mac b/sakura_editor_macro/grep_blog.mac new file mode 100644 index 0000000..6e9ea9f --- /dev/null +++ b/sakura_editor_macro/grep_blog.mac @@ -0,0 +1,2 @@ +//キーボードマクロã®ãƒ•ァイル +S_Grep('\\d+', '*', 'C:\\Users\\Shin.SHINJI-PC2\\Documents\\Code\\blog\\hugo-source\\content', 25393, 99); // Grep diff --git a/sakura_editor_macro/import_from_excel.mac b/sakura_editor_macro/import_from_excel.mac new file mode 100644 index 0000000..6798ce1 --- /dev/null +++ b/sakura_editor_macro/import_from_excel.mac @@ -0,0 +1,7 @@ +//Keyboard macro file +ReplaceAll('\\t', '\',\'', 6); // Replace All +ReDraw(0); // Redraw +ReplaceAll('^', 'insert into XXX_YYY select \'', 6); // Replace All +ReDraw(0); // Redraw +ReplaceAll('$', '\' from dual ;commit;', 6); // Replace All +ReDraw(0); // Redraw \ No newline at end of file diff --git a/shell/encoding.sh b/shell/encoding.sh new file mode 100644 index 0000000..b5a79ae --- /dev/null +++ b/shell/encoding.sh @@ -0,0 +1,12 @@ +#!/bin/bash +#enter input encoding here +FROM_ENCODING="GB2312" +#output encoding(UTF-8) +TO_ENCODING="UTF-8" +#convert +CONVERT=" iconv -f $FROM_ENCODING -t $TO_ENCODING" +#loop to convert multiple files +for file in *.txt; do + $CONVERT "$file" "$file" > "../${file}" +done +exit 0 \ No newline at end of file diff --git a/shell/ic.sh b/shell/ic.sh new file mode 100644 index 0000000..730875e --- /dev/null +++ b/shell/ic.sh @@ -0,0 +1,11 @@ +#!/bin/bash +#function batch_convert() { + for file in *.srt + do + # iconv -f BIG-5 -t UTF-8 "$file" > "sub/_$file" +# mv -f "$file.new" "$file" +recode BIG-5..UTF-8 "$file" + done +#} + +#batch_convert ~/Media/sf_share/sub \ No newline at end of file diff --git a/shell/subrename/mergesub.sh b/shell/subrename/mergesub.sh new file mode 100644 index 0000000..acf3307 --- /dev/null +++ b/shell/subrename/mergesub.sh @@ -0,0 +1,60 @@ +if [ $# -ne 2 ]; then + echo "--- need argument ---"; + exit 1; +fi +if [[ $1 == *"\\"* ]]; then + echo "--- path cannot contains \\ ---"; + exit 1; +fi +if [[ $2 == *"\\"* ]]; then + echo "--- path cannot contains \\ ---"; + exit 1; +fi + +if [[ ! -d $1 || ! -d $2 ]]; then + echo "--- path does not exist\\ ---"; + exit 1; +fi + +IFS=$'\n' + +root=`realpath ${0%/*}` +log=$root/`date '+%Y_%m_%d_%H_%M_%S'`.log + +cd $1; + +origin=(`ls`); + +echo ${origin[*]}>`expr $log` +echo "---">>`expr $log` +echo "origin files:" +for i in ${origin[*]};do + echo " "$i +done +echo + +echo "origin length is "${#origin[*]} + +cd .. +cd $2; +sub=(`ls`); +echo ${sub[*]}>>`expr $log` +echo "sub files:" +for i in ${sub[*]}; do + echo " "$i; +done +echo "sub length is "${#sub[*]}ï¼› +echo + +if [ ${#origin[*]} != ${#sub[*]} ]; then + echo "---- file num do not equal ---"; + exit 1; +fi + +for((i=0;i<`expr ${#sub[*]}`;i++));do + echo ${sub[`expr $i`]}; + echo ${origin[`expr $i`]%.*} + cat $1/"${origin[`expr $i`]}" >> "${sub[`expr $i`]}" + #mv ${sub[`expr $i`]} ${origin[`expr $i`]%.*}.srt +done +echo result $?; \ No newline at end of file diff --git a/shell/subrename/presubrename.sh b/shell/subrename/presubrename.sh index 27b8921..530bdc2 100644 --- a/shell/subrename/presubrename.sh +++ b/shell/subrename/presubrename.sh @@ -23,7 +23,7 @@ log=$root/`date '+%Y_%m_%d_%H_%M_%S'`.log cd $1; -origin=(*); +origin=(`ls`); echo ${origin[*]}>`expr $log` echo "---">>`expr $log` @@ -33,17 +33,17 @@ for i in ${origin[*]};do done echo -echo "origin length is "${#origin[*]} +echo "origin length is `ls | wc -l`" cd .. cd $2; -sub=(*); +sub=(`ls`); echo ${sub[*]}>>`expr $log` echo "sub files:" for i in ${sub[*]}; do echo " "$i; done -echo "sub length is "${#sub[*]}ï¼› +echo "sub length is `ls | wc -l`" echo if [ ${#origin[*]} != ${#sub[*]} ]; then diff --git a/shell/subrename/rename.sh b/shell/subrename/rename.sh new file mode 100644 index 0000000..22c80a6 --- /dev/null +++ b/shell/subrename/rename.sh @@ -0,0 +1,8 @@ +mv *S01E01* The.Staircase.2022.S01E01.AMZN.WEB-DL.DDP2.0.H.264-BTW.ass +mv *S01E02* The.Staircase.2022.S01E02.AMZN.WEB-DL.DDP2.0.H.264-BTW.ass +mv *S01E03* The.Staircase.2022.S01E03.AMZN.WEB-DL.DDP2.0.H.264-BTW.ass +mv *S01E04* The.Staircase.2022.S01E04.AMZN.WEB-DL.DDP2.0.H.264-BTW.ass +mv *S01E05* The.Staircase.2022.S01E05.AMZN.WEB-DL.DDP2.0.H.264-BTW.ass +mv *S01E06* The.Staircase.2022.S01E06.AMZN.WEB-DL.DDP2.0.H.264-BTW.ass +mv *S01E07* The.Staircase.2022.S01E07.AMZN.WEB-DL.DDP2.0.H.264-BTW.ass +mv *S01E08* The.Staircase.2022.S01E08.AMZN.WEB-DL.DDP2.0.H.264-BTW.ass \ No newline at end of file diff --git a/shell/subrename/subrename.sh b/shell/subrename/subrename.sh index ddaf066..023794a 100644 --- a/shell/subrename/subrename.sh +++ b/shell/subrename/subrename.sh @@ -23,7 +23,7 @@ log=$root/`date '+%Y_%m_%d_%H_%M_%S'`.log cd $1; -origin=(*); +origin=(`ls`); echo ${origin[*]}>`expr $log` echo "---">>`expr $log` @@ -33,17 +33,17 @@ for i in ${origin[*]};do done echo -echo "origin length is "${#origin[*]} +echo "origin length is `ls | wc -l`" cd .. cd $2; -sub=(*); +sub=(`ls`); echo ${sub[*]}>>`expr $log` echo "sub files:" for i in ${sub[*]}; do echo " "$i; done -echo "sub length is "${#sub[*]}ï¼› +echo "sub length is `ls | wc -l`" echo if [ ${#origin[*]} != ${#sub[*]} ]; then diff --git a/shell/subrename/zipmanga.sh b/shell/subrename/zipmanga.sh new file mode 100644 index 0000000..54d1c24 --- /dev/null +++ b/shell/subrename/zipmanga.sh @@ -0,0 +1 @@ +for dir in */; do ( cd "$dir" && zip -r ../"${dir%/}".zip . ) done