From 66e1fd43c2cc6e6dfb28815b4d35e5ad1e54cf09 Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Sat, 25 Jun 2022 23:18:55 +0800 Subject: [PATCH 01/13] add zipmanga folder shell --- shell/subrename/zipmanga.sh | 1 + 1 file changed, 1 insertion(+) create mode 100644 shell/subrename/zipmanga.sh 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 From b8917165c06c725a9ef59512f42bc529c593c579 Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Sat, 29 Oct 2022 11:57:10 +0800 Subject: [PATCH 02/13] try bookmarklet. --- bookmarklet/demo.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 bookmarklet/demo.js 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 From fe298ab972e00ba32914b28977594d5e505efacb Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:49:28 +0800 Subject: [PATCH 03/13] add sukura editor macro --- npp_macro/shortcuts.xml | 107 ++++++++++++++++++++++ sakura_editor_macro/escape_regex.mac | 5 + sakura_editor_macro/grep_blog.mac | 2 + sakura_editor_macro/import_from_excel.mac | 7 ++ shell/encoding.sh | 12 +++ shell/ic.sh | 11 +++ shell/subrename/rename.sh | 8 ++ 7 files changed, 152 insertions(+) create mode 100644 npp_macro/shortcuts.xml create mode 100644 sakura_editor_macro/escape_regex.mac create mode 100644 sakura_editor_macro/grep_blog.mac create mode 100644 sakura_editor_macro/import_from_excel.mac create mode 100644 shell/encoding.sh create mode 100644 shell/ic.sh create mode 100644 shell/subrename/rename.sh 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/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/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 From 09013b63a3aca24d05cc558dc417d9ba1e58d209 Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Wed, 26 Jul 2023 21:11:21 +0800 Subject: [PATCH 04/13] schedule-windows-to-sleep-at-specific-time --- bat/win10sleep.bat | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 bat/win10sleep.bat 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 From 7607170caf894e14251d67a9de7f3d70f9cb9e98 Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Thu, 27 Jul 2023 09:59:53 +0800 Subject: [PATCH 05/13] how-do-i-make-a-hibernation-batch-file-Windows 10 --- bat/win10Hibernate.bat | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 bat/win10Hibernate.bat diff --git a/bat/win10Hibernate.bat b/bat/win10Hibernate.bat new file mode 100644 index 0000000..2bb2330 --- /dev/null +++ b/bat/win10Hibernate.bat @@ -0,0 +1,4 @@ +:: 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 +cd c:\ +shutdown /h \ No newline at end of file From 2b2d9d271aa9104bd2e8235e9b783b38e1ac533e Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Tue, 1 Aug 2023 09:17:19 +0800 Subject: [PATCH 06/13] u --- bat/win10Hibernate.bat | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bat/win10Hibernate.bat b/bat/win10Hibernate.bat index 2bb2330..b60df93 100644 --- a/bat/win10Hibernate.bat +++ b/bat/win10Hibernate.bat @@ -1,4 +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 From 8827e8360f8d630942cb753fcc604f88ce41b0c2 Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Wed, 2 Aug 2023 23:32:21 +0800 Subject: [PATCH 07/13] u --- bat/win10offwork.bat | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 bat/win10offwork.bat diff --git a/bat/win10offwork.bat b/bat/win10offwork.bat new file mode 100644 index 0000000..d0f0ae6 --- /dev/null +++ b/bat/win10offwork.bat @@ -0,0 +1,3 @@ +C:\Software\nircmd-x64\nircmd.exe mutesysvolume 0 +C:\Software\nircmd-x64\nircmd.exe changesysvolume 15000 +C:\Software\nircmd-x64\nircmd.exe setbrightness 100 From 05be94a72c6f45d54b574f9925faf058cdecdc2c Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Sun, 5 Nov 2023 17:29:05 +0800 Subject: [PATCH 08/13] update --- shell/subrename/mergesub.sh | 60 +++++++++++++++++++++++++++++++++ shell/subrename/presubrename.sh | 8 ++--- shell/subrename/subrename.sh | 8 ++--- 3 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 shell/subrename/mergesub.sh 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/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 From 778b4df3eb21942c3963a745e1fb2eafd37a22fd Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Mon, 3 Mar 2025 21:08:07 +0800 Subject: [PATCH 09/13] =?UTF-8?q?=E4=BD=BF=E7=94=A8ffmpeg=E5=B0=86?= =?UTF-8?q?=E9=9F=B3=E4=B9=90=E6=96=87=E4=BB=B6=E8=BD=AC=E6=8D=A2=E4=B8=BA?= =?UTF-8?q?flac=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bat/convertMusic2Flac.bat | 8 ++++++++ bat/win10offwork.bat | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 bat/convertMusic2Flac.bat 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/win10offwork.bat b/bat/win10offwork.bat index d0f0ae6..242b3cd 100644 --- a/bat/win10offwork.bat +++ b/bat/win10offwork.bat @@ -1,3 +1,3 @@ C:\Software\nircmd-x64\nircmd.exe mutesysvolume 0 -C:\Software\nircmd-x64\nircmd.exe changesysvolume 15000 +C:\Software\nircmd-x64\nircmd.exe setsysvolume 15000 C:\Software\nircmd-x64\nircmd.exe setbrightness 100 From 902444611debf5ba730583c00a603c9b0cc3184c Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:18:31 +0800 Subject: [PATCH 10/13] merge pdf file and page --- pdf621/pdf-nup-merger.html | 544 +++++++++++++++++++++++++++++++++++++ 1 file changed, 544 insertions(+) create mode 100644 pdf621/pdf-nup-merger.html diff --git a/pdf621/pdf-nup-merger.html b/pdf621/pdf-nup-merger.html new file mode 100644 index 0000000..c1fe202 --- /dev/null +++ b/pdf621/pdf-nup-merger.html @@ -0,0 +1,544 @@ + + + + + + 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. + Default is 6 pages per sheet (2x3 grid). +
+
+ + + + From 781f80ee72d3b5802dc006e52ed5284356f53057 Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:32:20 +0800 Subject: [PATCH 11/13] set as a4 size --- pdf621/pdf-nup-merger.html | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pdf621/pdf-nup-merger.html b/pdf621/pdf-nup-merger.html index c1fe202..b69b198 100644 --- a/pdf621/pdf-nup-merger.html +++ b/pdf621/pdf-nup-merger.html @@ -254,8 +254,9 @@

PDF N-Up Merger

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. - Default is 6 pages per sheet (2x3 grid). + 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.
@@ -437,6 +438,10 @@

PDF N-Up Merger

updateProgress(50); + // Standard A4 dimensions in points (210mm x 297mm) + const A4_WIDTH = 595.28; + const A4_HEIGHT = 841.89; + // Process pages in batches const totalPages = allPages.length; for (let batch = 0; batch < totalPages; batch += pagesPerSheet) { @@ -466,10 +471,11 @@

PDF N-Up Merger

if (embeddedPages.length === 0) continue; - // Get the size of the first page as reference - const { width, height } = pageSizes[0]; + // Use A4 dimensions for the output page + const width = A4_WIDTH; + const height = A4_HEIGHT; - // Create a new page with the same dimensions + // Create a new A4 page const newPage = mergedPdf.addPage([width, height]); // Zero margins for seamless tiling From da88ca745e0ebacd81ee8acfd0a1d5b9e124a12e Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Sat, 23 May 2026 11:15:50 +0800 Subject: [PATCH 12/13] reduce video size with h265 --- bat/batch_compress.md | 110 ++++++++++++++++++++ bat/batch_compress.ps1 | 193 +++++++++++++++++++++++++++++++++++ bat/batch_compress_cpu.ps1 | 200 +++++++++++++++++++++++++++++++++++++ 3 files changed, 503 insertions(+) create mode 100644 bat/batch_compress.md create mode 100644 bat/batch_compress.ps1 create mode 100644 bat/batch_compress_cpu.ps1 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 From d6796d6089a4cb14b7638f07a5abe166decab34d Mon Sep 17 00:00:00 2001 From: deletefromuser <45934893+deletefromuser@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:11:24 +0800 Subject: [PATCH 13/13] add price monitor script --- jsscript/pricemonitor/AGENTS.md | 31 +++ jsscript/pricemonitor/README.md | 37 +++ jsscript/pricemonitor/goldfresh.js | 187 +++++++++++++++ jsscript/pricemonitor/goldfresh_v2.js | 330 ++++++++++++++++++++++++++ jsscript/pricemonitor/test.html | 133 +++++++++++ 5 files changed, 718 insertions(+) create mode 100644 jsscript/pricemonitor/AGENTS.md create mode 100644 jsscript/pricemonitor/README.md create mode 100644 jsscript/pricemonitor/goldfresh.js create mode 100644 jsscript/pricemonitor/goldfresh_v2.js create mode 100644 jsscript/pricemonitor/test.html 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

+ +
+ + + + + +
+
+ + + + +