add vibeui player
This commit is contained in:
parent
a5433e7bd5
commit
9a708fce1d
29
services/pocketbase/.gitignore
vendored
29
services/pocketbase/.gitignore
vendored
@ -3,3 +3,32 @@ node_modules
|
|||||||
pb_data
|
pb_data
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.secrets
|
.secrets
|
||||||
|
|
||||||
|
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/go
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=go
|
||||||
|
|
||||||
|
### Go ###
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/go
|
||||||
|
|||||||
46
services/pocketbase/go.mod
Normal file
46
services/pocketbase/go.mod
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
module futureporn
|
||||||
|
|
||||||
|
go 1.25.1
|
||||||
|
|
||||||
|
require github.com/pocketbase/pocketbase v0.34.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
|
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||||
|
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 // indirect
|
||||||
|
github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7 // indirect
|
||||||
|
github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||||
|
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
|
||||||
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/pocketbase/dbx v1.11.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/cobra v1.10.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||||
|
golang.org/x/image v0.33.0 // indirect
|
||||||
|
golang.org/x/net v0.47.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.33.0 // indirect
|
||||||
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/text v0.31.0 // indirect
|
||||||
|
modernc.org/libc v1.66.10 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.40.1 // indirect
|
||||||
|
)
|
||||||
141
services/pocketbase/go.sum
Normal file
141
services/pocketbase/go.sum
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
|
||||||
|
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
||||||
|
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY=
|
||||||
|
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM=
|
||||||
|
github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7 h1:jxmXU5V9tXxJnydU5v/m9SG8TRUa/Z7IXODBpMs/P+U=
|
||||||
|
github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||||
|
github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 h1:fuHXpEVTTk7TilRdfGRLHpiTD6tnT0ihEowCfWjlFvw=
|
||||||
|
github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
||||||
|
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||||
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||||
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=
|
||||||
|
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||||
|
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||||
|
github.com/pocketbase/pocketbase v0.34.0 h1:5W80PrGvkRYIMAIK90F7w031/hXgZVz1KSuCJqSpgJo=
|
||||||
|
github.com/pocketbase/pocketbase v0.34.0/go.mod h1:K/9z/Zb9PR9yW2Qyoc73jHV/EKT8cMTk9bQWyrzYlvI=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||||
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||||
|
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||||
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
|
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||||
|
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
|
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||||
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||||
|
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||||
|
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||||
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||||
|
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
||||||
|
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
148
services/pocketbase/main.go
Normal file
148
services/pocketbase/main.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// @see https://github.com/pocketbase/pocketbase/blob/master/examples/base/main.go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"futureporn/src/handlers"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/plugins/ghupdate"
|
||||||
|
"github.com/pocketbase/pocketbase/plugins/jsvm"
|
||||||
|
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/hook"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := pocketbase.New()
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Optional plugin flags:
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
var hooksDir string
|
||||||
|
app.RootCmd.PersistentFlags().StringVar(
|
||||||
|
&hooksDir,
|
||||||
|
"hooksDir",
|
||||||
|
"",
|
||||||
|
"the directory with the JS app hooks",
|
||||||
|
)
|
||||||
|
|
||||||
|
var hooksWatch bool
|
||||||
|
app.RootCmd.PersistentFlags().BoolVar(
|
||||||
|
&hooksWatch,
|
||||||
|
"hooksWatch",
|
||||||
|
true,
|
||||||
|
"auto restart the app on pb_hooks file change; it has no effect on Windows",
|
||||||
|
)
|
||||||
|
|
||||||
|
var hooksPool int
|
||||||
|
app.RootCmd.PersistentFlags().IntVar(
|
||||||
|
&hooksPool,
|
||||||
|
"hooksPool",
|
||||||
|
15,
|
||||||
|
"the total prewarm goja.Runtime instances for the JS app hooks execution",
|
||||||
|
)
|
||||||
|
|
||||||
|
var migrationsDir string
|
||||||
|
app.RootCmd.PersistentFlags().StringVar(
|
||||||
|
&migrationsDir,
|
||||||
|
"migrationsDir",
|
||||||
|
"",
|
||||||
|
"the directory with the user defined migrations",
|
||||||
|
)
|
||||||
|
|
||||||
|
var automigrate bool
|
||||||
|
app.RootCmd.PersistentFlags().BoolVar(
|
||||||
|
&automigrate,
|
||||||
|
"automigrate",
|
||||||
|
true,
|
||||||
|
"enable/disable auto migrations",
|
||||||
|
)
|
||||||
|
|
||||||
|
var publicDir string
|
||||||
|
app.RootCmd.PersistentFlags().StringVar(
|
||||||
|
&publicDir,
|
||||||
|
"publicDir",
|
||||||
|
defaultPublicDir(),
|
||||||
|
"the directory to serve static files",
|
||||||
|
)
|
||||||
|
|
||||||
|
var indexFallback bool
|
||||||
|
app.RootCmd.PersistentFlags().BoolVar(
|
||||||
|
&indexFallback,
|
||||||
|
"indexFallback",
|
||||||
|
true,
|
||||||
|
"fallback the request to index.html on missing static path, e.g. when pretty urls are used with SPA",
|
||||||
|
)
|
||||||
|
|
||||||
|
app.RootCmd.ParseFlags(os.Args[1:])
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Plugins and hooks:
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// load jsvm (pb_hooks and pb_migrations)
|
||||||
|
jsvm.MustRegister(app, jsvm.Config{
|
||||||
|
MigrationsDir: migrationsDir,
|
||||||
|
HooksDir: hooksDir,
|
||||||
|
HooksWatch: hooksWatch,
|
||||||
|
HooksPoolSize: hooksPool,
|
||||||
|
})
|
||||||
|
|
||||||
|
// migrate command (with js templates)
|
||||||
|
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
|
||||||
|
TemplateLang: migratecmd.TemplateLangJS,
|
||||||
|
Automigrate: automigrate,
|
||||||
|
Dir: migrationsDir,
|
||||||
|
})
|
||||||
|
|
||||||
|
// GitHub selfupdate
|
||||||
|
ghupdate.MustRegister(app, app.RootCmd, ghupdate.Config{})
|
||||||
|
|
||||||
|
// route for generating hls playlists with BunnyCDN signed URLs
|
||||||
|
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||||
|
se.Router.GET("/api/hls/{collectionId}/{recordId}/hls/{fileName}", handlers.PackageHandler)
|
||||||
|
return se.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// route for direct downloads (patrons only)
|
||||||
|
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||||
|
se.Router.GET("/api/download/{collectionId}/{recordId}/{fileName}", handlers.S3Download)
|
||||||
|
return se.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// static route to serves files from the provided public dir
|
||||||
|
// (if publicDir exists and the route path is not already defined)
|
||||||
|
app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{
|
||||||
|
Func: func(e *core.ServeEvent) error {
|
||||||
|
if !e.Router.HasRoute(http.MethodGet, "/{path...}") {
|
||||||
|
e.Router.GET("/{path...}", apis.Static(os.DirFS(publicDir), indexFallback))
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Next()
|
||||||
|
},
|
||||||
|
Priority: 999, // execute as latest as possible to allow users to provide their own route
|
||||||
|
})
|
||||||
|
|
||||||
|
app.OnFileDownloadRequest().BindFunc(handlers.HandleFileDownload)
|
||||||
|
|
||||||
|
if err := app.Start(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the default pb_public dir location is relative to the executable
|
||||||
|
func defaultPublicDir() string {
|
||||||
|
if osutils.IsProbablyGoRun() {
|
||||||
|
return "./pb_public"
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(os.Args[0], "../pb_public")
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "futureporn",
|
"name": "futureporn",
|
||||||
"version": "4.0.0",
|
"version": "4.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Dedication to the preservation of lewdtuber history",
|
"description": "Dedication to the preservation of lewdtuber history",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
/// <reference path="../pb_data/types.d.ts" />
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onFileDownloadRequest hook is triggered before each API File download request. Could be used to validate or modify the file response before returning it to the client.
|
* onFileDownloadRequest hook is triggered before each API File download request. Could be used to validate or modify the file response before returning it to the client.
|
||||||
* @see https://pocketbase.io/docs/js-event-hooks/#onfiledownloadrequest
|
* @see https://pocketbase.io/docs/js-event-hooks/#onfiledownloadrequest
|
||||||
@ -13,7 +11,12 @@
|
|||||||
onFileDownloadRequest((event) => {
|
onFileDownloadRequest((event) => {
|
||||||
|
|
||||||
|
|
||||||
// console.log('event', JSON.stringify(event))
|
console.log('>>> We got a file download request event', JSON.stringify(event))
|
||||||
|
|
||||||
|
|
||||||
|
console.log('the below the following is event.request');
|
||||||
|
console.log(JSON.stringify(event.request));
|
||||||
|
console.log('the above the previous line is event.request');
|
||||||
// e.app
|
// e.app
|
||||||
// e.collection
|
// e.collection
|
||||||
// e.record
|
// e.record
|
||||||
@ -24,7 +27,7 @@ onFileDownloadRequest((event) => {
|
|||||||
const securityKey = process.env?.BUNNY_TOKEN_KEY;
|
const securityKey = process.env?.BUNNY_TOKEN_KEY;
|
||||||
const baseUrl = process.env?.BUNNY_ZONE_URL;
|
const baseUrl = process.env?.BUNNY_ZONE_URL;
|
||||||
|
|
||||||
// console.log(`securityKey=${securityKey}, baseUrl=${baseUrl}`)
|
console.log(`securityKey=${securityKey}, baseUrl=${baseUrl}`)
|
||||||
|
|
||||||
if (!securityKey) {
|
if (!securityKey) {
|
||||||
console.error('BUNNY_TOKEN_KEY was missing from env');
|
console.error('BUNNY_TOKEN_KEY was missing from env');
|
||||||
@ -37,34 +40,52 @@ onFileDownloadRequest((event) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a signed BunnyCDN URL.
|
* Generate a signed BunnyCDN URL.
|
||||||
|
*
|
||||||
|
* @see https://support.bunny.net/hc/en-us/articles/360016055099-How-to-sign-URLs-for-BunnyCDN-Token-Authentication
|
||||||
|
*
|
||||||
* @param {string} securityKey - Your BunnyCDN security token
|
* @param {string} securityKey - Your BunnyCDN security token
|
||||||
* @param {string} baseUrl - The base URL (protocol + host)
|
* @param {string} inputUrl - The base URL (protocol + host + path + (optional) qs)
|
||||||
* @param {string} path - Path to the file (starting with /)
|
|
||||||
* @param {string} rawQuery - Raw query string, e.g., "width=500&quality=5"
|
|
||||||
* @param {number} expires - Unix timestamp for expiration
|
* @param {number} expires - Unix timestamp for expiration
|
||||||
|
* @param {string} tokenPath - (optional) create a token that has access to any file within that path
|
||||||
*/
|
*/
|
||||||
function signUrlCool(securityKey, baseUrl, path, rawQuery = "", expires) {
|
function signUrlCool(securityKey, inputUrl, expires, tokenPath = '') {
|
||||||
|
|
||||||
if (!path.startsWith('/')) path = '/' + path;
|
if (inputUrl.endsWith('/')) throw new Error(`url must not end with a slash. got inputUrl=${inputUrl}`);
|
||||||
if (baseUrl.endsWith('/')) throw new Error(`baseUrl must not end with a slash. got baseUrl=${baseUrl}`);
|
|
||||||
|
|
||||||
|
// reading query parameters
|
||||||
|
// let search = e.request.url.query().get("search")
|
||||||
|
const URL = event.request.url
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let parsedURL = URL.parse(inputUrl); // this is pre-parsed. no need to parse again
|
||||||
|
let params = (URL.parse(parsedURL)).searchParams; // these are pre-parsed as event.request.url.rawQuery. no need to parse again
|
||||||
|
let signaturePath = '';
|
||||||
|
let parameterData = '';
|
||||||
|
let parameterDataUrl = '';
|
||||||
|
|
||||||
|
console.log(`eyy we got parsedURL=${parsedURL}`);
|
||||||
|
|
||||||
|
throw new Error('@todo');
|
||||||
|
|
||||||
// Build parameter string (sort keys alphabetically)
|
// Build parameter string (sort keys alphabetically)
|
||||||
let parameterData = "";
|
// let parameterData = "";
|
||||||
if (rawQuery) {
|
// if (rawQuery) {
|
||||||
const params = rawQuery
|
// const params = rawQuery
|
||||||
.split("&")
|
// .split("&")
|
||||||
.map(p => p.split("="))
|
// .map(p => p.split("="))
|
||||||
.filter(([key]) => key && key !== "token" && key !== "expires")
|
// .filter(([key]) => key && key !== "token" && key !== "expires")
|
||||||
.sort(([a], [b]) => a.localeCompare(b));
|
// .sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
|
||||||
if (params.length) {
|
// if (params.length) {
|
||||||
parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
|
// parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Build hashable base
|
// Build hashable base
|
||||||
const hashableBase = securityKey + path + expires + parameterData;
|
const hashableBase = securityKey + path + expires + parameterData;
|
||||||
// console.log(`hashableBase`, hashableBase)
|
console.log(`hashableBase`, hashableBase)
|
||||||
|
|
||||||
// Compute token using your $security.sha256 workflow
|
// Compute token using your $security.sha256 workflow
|
||||||
const tokenH = $security.sha256(hashableBase);
|
const tokenH = $security.sha256(hashableBase);
|
||||||
@ -79,6 +100,7 @@ onFileDownloadRequest((event) => {
|
|||||||
let tokenUrl = baseUrl + path + "?token=" + token;
|
let tokenUrl = baseUrl + path + "?token=" + token;
|
||||||
if (parameterData) tokenUrl += "&" + parameterData;
|
if (parameterData) tokenUrl += "&" + parameterData;
|
||||||
tokenUrl += "&expires=" + expires;
|
tokenUrl += "&expires=" + expires;
|
||||||
|
if (tokenPath) tokenUrl += "&token_path=" + tokenPath;
|
||||||
|
|
||||||
return tokenUrl;
|
return tokenUrl;
|
||||||
}
|
}
|
||||||
@ -87,12 +109,12 @@ onFileDownloadRequest((event) => {
|
|||||||
|
|
||||||
const rawQuery = event.requestEvent.request.url.rawQuery;
|
const rawQuery = event.requestEvent.request.url.rawQuery;
|
||||||
|
|
||||||
// console.log(`record: ${JSON.stringify(event.record)}`)
|
console.log(`record: ${JSON.stringify(event.record)}`)
|
||||||
// // console.log(`collection: ${JSON.stringify(event.collection)}`)
|
// console.log(`collection: ${JSON.stringify(event.collection)}`)
|
||||||
// console.log(`app: ${JSON.stringify(event.app)}`)
|
console.log(`app: ${JSON.stringify(event.app)}`)
|
||||||
// console.log(`fileField: ${JSON.stringify(event.fileField)}`)
|
console.log(`fileField: ${JSON.stringify(event.fileField)}`)
|
||||||
// console.log(`servedPath: ${JSON.stringify(event.servedPath)}`)
|
console.log(`servedPath: ${JSON.stringify(event.servedPath)}`)
|
||||||
// console.log(`servedName: ${JSON.stringify(event.servedName)}`)
|
console.log(`servedName: ${JSON.stringify(event.servedName)}`)
|
||||||
|
|
||||||
// Our job here is to take the servedPath, and sign it using bunnycdn method
|
// Our job here is to take the servedPath, and sign it using bunnycdn method
|
||||||
// Then serve a 302 redirect instead of serving the file proxied thru PB
|
// Then serve a 302 redirect instead of serving the file proxied thru PB
|
||||||
@ -100,8 +122,8 @@ onFileDownloadRequest((event) => {
|
|||||||
const path = event.servedPath;
|
const path = event.servedPath;
|
||||||
const expires = Math.round(Date.now() / 1000) + 7 * 24 * 3600; // 7 days
|
const expires = Math.round(Date.now() / 1000) + 7 * 24 * 3600; // 7 days
|
||||||
const signedUrl = signUrlCool(securityKey, baseUrl, path, rawQuery, expires);
|
const signedUrl = signUrlCool(securityKey, baseUrl, path, rawQuery, expires);
|
||||||
// console.log(`rawQUery`, rawQuery, 'path', path);
|
console.log(`rawQuery`, rawQuery, 'path', path);
|
||||||
// console.log(`signedUrl=${signedUrl}`);
|
console.log(`signedUrl=${signedUrl}`);
|
||||||
|
|
||||||
// This redirect is a tricky thing. We do this to avoid proxying file requests via our pocketbase origin server.
|
// This redirect is a tricky thing. We do this to avoid proxying file requests via our pocketbase origin server.
|
||||||
// The idea is to reduce load.
|
// The idea is to reduce load.
|
||||||
@ -71,8 +71,9 @@
|
|||||||
<footer class="footer mt-5">
|
<footer class="footer mt-5">
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
<p>
|
<p>
|
||||||
<strong>Futureporn <%= meta('version') %></strong> made with love by <a href="https://cjclippy.carrd.co/">@CJ_Clippy</a>.
|
<strong>🔞💦 Futureporn <%= meta('version') %></strong> made with love by <a href="https://cjclippy.carrd.co/">@CJ_Clippy</a>.
|
||||||
</p>
|
</p>
|
||||||
|
<p><i><a href="/about">Dedication to the preservation of lewdtuber history</a></i></p>
|
||||||
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
19
services/pocketbase/pb_hooks/pages/(site)/about/index.ejs
Normal file
19
services/pocketbase/pb_hooks/pages/(site)/about/index.ejs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<div class="content">
|
||||||
|
<h1 class="heading is-1">About</h1>
|
||||||
|
<p><b>R18</b>. For adults only.</p>
|
||||||
|
|
||||||
|
<p><i>Dedication to the preservation of VTuber history.</i></p>
|
||||||
|
|
||||||
|
<h2 class="heading is-2">Mission</h2>
|
||||||
|
|
||||||
|
<p>We archive what would otherwise disappear. Streams vanish, accounts get banned, VODs get wiped. We're here to make sure VTuber culture doesn't evaporate overnight.</p>
|
||||||
|
|
||||||
|
<p>Our goal is to document creators' work so fans, researchers, and future communities can look back on it.</p>
|
||||||
|
|
||||||
|
<p>We record and share and want our favorite lewdtuber moments to last long into the future.</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h2 class="heading is-2">Our patrons keep us going</h2>
|
||||||
|
|
||||||
|
<p>We couldn't do this work without continuing support from our <a href="/patrons">patrons</a></p>
|
||||||
|
</div>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
<% providers.forEach(provider => { %>
|
<% providers.forEach(provider => { %>
|
||||||
<form method="POST" action="/auth/oauth/login">
|
<form method="POST" action="/auth/oauth/login">
|
||||||
<input type="hidden" name="provider" value="<%=provider.name%>">
|
<input type="hidden" name="provider" value="<%=provider.name%>">
|
||||||
<button class="button" type="submit">Log in with <%=provider.displayName%></button>
|
<button class="button is-primary" type="submit">Log in with <%=provider.displayName%></button>
|
||||||
</form>
|
</form>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% if (providers.length === 0) { %>
|
<% if (providers.length === 0) { %>
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<% const buttplugSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -10 51.480372 52.012249"><path d="m 20.221554,24.21173 3.6797,3.6797 c 0.53906,0.53906 0.73047,1.3281 0.48828,2.058599 l -1.6211,4.878901 c -0.51953,1.558599 -0.30859,3.261698 0.578121,4.6719 0.980469,1.539099 2.952215,2.636214 4.730499,2.5 5.799113,-1.246595 18.520815,-15.042558 18.4025,-18.562601 -0.200697,-2.557237 -2.4414,-5.441399 -5.4414,-5.441399 -0.57813,0 -1.1602,0.08984 -1.7188,0.28125 -1.808761,0.530862 -3.664125,1.462123 -5.48046,1.71874 -0.533759,0.07541 -1.078099,-0.21094 -1.4492,-0.57812 l -3.6797,-3.679701 C 31.639694,10.239 30.889819,4.5008743 26.659194,0.2699988 22.42882,-3.960626 11.331194,-10.000001 3.4091936,-10 c -16.3546746,0.5198707 -4.9007052,25.702135 1.3475999,32.1723 4.7260967,4.893907 9.7749655,4.584497 15.4647605,2.03943 z" fill="currentColor"/></svg>' %>
|
||||||
|
|
||||||
|
|
||||||
<% if (!data?.user) { %>
|
<% if (!data?.user) { %>
|
||||||
<div class="notification is-warning">
|
<div class="notification is-warning">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
@ -16,9 +19,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
|
||||||
<div class="players" <% if (data?.user?.get('patron')) { %> data-signals="{'selected':'cdn1'}" <% } else { %> data-signals="{'selected':'cdn2'}" <% } %>>
|
<div class="players" <% if (data?.user?.get('patron')) { %> data-signals="{'selected':'vibeui'}" <% } else { %> data-signals="{'selected':'cdn2'}" <% } %>>
|
||||||
|
|
||||||
<!-- CDN2 Player (B2) -->
|
<%# CDN2 Player (B2) %>
|
||||||
<div class="b2-player" data-show="$selected == 'cdn2'">
|
<div class="b2-player" data-show="$selected == 'cdn2'">
|
||||||
<video controls <% if (!data?.user?.get('patron')) { %> preload="none" <% } %>>
|
<video controls <% if (!data?.user?.get('patron')) { %> preload="none" <% } %>>
|
||||||
<% if (data.vod?.get('videoSrcB2')) { %>
|
<% if (data.vod?.get('videoSrcB2')) { %>
|
||||||
@ -29,22 +32,115 @@
|
|||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CDN1 Player (Mux, patrons only) -->
|
|
||||||
<% if (data?.user?.get('patron')) { %>
|
<% if (data?.user?.get('patron')) { %>
|
||||||
|
<%# CDN1 Player (Mux, patrons only) %>
|
||||||
<div class="mux-player" data-show="$selected == 'cdn1'">
|
<div class="mux-player" data-show="$selected == 'cdn1'">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@mux/mux-player" defer></script>
|
<script src="https://cdn.jsdelivr.net/npm/@mux/mux-player" defer></script>
|
||||||
<mux-player playback-id="<%= data.vod?.get('muxPlaybackId') %>" playback-token="<%= data.vod?.get('muxPlaybackToken') %>"></mux-player>
|
<mux-player playback-id="<%= data.vod?.get('muxPlaybackId') %>" playback-token="<%= data.vod?.get('muxPlaybackToken') %>"></mux-player>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<%# vibeui player (patrons only) %>
|
||||||
|
<% if (
|
||||||
|
data?.user?.get('patron') &&
|
||||||
|
data?.vod?.get('funscriptThrust') || data?.vod?.get('funscriptVibrate') &&
|
||||||
|
data?.vod?.get('hlsPlaylist')
|
||||||
|
) { %>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="vibeui-player" data-show="$selected == 'vibeui'">
|
||||||
|
<style>
|
||||||
|
.vjs-menu-button.vjs-funscript-selector button {
|
||||||
|
/* match control size */
|
||||||
|
display: flex;
|
||||||
|
/* center contents */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-menu-button.vjs-funscript-selector button::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
background: no-repeat center/contain url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-5 -10 51.480372 52.012249'><path d='m 20.221554,24.21173 3.6797,3.6797 c 0.53906,0.53906 0.73047,1.3281 0.48828,2.058599 l -1.6211,4.878901 c -0.51953,1.558599 -0.30859,3.261698 0.578121,4.6719 0.980469,1.539099 2.952215,2.636214 4.730499,2.5 5.799113,-1.246595 18.520815,-15.042558 18.4025,-18.562601 -0.200697,-2.557237 -2.4414,-5.441399 -5.4414,-5.441399 -0.57813,0 -1.1602,0.08984 -1.7188,0.28125 -1.808761,0.530862 -3.664125,1.462123 -5.48046,1.71874 -0.533759,0.07541 -1.078099,-0.21094 -1.4492,-0.57812 l -3.6797,-3.679701 C 31.639694,10.239 30.889819,4.5008743 26.659194,0.2699988 22.42882,-3.960626 11.331194,-10.000001 3.4091936,-10 c -16.3546746,0.5198707 -4.9007052,25.702135 1.3475999,32.1723 4.7260967,4.893907 9.7749655,4.584497 15.4647605,2.03943 z' fill='white'/></svg>");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/video.js@8/dist/video-js.min.css" rel="stylesheet" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/video.js@8.23.4/dist/video.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/buttplug@3.2.2/dist/web/buttplug.min.js"></script>
|
||||||
|
<script src="/Funscripts.js"></script>
|
||||||
|
<script>
|
||||||
|
function collectFunscripts() {
|
||||||
|
return Array.from(document.querySelectorAll('.funscript'))
|
||||||
|
.map(div => {
|
||||||
|
const url = div.getAttribute('data-url');
|
||||||
|
if (!url) return null;
|
||||||
|
|
||||||
|
const name = url.split('/').pop().split('?')[0]; // filename without query
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
url
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean); // remove nulls
|
||||||
|
}
|
||||||
|
|
||||||
|
window.HELP_IMPROVE_VIDEOJS = false; // disable videojs tracking
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const player = videojs('vibeplayer');
|
||||||
|
|
||||||
|
player.ready(() => {
|
||||||
|
console.log('setting up plugins')
|
||||||
|
const funscripts = collectFunscripts()
|
||||||
|
console.log(funscripts)
|
||||||
|
const funscriptsOptions = {
|
||||||
|
buttplugClientName: "futureporn.net",
|
||||||
|
debug: true,
|
||||||
|
funscripts,
|
||||||
|
}
|
||||||
|
|
||||||
|
player.funscriptPlayer(funscriptsOptions);
|
||||||
|
|
||||||
|
|
||||||
|
// let qualityLevels = player.qualityLevels();
|
||||||
|
// console.log(qualityLevels)
|
||||||
|
// player.hlsQualitySelector({
|
||||||
|
// displayCurrentQuality: true,
|
||||||
|
// });
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<video id="vibeplayer" class="video-js vjs-fluid" controls preload="auto" poster="/api/files/vods/<%= data.vod?.get('id') %>/<%= data.vod?.get('thumbnail') %>" data-playlist="<%= data.vod?.get('hlsPlaylist') %>">
|
||||||
|
<source src="/api/hls/<%= data.vod?.get('hlsPlaylist') %>" type="application/x-mpegURL">
|
||||||
|
<track kind="captions" src="<%= data.vod?.get('asrVtt') %>" srclang="en" label="English" default>
|
||||||
|
<div class="funscript" data-url="/api/files/vods/<%= data.vod?.get('id') %>/<%= data.vod?.get('funscriptVibrate') %>"></div>
|
||||||
|
<div class="funscript" data-url="/api/files/vods/<%= data.vod?.get('id') %>/<%= data.vod?.get('funscriptThrust') %>"></div>
|
||||||
|
<p class="vjs-no-js">
|
||||||
|
To view this video please enable JavaScript, and consider upgrading to a
|
||||||
|
web browser that
|
||||||
|
<a href="https://videojs.com/html5-video-support/" target="_blank">
|
||||||
|
supports HTML5 video
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<% } %> <%# /if %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Player toggle buttons -->
|
<%# Player toggle buttons %>
|
||||||
<nav class="level mt-5">
|
<nav class="level mt-5">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<% if (data?.vod?.get('muxAssetId')) { %>
|
<% if (data?.vod?.get('muxAssetId')) { %>
|
||||||
<% if (data?.user?.get('patron')) { %>
|
<% if (data?.user?.get('patron')) { %>
|
||||||
|
<button class="button is-success" data-on-click="$selected = 'vibeui'">vibe player</button>
|
||||||
<button class="button is-success" data-on-click="$selected = 'cdn1'">CDN1 player</button>
|
<button class="button is-success" data-on-click="$selected = 'cdn1'">CDN1 player</button>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
<button disabled class="button is-danger">vibe player (patrons only)</button>
|
||||||
<button disabled class="button is-danger">CDN1 player (patrons only)</button>
|
<button disabled class="button is-danger">CDN1 player (patrons only)</button>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
@ -97,12 +193,109 @@
|
|||||||
<div class="p-2 level"><%- data.vod?.get('notes') %></div>
|
<div class="p-2 level"><%- data.vod?.get('notes') %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (data.vod?.get('thumbnail')) { %>
|
|
||||||
<p><b id="thumbnail">Thumbnail:</b></p>
|
<% if (data.vod?.get('funscriptThrust') || data.vod?.get('funscriptVibrate')) { %>
|
||||||
<figure class="image">
|
<p><b>Funscripts</b> <i class="icon is-small"><%- buttplugSVG %></i></p>
|
||||||
<img src="/api/files/vods/<%= data.vod?.get('id') %>/<%= data.vod?.get('thumbnail') %>" />
|
|
||||||
</figure>
|
<div class="buttons">
|
||||||
|
|
||||||
|
<% if (data.vod?.get('funscriptThrust') && data?.user?.get('patron')) { %>
|
||||||
|
<a class="funscript-thrust button" href="/api/download/<%= data.vod.get('funscriptThrust') %>">
|
||||||
|
thrust
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
<button class="button" disabled>thrust funscript (patrons only)</button>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
<% if (data.vod?.get('funscriptVibrate') && data?.user?.get('patron')) { %>
|
||||||
|
<a class="funscript-vibrate button" href="/api/download/<%= data.vod.get('funscriptVibrate') %>">
|
||||||
|
vibrate
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
<button class="button" disabled>vibrate funscript (patrons only)</button>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (data?.user?.get('patron')) { %>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="notification">
|
||||||
|
<div class="content">
|
||||||
|
To get started with Futureporn vibrator integration, follow these steps.
|
||||||
|
<ol type="1" class="is-lower-alpha">
|
||||||
|
<li>Download and run <a target="_blank" href="https://intiface.com/central/">Intiface Central</a> on your device.</li>
|
||||||
|
<li>Pair your sex toy with Intiface Central.</li>
|
||||||
|
<li>Reload this page.</li>
|
||||||
|
<li>Click the vibe player button.</li>
|
||||||
|
<li>(optional) Assign funscripts to each of your sex toy's actuators in the video menu.</li>
|
||||||
|
<li>Play the video. Your vibrator will activate when Lovense UI is present in the video.</li>
|
||||||
|
</ol>
|
||||||
|
<figure class="image is-128x128">
|
||||||
|
<img src="/vibrator-controls.png" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (data.vod?.get('thumbnail')) { %>
|
||||||
|
<div class="mb-5">
|
||||||
|
<p><b id="thumbnail">Thumbnail:</b></p>
|
||||||
|
<figure class="image">
|
||||||
|
<img width="830" height="465" src="/api/files/vods/<%= data.vod?.get('id') %>/<%= data.vod?.get('thumbnail') %>" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h2 id="advanced" class="title is-2">Advanced VOD Details</h2>
|
||||||
|
<h3 class="title is-3">Download links</h3>
|
||||||
|
<div class="mb-5">
|
||||||
|
<% if (data?.user?.get('patron')) { %>
|
||||||
|
<p><b>HLS Playlist:</b> <a href="/api/hls/<%= data.vod?.get('hlsPlaylist') %>"><%= data.vod?.get('hlsPlaylist') %> </a></p>
|
||||||
|
<p><b>Source Video: </b><a href="/api/download/<%= data.vod?.get('sourceVideo') %>"><%= data.vod?.get('sourceVideo') %> </a></p>
|
||||||
|
</p>
|
||||||
|
<% } else { %>
|
||||||
|
<p><b>HLS Playlist:</b> <span><i>(patrons only)</i></span></p>
|
||||||
|
<p><b>Source Video: </b> <span><i>(patrons only)</i></span></p>
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<% if (data?.vod.get('audioIntegratedLufs')) { %>
|
||||||
|
<h3 id="audio-analysis" class="title is-3">Audio Analysis</h3>
|
||||||
|
<div class="ml-5 mt-5 mb-5">
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
<abbr title="Loudness Units relative to Full Scale - the overall perceived loudness of the audio">LUFS-I</abbr>
|
||||||
|
<strong><%= data?.vod.get('audioIntegratedLufs') %></strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<abbr title="Loudness Range - the dynamic range of the audio in LUFS">LRA</abbr>
|
||||||
|
<strong><%= data?.vod.get('audioLoudnessRange') %></strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<abbr title="True Peak: the maximum instantaneous signal level in dBTP">TP</abbr>
|
||||||
|
<strong><%= data?.vod.get('audioTruePeak') %></strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <%# /.vod-details %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5"></div>
|
<div class="mb-5"></div>
|
||||||
@ -4,5 +4,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<h3>Vtubers</h3>
|
<div class="content">
|
||||||
<%- include('vtuber-list.ejs', { vtubers }) %>
|
<h1 class="heading is-1">Vtubers</h1>
|
||||||
|
<%- include('vtuber-list.ejs', { vtubers }) %>
|
||||||
|
</div>
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
<% const buttplugSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -10 51.480372 52.012249"><path d="m 20.221554,24.21173 3.6797,3.6797 c 0.53906,0.53906 0.73047,1.3281 0.48828,2.058599 l -1.6211,4.878901 c -0.51953,1.558599 -0.30859,3.261698 0.578121,4.6719 0.980469,1.539099 2.952215,2.636214 4.730499,2.5 5.799113,-1.246595 18.520815,-15.042558 18.4025,-18.562601 -0.200697,-2.557237 -2.4414,-5.441399 -5.4414,-5.441399 -0.57813,0 -1.1602,0.08984 -1.7188,0.28125 -1.808761,0.530862 -3.664125,1.462123 -5.48046,1.71874 -0.533759,0.07541 -1.078099,-0.21094 -1.4492,-0.57812 l -3.6797,-3.679701 C 31.639694,10.239 30.889819,4.5008743 26.659194,0.2699988 22.42882,-3.960626 11.331194,-10.000001 3.4091936,-10 c -16.3546746,0.5198707 -4.9007052,25.702135 1.3475999,32.1723 4.7260967,4.893907 9.7749655,4.584497 15.4647605,2.03943 z" fill="currentColor"/></svg>' %>
|
||||||
|
|
||||||
<% if (Array.isArray(data.vods.items) && data.vods.items.length > 0) { %>
|
<% if (Array.isArray(data.vods.items) && data.vods.items.length > 0) { %>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-striped is-hoverable is-fullwidth">
|
<table class="table is-striped is-hoverable is-fullwidth">
|
||||||
@ -8,6 +10,7 @@
|
|||||||
<th>Thumbnail</th>
|
<th>Thumbnail</th>
|
||||||
<th>Torrent</th>
|
<th>Torrent</th>
|
||||||
<th>Magnet Link</th>
|
<th>Magnet Link</th>
|
||||||
|
<th>Funscript</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -60,7 +63,17 @@
|
|||||||
<path fill="#f8312f" d="M11 23v-7.94c0-2.75 2.2-5.04 4.95-5.06c2.78-.03 5.05 2.23 5.05 5v8h8v-8c0-7.18-5.82-13-13-13S3 7.82 3 15v8z" />
|
<path fill="#f8312f" d="M11 23v-7.94c0-2.75 2.2-5.04 4.95-5.06c2.78-.03 5.05 2.23 5.05 5v8h8v-8c0-7.18-5.82-13-13-13S3 7.82 3 15v8z" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</span></a>
|
</span>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if (vod?.funscriptVibrate || vod?.vunscriptThrust) { %>
|
||||||
|
<a target="_blank" href="/vods/<%= vod.id %>#funscripts">
|
||||||
|
<span class="icon">
|
||||||
|
<%- buttplugSVG %>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
29
services/pocketbase/pb_migrations/1764156924_updated_vods.js
Normal file
29
services/pocketbase/pb_migrations/1764156924_updated_vods.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(17, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "file3704076214",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 0,
|
||||||
|
"mimeTypes": [],
|
||||||
|
"name": "torrent",
|
||||||
|
"presentable": false,
|
||||||
|
"protected": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"thumbs": [],
|
||||||
|
"type": "file"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("file3704076214")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
48
services/pocketbase/pb_migrations/1764481404_updated_vods.js
Normal file
48
services/pocketbase/pb_migrations/1764481404_updated_vods.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(18, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "file2376425770",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 0,
|
||||||
|
"mimeTypes": [],
|
||||||
|
"name": "funscriptVibrate",
|
||||||
|
"presentable": false,
|
||||||
|
"protected": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"thumbs": [],
|
||||||
|
"type": "file"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(19, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "file4278982190",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 0,
|
||||||
|
"mimeTypes": [],
|
||||||
|
"name": "funscriptThrust",
|
||||||
|
"presentable": false,
|
||||||
|
"protected": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"thumbs": [],
|
||||||
|
"type": "file"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("file2376425770")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("file4278982190")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
29
services/pocketbase/pb_migrations/1764574888_updated_vods.js
Normal file
29
services/pocketbase/pb_migrations/1764574888_updated_vods.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(20, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3367203181",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "hlsPlaylist",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text3367203181")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
48
services/pocketbase/pb_migrations/1764648351_updated_vods.js
Normal file
48
services/pocketbase/pb_migrations/1764648351_updated_vods.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text3367203181")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(20, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "file3367203181",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 0,
|
||||||
|
"mimeTypes": [],
|
||||||
|
"name": "hlsPlaylist",
|
||||||
|
"presentable": false,
|
||||||
|
"protected": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"thumbs": [],
|
||||||
|
"type": "file"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(20, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3367203181",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "hlsPlaylist",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("file3367203181")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
48
services/pocketbase/pb_migrations/1764723356_updated_vods.js
Normal file
48
services/pocketbase/pb_migrations/1764723356_updated_vods.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("file3367203181")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(20, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3367203181",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "hlsPlaylist",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(20, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "file3367203181",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 0,
|
||||||
|
"mimeTypes": [],
|
||||||
|
"name": "hlsPlaylist",
|
||||||
|
"presentable": false,
|
||||||
|
"protected": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"thumbs": [],
|
||||||
|
"type": "file"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text3367203181")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
48
services/pocketbase/pb_migrations/1764758197_updated_vods.js
Normal file
48
services/pocketbase/pb_migrations/1764758197_updated_vods.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("file2376425770")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("file4278982190")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(18, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "file2376425770",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 0,
|
||||||
|
"mimeTypes": [],
|
||||||
|
"name": "funscriptVibrate",
|
||||||
|
"presentable": false,
|
||||||
|
"protected": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"thumbs": [],
|
||||||
|
"type": "file"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(19, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "file4278982190",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 0,
|
||||||
|
"mimeTypes": [],
|
||||||
|
"name": "funscriptThrust",
|
||||||
|
"presentable": false,
|
||||||
|
"protected": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"thumbs": [],
|
||||||
|
"type": "file"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
105
services/pocketbase/pb_migrations/1764758246_updated_vods.js
Normal file
105
services/pocketbase/pb_migrations/1764758246_updated_vods.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(19, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text2376425770",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "funscriptVibrate",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(20, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text4278982190",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "funscriptThrust",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(21, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text7357270",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "audioIntegratedLufs",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(22, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text513636770",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "audioLoudnessRange",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(23, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text2917960678",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "audioTruePeak",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_144770472")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text2376425770")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text4278982190")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text7357270")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text513636770")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text2917960678")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
305
services/pocketbase/pb_public/Funscripts.js
Normal file
305
services/pocketbase/pb_public/Funscripts.js
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
const MenuButton = videojs.getComponent('MenuButton');
|
||||||
|
const VideoJsMenuItem = videojs.getComponent('MenuItem');
|
||||||
|
|
||||||
|
/* ------------------------- Menu Components ------------------------- */
|
||||||
|
|
||||||
|
class ActuatorMenuButton extends MenuButton {
|
||||||
|
constructor(player, { actuator, funscripts = [], onAssign } = {}) {
|
||||||
|
super(player, {
|
||||||
|
title: actuator?.name ?? 'Actuator',
|
||||||
|
name: 'ActuatorMenuButton',
|
||||||
|
actuator,
|
||||||
|
funscripts,
|
||||||
|
onAssign,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createItems() {
|
||||||
|
const { actuator, funscripts, onAssign } = this.options_ || {};
|
||||||
|
if (!actuator || !funscripts?.length) return [];
|
||||||
|
|
||||||
|
return funscripts.map(funscript =>
|
||||||
|
new FunscriptMenuItem(this.player_, {
|
||||||
|
actuator,
|
||||||
|
funscript,
|
||||||
|
onAssign,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FunscriptMenuItem extends VideoJsMenuItem {
|
||||||
|
constructor(player, { actuator, funscript, onAssign } = {}) {
|
||||||
|
super(player, { label: funscript.name, selectable: true });
|
||||||
|
|
||||||
|
this.actuator = actuator;
|
||||||
|
this.funscript = funscript;
|
||||||
|
this.onAssign = onAssign;
|
||||||
|
|
||||||
|
// Pre-select if this actuator already has this funscript assigned
|
||||||
|
this.selected(this.actuator.assignedFunscript?.url === this.funscript.url);
|
||||||
|
|
||||||
|
this.on('click', () => {
|
||||||
|
if (!this.actuator || !this.onAssign) return;
|
||||||
|
|
||||||
|
// Use centralized assignment method
|
||||||
|
this.onAssign?.(this, this.actuator, this.funscript);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------- Helper ------------------------- */
|
||||||
|
function pickInitialFunscript(actuatorType, availableFunscripts) {
|
||||||
|
actuatorType = actuatorType.toLowerCase();
|
||||||
|
|
||||||
|
if (actuatorType.includes('vibrator')) {
|
||||||
|
return availableFunscripts.find(f => f.name.includes('vibrate')) ?? availableFunscripts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actuatorType.includes('thrust')) {
|
||||||
|
return availableFunscripts.find(f => f.name.includes('thrust')) ?? availableFunscripts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableFunscripts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------- Main Plugin ------------------------- */
|
||||||
|
|
||||||
|
class FunscriptPlayer {
|
||||||
|
constructor(player, { debug = false, funscripts, ...options } = {}) {
|
||||||
|
this.player = player;
|
||||||
|
this.debug = debug;
|
||||||
|
this.funscriptCache = new Map(); // key: url, value: parsed funscript
|
||||||
|
this.funscripts = (funscripts || []).map(f => {
|
||||||
|
if (typeof f === 'string') {
|
||||||
|
const url = f;
|
||||||
|
const name = url.split('/').pop().split('?')[0];
|
||||||
|
return { name, url };
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
});
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
this.menuButton = null;
|
||||||
|
this.devices = [];
|
||||||
|
|
||||||
|
player.on('toyConnected', () => this.refreshMenu());
|
||||||
|
player.on('toyDisconnected', () => this.refreshMenu());
|
||||||
|
player.on('pause', () => this.stopAllDevices());
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const connector = new window.buttplug.ButtplugBrowserWebsocketClientConnector("ws://localhost:12345");
|
||||||
|
this.client = new window.buttplug.ButtplugClient(this.options?.buttplugClientName || "Funscripts VideoJS Client");
|
||||||
|
|
||||||
|
this.client.addListener("deviceadded", device => {
|
||||||
|
this.addDevice(device);
|
||||||
|
this.debug && console.log("Device added:", device.name, device);
|
||||||
|
this.player.trigger('toyConnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.addListener("deviceremoved", device => {
|
||||||
|
this.removeDevice(device);
|
||||||
|
this.debug && console.log("Device removed:", device.name);
|
||||||
|
this.player.trigger('toyDisconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.connect(connector);
|
||||||
|
await this.client.startScanning();
|
||||||
|
this.initVideoListeners();
|
||||||
|
this.debug && console.log("[buttplug] Connected and scanning");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[buttplug] Connection error:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFunscript(funscript) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(funscript.url);
|
||||||
|
if (!response.ok) throw new Error(`Failed to load ${funscript.name}`);
|
||||||
|
return await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[funscripts] Error loading funscript:", funscript.name, err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFunscriptValueAtTime(funscriptData, timeMs) {
|
||||||
|
if (!funscriptData?.actions?.length) return null;
|
||||||
|
|
||||||
|
let lastAction = null;
|
||||||
|
for (const action of funscriptData.actions) {
|
||||||
|
if (action.at <= timeMs) {
|
||||||
|
lastAction = action;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastAction ? lastAction.pos / 100 : null; // normalize 0–1
|
||||||
|
}
|
||||||
|
|
||||||
|
initVideoListeners() {
|
||||||
|
this.player.on('pause', () => {
|
||||||
|
this.debug && console.log('Video paused. Stopping devices...');
|
||||||
|
this.stopAllDevices();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.player.on('timeupdate', () => {
|
||||||
|
if (this.player.paused() || this.player.ended()) return;
|
||||||
|
|
||||||
|
const currentTime = this.player.currentTime() * 1000;
|
||||||
|
|
||||||
|
this.devices.forEach(device => {
|
||||||
|
device.actuators.forEach(actuator => {
|
||||||
|
const fun = actuator.assignedFunscript;
|
||||||
|
if (!fun) return;
|
||||||
|
|
||||||
|
const funscriptData = this.funscriptCache.get(fun.url);
|
||||||
|
if (!funscriptData) return;
|
||||||
|
|
||||||
|
const position = this.getFunscriptValueAtTime(funscriptData, currentTime);
|
||||||
|
this.debug && console.log(`${fun.name} position=${position}`);
|
||||||
|
|
||||||
|
if (position !== null) {
|
||||||
|
this.sendToDevice(device, actuator, position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopAllDevices() {
|
||||||
|
if (!this.client || !this.client.devices) return;
|
||||||
|
|
||||||
|
for (const [deviceIndex, device] of this.client.devices.entries()) {
|
||||||
|
const scalarCmds = device.messageAttributes?.ScalarCmd || [];
|
||||||
|
for (const cmd of scalarCmds) {
|
||||||
|
try {
|
||||||
|
await this.sendToDevice(device, {
|
||||||
|
index: cmd.Index,
|
||||||
|
name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}`
|
||||||
|
}, 0);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[buttplug] Failed to stop ${device.name}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendToDevice(device, actuator, position) {
|
||||||
|
try {
|
||||||
|
this.client.devices[device.index].vibrate(position);
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(`[buttplug] Sent to ${actuator.name}:`, position);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[buttplug] Error sending command:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------- Device Tracking ------------------------- */
|
||||||
|
|
||||||
|
addDevice(device) {
|
||||||
|
const scalarCmds = device._deviceInfo?.DeviceMessages?.ScalarCmd || [];
|
||||||
|
|
||||||
|
const actuators = scalarCmds.map((cmd, i) => {
|
||||||
|
const assignedFunscript = pickInitialFunscript(cmd.ActuatorType, this.funscripts);
|
||||||
|
|
||||||
|
if (assignedFunscript && !this.funscriptCache.has(assignedFunscript.url)) {
|
||||||
|
this.loadFunscript(assignedFunscript).then(data => {
|
||||||
|
if (data) this.funscriptCache.set(assignedFunscript.url, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}`,
|
||||||
|
type: cmd.ActuatorType,
|
||||||
|
index: cmd.Index,
|
||||||
|
assignedFunscript,
|
||||||
|
deviceIndex: this.devices.length, // track device index
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.devices.push({
|
||||||
|
name: device._deviceInfo.DeviceName,
|
||||||
|
index: this.devices.length,
|
||||||
|
actuators,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDevice(device) {
|
||||||
|
this.devices = this.devices.filter(d => d.name !== device.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------- Central Assignment ------------------------- */
|
||||||
|
async assignFunscript(deviceIndex, actuatorIndex, funscript) {
|
||||||
|
const actuator = this.devices[deviceIndex]?.actuators[actuatorIndex];
|
||||||
|
if (!actuator) return;
|
||||||
|
|
||||||
|
actuator.assignedFunscript = funscript;
|
||||||
|
|
||||||
|
if (!this.funscriptCache.has(funscript.url)) {
|
||||||
|
const data = await this.loadFunscript(funscript);
|
||||||
|
if (data) this.funscriptCache.set(funscript.url, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debug && console.log(`Assigned ${funscript.name} to ${actuator.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------- Menu Management ------------------------- */
|
||||||
|
|
||||||
|
createMenuButtons() {
|
||||||
|
if (this.menuButtons?.length) {
|
||||||
|
this.menuButtons.forEach(btn => this.player.controlBar.removeChild(btn));
|
||||||
|
}
|
||||||
|
this.menuButtons = [];
|
||||||
|
|
||||||
|
this.devices.forEach(device => {
|
||||||
|
device.actuators.forEach(actuator => {
|
||||||
|
const button = new ActuatorMenuButton(this.player, {
|
||||||
|
actuator,
|
||||||
|
funscripts: this.funscripts,
|
||||||
|
onAssign: async (item, act, funscript) => {
|
||||||
|
await this.assignFunscript(device.index, act.index, funscript);
|
||||||
|
|
||||||
|
const menuItems = item.parentComponent_.children()
|
||||||
|
.filter(c => c instanceof FunscriptMenuItem);
|
||||||
|
menuItems.forEach(i => {
|
||||||
|
i.selected(i.funscript.url === funscript.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const placementIndex = Math.max(
|
||||||
|
0,
|
||||||
|
this.options?.placementIndex ?? (this.player.controlBar.children().length - 2)
|
||||||
|
);
|
||||||
|
this.player.controlBar.addChild(button, { componentClass: 'funscriptSelector' }, placementIndex);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.addClass('vjs-funscript-selector');
|
||||||
|
button.removeClass('vjs-hidden');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
this.menuButtons.push(button);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMenu() {
|
||||||
|
this.createMenuButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------- Register Plugin ------------------------- */
|
||||||
|
|
||||||
|
videojs.registerPlugin("funscriptPlayer", async function (options) {
|
||||||
|
try {
|
||||||
|
const plugin = new FunscriptPlayer(this, options);
|
||||||
|
await plugin.init();
|
||||||
|
return plugin;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Funscripts plugin failed to initialize:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
services/pocketbase/pb_public/vibrator-controls.png
Normal file
BIN
services/pocketbase/pb_public/vibrator-controls.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
70
services/pocketbase/src/handlers/file_download.go
Normal file
70
services/pocketbase/src/handlers/file_download.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"futureporn/src/misc"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getPathAllowedForMaster returns the pathAllowedRoute for master playlists,
|
||||||
|
// or an empty string if the request is not a master playlist.
|
||||||
|
func getPathAllowedForMaster(e *core.FileDownloadRequestEvent) string {
|
||||||
|
if strings.HasPrefix(e.ServedName, "master_") && strings.HasSuffix(e.ServedName, ".m3u8") {
|
||||||
|
dir := path.Dir(misc.GetCDNPath(e.Collection.Id, e.Record.Id, e.ServedName, true))
|
||||||
|
return dir + "/"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFileDownload redirects a file request to a signed BunnyCDN URL.
|
||||||
|
// We use this to avoid having Pocketbase acting as a proxy to assets on S3. (Avoiding a performance hit)
|
||||||
|
// @see https://pocketbase.io/docs/go-event-hooks/#onfiledownloadrequest
|
||||||
|
func HandleFileDownload(e *core.FileDownloadRequestEvent) error {
|
||||||
|
|
||||||
|
// e.App
|
||||||
|
// e.Collection
|
||||||
|
// e.Record
|
||||||
|
// e.FileField
|
||||||
|
// e.ServedPath
|
||||||
|
// e.ServedName
|
||||||
|
// and all RequestEvent fields...
|
||||||
|
|
||||||
|
securityKey := os.Getenv("BUNNY_TOKEN_KEY")
|
||||||
|
if securityKey == "" {
|
||||||
|
log.Fatal("missing BUNNY_TOKEN_KEY env var")
|
||||||
|
}
|
||||||
|
|
||||||
|
cdnUrl := os.Getenv("BUNNY_ZONE_URL")
|
||||||
|
if cdnUrl == "" {
|
||||||
|
log.Fatal("missing BUNNY_ZONE_URL env var")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(">> ServedName=", e.ServedName)
|
||||||
|
|
||||||
|
expirationTime := int64(3600)
|
||||||
|
|
||||||
|
cdnPath := misc.GetCDNPath(e.Collection.Id, e.Record.Id, e.ServedName, false)
|
||||||
|
urlStr := cdnUrl + cdnPath
|
||||||
|
|
||||||
|
log.Println(">> We are handling a file download request with urlStr:", urlStr)
|
||||||
|
log.Println(">> FileField", e.FileField)
|
||||||
|
log.Println(">> Record", e.Record)
|
||||||
|
log.Println(">> Collection", e.Collection)
|
||||||
|
|
||||||
|
// If the request is for a master .m3u8 file,
|
||||||
|
// we respond with a signed URL that has a pathAllowedRoute. ("" otherwise)
|
||||||
|
pathAllowedRoute := getPathAllowedForMaster(e)
|
||||||
|
log.Println(">> pathAllowedRoute:", pathAllowedRoute)
|
||||||
|
|
||||||
|
signedUrl := misc.SignBunnyCDNURL(securityKey, urlStr, expirationTime, pathAllowedRoute)
|
||||||
|
log.Println(">> Signed URL is", signedUrl)
|
||||||
|
|
||||||
|
e.Redirect(http.StatusTemporaryRedirect, signedUrl)
|
||||||
|
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
52
services/pocketbase/src/handlers/hls_package.go
Normal file
52
services/pocketbase/src/handlers/hls_package.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"futureporn/src/misc"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// func getCDNPath(e *core.FileDownloadRequestEvent, mediaPackage bool) string {
|
||||||
|
// // e.Collection.Name is "vods"
|
||||||
|
// // e.Record.ID is the record ID
|
||||||
|
// // e.ServedName is the filename
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// PackageHandler handles /api/package/:vodId requests
|
||||||
|
func PackageHandler(e *core.RequestEvent) error {
|
||||||
|
securityKey := os.Getenv("BUNNY_TOKEN_KEY")
|
||||||
|
if securityKey == "" {
|
||||||
|
log.Fatal("missing BUNNY_TOKEN_KEY env var")
|
||||||
|
}
|
||||||
|
|
||||||
|
cdnUrl := os.Getenv("BUNNY_ZONE_URL")
|
||||||
|
if cdnUrl == "" {
|
||||||
|
log.Fatal("missing BUNNY_ZONE_URL env var")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the vodId from the path
|
||||||
|
recordId := e.Request.PathValue("recordId")
|
||||||
|
collectionId := e.Request.PathValue("collectionId")
|
||||||
|
fileName := e.Request.PathValue("fileName")
|
||||||
|
|
||||||
|
log.Println("getting CDNPath")
|
||||||
|
urlStr := cdnUrl + misc.GetCDNPath(collectionId, recordId, fileName, true)
|
||||||
|
log.Println(">>> urlStr:", urlStr)
|
||||||
|
expirationTime := int64(3600)
|
||||||
|
pathAllowed := fmt.Sprintf("/%s/%s/hls/", collectionId, recordId)
|
||||||
|
// pathAllowed := path // fileName is something like `master_1234.m3u8` and the urlStr is something like `https://fppbdev.b-cdn.net/`
|
||||||
|
// urlStr is something like https://fppbdev.b-cdn.net/pbc_144770472/j8cgd0qqjbzyfi3/hls/master_j50o7q3n0i.m3u8
|
||||||
|
// we need to use existing variables and set pathAllowed to `/pbc_144770472/j8cgd0qqjbzyfi3/hls/`
|
||||||
|
|
||||||
|
log.Println(">>> pathAllowed:", pathAllowed, "urlStr:", urlStr)
|
||||||
|
|
||||||
|
signedUrl := misc.SignBunnyCDNURL(securityKey, urlStr, expirationTime, pathAllowed)
|
||||||
|
|
||||||
|
// redirect the client
|
||||||
|
return e.Redirect(http.StatusFound, signedUrl)
|
||||||
|
}
|
||||||
56
services/pocketbase/src/handlers/s3_download.go
Normal file
56
services/pocketbase/src/handlers/s3_download.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"futureporn/src/misc"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// S3Download redirects a file request to a signed BunnyCDN URL.
|
||||||
|
// This bypasses Pocketbase file downloading API
|
||||||
|
// This is useful for serving files which have been uploaded outside of Pocketbase
|
||||||
|
// Instead of being uploaded directly to Pocketbase using FormData.
|
||||||
|
// We use this method for serving large VOD files.
|
||||||
|
// @see https://pocketbase.io/docs/go-event-hooks/#onfiledownloadrequest
|
||||||
|
func S3Download(e *core.RequestEvent) error {
|
||||||
|
|
||||||
|
// e.App
|
||||||
|
// e.Collection
|
||||||
|
// e.Record
|
||||||
|
// e.FileField
|
||||||
|
// e.ServedPath
|
||||||
|
// e.ServedName
|
||||||
|
// and all RequestEvent fields...
|
||||||
|
|
||||||
|
securityKey := os.Getenv("BUNNY_TOKEN_KEY")
|
||||||
|
if securityKey == "" {
|
||||||
|
log.Fatal("missing BUNNY_TOKEN_KEY env var")
|
||||||
|
}
|
||||||
|
|
||||||
|
cdnUrl := os.Getenv("BUNNY_ZONE_URL")
|
||||||
|
if cdnUrl == "" {
|
||||||
|
log.Fatal("missing BUNNY_ZONE_URL env var")
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationTime := int64(3600)
|
||||||
|
|
||||||
|
recordId := e.Request.PathValue("recordId")
|
||||||
|
collectionId := e.Request.PathValue("collectionId")
|
||||||
|
fileName := e.Request.PathValue("fileName")
|
||||||
|
|
||||||
|
cdnPath := misc.GetCDNPath(collectionId, recordId, fileName, false)
|
||||||
|
|
||||||
|
urlStr := cdnUrl + cdnPath
|
||||||
|
|
||||||
|
log.Println(">>>>>>>>>>>>>>>>>>>>>>>>> urlStr:", urlStr)
|
||||||
|
|
||||||
|
signedUrl := misc.SignBunnyCDNURL(securityKey, urlStr, expirationTime, "")
|
||||||
|
log.Println(">>>>>>>>>>>>>>>> Signed URL is", signedUrl)
|
||||||
|
|
||||||
|
e.Redirect(http.StatusTemporaryRedirect, signedUrl)
|
||||||
|
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
80
services/pocketbase/src/misc/bunny-cdn.go
Normal file
80
services/pocketbase/src/misc/bunny-cdn.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package misc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCDNPath(collectionId string, recordId string, fileName string, mediaPackage bool) string {
|
||||||
|
var base string
|
||||||
|
|
||||||
|
if mediaPackage {
|
||||||
|
base = fmt.Sprintf("/%s/%s/hls/%s", collectionId, recordId, fileName)
|
||||||
|
} else {
|
||||||
|
base = fmt.Sprintf("/%s/%s/%s", collectionId, recordId, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func SignBunnyCDNURL(securityKey string, urlStr string, expirationTime int64, pathAllowed string) string {
|
||||||
|
expires := time.Now().Unix() + expirationTime
|
||||||
|
|
||||||
|
parsedURL, _ := url.Parse(urlStr)
|
||||||
|
parameters := parsedURL.Query()
|
||||||
|
|
||||||
|
var signaturePath string
|
||||||
|
if pathAllowed != "" {
|
||||||
|
signaturePath = pathAllowed
|
||||||
|
parameters.Set("token_path", signaturePath)
|
||||||
|
} else {
|
||||||
|
signaturePath, _ = url.QueryUnescape(parsedURL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually sort the parameters
|
||||||
|
var keys []string
|
||||||
|
for key := range parameters {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var parameterData string
|
||||||
|
var parameterDataURL string
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
values := parameters[key]
|
||||||
|
sort.Strings(values)
|
||||||
|
for _, value := range values {
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if parameterData != "" {
|
||||||
|
parameterData += "&"
|
||||||
|
}
|
||||||
|
parameterData += key + "=" + value
|
||||||
|
parameterDataURL += "&" + key + "=" + url.QueryEscape(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hashableBase := securityKey + signaturePath + fmt.Sprint(expires) + parameterData
|
||||||
|
hash := sha256.New()
|
||||||
|
hash.Write([]byte(hashableBase))
|
||||||
|
token := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||||
|
token = strings.ReplaceAll(token, "\n", "")
|
||||||
|
token = strings.ReplaceAll(token, "+", "-")
|
||||||
|
token = strings.ReplaceAll(token, "/", "_")
|
||||||
|
token = strings.ReplaceAll(token, "=", "")
|
||||||
|
|
||||||
|
isDirectory := pathAllowed != ""
|
||||||
|
|
||||||
|
if isDirectory {
|
||||||
|
return parsedURL.Scheme + "://" + parsedURL.Host + "/bcdn_token=" + token + parameterDataURL + "&expires=" + fmt.Sprint(expires) + parsedURL.Path
|
||||||
|
} else {
|
||||||
|
return parsedURL.Scheme + "://" + parsedURL.Host + parsedURL.Path + "?token=" + token + parameterDataURL + "&expires=" + fmt.Sprint(expires)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,957 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "pocketpages-plugin-patreon",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "pocketpages-plugin-patreon",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"license": "Unlicense",
|
|
||||||
"dependencies": {
|
|
||||||
"pocketpages-plugin-js-sdk": "0.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"tsdown": "^0.15.12",
|
|
||||||
"typescript": "^5.7.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/generator": {
|
|
||||||
"version": "7.28.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
|
|
||||||
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.28.5",
|
|
||||||
"@babel/types": "^7.28.5",
|
|
||||||
"@jridgewell/gen-mapping": "^0.3.12",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.28",
|
|
||||||
"jsesc": "^3.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-string-parser": {
|
|
||||||
"version": "7.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
|
||||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
|
||||||
"version": "7.28.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
|
||||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/parser": {
|
|
||||||
"version": "7.28.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
|
||||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.28.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"parser": "bin/babel-parser.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/types": {
|
|
||||||
"version": "7.28.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
|
||||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
|
||||||
"@babel/helper-validator-identifier": "^7.28.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/core": {
|
|
||||||
"version": "1.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz",
|
|
||||||
"integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/wasi-threads": "1.1.0",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
|
|
||||||
"integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
|
||||||
"version": "0.3.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
|
||||||
"version": "1.5.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
|
||||||
"version": "0.3.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
|
||||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
|
||||||
"version": "1.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
|
|
||||||
"integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/core": "^1.5.0",
|
|
||||||
"@emnapi/runtime": "^1.5.0",
|
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-project/types": {
|
|
||||||
"version": "0.95.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.95.0.tgz",
|
|
||||||
"integrity": "sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@quansync/fs": {
|
|
||||||
"version": "0.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-0.1.5.tgz",
|
|
||||||
"integrity": "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"quansync": "^0.2.11"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-bfgKYhFiXJALeA/riil908+2vlyWGdwa7Ju5S+JgWZYdR4jtiPOGdM6WLfso1dojCh+4ZWeiTwPeV9IKQEX+4g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-xjCv4CRVsSnnIxTuyH1RDJl5OEQ1c9JYOwfDAHddjJDxCw46ZX9q80+xq7Eok7KC4bRSZudMJllkvOKv0T9SeA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-ddcO9TD3D/CLUa/l8GO8LHzBOaZqWg5ClMy3jICoxwCuoz47h9dtqPsIeTiB6yR501LQTeDsjA4lIFd7u3Ljfw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-MBTWdrzW9w+UMYDUvnEuh0pQvLENkl2Sis15fHTfHVW7ClbGuez+RWopZudIDEGkpZXdeI4CkRXk+vdIIebrmg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-4YgoCFiki1HR6oSg+GxxfzfnVCesQxLF1LEnw9uXS/MpBmuog0EOO2rYfy69rWP4tFZL9IWp6KEfGZLrZ7aUog==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-LE1gjAwQRrbCOorJJ7LFr10s5vqYf5a00V5Ea9wXcT2+56n5YosJkcp8eQ12FxRBv2YX8dsdQJb+ZTtYJwb6XQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-tdy8ThO/fPp40B81v0YK3QC+KODOmzJzSUOO37DinQxzlTJ026gqUSOM8tzlVixRbQJltgVDCTYF8HNPRErQTA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-lS082ROBWdmOyVY/0YB3JmsiClaWoxvC+dA8/rbhyB9VLkvVEaihLEOr4CYmrMse151C4+S6hCw6oa1iewox7g==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-Hi73aYY0cBkr1/SvNQqH8Cd+rSV6S9RB5izCv0ySBcRnd/Wfn5plguUoGYwBnhHgFbh6cPw9m2dUVBR6BG1gxA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-fljEqbO7RHHogNDxYtTzr+GNjlfOx21RUyGmF+NrkebZ8emYYiIqzPxsaMZuRx0rgZmVmliOzEp86/CQFDKhJQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openharmony"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-ZJDB7lkuZE9XUnWQSYrBObZxczut+8FZ5pdanm8nNS1DAo8zsrPuvGwn+U3fwU98WaiFsNrA4XHngesCGr8tEQ==",
|
|
||||||
"cpu": [
|
|
||||||
"wasm32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@napi-rs/wasm-runtime": "^1.0.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-zyzAjItHPUmxg6Z8SyRhLdXlJn3/D9KL5b9mObUrBHhWS/GwRH4665xCiFqeuktAhhWutqfc+rOV2LjK4VYQGQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-win32-ia32-msvc": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-wODcGzlfxqS6D7BR0srkJk3drPwXYLu7jPHN27ce2c4PUnVVmJnp9mJzUQGT4LpmHmmVdMZ+P6hKvyTGBzc1CA==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-wiU40G1nQo9rtfvF9jLbl79lUgjfaD/LTyUEw2Wg/gdF5OhjzpKMVugZQngO+RNdwYaNj+Fs+kWBWfp4VXPMHA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/pluginutils": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-Le9ulGCrD8ggInzWw/k2J8QcbPz7eGIOWqfJ2L+1R0Opm7n6J37s2hiDWlh6LJN0Lk9L5sUzMvRHKW7UxBZsQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@tybys/wasm-util": {
|
|
||||||
"version": "0.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ansis": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ast-kit": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.28.4",
|
|
||||||
"pathe": "^2.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.19.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/birpc": {
|
|
||||||
"version": "2.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz",
|
|
||||||
"integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cac": {
|
|
||||||
"version": "6.7.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
|
||||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"readdirp": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.16.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
|
||||||
"version": "4.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/defu": {
|
|
||||||
"version": "6.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
|
||||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/diff": {
|
|
||||||
"version": "8.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
|
|
||||||
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dts-resolver": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.18.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"oxc-resolver": ">=11.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"oxc-resolver": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/empathic": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fdir": {
|
|
||||||
"version": "6.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"picomatch": "^3 || ^4"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"picomatch": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-tsconfig": {
|
|
||||||
"version": "4.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
|
||||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"resolve-pkg-maps": "^1.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hookable": {
|
|
||||||
"version": "5.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
|
||||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/jiti": {
|
|
||||||
"version": "2.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
|
||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jsesc": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"jsesc": "bin/jsesc"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/magic-string": {
|
|
||||||
"version": "0.30.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
|
||||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pathe": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/picomatch": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pocketbase-js-sdk-jsvm": {
|
|
||||||
"version": "0.25.10004",
|
|
||||||
"resolved": "https://registry.npmjs.org/pocketbase-js-sdk-jsvm/-/pocketbase-js-sdk-jsvm-0.25.10004.tgz",
|
|
||||||
"integrity": "sha512-/0RkFa6X4LeKMdv4KCXhC5i2cvpG0elvXInRyk8bApjG8jyyPiNDL6o9VBvgxjl6j8/HcQYf3oKYXmZteqAFvQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pocketpages-plugin-js-sdk": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pocketpages-plugin-js-sdk/-/pocketpages-plugin-js-sdk-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-/uY1nCz7Zt19C/kyoOraH5ekTZa4PLPpHd0qIEoyf1XBPtEpHL4k7jZE0IPEMu1dH/8RfsKb/YzN52GR+9DdzQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"pocketbase-js-sdk-jsvm": "^0.25.10004"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/quansync": {
|
|
||||||
"version": "0.2.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
|
||||||
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/readdirp": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.18.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/resolve-pkg-maps": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/rolldown": {
|
|
||||||
"version": "1.0.0-beta.45",
|
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.45.tgz",
|
|
||||||
"integrity": "sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@oxc-project/types": "=0.95.0",
|
|
||||||
"@rolldown/pluginutils": "1.0.0-beta.45"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rolldown": "bin/cli.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.45",
|
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-beta.45"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/rolldown-plugin-dts": {
|
|
||||||
"version": "0.17.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.17.3.tgz",
|
|
||||||
"integrity": "sha512-8mGnNUVNrqEdTnrlcaDxs4sAZg0No6njO+FuhQd4L56nUbJO1tHxOoKDH3mmMJg7f/BhEj/1KjU5W9kZ9zM/kQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/generator": "^7.28.5",
|
|
||||||
"@babel/parser": "^7.28.5",
|
|
||||||
"@babel/types": "^7.28.5",
|
|
||||||
"ast-kit": "^2.1.3",
|
|
||||||
"birpc": "^2.6.1",
|
|
||||||
"debug": "^4.4.3",
|
|
||||||
"dts-resolver": "^2.1.2",
|
|
||||||
"get-tsconfig": "^4.13.0",
|
|
||||||
"magic-string": "^0.30.21"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.18.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@ts-macro/tsc": "^0.3.6",
|
|
||||||
"@typescript/native-preview": ">=7.0.0-dev.20250601.1",
|
|
||||||
"rolldown": "^1.0.0-beta.44",
|
|
||||||
"typescript": "^5.0.0",
|
|
||||||
"vue-tsc": "~3.1.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@ts-macro/tsc": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@typescript/native-preview": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"vue-tsc": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
|
||||||
"version": "7.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tinyexec": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tinyglobby": {
|
|
||||||
"version": "0.2.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fdir": "^6.5.0",
|
|
||||||
"picomatch": "^4.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tree-kill": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"tree-kill": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsdown": {
|
|
||||||
"version": "0.15.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.15.12.tgz",
|
|
||||||
"integrity": "sha512-c8VLlQm8/lFrOAg5VMVeN4NAbejZyVQkzd+ErjuaQgJFI/9MhR9ivr0H/CM7UlOF1+ELlF6YaI7sU/4itgGQ8w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansis": "^4.2.0",
|
|
||||||
"cac": "^6.7.14",
|
|
||||||
"chokidar": "^4.0.3",
|
|
||||||
"debug": "^4.4.3",
|
|
||||||
"diff": "^8.0.2",
|
|
||||||
"empathic": "^2.0.0",
|
|
||||||
"hookable": "^5.5.3",
|
|
||||||
"rolldown": "1.0.0-beta.45",
|
|
||||||
"rolldown-plugin-dts": "^0.17.2",
|
|
||||||
"semver": "^7.7.3",
|
|
||||||
"tinyexec": "^1.0.1",
|
|
||||||
"tinyglobby": "^0.2.15",
|
|
||||||
"tree-kill": "^1.2.2",
|
|
||||||
"unconfig": "^7.3.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"tsdown": "dist/run.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.19.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sxzz"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@arethetypeswrong/core": "^0.18.1",
|
|
||||||
"publint": "^0.3.0",
|
|
||||||
"typescript": "^5.0.0",
|
|
||||||
"unplugin-lightningcss": "^0.4.0",
|
|
||||||
"unplugin-unused": "^0.5.0",
|
|
||||||
"unrun": "^0.2.1"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@arethetypeswrong/core": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"publint": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"unplugin-lightningcss": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"unplugin-unused": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"unrun": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tslib": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.9.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unconfig": {
|
|
||||||
"version": "7.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.3.3.tgz",
|
|
||||||
"integrity": "sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@quansync/fs": "^0.1.5",
|
|
||||||
"defu": "^6.1.4",
|
|
||||||
"jiti": "^2.5.1",
|
|
||||||
"quansync": "^0.2.11"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "pocketpages-plugin-patreon",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"build": "tsdown",
|
|
||||||
"dev": "tsdown --watch"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"pocketpages-plugin-js-sdk": "0.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"tsdown": "^0.15.12",
|
|
||||||
"typescript": "^5.7.3"
|
|
||||||
},
|
|
||||||
"license": "Unlicense"
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { type ExtendContextApiContext } from 'pocketpages'
|
|
||||||
import PocketBase, {
|
|
||||||
AuthModel,
|
|
||||||
RecordAuthResponse,
|
|
||||||
} from 'pocketbase-js-sdk-jsvm'
|
|
||||||
import { type OAuth2SignInOptions } from 'pocketpages-plugin-auth'
|
|
||||||
import { PluginFactory } from 'pocketpages'
|
|
||||||
import { OAuth2ProviderInfo } from 'pocketpages-plugin-auth'
|
|
||||||
|
|
||||||
const authPluginFactory: PluginFactory = (config) => {
|
|
||||||
return {
|
|
||||||
name: 'patreon',
|
|
||||||
onExtendContextApi(context: ExtendContextApiContext) {
|
|
||||||
const { api } = context;
|
|
||||||
const pb = () => api.pb() as PocketBase
|
|
||||||
|
|
||||||
|
|
||||||
api.savePatreonData = (
|
|
||||||
authData: AuthModel
|
|
||||||
) => {
|
|
||||||
pb.collection('users')
|
|
||||||
},
|
|
||||||
|
|
||||||
api.getPatreonUser = (
|
|
||||||
authData: AuthModel,
|
|
||||||
) => {
|
|
||||||
|
|
||||||
|
|
||||||
console.log("AYO we got authData as follo");
|
|
||||||
console.log(JSON.stringify(authData, null, 2));
|
|
||||||
|
|
||||||
// make a request to Patreon
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default authPluginFactory;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": [
|
|
||||||
"pocketbase-jsvm"
|
|
||||||
],
|
|
||||||
"strict": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"target": "ES6",
|
|
||||||
"module": "CommonJS",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { defineConfig } from 'tsdown'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
entry: {
|
|
||||||
index: 'src/index.ts',
|
|
||||||
},
|
|
||||||
format: ['cjs'],
|
|
||||||
clean: true,
|
|
||||||
outDir: 'dist',
|
|
||||||
})
|
|
||||||
@ -14,7 +14,7 @@ const env = (() => {
|
|||||||
if (!process.env.MUX_SIGNING_KEY_PRIVATE_KEY) throw new Error('MUX_SIGNING_KEY_PRIVATE_KEY missing in env');
|
if (!process.env.MUX_SIGNING_KEY_PRIVATE_KEY) throw new Error('MUX_SIGNING_KEY_PRIVATE_KEY missing in env');
|
||||||
if (!process.env.PATREON_CREATOR_ACCESS_TOKEN) throw new Error('PATREON_CREATOR_ACCESS_TOKEN missing in env');
|
if (!process.env.PATREON_CREATOR_ACCESS_TOKEN) throw new Error('PATREON_CREATOR_ACCESS_TOKEN missing in env');
|
||||||
if (!process.env.VIBEUI_DIR) throw new Error('VIBEUI_DIR missing in env');
|
if (!process.env.VIBEUI_DIR) throw new Error('VIBEUI_DIR missing in env');
|
||||||
if (!process.env.APP_DIR) throw new Error('APP_DIR missing in env');
|
if (!process.env.WORKER_DIR) throw new Error('WORKER_DIR missing in env');
|
||||||
if (!process.env.AWS_BUCKET) throw new Error('AWS_BUCKET missing in env');
|
if (!process.env.AWS_BUCKET) throw new Error('AWS_BUCKET missing in env');
|
||||||
if (!process.env.AWS_ACCESS_KEY_ID) throw new Error('AWS_ACCESS_KEY_ID missing in env');
|
if (!process.env.AWS_ACCESS_KEY_ID) throw new Error('AWS_ACCESS_KEY_ID missing in env');
|
||||||
if (!process.env.AWS_SECRET_ACCESS_KEY) throw new Error('AWS_SECRET_ACCESS_KEY missing in env');
|
if (!process.env.AWS_SECRET_ACCESS_KEY) throw new Error('AWS_SECRET_ACCESS_KEY missing in env');
|
||||||
@ -33,6 +33,7 @@ const env = (() => {
|
|||||||
if (!process.env.SEEDBOX_SFTP_PORT) throw new Error('SEEDBOX_SFTP_PORT missing in env');
|
if (!process.env.SEEDBOX_SFTP_PORT) throw new Error('SEEDBOX_SFTP_PORT missing in env');
|
||||||
if (!process.env.SEEDBOX_SFTP_USERNAME) throw new Error('SEEDBOX_SFTP_USERNAME missing in env');
|
if (!process.env.SEEDBOX_SFTP_USERNAME) throw new Error('SEEDBOX_SFTP_USERNAME missing in env');
|
||||||
if (!process.env.SEEDBOX_SFTP_PASSWORD) throw new Error('SEEDBOX_SFTP_PASSWORD missing in env');
|
if (!process.env.SEEDBOX_SFTP_PASSWORD) throw new Error('SEEDBOX_SFTP_PASSWORD missing in env');
|
||||||
|
if (!process.env.SEEDBOX_SFTP_KEY_FILE) throw new Error('SEEDBOX_SFTP_KEY_FILE missing in env');
|
||||||
if (!process.env.QBT_HOST) throw new Error('QBT_HOST missing in env');
|
if (!process.env.QBT_HOST) throw new Error('QBT_HOST missing in env');
|
||||||
if (!process.env.QBT_PORT) throw new Error('QBT_PORT missing in env');
|
if (!process.env.QBT_PORT) throw new Error('QBT_PORT missing in env');
|
||||||
if (!process.env.QBT_PASSWORD) throw new Error('QBT_PASSWORD missing in env');
|
if (!process.env.QBT_PASSWORD) throw new Error('QBT_PASSWORD missing in env');
|
||||||
@ -40,6 +41,10 @@ const env = (() => {
|
|||||||
if (!process.env.WORKER_WORKERS) throw new Error('WORKER_WORKERS missing in env');
|
if (!process.env.WORKER_WORKERS) throw new Error('WORKER_WORKERS missing in env');
|
||||||
if (!process.env.BUNNY_ZONE_URL) throw new Error('BUNNY_ZONE_URL missing in env');
|
if (!process.env.BUNNY_ZONE_URL) throw new Error('BUNNY_ZONE_URL missing in env');
|
||||||
if (!process.env.BUNNY_TOKEN_KEY) throw new Error('BUNNY_TOKEN_KEY missing in env');
|
if (!process.env.BUNNY_TOKEN_KEY) throw new Error('BUNNY_TOKEN_KEY missing in env');
|
||||||
|
if (!process.env.TRACKER_SFTP_HOST) throw new Error('TRACKER_SFTP_HOST missing in env');
|
||||||
|
if (!process.env.TRACKER_SFTP_PORT) throw new Error('TRACKER_SFTP_PORT missing in env');
|
||||||
|
if (!process.env.TRACKER_SFTP_USERNAME) throw new Error('TRACKER_SFTP_USERNAME missing in env');
|
||||||
|
if (!process.env.TRACKER_SFTP_KEY_FILE) throw new Error('TRACKER_SFTP_KEY_FILE missing in env');
|
||||||
|
|
||||||
const CACHE_ROOT = process.env?.CACHE_ROOT || paths.cache;
|
const CACHE_ROOT = process.env?.CACHE_ROOT || paths.cache;
|
||||||
|
|
||||||
@ -55,7 +60,7 @@ const env = (() => {
|
|||||||
MUX_SIGNING_KEY_PRIVATE_KEY,
|
MUX_SIGNING_KEY_PRIVATE_KEY,
|
||||||
PATREON_CREATOR_ACCESS_TOKEN,
|
PATREON_CREATOR_ACCESS_TOKEN,
|
||||||
VIBEUI_DIR,
|
VIBEUI_DIR,
|
||||||
APP_DIR,
|
WORKER_DIR,
|
||||||
AWS_BUCKET,
|
AWS_BUCKET,
|
||||||
AWS_ACCESS_KEY_ID,
|
AWS_ACCESS_KEY_ID,
|
||||||
AWS_SECRET_ACCESS_KEY,
|
AWS_SECRET_ACCESS_KEY,
|
||||||
@ -74,6 +79,12 @@ const env = (() => {
|
|||||||
SEEDBOX_SFTP_PORT,
|
SEEDBOX_SFTP_PORT,
|
||||||
SEEDBOX_SFTP_USERNAME,
|
SEEDBOX_SFTP_USERNAME,
|
||||||
SEEDBOX_SFTP_PASSWORD,
|
SEEDBOX_SFTP_PASSWORD,
|
||||||
|
SEEDBOX_SFTP_KEY_FILE,
|
||||||
|
TRACKER_SFTP_HOST,
|
||||||
|
TRACKER_SFTP_PORT,
|
||||||
|
TRACKER_SFTP_USERNAME,
|
||||||
|
TRACKER_SFTP_PASSWORD,
|
||||||
|
TRACKER_SFTP_KEY_FILE,
|
||||||
VALKEY_PORT,
|
VALKEY_PORT,
|
||||||
QBT_HOST,
|
QBT_HOST,
|
||||||
QBT_USERNAME,
|
QBT_USERNAME,
|
||||||
@ -96,7 +107,7 @@ const env = (() => {
|
|||||||
MUX_SIGNING_KEY_PRIVATE_KEY,
|
MUX_SIGNING_KEY_PRIVATE_KEY,
|
||||||
PATREON_CREATOR_ACCESS_TOKEN,
|
PATREON_CREATOR_ACCESS_TOKEN,
|
||||||
VIBEUI_DIR,
|
VIBEUI_DIR,
|
||||||
APP_DIR,
|
WORKER_DIR,
|
||||||
AWS_BUCKET,
|
AWS_BUCKET,
|
||||||
AWS_ACCESS_KEY_ID,
|
AWS_ACCESS_KEY_ID,
|
||||||
AWS_SECRET_ACCESS_KEY,
|
AWS_SECRET_ACCESS_KEY,
|
||||||
@ -115,6 +126,12 @@ const env = (() => {
|
|||||||
SEEDBOX_SFTP_PORT,
|
SEEDBOX_SFTP_PORT,
|
||||||
SEEDBOX_SFTP_USERNAME,
|
SEEDBOX_SFTP_USERNAME,
|
||||||
SEEDBOX_SFTP_PASSWORD,
|
SEEDBOX_SFTP_PASSWORD,
|
||||||
|
SEEDBOX_SFTP_KEY_FILE,
|
||||||
|
TRACKER_SFTP_HOST,
|
||||||
|
TRACKER_SFTP_PORT,
|
||||||
|
TRACKER_SFTP_USERNAME,
|
||||||
|
TRACKER_SFTP_PASSWORD,
|
||||||
|
TRACKER_SFTP_KEY_FILE,
|
||||||
VALKEY_PORT,
|
VALKEY_PORT,
|
||||||
QBT_HOST,
|
QBT_HOST,
|
||||||
QBT_USERNAME,
|
QBT_USERNAME,
|
||||||
|
|||||||
102
services/worker/package-lock.json
generated
102
services/worker/package-lock.json
generated
@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "worker",
|
"name": "worker",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "worker",
|
"name": "worker",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/express": "^6.14.0",
|
"@bull-board/express": "^6.14.0",
|
||||||
"@mux/mux-node": "^12.8.0",
|
"@mux/mux-node": "^12.8.0",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/magnet-uri": "^5.1.5",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/ssh2": "^1.15.5",
|
"@types/ssh2": "^1.15.5",
|
||||||
@ -20,6 +22,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"fs-extra": "^11.3.2",
|
"fs-extra": "^11.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"magnet-uri": "^7.0.7",
|
||||||
"nano-spawn": "^2.0.0",
|
"nano-spawn": "^2.0.0",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"onnxruntime-web": "^1.23.2",
|
"onnxruntime-web": "^1.23.2",
|
||||||
@ -1586,6 +1589,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@thaunknown/thirty-two": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@thaunknown/thirty-two/-/thirty-two-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-Q53KyCXweV1CS62EfqtPDqfpksn5keQ59PGqzzkK+g8Vif1jB4inoBCcs/BUSdsqddhE3G+2Fn+4RX3S6RqT0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uint8-util": "^2.2.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.2.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tootallnate/quickjs-emscripten": {
|
"node_modules/@tootallnate/quickjs-emscripten": {
|
||||||
"version": "0.23.0",
|
"version": "0.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
|
||||||
@ -1668,6 +1683,16 @@
|
|||||||
"@types/send": "*"
|
"@types/send": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/fs-extra": {
|
||||||
|
"version": "11.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
|
||||||
|
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jsonfile": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/http-errors": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||||
@ -1680,6 +1705,24 @@
|
|||||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonfile": {
|
||||||
|
"version": "6.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
|
||||||
|
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/magnet-uri": {
|
||||||
|
"version": "5.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/magnet-uri/-/magnet-uri-5.1.5.tgz",
|
||||||
|
"integrity": "sha512-SbBjlb1KGe38VfjRR+mwqztJd/4skhdKkRbIzPDhTy7IAeEAPZWIVSEkZw00Qr4ZZOGR3/ATJ20WWPBfrKHGdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@ -2191,6 +2234,15 @@
|
|||||||
"bare-path": "^3.0.0"
|
"bare-path": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/basic-ftp": {
|
"node_modules/basic-ftp": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||||
@ -2209,6 +2261,15 @@
|
|||||||
"tweetnacl": "^0.14.3"
|
"tweetnacl": "^0.14.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bep53-range": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bep53-range/-/bep53-range-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-sMm2sV5PRs0YOVk0LTKtjuIprVzxgTQUsrGX/7Yph2Rm4FO2Fqqtq7hNjsOB5xezM4v4+5rljCgK++UeQJZguA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||||
@ -3754,6 +3815,34 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/magnet-uri": {
|
||||||
|
"version": "7.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/magnet-uri/-/magnet-uri-7.0.7.tgz",
|
||||||
|
"integrity": "sha512-z/+dB2NQsXaDuxVBjoPLpZT8ePaacUmoontoFheRBl++nALHYs4qV9MmhTur9e4SaMbkCR/uPX43UMzEOoeyaw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@thaunknown/thirty-two": "^1.0.5",
|
||||||
|
"bep53-range": "^2.0.0",
|
||||||
|
"uint8-util": "^2.2.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -5274,6 +5363,15 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uint8-util": {
|
||||||
|
"version": "2.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/uint8-util/-/uint8-util-2.2.5.tgz",
|
||||||
|
"integrity": "sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
|||||||
@ -11,7 +11,9 @@
|
|||||||
"@bull-board/express": "^6.14.0",
|
"@bull-board/express": "^6.14.0",
|
||||||
"@mux/mux-node": "^12.8.0",
|
"@mux/mux-node": "^12.8.0",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/magnet-uri": "^5.1.5",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/ssh2": "^1.15.5",
|
"@types/ssh2": "^1.15.5",
|
||||||
@ -20,6 +22,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"fs-extra": "^11.3.2",
|
"fs-extra": "^11.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"magnet-uri": "^7.0.7",
|
||||||
"nano-spawn": "^2.0.0",
|
"nano-spawn": "^2.0.0",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"onnxruntime-web": "^1.23.2",
|
"onnxruntime-web": "^1.23.2",
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import env from '../.config/env.ts';
|
||||||
|
import { version } from '../package.json';
|
||||||
import { createBullBoard } from '@bull-board/api';
|
import { createBullBoard } from '@bull-board/api';
|
||||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||||
import { ExpressAdapter } from '@bull-board/express';
|
import { ExpressAdapter } from '@bull-board/express';
|
||||||
@ -5,13 +7,12 @@ import express, { type Request, type Response } from 'express';
|
|||||||
import { generalQueue } from './queues/generalQueue.ts';
|
import { generalQueue } from './queues/generalQueue.ts';
|
||||||
import { gpuQueue } from './queues/gpuQueue.ts';
|
import { gpuQueue } from './queues/gpuQueue.ts';
|
||||||
import { highPriorityQueue } from './queues/highPriorityQueue.ts';
|
import { highPriorityQueue } from './queues/highPriorityQueue.ts';
|
||||||
import env from '../.config/env.ts';
|
|
||||||
import { version } from '../package.json';
|
|
||||||
import { downloadQueue } from './queues/downloadQueue.ts';
|
import { downloadQueue } from './queues/downloadQueue.ts';
|
||||||
import { cacheQueue } from './queues/cacheQueue.ts';
|
import { cacheQueue } from './queues/cacheQueue.ts';
|
||||||
import { muxQueue } from './queues/muxQueue.ts';
|
import { muxQueue } from './queues/muxQueue.ts';
|
||||||
import { b2Queue } from './queues/b2Queue.ts';
|
import { b2Queue } from './queues/b2Queue.ts';
|
||||||
|
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
|
|
||||||
|
|
||||||
@ -77,6 +78,9 @@ const run = async () => {
|
|||||||
<li><a href="/task?name=createTorrent&vodId=1234">Task: createTorrent</a></li>
|
<li><a href="/task?name=createTorrent&vodId=1234">Task: createTorrent</a></li>
|
||||||
<li><a href="/task?name=createMuxAsset&vodId=">Task: createMuxAsset</a></li>
|
<li><a href="/task?name=createMuxAsset&vodId=">Task: createMuxAsset</a></li>
|
||||||
<li><a href="/task?name=cacheGet&vodId=1234">Task: cacheGet</a></li>
|
<li><a href="/task?name=cacheGet&vodId=1234">Task: cacheGet</a></li>
|
||||||
|
<li><a href="/task?name=updateTrackerWhitelist">Task: updateTrackerWhitelist</a></li>
|
||||||
|
<li><a href="/task?name=createFunscript&vodId=1234">Task: createFunscript</a></li>
|
||||||
|
<li><a href="/task?name=createHlsPlaylist&vodId=1234">Task: createHlsPlaylist</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -103,6 +107,15 @@ const run = async () => {
|
|||||||
case 'download':
|
case 'download':
|
||||||
await downloadQueue.add(name, data);
|
await downloadQueue.add(name, data);
|
||||||
break;
|
break;
|
||||||
|
case 'createFunscript':
|
||||||
|
await gpuQueue.add(name, data);
|
||||||
|
break;
|
||||||
|
case 'createHlsPlaylist':
|
||||||
|
await gpuQueue.add(name, data);
|
||||||
|
break;
|
||||||
|
case 'updateTrackerWhitelist':
|
||||||
|
await highPriorityQueue.add(name, data);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
await highPriorityQueue.add(name, data);
|
await highPriorityQueue.add(name, data);
|
||||||
break;
|
break;
|
||||||
|
|||||||
16
services/worker/src/processors/announceTorrent.spec.ts
Normal file
16
services/worker/src/processors/announceTorrent.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { test, expect, describe, vi } from 'vitest';
|
||||||
|
import { decode } from 'magnet-uri';
|
||||||
|
|
||||||
|
|
||||||
|
describe('announceTorrent integration', () => {
|
||||||
|
test("magnet-uri", () => {
|
||||||
|
const fixture = 'magnet:?xt=urn:btih:ca2b3ba127c17e689cc50b0f2190d0dd4b40d981&xt=urn:btmh:12207892c2345ea1e0460dc642219b76bc60f303569b6f9dbc911d1af78dd7301f3d&dn=2025-11-16-undefined-jwz52dszvnagplf.mp4&xl=13629592283&tr=udp%3A%2F%2Ftracker.future.porn%3A6969%2Fannounce';
|
||||||
|
const parsed = decode(fixture);
|
||||||
|
console.log(parsed)
|
||||||
|
expect(parsed).toHaveProperty('infoHash', 'ca2b3ba127c17e689cc50b0f2190d0dd4b40d981');
|
||||||
|
expect(parsed).toHaveProperty('infoHashV2', '7892c2345ea1e0460dc642219b76bc60f303569b6f9dbc911d1af78dd7301f3d');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
56
services/worker/src/processors/announceTorrent.ts
Normal file
56
services/worker/src/processors/announceTorrent.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Job } from "bullmq";
|
||||||
|
import { getPocketBaseClient } from "../util/pocketbase";
|
||||||
|
import { Vod } from "../types";
|
||||||
|
import { Client, ConnectConfig, SFTPWrapper } from 'ssh2';
|
||||||
|
import { opentrackerSSHClient } from "../util/sftp";
|
||||||
|
import magnet from 'magnet-uri';
|
||||||
|
|
||||||
|
interface Payload {
|
||||||
|
vodId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function assertPayload(payload: any): asserts payload is Payload {
|
||||||
|
if (typeof payload !== "object" || !payload) throw new Error("invalid payload-- was not an object.");
|
||||||
|
if (typeof payload.vodId !== "string") throw new Error("invalid payload-- was missing vodId");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* announceTorrent
|
||||||
|
*
|
||||||
|
* We use ssh to write a whitelist.txt file to our opentracker server
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @todo we aren't using this yet. Right now we are only bulk updating the whitelist at the top of the hour.
|
||||||
|
* As we get more torrents, we will want to implement announcing a single torrent by patching the whitelist using this method
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default async function announceTorrent(job: Job) {
|
||||||
|
|
||||||
|
throw new Error('@todo please implement');
|
||||||
|
|
||||||
|
assertPayload(job.data);
|
||||||
|
const pb = await getPocketBaseClient();
|
||||||
|
|
||||||
|
const vodId = job.data.vodId;
|
||||||
|
|
||||||
|
const vod = await pb.collection('vods').getOne(vodId);
|
||||||
|
|
||||||
|
const magnetLink = vod.magnetLink
|
||||||
|
if (!magnetLink) throw new Error(`vod ${vodId} has no magnetLink!`);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const parsed = magnet.decode(magnetLink)
|
||||||
|
job.log('dn=' + parsed.dn) // "Leaves of Grass by Walt Whitman.epub"
|
||||||
|
job.log('infoHash=' + parsed.infoHash) // "d2474e86c95b19b8bcfdb92bc12c9d44667cfa36"
|
||||||
|
|
||||||
|
const whitelist = ''
|
||||||
|
const localWhitelistPath = '@todo';
|
||||||
|
const remoteWhitelistPath = '/etc/opentracker/whitelist.txt';
|
||||||
|
await opentrackerSSHClient.uploadFileAs(localWhitelistPath, remoteWhitelistPath);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,43 +2,72 @@ import { Job } from "bullmq";
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import env from "../../.config/env";
|
import env from "../../.config/env";
|
||||||
|
import { subDays } from "date-fns";
|
||||||
|
|
||||||
const retainmentDayCount = 2;
|
const retainmentDayCount = 365;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* cacheCleanup
|
* Recursively delete files older than retainmentDayCount days
|
||||||
*
|
|
||||||
* Deletes files in the cache directory that are older than retainmentDayCount days
|
|
||||||
*/
|
*/
|
||||||
export default async function cacheCleanup(job: Job) {
|
export default async function cacheCleanup(job: Job) {
|
||||||
const cacheDir = join(env.CACHE_ROOT, 'worker');
|
const cacheDir = join(env.CACHE_ROOT, 'worker');
|
||||||
let cleanedCount = 0;
|
let cleanedCount = 0;
|
||||||
|
|
||||||
try {
|
const cutoffDate = subDays(new Date(), retainmentDayCount);
|
||||||
// read all files in the cache directory
|
const cutoffMs = cutoffDate.getTime();
|
||||||
const files = await fs.readdir(cacheDir);
|
|
||||||
|
|
||||||
const now = Date.now();
|
job.log(`Starting recursive cache cleanup in directory: ${cacheDir}`);
|
||||||
const retainMs = retainmentDayCount * 24 * 60 * 60 * 1000; // days → ms
|
job.log(`Deleting files older than: ${cutoffDate.toISOString()} (${cutoffMs} ms)`);
|
||||||
|
|
||||||
|
async function cleanDir(dir: string) {
|
||||||
|
let files: string[];
|
||||||
|
try {
|
||||||
|
files = await fs.readdir(dir);
|
||||||
|
} catch (err) {
|
||||||
|
job.log(`Failed to read directory ${dir}: ${(err as Error).message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = join(cacheDir, file);
|
const filePath = join(dir, file);
|
||||||
try {
|
try {
|
||||||
const stat = await fs.stat(filePath);
|
const stat = await fs.stat(filePath);
|
||||||
// only delete files older than retainment
|
|
||||||
if (now - stat.mtimeMs > retainMs) {
|
if (stat.isDirectory()) {
|
||||||
await fs.unlink(filePath);
|
job.log(`Entering directory: ${filePath}`);
|
||||||
cleanedCount++;
|
await cleanDir(filePath); // recurse into subdirectory
|
||||||
|
} else {
|
||||||
|
job.log(`Checking file: ${filePath}, last access: ${new Date(stat.atimeMs).toISOString()}`);
|
||||||
|
if (stat.atimeMs < cutoffMs) {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
cleanedCount++;
|
||||||
|
job.log(`Deleted file: ${filePath}`);
|
||||||
|
} else {
|
||||||
|
job.log(`Keeping file: ${filePath}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// skip errors per-file, but log them
|
job.log(`Failed to check/delete ${filePath}: ${(err as Error).message}`);
|
||||||
job.log(`failed to check/delete ${file}: ${(err as Error).message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
job.log(`cleaned ${cleanedCount} stale cache files`);
|
// Optionally remove empty directories
|
||||||
|
try {
|
||||||
|
const remaining = await fs.readdir(dir);
|
||||||
|
if (remaining.length === 0 && dir !== cacheDir) {
|
||||||
|
await fs.rmdir(dir);
|
||||||
|
job.log(`Deleted empty directory: ${dir}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
job.log(`Failed to delete directory ${dir}: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cleanDir(cacheDir);
|
||||||
|
job.log(`Cache cleanup complete. Deleted ${cleanedCount} files`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
job.log(`cache cleanup failed: ${(err as Error).message}`);
|
job.log(`Cache cleanup failed: ${(err as Error).message}`);
|
||||||
throw err; // allow BullMQ to handle retry/failure
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +1,104 @@
|
|||||||
import { getOrDownloadAsset } from "../util/cache.ts";
|
import env from "../../.config/env.ts";
|
||||||
import env from "../../env.ts";
|
|
||||||
import { getS3Client, uploadFile } from "../util/s3.ts";
|
|
||||||
import { inference } from "../util/vibeui.ts";
|
import { inference } from "../util/vibeui.ts";
|
||||||
import { buildFunscript } from "../util/funscripts.ts";
|
import { buildFunscript } from "../util/funscripts.ts";
|
||||||
import { generateS3Path } from "../util/formatters.ts";
|
import { generateS3Path } from "../util/formatters.ts";
|
||||||
import { type Job } from "bullmq";
|
import { cacheQueue, cacheQueueEvents } from "../queues/cacheQueue.ts";
|
||||||
import { getPocketBaseClient } from '../util/pocketbase';
|
import { Job } from "bullmq";
|
||||||
|
import { basename } from "node:path";
|
||||||
|
import { getPocketBaseClient } from "../util/pocketbase";
|
||||||
|
import Client, { RecordModel } from "pocketbase";
|
||||||
|
import { Vod } from "../types";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { getS3KeyTarget } from '../util/b2.ts';
|
||||||
|
import spawn from "nano-spawn";
|
||||||
|
|
||||||
interface Payload {
|
interface Payload {
|
||||||
vodId: string;
|
vodId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Funscripts { vibratePath: string, thrustPath: string }
|
||||||
|
|
||||||
|
|
||||||
function assertPayload(payload: any): asserts payload is Payload {
|
function assertPayload(payload: any): asserts payload is Payload {
|
||||||
if (typeof payload !== "object" || !payload)
|
if (typeof payload !== "object" || !payload) throw new Error("invalid payload-- was not an object.");
|
||||||
throw new Error("invalid payload-- was not an object.");
|
if (typeof payload.vodId !== "string") throw new Error("invalid payload-- was missing vodId");
|
||||||
if (typeof payload.vodId !== "string")
|
|
||||||
throw new Error("invalid payload-- was missing vodId");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVod(vodId: string) {
|
|
||||||
// return prisma.vod.findFirstOrThrow({
|
|
||||||
// where: { id: vodId },
|
/**
|
||||||
// include: { vtubers: true },
|
* createFunscript
|
||||||
// });
|
*/
|
||||||
// funscriptVibrate: funscriptKeys.vibrateKey,
|
export async function createFunscript(job: Job) {
|
||||||
// funscriptThrust: funscriptKeys.thrustKey,
|
assertPayload(job.data);
|
||||||
|
const vodId = job.data.vodId;
|
||||||
const pb = await getPocketBaseClient();
|
const pb = await getPocketBaseClient();
|
||||||
const vod = await pb.collection('vods').getOne(vodId, {
|
const vod = await pb.collection('vods').getOne(vodId, { expand: 'vtubers' });
|
||||||
expand: 'vtubers'
|
|
||||||
});
|
job.log(`createFunscript running on vodId ${vodId}`);
|
||||||
return vod;
|
|
||||||
|
ensureVodReady(job, vod);
|
||||||
|
|
||||||
|
const cachePath = await getVodFromCache(job, vodId);
|
||||||
|
|
||||||
|
job.log(`Running inference on video...`);
|
||||||
|
const predictionOutputPath = await inference(cachePath);
|
||||||
|
|
||||||
|
const funscripts = await buildFunscripts(
|
||||||
|
job,
|
||||||
|
predictionOutputPath,
|
||||||
|
cachePath
|
||||||
|
);
|
||||||
|
|
||||||
|
await uploadFunscripts(job, pb, vod, funscripts);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureVodReady(vod: Awaited<ReturnType<typeof getVod>>) {
|
async function uploadFunscript(job: Job, vod: RecordModel, assetPath: string): Promise<string> {
|
||||||
|
if (!job) throw new Error('job missing');
|
||||||
|
if (!vod) throw new Error('vod missing');
|
||||||
|
if (!assetPath) throw new Error('vod missing');
|
||||||
|
const s3KeyTarget = getS3KeyTarget(vod.collectionId, vod.id, assetPath, null);
|
||||||
|
job.log(`uploading funscript ${assetPath} to ${s3KeyTarget}`);
|
||||||
|
await spawn('b2', ['file', 'upload', env.AWS_BUCKET, assetPath, s3KeyTarget]);
|
||||||
|
return s3KeyTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* uploadFunscripts
|
||||||
|
*
|
||||||
|
* Update the record in the database
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function uploadFunscripts(job: Job, pb: Client, vod: RecordModel, funscripts: Funscripts) {
|
||||||
|
const { vibratePath, thrustPath } = funscripts;
|
||||||
|
const funscriptVibrate = await uploadFunscript(job, vod, vibratePath);
|
||||||
|
const funscriptThrust = await uploadFunscript(job, vod, thrustPath);
|
||||||
|
const data = {
|
||||||
|
funscriptVibrate,
|
||||||
|
funscriptThrust,
|
||||||
|
}
|
||||||
|
await pb.collection('vods').update(vod.id, data);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVodFromCache(job: Job, vodId: string): Promise<string> {
|
||||||
|
|
||||||
|
job.log('getting vod from cache...');
|
||||||
|
const cacheGetJob = await cacheQueue.add(
|
||||||
|
'cacheGet',
|
||||||
|
{ vodId },
|
||||||
|
{ jobId: `cacheGet-${vodId}` }
|
||||||
|
);
|
||||||
|
|
||||||
|
job.log('waiting up to 5 hours for download to finish...');
|
||||||
|
const result = (await cacheGetJob.waitUntilFinished(cacheQueueEvents, 1000 * 60 * 60 * 5));
|
||||||
|
return result.cachePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ensureVodReady(job: Job, vod: RecordModel) {
|
||||||
if (vod.funscriptVibrate && vod.funscriptThrust) {
|
if (vod.funscriptVibrate && vod.funscriptThrust) {
|
||||||
job.log(`Doing nothing-- vod ${vod.id} already has funscripts.`);
|
job.log(`Doing nothing-- vod ${vod.id} already has funscripts.`);
|
||||||
return false;
|
return false;
|
||||||
@ -48,28 +113,16 @@ function ensureVodReady(vod: Awaited<ReturnType<typeof getVod>>) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadVideo(sourceVideo: string) {
|
|
||||||
const s3Client = getS3Client();
|
|
||||||
const videoFilePath = await getOrDownloadAsset(
|
|
||||||
s3Client,
|
|
||||||
env.S3_BUCKET,
|
|
||||||
sourceVideo
|
|
||||||
);
|
|
||||||
job.log(`Downloaded video to ${videoFilePath}`);
|
|
||||||
return { s3Client, videoFilePath };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runInference(videoFilePath: string) {
|
async function runInference(videoFilePath: string) {
|
||||||
job.log(`Running inference on video...`);
|
|
||||||
const predictionOutputPath = await inference(videoFilePath);
|
const predictionOutputPath = await inference(videoFilePath);
|
||||||
job.log(`Prediction output at ${predictionOutputPath}`);
|
|
||||||
return predictionOutputPath;
|
return predictionOutputPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildFunscripts(
|
async function buildFunscripts(
|
||||||
|
job: Job,
|
||||||
predictionOutputPath: string,
|
predictionOutputPath: string,
|
||||||
videoFilePath: string
|
videoFilePath: string
|
||||||
) {
|
): Promise<Funscripts> {
|
||||||
job.log(`Building funscripts...`);
|
job.log(`Building funscripts...`);
|
||||||
const vibratePath = await buildFunscript(
|
const vibratePath = await buildFunscript(
|
||||||
predictionOutputPath,
|
predictionOutputPath,
|
||||||
@ -87,95 +140,97 @@ async function buildFunscripts(
|
|||||||
return { vibratePath, thrustPath };
|
return { vibratePath, thrustPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFunscripts(
|
// async function uploadFunscripts(
|
||||||
s3Client: ReturnType<typeof getS3Client>,
|
// job: Job,
|
||||||
slug: string,
|
// s3Client: ReturnType<typeof getS3Client>,
|
||||||
streamDate: Date,
|
// slug: string,
|
||||||
vodId: string,
|
// streamDate: Date,
|
||||||
funscripts: { vibratePath: string; thrustPath: string }
|
// vodId: string,
|
||||||
) {
|
// funscripts: { vibratePath: string; thrustPath: string }
|
||||||
const vibrateKey = generateS3Path(
|
// ) {
|
||||||
slug,
|
// const vibrateKey = generateS3Path(
|
||||||
streamDate,
|
// slug,
|
||||||
vodId,
|
// streamDate,
|
||||||
`funscripts/vibrate.funscript`
|
// vodId,
|
||||||
);
|
// `funscripts/vibrate.funscript`
|
||||||
const vibrateUrl = await uploadFile(
|
// );
|
||||||
s3Client,
|
// const vibrateUrl = await uploadFile(
|
||||||
env.S3_BUCKET,
|
// s3Client,
|
||||||
vibrateKey,
|
// env.S3_BUCKET,
|
||||||
funscripts.vibratePath,
|
// vibrateKey,
|
||||||
"application/json"
|
// funscripts.vibratePath,
|
||||||
);
|
// "application/json"
|
||||||
|
// );
|
||||||
|
|
||||||
const thrustKey = generateS3Path(
|
// const thrustKey = generateS3Path(
|
||||||
slug,
|
// slug,
|
||||||
streamDate,
|
// streamDate,
|
||||||
vodId,
|
// vodId,
|
||||||
`funscripts/thrust.funscript`
|
// `funscripts/thrust.funscript`
|
||||||
);
|
// );
|
||||||
const thrustUrl = await uploadFile(
|
// const thrustUrl = await uploadFile(
|
||||||
s3Client,
|
// s3Client,
|
||||||
env.S3_BUCKET,
|
// env.S3_BUCKET,
|
||||||
thrustKey,
|
// thrustKey,
|
||||||
funscripts.thrustPath,
|
// funscripts.thrustPath,
|
||||||
"application/json"
|
// "application/json"
|
||||||
);
|
// );
|
||||||
|
|
||||||
job.log(`Uploaded funscriptVibrate to S3: ${vibrateUrl}`);
|
// job.log(`Uploaded funscriptVibrate to S3: ${vibrateUrl}`);
|
||||||
job.log(`Uploaded funscriptThrust to S3: ${thrustUrl}`);
|
// job.log(`Uploaded funscriptThrust to S3: ${thrustUrl}`);
|
||||||
|
|
||||||
return { vibrateKey, thrustKey };
|
// return { vibrateKey, thrustKey };
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function saveToDatabase(
|
// async function saveToDatabase(
|
||||||
vodId: string,
|
// job: Job,
|
||||||
funscriptKeys: { vibrateKey: string; thrustKey: string }
|
// vodId: string,
|
||||||
) {
|
// funscriptKeys: { vibrateKey: string; thrustKey: string }
|
||||||
const pb = await getPocketBaseClient();
|
// ) {
|
||||||
|
// const pb = await getPocketBaseClient();
|
||||||
|
|
||||||
await pb.collection('users').update(vodId, {
|
// await pb.collection('users').update(vodId, {
|
||||||
funscriptVibrate: funscriptKeys.vibrateKey,
|
// funscriptVibrate: funscriptKeys.vibrateKey,
|
||||||
funscriptThrust: funscriptKeys.thrustKey,
|
// funscriptThrust: funscriptKeys.thrustKey,
|
||||||
});
|
// });
|
||||||
|
|
||||||
job.log(`Funscripts saved to database for vod ${vodId}`);
|
// job.log(`Funscripts saved to database for vod ${vodId}`);
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
export async function createFunscriptTask(job: Job) {
|
// export async function createFunscriptTask(job: Job) {
|
||||||
|
|
||||||
const pb = await getPocketBaseClient();
|
// const pb = await getPocketBaseClient();
|
||||||
|
|
||||||
job.log(`the job '${job.name}' is running`);
|
// job.log(`the job '${job.name}' is running`);
|
||||||
|
|
||||||
const { vodId } = job.data.vodId;
|
// const { vodId } = job.data.vodId;
|
||||||
job.log(`createFunscript called with vodId=${vodId}`);
|
// job.log(`createFunscript called with vodId=${vodId}`);
|
||||||
|
|
||||||
const vod = await getVod(vodId);
|
// const vod = await getVod(vodId);
|
||||||
if (!ensureVodReady(vod)) return;
|
// if (!ensureVodReady(vod)) return;
|
||||||
|
|
||||||
const { s3Client, videoFilePath } = await downloadVideo(vod.sourceVideo!);
|
// const { s3Client, videoFilePath } = await downloadVideo(vod.sourceVideo!);
|
||||||
const predictionOutputPath = await runInference(videoFilePath);
|
// const predictionOutputPath = await runInference(videoFilePath);
|
||||||
|
|
||||||
const slug = vod.vtubers[0].slug;
|
// const slug = vod.vtubers[0].slug;
|
||||||
if (!slug)
|
// if (!slug)
|
||||||
throw new Error(`vod.vtubers[0].slug for vod ${vod.id} was falsy.`);
|
// throw new Error(`vod.vtubers[0].slug for vod ${vod.id} was falsy.`);
|
||||||
|
|
||||||
const funscripts = await buildFunscripts(
|
// const funscripts = await buildFunscripts(
|
||||||
predictionOutputPath,
|
// predictionOutputPath,
|
||||||
videoFilePath
|
// videoFilePath
|
||||||
);
|
// );
|
||||||
|
|
||||||
const funscriptKeys = await uploadFunscripts(
|
// const funscriptKeys = await uploadFunscripts(
|
||||||
s3Client,
|
// s3Client,
|
||||||
slug,
|
// slug,
|
||||||
vod.streamDate,
|
// vod.streamDate,
|
||||||
vod.id,
|
// vod.id,
|
||||||
funscripts
|
// funscripts
|
||||||
);
|
// );
|
||||||
|
|
||||||
await saveToDatabase(vodId, funscriptKeys);
|
// await saveToDatabase(vodId, funscriptKeys);
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +1,23 @@
|
|||||||
import type { Task, Helpers } from "graphile-worker";
|
|
||||||
import { PrismaClient } from "../../generated/prisma";
|
import env from "../../.config/env.ts";
|
||||||
import { withAccelerate } from "@prisma/extension-accelerate";
|
|
||||||
import { getOrDownloadAsset } from "../utils/cache";
|
|
||||||
import { env } from "../config/env";
|
|
||||||
import { S3Client } from "@aws-sdk/client-s3";
|
|
||||||
import { getS3Client, uploadFile } from "../utils/s3";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { basename, join, dirname } from "node:path";
|
import { basename, join } from "node:path";
|
||||||
import { mkdirp } from "fs-extra";
|
import { mkdirp } from "fs-extra";
|
||||||
import { listFilesRecursive } from "../utils/filesystem";
|
import { listFilesRecursive } from "../util/fsExtra.ts";
|
||||||
import { getMimeType } from "../utils/mimetype";
|
import spawn from "nano-spawn";
|
||||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
import { Job } from "bullmq";
|
||||||
import logger from "../utils/logger";
|
import { getSourceVideo } from "../util/cache.ts";
|
||||||
|
import { getPocketBaseClient } from "../util/pocketbase";
|
||||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
import PocketBase, { RecordModel } from 'pocketbase';
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { getS3KeyTarget } from "../util/b2.ts";
|
||||||
|
|
||||||
interface Payload {
|
interface Payload {
|
||||||
vodId: string;
|
vodId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface HlsVariant {
|
interface HlsVariant {
|
||||||
videoPath: string;
|
videoPath: string;
|
||||||
resolution: string; // e.g. "1920x1080"
|
resolution: string; // e.g. "1920x1080"
|
||||||
@ -29,31 +26,11 @@ interface HlsVariant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// // Create a fragmented MP4 for use with fMP4 HLS
|
|
||||||
// async function createFragmentedMp4(helpers: Helpers, inputFilePath: string) {
|
|
||||||
|
|
||||||
// const outputFilePath = join(env.CACHE_ROOT, `${nanoid()}.mp4`)
|
export async function createVariants(job: Job, inputFilePath: string): Promise<HlsVariant[]> {
|
||||||
// await spawn('ffmpeg', [
|
|
||||||
// '-i', inputFilePath,
|
|
||||||
// '-c', 'copy',
|
|
||||||
// '-f', 'mp4',
|
|
||||||
// '-movflags', 'frag_keyframe+empty_moov',
|
|
||||||
// outputFilePath
|
|
||||||
// ], {
|
|
||||||
// stdout: 'inherit',
|
|
||||||
// stderr: 'inherit',
|
|
||||||
// })
|
|
||||||
// return outputFilePath
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function createVariants(helpers: Helpers, inputFilePath: string): Promise<HlsVariant[]> {
|
|
||||||
const workdir = join(env.CACHE_ROOT, nanoid());
|
const workdir = join(env.CACHE_ROOT, nanoid());
|
||||||
await mkdirp(workdir);
|
await mkdirp(workdir);
|
||||||
const baseName = basename(inputFilePath, '.mp4');
|
const baseName = basename(inputFilePath, '.mp4');
|
||||||
const spawn = await getNanoSpawn()
|
|
||||||
|
|
||||||
const resolutions = [
|
const resolutions = [
|
||||||
{ width: 1920, height: 1080, bitrate: 4000000, name: '1080p' }, // 4Mbps
|
{ width: 1920, height: 1080, bitrate: 4000000, name: '1080p' }, // 4Mbps
|
||||||
@ -96,13 +73,14 @@ export async function createVariants(helpers: Helpers, inputFilePath: string): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function packageHls(
|
export async function packageHls(
|
||||||
helpers: Helpers,
|
job: Job,
|
||||||
variants: HlsVariant[],
|
variants: HlsVariant[],
|
||||||
outputDir: string
|
outputDir: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
const spawn = await getNanoSpawn();
|
job.log(`packageHls called with ${variants.length} variants. ${Object.entries(variants).map(([i, variant]) => `${i}: ${variant.videoPath} ${variant.resolution}`).join(', ')}`);
|
||||||
|
|
||||||
// Sort by bandwidth (highest first)
|
// Sort by bandwidth (highest first)
|
||||||
variants.sort((a, b) => b.bandwidth - a.bandwidth);
|
variants.sort((a, b) => b.bandwidth - a.bandwidth);
|
||||||
|
|
||||||
@ -130,15 +108,33 @@ export async function packageHls(
|
|||||||
args.push("--generate_static_live_mpd");
|
args.push("--generate_static_live_mpd");
|
||||||
args.push("--segment_duration=2");
|
args.push("--segment_duration=2");
|
||||||
|
|
||||||
await spawn("packager", args, {
|
await spawn("packager", args);
|
||||||
stdout: "inherit",
|
|
||||||
stderr: "inherit",
|
|
||||||
});
|
|
||||||
|
|
||||||
return masterPlaylist;
|
return masterPlaylist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* uploadHlsAsset
|
||||||
|
*
|
||||||
|
* Save the playlist .m3u8 to S3.
|
||||||
|
* Then update the vod record in Pocketbase with the s3 key target path.
|
||||||
|
*
|
||||||
|
* example: `pbc_144770472/j8cgd0qqjbzyfi3/hls/master_j8cgd0qqjbzyfi3.m3u8`
|
||||||
|
*
|
||||||
|
* these files are later made available to users via our `/api/hls` route
|
||||||
|
* which creates 302 redirects with BunnyCDN token signed URLs to the CDN asssets
|
||||||
|
*/
|
||||||
|
export async function uploadHlsAsset(job: Job, pb: PocketBase, vod: RecordModel, assetPath: string): Promise<void> {
|
||||||
|
const vodId = vod.id;
|
||||||
|
const s3KeyTarget = getS3KeyTarget(vod.collectionId, vodId, assetPath, 'hls');
|
||||||
|
job.log(`uploading hls asset ${assetPath} to ${s3KeyTarget}`);
|
||||||
|
await spawn('b2', ['file', 'upload', env.AWS_BUCKET, assetPath, s3KeyTarget]);
|
||||||
|
|
||||||
|
await pb.collection('vods').update(vodId, { hlsPlaylist: s3KeyTarget });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function assertPayload(payload: any): asserts payload is Payload {
|
function assertPayload(payload: any): asserts payload is Payload {
|
||||||
@ -147,19 +143,19 @@ function assertPayload(payload: any): asserts payload is Payload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default async function createHlsPlaylist(payload: any, helpers: Helpers) {
|
|
||||||
assertPayload(payload)
|
|
||||||
const { vodId } = payload
|
export async function createHlsPlaylist(job: Job) {
|
||||||
const vod = await prisma.vod.findFirstOrThrow({
|
assertPayload(job.data)
|
||||||
where: {
|
const { vodId } = job.data;
|
||||||
id: vodId
|
|
||||||
}
|
const pb = await getPocketBaseClient();
|
||||||
})
|
const vod = await pb.collection('vods').getOne(vodId);
|
||||||
// * [x] load vod
|
|
||||||
|
|
||||||
// * [x] exit if video.hlsPlaylist already defined
|
// * [x] exit if video.hlsPlaylist already defined
|
||||||
if (vod.hlsPlaylist) {
|
if (vod.hlsPlaylist) {
|
||||||
logger.info(`Doing nothing-- vod ${vodId} already has a hlsPlaylist.`)
|
job.log(`Doing nothing-- vod ${vodId} already has a hlsPlaylist.`)
|
||||||
return; // Exit the function early
|
return; // Exit the function early
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,54 +163,46 @@ export default async function createHlsPlaylist(payload: any, helpers: Helpers)
|
|||||||
throw new Error(`Failed to create hlsPlaylist-- vod ${vodId} is missing a sourceVideo.`);
|
throw new Error(`Failed to create hlsPlaylist-- vod ${vodId} is missing a sourceVideo.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Creating HLS Playlist.`)
|
job.log(`Creating HLS Playlist.`)
|
||||||
const s3Client = getS3Client()
|
|
||||||
const taskId = nanoid()
|
const taskId = nanoid()
|
||||||
const workDirPath = join(env.CACHE_ROOT, taskId)
|
const workDirPath = join(env.CACHE_ROOT, 'worker', taskId)
|
||||||
const packageDirPath = join(workDirPath, 'package', 'hls')
|
const packageDirPath = join(workDirPath, 'package', 'hls')
|
||||||
await mkdirp(packageDirPath)
|
await mkdirp(packageDirPath)
|
||||||
|
|
||||||
logger.info("download source video from pull-thru cache")
|
job.log("download source video from pull-thru cache")
|
||||||
const videoFilePath = await getOrDownloadAsset(s3Client, env.S3_BUCKET, vod.sourceVideo)
|
const videoFilePath = await getSourceVideo(job);
|
||||||
logger.info(`videoFilePath=${videoFilePath}`)
|
|
||||||
|
|
||||||
logger.info("create ABR variants")
|
|
||||||
const variants = await createVariants(helpers, videoFilePath)
|
|
||||||
logger.info('variants as follows')
|
|
||||||
logger.info(JSON.stringify(variants))
|
|
||||||
|
|
||||||
|
|
||||||
logger.info("run shaka packager")
|
job.log(`videoFilePath=${videoFilePath}`)
|
||||||
const masterPlaylistPath = await packageHls(helpers, variants, packageDirPath)
|
|
||||||
logger.debug(`masterPlaylistPath=${masterPlaylistPath}`)
|
job.log("create ABR variants")
|
||||||
|
const variants = await createVariants(job, videoFilePath)
|
||||||
|
job.log('variants as follows')
|
||||||
|
job.log(JSON.stringify(variants))
|
||||||
|
|
||||||
|
|
||||||
logger.info('uploading assets')
|
job.log("run shaka packager")
|
||||||
|
const masterPlaylistPath = await packageHls(job, variants, packageDirPath)
|
||||||
|
job.log(`masterPlaylistPath=${masterPlaylistPath}`)
|
||||||
|
|
||||||
|
|
||||||
|
job.log('uploading assets')
|
||||||
let assets = await listFilesRecursive(workDirPath)
|
let assets = await listFilesRecursive(workDirPath)
|
||||||
logger.info('assets as follows')
|
job.log('assets as follows')
|
||||||
logger.info(JSON.stringify(assets))
|
job.log(JSON.stringify(assets))
|
||||||
for (let i = 0; i < assets.length; i++) {
|
for (let i = 0; i < assets.length; i++) {
|
||||||
const asset = assets[i]
|
const asset = assets[i];
|
||||||
const s3Key = `package/${taskId}/hls/${basename(asset)}`
|
// const s3Key = `package/${taskId}/hls/${basename(asset)}`;
|
||||||
const mimetype = getMimeType(asset)
|
// const mimetype = mime.lookup(asset);
|
||||||
await uploadFile(s3Client, env.S3_BUCKET, s3Key, asset, mimetype)
|
const s3KeyTarget = getS3KeyTarget(vod.collectionId, vod.id, asset, 'hls');
|
||||||
|
job.log(`uploading asset ${asset} to ${s3KeyTarget}`);
|
||||||
|
await uploadHlsAsset(job, pb, vod, asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// await spawn('b2', ['file', 'upload', env.AWS_BUCKET, asset, s3KeyTarget]);
|
||||||
logger.info("generate thumbnail s3 key")
|
job.log("generate thumbnail s3 key");
|
||||||
const s3Key = `package/${taskId}/hls/master.m3u8`
|
await uploadHlsAsset(job, pb, vod, masterPlaylistPath);
|
||||||
|
await job.log('Finished creating HLS playlist');
|
||||||
|
|
||||||
// * [x] upload assets to s3
|
|
||||||
await uploadFile(s3Client, env.S3_BUCKET, s3Key, masterPlaylistPath, 'application/vnd.apple.mpegurl')
|
|
||||||
// await uploadFile(s3Client, env.S3_BUCKET, s3Key, masterPlaylistPath, 'application/vnd.apple.mpegurl')
|
|
||||||
|
|
||||||
// * [x] update vod record
|
|
||||||
await prisma.vod.update({
|
|
||||||
where: { id: vodId },
|
|
||||||
data: { hlsPlaylist: s3Key }
|
|
||||||
});
|
|
||||||
|
|
||||||
// * [x] done
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,12 +20,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { sshClient } from "../util/sftp";
|
import { seedboxSSHClient } from "../util/sftp";
|
||||||
import { qbtClient, QBTorrentInfo } from "../util/qbittorrent";
|
import { qbtClient, QBTorrentInfo } from "../util/qbittorrent";
|
||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
import { getPocketBaseClient } from "../util/pocketbase";
|
import { getPocketBaseClient } from "../util/pocketbase";
|
||||||
import { basename } from 'node:path';
|
import { basename } from 'node:path';
|
||||||
import { cacheQueue, cacheQueueEvents } from "../queues/cacheQueue";
|
import { cacheQueue, cacheQueueEvents } from "../queues/cacheQueue";
|
||||||
|
import { highPriorityQueue } from "../queues/highPriorityQueue";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
|
||||||
@ -163,7 +164,7 @@ async function uploadTorrentToSeedbox(job: Job, videoFilePath: string, torrentFi
|
|||||||
job.log(`Uploading ${videoFilePath} to seedbox...`);
|
job.log(`Uploading ${videoFilePath} to seedbox...`);
|
||||||
let lastLog = 0; // timestamp in ms
|
let lastLog = 0; // timestamp in ms
|
||||||
|
|
||||||
await sshClient.uploadFile(videoFilePath, './data', async ({ percent }) => {
|
await seedboxSSHClient.uploadFileToDir(videoFilePath, './data', async ({ percent }) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastLog >= 1_000) { // 10 seconds
|
if (now - lastLog >= 1_000) { // 10 seconds
|
||||||
lastLog = now;
|
lastLog = now;
|
||||||
@ -172,7 +173,7 @@ async function uploadTorrentToSeedbox(job: Job, videoFilePath: string, torrentFi
|
|||||||
});
|
});
|
||||||
|
|
||||||
job.log(`Uploading ${torrentFilePath} to seedbox...`);
|
job.log(`Uploading ${torrentFilePath} to seedbox...`);
|
||||||
await sshClient.uploadFile(torrentFilePath, './watch');
|
await seedboxSSHClient.uploadFileToDir(torrentFilePath, './watch');
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -231,12 +232,17 @@ export async function createTorrent(job: Job) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
const torrentBuffer = await readFile(torrentFilePath)
|
const torrentBuffer = await readFile(torrentFilePath)
|
||||||
formData.append('torrent', new Blob([torrentBuffer]), basename(torrentFilePath));
|
formData.append('torrent', new Blob([torrentBuffer as any]), basename(torrentFilePath));
|
||||||
formData.append('magnetLink', magnetLink);
|
formData.append('magnetLink', magnetLink);
|
||||||
|
|
||||||
await pb.collection('vods').update(vod.id, formData);
|
await pb.collection('vods').update(vod.id, formData);
|
||||||
|
|
||||||
|
// fire off an announce job so opentracker gets to know about this torrent
|
||||||
|
await highPriorityQueue.add(
|
||||||
|
'announceTorrent',
|
||||||
|
{ vodId },
|
||||||
|
{ jobId: `announceTorrent-${vodId}` }
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import { generalQueue } from "../queues/generalQueue.ts";
|
|||||||
import { muxQueue } from "../queues/muxQueue.ts";
|
import { muxQueue } from "../queues/muxQueue.ts";
|
||||||
import { b2Queue } from "../queues/b2Queue.ts";
|
import { b2Queue } from "../queues/b2Queue.ts";
|
||||||
import { shuffle } from "../util/random.ts";
|
import { shuffle } from "../util/random.ts";
|
||||||
|
import { gpuQueue } from "../queues/gpuQueue.ts";
|
||||||
|
|
||||||
const queues: Record<string, Queue> = {
|
const queues: Record<string, Queue> = {
|
||||||
generalQueue: generalQueue,
|
generalQueue: generalQueue,
|
||||||
muxQueue: muxQueue,
|
muxQueue: muxQueue,
|
||||||
b2Queue: b2Queue,
|
b2Queue: b2Queue,
|
||||||
|
gpuQueue: gpuQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
type VodJobConfig = {
|
type VodJobConfig = {
|
||||||
@ -31,7 +33,7 @@ async function handleMissing(job: Job, pb: Client, config: VodJobConfig) {
|
|||||||
// @todo figure out a better way to handle permafailed vod processing tasks that isn't a ticking timebomb.
|
// @todo figure out a better way to handle permafailed vod processing tasks that isn't a ticking timebomb.
|
||||||
const results = await pb.collection('vods').getList(1, 3, {
|
const results = await pb.collection('vods').getList(1, 3, {
|
||||||
filter: config.filter,
|
filter: config.filter,
|
||||||
sort: '-created',
|
sort: '-created', // newest first
|
||||||
});
|
});
|
||||||
|
|
||||||
const vods = results.items;
|
const vods = results.items;
|
||||||
@ -53,6 +55,8 @@ async function handleMissing(job: Job, pb: Client, config: VodJobConfig) {
|
|||||||
const attempts = 3;
|
const attempts = 3;
|
||||||
|
|
||||||
const queue = queues[config.queueName];
|
const queue = queues[config.queueName];
|
||||||
|
if (!queue?.name) throw new Error(`failed to get a valid queue: ${config.queueName}. Did you forget to add the queue to the findWork queue map at the top of the module?`);
|
||||||
|
|
||||||
await queue.add(config.processorName, { vodId }, { jobId, attempts });
|
await queue.add(config.processorName, { vodId }, { jobId, attempts });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +128,17 @@ async function handleMissing(job: Job, pb: Client, config: VodJobConfig) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function handleMissingFunscripts(job: Job, pb: Client) {
|
||||||
|
return handleMissing(job, pb, {
|
||||||
|
filter: "sourceVideo != '' && (funscriptVibrate = '' || funscriptThrust = '')",
|
||||||
|
queueName: 'gpuQueue',
|
||||||
|
processorName: 'createFunscript',
|
||||||
|
logMessage: (id) => `findWork found ${id} in need of a funscript.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function handleMissingTorrent(job: Job, pb: Client) {
|
export async function handleMissingTorrent(job: Job, pb: Client) {
|
||||||
return handleMissing(job, pb, {
|
return handleMissing(job, pb, {
|
||||||
filter: "sourceVideo != '' && magnetLink = ''",
|
filter: "sourceVideo != '' && magnetLink = ''",
|
||||||
@ -195,9 +210,9 @@ export async function findWork(job: Job) {
|
|||||||
await handleMissingSourceVideo(job, pb);
|
await handleMissingSourceVideo(job, pb);
|
||||||
await handleMissingMuxAsset(job, pb);
|
await handleMissingMuxAsset(job, pb);
|
||||||
await handleMissingThumbnail(job, pb);
|
await handleMissingThumbnail(job, pb);
|
||||||
|
await handleMissingFunscripts(job, pb);
|
||||||
|
|
||||||
|
|
||||||
// findMissingThumbnail
|
|
||||||
// findMissingAudioAnalysis
|
// findMissingAudioAnalysis
|
||||||
// ... etc.
|
// ... etc.
|
||||||
|
|
||||||
|
|||||||
111
services/worker/src/processors/updateTrackerWhitelist.ts
Normal file
111
services/worker/src/processors/updateTrackerWhitelist.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { Job } from "bullmq";
|
||||||
|
import { getPocketBaseClient } from "../util/pocketbase";
|
||||||
|
import Client, { type RecordModel } from "pocketbase";
|
||||||
|
import { Vod } from "../types";
|
||||||
|
import { writeFile } from "fs/promises";
|
||||||
|
import { decode } from "magnet-uri";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import env from "../../.config/env";
|
||||||
|
import { join } from "path";
|
||||||
|
import { opentrackerSSHClient } from '../util/sftp.ts';
|
||||||
|
import spawn from 'nano-spawn';
|
||||||
|
import retry from "../util/retry.ts";
|
||||||
|
|
||||||
|
interface Payload { }
|
||||||
|
|
||||||
|
|
||||||
|
function assertPayload(payload: any): asserts payload is Payload {
|
||||||
|
if (typeof payload !== "object" || !payload) throw new Error("invalid payload-- was not an object.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getApplicableVods
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function getApplicableVods(pb: Client) {
|
||||||
|
const vods = await pb.collection('vods').getFullList({
|
||||||
|
filter: "magnetLink != ''"
|
||||||
|
})
|
||||||
|
|
||||||
|
return vods;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateWhitelist(job: Job, vods: RecordModel[]) {
|
||||||
|
let whitelistLines = [];
|
||||||
|
for (let i = 0; i < vods.length; i++) {
|
||||||
|
const vod = vods[i];
|
||||||
|
|
||||||
|
const magnetLink = vod.magnetLink;
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = decode(magnetLink);
|
||||||
|
} catch (err) {
|
||||||
|
await job.log(`Failed to parse magnet link for VOD ${vod.id}: ${err}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoHash = parsed?.infoHash;
|
||||||
|
// @ts-ignore I think @types/magnet-uri is missing infoHashV2 property
|
||||||
|
const infoHashV2 = parsed?.infoHashV2;
|
||||||
|
if (!infoHash) {
|
||||||
|
throw new Error(`vod ${vod.id} is missing infoHash`);
|
||||||
|
}
|
||||||
|
if (!infoHashV2) {
|
||||||
|
throw new Error(`vod ${vod.id} is missing infoHashV2`);
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelistLines.push(infoHash);
|
||||||
|
whitelistLines.push(infoHashV2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await job.log(`whitelist contains ${whitelistLines.length} lines.`);
|
||||||
|
|
||||||
|
const whitelistTmpFile = join(env.CACHE_ROOT, 'worker', `tracker-whitelist-${nanoid()}.txt`);
|
||||||
|
await job.log('write whitelist file to ' + whitelistTmpFile);
|
||||||
|
await writeFile(whitelistTmpFile, whitelistLines.join('\n') + '\n', { encoding: 'utf-8' });
|
||||||
|
return whitelistTmpFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function uploadWhitelist(job: Job, whitelistTmpFile: string) {
|
||||||
|
await job.updateProgress(0);
|
||||||
|
|
||||||
|
return retry(
|
||||||
|
async () => {
|
||||||
|
await opentrackerSSHClient.uploadFileAs(
|
||||||
|
whitelistTmpFile,
|
||||||
|
'/etc/opentracker/whitelist.txt',
|
||||||
|
async ({ transferred, total, percent }) => {
|
||||||
|
await job.updateProgress(percent / 100); // updateProgress expects 0..1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
3,
|
||||||
|
3000
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function updateTrackerWhitelist(job: Job) {
|
||||||
|
|
||||||
|
assertPayload(job.data);
|
||||||
|
const pb = await getPocketBaseClient();
|
||||||
|
const vods = await getApplicableVods(pb);
|
||||||
|
|
||||||
|
await job.log(`There are ${vods.length} vods with magnetLink that we will use to populate the opentracker whitelist.`);
|
||||||
|
|
||||||
|
const whitelistTmpFile = await generateWhitelist(job, vods);
|
||||||
|
|
||||||
|
await job.log('uploading whitelist to tracker...');
|
||||||
|
await uploadWhitelist(job, whitelistTmpFile);
|
||||||
|
|
||||||
|
await job.log('Reloading opentracker');
|
||||||
|
await spawn('systemctl', ['--host=tracker', 'reload', 'opentracker.service']);
|
||||||
|
|
||||||
|
await job.log('All done.');
|
||||||
|
}
|
||||||
@ -5,4 +5,3 @@ export const downloadQueueEvents = new QueueEvents("downloadQueue", {
|
|||||||
connection
|
connection
|
||||||
});
|
});
|
||||||
|
|
||||||
await downloadQueue.setGlobalConcurrency(1);
|
|
||||||
@ -13,3 +13,15 @@ await highPriorityQueue.upsertJobScheduler(
|
|||||||
opts: {}
|
opts: {}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await highPriorityQueue.upsertJobScheduler(
|
||||||
|
'updateTrackerWhitelist-recurring',
|
||||||
|
{
|
||||||
|
pattern: '0 * * * *'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updateTrackerWhitelist',
|
||||||
|
data: {},
|
||||||
|
opts: {}
|
||||||
|
}
|
||||||
|
)
|
||||||
@ -1,7 +1,28 @@
|
|||||||
import spawn from 'nano-spawn';
|
import spawn from 'nano-spawn';
|
||||||
import env from '../../.config/env';
|
import env from '../../.config/env';
|
||||||
|
import { basename } from 'node:path';
|
||||||
|
|
||||||
export async function b2Download(fromS3Key: string, tmpFilePath: string): Promise<string> {
|
export async function b2Download(fromS3Key: string, tmpFilePath: string): Promise<string> {
|
||||||
await spawn('b2', ['file', 'download', `b2://${env.AWS_BUCKET}/${fromS3Key}`, tmpFilePath]);
|
await spawn('b2', ['file', 'download', `b2://${env.AWS_BUCKET}/${fromS3Key}`, tmpFilePath]);
|
||||||
return fromS3Key;
|
return fromS3Key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We put the hls package under `/:collection/:id/hls/` so we can use Bunny CDN's token singing
|
||||||
|
* to allow token access to all the files within that folder
|
||||||
|
* @see https://support.bunny.net/hc/en-us/articles/360016055099-How-to-sign-URLs-for-BunnyCDN-Token-Authentication
|
||||||
|
*/
|
||||||
|
export function getS3KeyTarget(collectionId: string, vodId: string, filename: string, subFolder: string | null) {
|
||||||
|
if (!filename) throw new Error('third param filename was missing');
|
||||||
|
if (!vodId) throw new Error('second param vodId was missing');
|
||||||
|
if (!collectionId) throw new Error('first param collectionId was missing');
|
||||||
|
|
||||||
|
let s3KeyTarget: string;
|
||||||
|
let name = basename(filename);
|
||||||
|
if (subFolder) {
|
||||||
|
s3KeyTarget = `${collectionId}/${vodId}/${subFolder}/${name}`;
|
||||||
|
} else {
|
||||||
|
s3KeyTarget = `${collectionId}/${vodId}/${name}`;
|
||||||
|
}
|
||||||
|
return s3KeyTarget
|
||||||
|
}
|
||||||
|
|||||||
@ -1,30 +1,58 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { signUrl } from "./bunnyCDN.ts";
|
import { signUrl } from "./bunnyCDN.ts";
|
||||||
|
import env from "../../.config/env.ts";
|
||||||
|
|
||||||
describe("signUrl", () => {
|
describe("signUrl unit", () => {
|
||||||
it("generates a correct signed BunnyCDN URL", () => {
|
it("generates a correct signed BunnyCDN URL", () => {
|
||||||
const securityKey = "my-secret";
|
const securityKey = "my-secret";
|
||||||
const baseUrl = "https://cdn.example.com";
|
const url = "https://cdn.example.com/videos/test.mp4?width=500&quality=80";
|
||||||
const path = "/videos/test.mp4";
|
|
||||||
const rawQuery = "width=500&quality=80";
|
|
||||||
const expires = 1732600000;
|
const expires = 1732600000;
|
||||||
|
|
||||||
const signed = signUrl(securityKey, baseUrl, path, rawQuery, expires);
|
const signed = signUrl(securityKey, url, expires);
|
||||||
|
|
||||||
// token part is deterministic but long, so let's just check it contains required parts
|
// token part is deterministic but long, so let's just check it contains required parts
|
||||||
expect(signed).toContain(baseUrl + path);
|
|
||||||
expect(signed).toContain("token=");
|
expect(signed).toContain("token=");
|
||||||
expect(signed).toContain("quality=80&width=500");
|
expect(signed).toContain("quality=80");
|
||||||
|
expect(signed).toContain("width=500");
|
||||||
expect(signed).toContain(`expires=${expires}`);
|
expect(signed).toContain(`expires=${expires}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws if baseUrl ends with slash", () => {
|
it("throws if baseUrl ends with slash", () => {
|
||||||
expect(() => signUrl("k", "https://example.com/", "/file.jpg", "", 123))
|
expect(() => signUrl("k", "https://example.com/file.jpg/", 123))
|
||||||
.toThrow(/must not end with a slash/);
|
.toThrow(/must not end with a slash/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prepends slash if path does not start with one", () => {
|
it("prepends slash if path does not start with one", () => {
|
||||||
const out = signUrl("k", "https://x", "file.jpg", "", 1);
|
const out = signUrl("k", "https://x/file.jpg", 1);
|
||||||
expect(out.startsWith("https://x/file.jpg")).toBe(true);
|
expect(out.startsWith("https://x/file.jpg")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows a token_path arg', () => {
|
||||||
|
const url = signUrl("k", "https://example.com/vods/abcd/hls/master_abcd.m3u8", 123, "/vods/abcd/hls/")
|
||||||
|
expect(url).toMatch(/token=[A-Za-z0-9\-_]+/);
|
||||||
|
expect(url).toMatch(/expires=123/);
|
||||||
|
expect(url).toContain(`token_path=${encodeURIComponent('/vods/abcd/hls/')}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("signUrl integration", () => {
|
||||||
|
const expires = Math.floor(Date.now() / 1000) + 3 * 60 * 60; // 3h from now
|
||||||
|
it('should generate a URL that returns HTTP 200', async () => {
|
||||||
|
const path = '/pbc_144770472/j8cgd0qqjbzyfi3/2025_11_29_test_j8cgd0qqjbzyfi3_thumb_fv49sz62ro.png'
|
||||||
|
const inputUrl = `${env.BUNNY_ZONE_URL}${path}`;
|
||||||
|
const signedUrl = signUrl(env.BUNNY_TOKEN_KEY, inputUrl, expires);
|
||||||
|
const res = await fetch(signedUrl);
|
||||||
|
expect(res.status).toBeOneOf([200, 201]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a token_path URL that returns HTTP 200', async () => {
|
||||||
|
const pathAllowed = '/pbc_144770472/j8cgd0qqjbzyfi3/hls/';
|
||||||
|
const path = pathAllowed + 'master.m3u8';
|
||||||
|
const url = signUrl(env.BUNNY_TOKEN_KEY, `${env.BUNNY_ZONE_URL}${path}`, expires, pathAllowed);
|
||||||
|
console.log(url);
|
||||||
|
const res = await fetch(url);
|
||||||
|
expect(res.status).toBeOneOf([200, 201]);
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
@ -1,36 +1,53 @@
|
|||||||
|
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import { escape } from 'node:querystring';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a signed BunnyCDN URL.
|
* Generate a signed BunnyCDN URL.
|
||||||
*
|
*
|
||||||
* @see https://support.bunny.net/hc/en-us/articles/360016055099-How-to-sign-URLs-for-BunnyCDN-Token-Authentication
|
* @see https://support.bunny.net/hc/en-us/articles/360016055099-How-to-sign-URLs-for-BunnyCDN-Token-Authentication
|
||||||
*/
|
*/
|
||||||
export function signUrl(securityKey: string, baseUrl: string, path: string, rawQuery = "", expires: number) {
|
export function signUrl(
|
||||||
if (!path) throw new Error('signUrl requires a path argument, but it was falsy.');
|
securityKey: string,
|
||||||
if (!path.startsWith('/')) path = '/' + path;
|
url: string,
|
||||||
if (baseUrl.endsWith('/')) throw new Error(`baseUrl must not end with a slash. got baseUrl=${baseUrl}`);
|
expires: number,
|
||||||
|
pathAllowed: string = '',
|
||||||
|
) {
|
||||||
|
if (url.endsWith('/')) throw new Error('url must not end with a slash.');
|
||||||
|
|
||||||
// Build parameter string (sort keys alphabetically)
|
let parsedURL = new URL(url);
|
||||||
let parameterData = "";
|
let params = (new URL(parsedURL)).searchParams;
|
||||||
if (rawQuery) {
|
let signaturePath = '';
|
||||||
const params = rawQuery
|
let parameterData = '';
|
||||||
.split("&")
|
let parameterDataUrl = '';
|
||||||
.map(p => p.split("="))
|
|
||||||
.filter(([key]) => key && key !== "token" && key !== "expires")
|
|
||||||
.sort(([a], [b]) => a.localeCompare(b));
|
|
||||||
|
|
||||||
if (params.length) {
|
if (pathAllowed) {
|
||||||
parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
|
signaturePath = pathAllowed;
|
||||||
}
|
params.set('token_path', signaturePath);
|
||||||
|
} else {
|
||||||
|
signaturePath = decodeURIComponent(parsedURL.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build hashable base
|
params.sort();
|
||||||
const hashableBase = securityKey + path + expires + parameterData;
|
if (Array.from(params).length > 0) {
|
||||||
// console.log(`hashableBase`, hashableBase)
|
params.forEach(function (value, key) {
|
||||||
|
if (value == "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parameterData.length > 0) {
|
||||||
|
parameterData += "&";
|
||||||
|
}
|
||||||
|
parameterData += key + "=" + value;
|
||||||
|
parameterDataUrl += "&" + key + "=" + escape(value);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const hashableBase = securityKey + signaturePath + expires + parameterData;
|
||||||
|
// const hashableBase = securityKey + path + expires + parameterData;
|
||||||
|
console.log(`hashableBase`, hashableBase)
|
||||||
|
|
||||||
// Compute token using your $security.sha256 workflow
|
|
||||||
|
|
||||||
const tokenH = crypto.createHash("sha256").update(hashableBase).digest("hex");
|
const tokenH = crypto.createHash("sha256").update(hashableBase).digest("hex");
|
||||||
const token = Buffer.from(tokenH, "hex")
|
const token = Buffer.from(tokenH, "hex")
|
||||||
@ -41,9 +58,7 @@ export function signUrl(securityKey: string, baseUrl: string, path: string, rawQ
|
|||||||
.replace(/=/g, "");
|
.replace(/=/g, "");
|
||||||
|
|
||||||
// Build final signed URL
|
// Build final signed URL
|
||||||
let tokenUrl = baseUrl + path + "?token=" + token;
|
return `${parsedURL.protocol}//${parsedURL.host}${parsedURL.pathname}?token=${token}${parameterDataUrl}&expires=${expires}`;
|
||||||
if (parameterData) tokenUrl += "&" + parameterData;
|
|
||||||
tokenUrl += "&expires=" + expires;
|
|
||||||
|
|
||||||
return tokenUrl;
|
|
||||||
}
|
}
|
||||||
21
services/worker/src/util/cache.ts
Normal file
21
services/worker/src/util/cache.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Job } from "bullmq";
|
||||||
|
import { cacheQueue, cacheQueueEvents } from "../queues/cacheQueue";
|
||||||
|
|
||||||
|
type CachePath = string;
|
||||||
|
|
||||||
|
export async function getSourceVideo(job: Job, timeout: number = 1000 * 60 * 60 * 5): Promise<CachePath> {
|
||||||
|
const vodId = job.data.vodId;
|
||||||
|
|
||||||
|
await job.log(`First we need to get the vod from cache...`);
|
||||||
|
const cacheGetJob = await cacheQueue.add(
|
||||||
|
'cacheGet',
|
||||||
|
{ vodId },
|
||||||
|
{ jobId: `cacheGet-${vodId}` }
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = (await cacheGetJob.waitUntilFinished(cacheQueueEvents, timeout));
|
||||||
|
await job.log(`cacheGet results: ${JSON.stringify(results)}`);
|
||||||
|
const { cachePath } = results;
|
||||||
|
|
||||||
|
return cachePath
|
||||||
|
}
|
||||||
29
services/worker/src/util/fsExtra.ts
Normal file
29
services/worker/src/util/fsExtra.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { writeFile, readdir } from "fs/promises";
|
||||||
|
import { isAbsolute } from "path";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
export async function writeJson(outputPath: string, json: any) {
|
||||||
|
if (!outputPath) throw new Error('writeJson needs an outputPath as first arg');
|
||||||
|
if (!json) throw new Error('writeJson needs json object as second arg');
|
||||||
|
if (!isAbsolute(outputPath)) throw new Error('first arg to writeJson must be an absolute path');
|
||||||
|
if (outputPath === '/') throw new Error('first arg outputPath cannot be /');
|
||||||
|
await writeFile(outputPath, JSON.stringify(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function listFilesRecursive(dir: string): Promise<string[]> {
|
||||||
|
const entries = await readdir(dir, { withFileTypes: true });
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const subFiles = await listFilesRecursive(fullPath);
|
||||||
|
files.push(...subFiles);
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
@ -1,11 +1,12 @@
|
|||||||
// src/utils/funscripts.ts
|
// src/utils/funscripts.ts
|
||||||
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { writeJson } from "fs-extra";
|
import env from "../../.config/env.ts";
|
||||||
import { env } from "../config/env";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { loadDataYaml, loadVideoMetadata, processLabelFiles } from "./vibeui";
|
import { loadDataYaml, loadVideoMetadata, processLabelFiles } from "./vibeui";
|
||||||
import logger from "./logger";
|
import { writeJson } from "./fsExtra.ts";
|
||||||
|
import { mkdirp } from "fs-extra";
|
||||||
|
|
||||||
|
|
||||||
export interface FunscriptAction {
|
export interface FunscriptAction {
|
||||||
at: number;
|
at: number;
|
||||||
@ -168,7 +169,7 @@ export function generateActions(
|
|||||||
export async function writeFunscript(outputPath: string, actions: FunscriptAction[]) {
|
export async function writeFunscript(outputPath: string, actions: FunscriptAction[]) {
|
||||||
const funscript: Funscript = { version: '1.0', actions };
|
const funscript: Funscript = { version: '1.0', actions };
|
||||||
await writeJson(outputPath, funscript);
|
await writeJson(outputPath, funscript);
|
||||||
logger.debug(`Funscript generated: ${outputPath} (${actions.length} actions)`);
|
console.log(`Funscript generated: ${outputPath} (${actions.length} actions)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -182,7 +183,9 @@ export async function buildFunscript(
|
|||||||
if (!type) throw new Error("buildFunscript requires type: 'vibrate' or 'thrust'");
|
if (!type) throw new Error("buildFunscript requires type: 'vibrate' or 'thrust'");
|
||||||
|
|
||||||
const labelDir = join(predictionOutput, 'labels');
|
const labelDir = join(predictionOutput, 'labels');
|
||||||
const outputPath = join(process.env.CACHE_ROOT ?? '/tmp', `${nanoid()}.funscript`);
|
const workDir = join(env.CACHE_ROOT, 'worker', nanoid());
|
||||||
|
const outputPath = join(workDir, `${type}.funscript`);
|
||||||
|
await mkdirp(workDir);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await loadDataYaml(join(env.VIBEUI_DIR, 'data.yaml'));
|
const data = await loadDataYaml(join(env.VIBEUI_DIR, 'data.yaml'));
|
||||||
@ -195,7 +198,7 @@ export async function buildFunscript(
|
|||||||
|
|
||||||
return outputPath;
|
return outputPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error generating Funscript: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
console.error(`Error generating Funscript: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,13 +22,14 @@ import { nanoid } from 'nanoid';
|
|||||||
import semverParse from 'semver/functions/parse';
|
import semverParse from 'semver/functions/parse';
|
||||||
import { type SemVer } from 'semver';
|
import { type SemVer } from 'semver';
|
||||||
import retry from "./retry";
|
import retry from "./retry";
|
||||||
|
import { Job } from "bullmq";
|
||||||
|
|
||||||
interface QBittorrentClientOptions {
|
interface QBittorrentClientOptions {
|
||||||
host?: string;
|
host?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
job?: Job;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -123,6 +124,7 @@ export class QBittorrentClient {
|
|||||||
private readonly username: string;
|
private readonly username: string;
|
||||||
private readonly password: string;
|
private readonly password: string;
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
|
private readonly job: Job | undefined;
|
||||||
private sidCookie: string | null = null;
|
private sidCookie: string | null = null;
|
||||||
|
|
||||||
constructor(options: QBittorrentClientOptions = {}) {
|
constructor(options: QBittorrentClientOptions = {}) {
|
||||||
@ -140,7 +142,7 @@ export class QBittorrentClient {
|
|||||||
password: env.QBT_PASSWORD!,
|
password: env.QBT_PASSWORD!,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { host, port, username, password } = {
|
const { host, port, username, password, job } = {
|
||||||
...defaults,
|
...defaults,
|
||||||
...envOptions,
|
...envOptions,
|
||||||
...options,
|
...options,
|
||||||
@ -149,10 +151,20 @@ export class QBittorrentClient {
|
|||||||
this.port = port;
|
this.port = port;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
|
this.job = job;
|
||||||
|
|
||||||
this.baseUrl = `http://${this.host}:${this.port}`;
|
this.baseUrl = `http://${this.host}:${this.port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(msg: string): void {
|
||||||
|
if (this.job) {
|
||||||
|
this.job.log(msg);
|
||||||
|
} else {
|
||||||
|
console.warn('A Job instance was not passed to qbittorrent constructor so we are logging to console.');
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* idempotently login to qBittorrent.
|
* idempotently login to qBittorrent.
|
||||||
*
|
*
|
||||||
@ -160,7 +172,7 @@ export class QBittorrentClient {
|
|||||||
*/
|
*/
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (!this.sidCookie) {
|
if (!this.sidCookie) {
|
||||||
console.log(`Connecting to qBittorrent at ${this.baseUrl}`);
|
this.log(`Connecting to qBittorrent at ${this.baseUrl}`);
|
||||||
await this.__login();
|
await this.__login();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -199,7 +211,7 @@ export class QBittorrentClient {
|
|||||||
* Then use the returned SID cookie for subsequent requests.
|
* Then use the returned SID cookie for subsequent requests.
|
||||||
*/
|
*/
|
||||||
private async __login(): Promise<void> {
|
private async __login(): Promise<void> {
|
||||||
console.log(`login() begin. using username=${this.username}, password=${this.password} env=${env.NODE_ENV}`);
|
this.log(`login() begin. using username=${this.username}, password=${this.password} env=${env.NODE_ENV}`);
|
||||||
const response = await fetch(`${this.baseUrl}/api/v2/auth/login`, {
|
const response = await fetch(`${this.baseUrl}/api/v2/auth/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -216,25 +228,25 @@ export class QBittorrentClient {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const msg = `Login request failed (${response.status} ${response.statusText}). body=${responseBody}`;
|
const msg = `Login request failed (${response.status} ${response.statusText}). body=${responseBody}`;
|
||||||
console.error(msg);
|
this.log(msg);
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Login response: status=${response.status} ${response.statusText}`);
|
this.log(`Login response: status=${response.status} ${response.statusText}`);
|
||||||
console.log(`Headers: ${JSON.stringify([...response.headers.entries()])}`);
|
this.log(`Headers: ${JSON.stringify([...response.headers.entries()])}`);
|
||||||
|
|
||||||
// Extract SID cookie
|
// Extract SID cookie
|
||||||
const setCookie = response.headers.get("set-cookie");
|
const setCookie = response.headers.get("set-cookie");
|
||||||
if (!setCookie) {
|
if (!setCookie) {
|
||||||
const msg = `Login failed: No SID cookie was returned. status=${response.status} ${response.statusText}. body=${responseBody}`;
|
const msg = `Login failed: No SID cookie was returned. status=${response.status} ${response.statusText}. body=${responseBody}`;
|
||||||
console.error(msg);
|
this.log(msg);
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sidCookie = setCookie;
|
this.sidCookie = setCookie;
|
||||||
console.log(`sidCookie=${this.sidCookie}`);
|
this.log(`sidCookie=${this.sidCookie}`);
|
||||||
|
|
||||||
console.log("Successfully logged into qBittorrent.");
|
this.log("Successfully logged into qBittorrent.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -252,10 +264,10 @@ export class QBittorrentClient {
|
|||||||
private async addTorrentCreationTask(sourcePath: string): Promise<string> {
|
private async addTorrentCreationTask(sourcePath: string): Promise<string> {
|
||||||
const torrentFilePath = join(tmpdir(), `${nanoid()}.torrent`);
|
const torrentFilePath = join(tmpdir(), `${nanoid()}.torrent`);
|
||||||
const url = `${this.baseUrl}/api/v2/torrentcreator/addTask`;
|
const url = `${this.baseUrl}/api/v2/torrentcreator/addTask`;
|
||||||
console.log(`addTorrentCreationTask using sourcePath=${sourcePath}, url=${url}`);
|
this.log(`addTorrentCreationTask using sourcePath=${sourcePath}, url=${url}`);
|
||||||
|
|
||||||
|
|
||||||
console.log(`addTorrent using sourcePath=${sourcePath}`)
|
this.log(`addTorrent using sourcePath=${sourcePath}`)
|
||||||
|
|
||||||
if (!this.sidCookie) {
|
if (!this.sidCookie) {
|
||||||
throw new Error("Not connected: SID cookie missing");
|
throw new Error("Not connected: SID cookie missing");
|
||||||
@ -293,7 +305,7 @@ export class QBittorrentClient {
|
|||||||
throw new Error(`addTorrentCreationTask failed: ${res.status} ${res.statusText} ${body}`);
|
throw new Error(`addTorrentCreationTask failed: ${res.status} ${res.statusText} ${body}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('addTorrent success.');
|
this.log('addTorrent success.');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -309,14 +321,14 @@ export class QBittorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
console.log({ addTaskResponse: data });
|
this.log(JSON.stringify({ addTaskResponse: data }));
|
||||||
return data.taskID;
|
return data.taskID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async pollTorrentStatus(taskId: string): Promise<TorrentCreatorTaskStatus> {
|
private async pollTorrentStatus(taskId: string): Promise<TorrentCreatorTaskStatus> {
|
||||||
while (true) {
|
while (true) {
|
||||||
console.log(`Polling torrent creation taskID=${taskId}`);
|
this.log(`Polling torrent creation taskID=${taskId}`);
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${this.baseUrl}/api/v2/torrentcreator/status?taskId=${taskId}`,
|
`${this.baseUrl}/api/v2/torrentcreator/status?taskId=${taskId}`,
|
||||||
@ -326,12 +338,12 @@ export class QBittorrentClient {
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`status failed: ${res.status} ${res.statusText}`);
|
throw new Error(`status failed: ${res.status} ${res.statusText}`);
|
||||||
}
|
}
|
||||||
console.log('the request to poll for torrent status was successful.')
|
this.log('the request to poll for torrent status was successful.')
|
||||||
|
|
||||||
const statusMap = (await res.json()) as TorrentCreatorTaskStatusMap;
|
const statusMap = (await res.json()) as TorrentCreatorTaskStatusMap;
|
||||||
console.log({ statusMap: statusMap })
|
this.log(JSON.stringify({ statusMap: statusMap }))
|
||||||
const task = Object.values(statusMap).find((t) => t.taskID === taskId);
|
const task = Object.values(statusMap).find((t) => t.taskID === taskId);
|
||||||
console.log({ task: task })
|
this.log(JSON.stringify({ task: task }))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -339,13 +351,13 @@ export class QBittorrentClient {
|
|||||||
throw new Error(`Task ${taskId} not found in status response`);
|
throw new Error(`Task ${taskId} not found in status response`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` Torrent creator task status=${task.status}`);
|
this.log(` Torrent creator task status=${task.status}`);
|
||||||
|
|
||||||
switch (task.status) {
|
switch (task.status) {
|
||||||
case "Failed":
|
case "Failed":
|
||||||
const msg = `Torrent creation failed: ${task.errorMessage}`;
|
const msg = `Torrent creation failed: ${task.errorMessage}`;
|
||||||
console.error(msg);
|
this.log(msg);
|
||||||
console.log('here is the task that failed', task);
|
this.log('here is the task that failed', task);
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
case "Finished":
|
case "Finished":
|
||||||
return task;
|
return task;
|
||||||
@ -417,9 +429,9 @@ export class QBittorrentClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!torrentsRes.ok) {
|
if (!torrentsRes.ok) {
|
||||||
console.error('__getTorrentInfos failed to fetch() torrent info.');
|
this.log('__getTorrentInfos failed to fetch() torrent info.');
|
||||||
const body = await torrentsRes.text();
|
const body = await torrentsRes.text();
|
||||||
console.error(`${torrentsRes.status} ${torrentsRes.statusText} ${body}`);
|
this.log(`${torrentsRes.status} ${torrentsRes.statusText} ${body}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const torrents = await torrentsRes.json() as Array<{ hash: string; name: string }>;
|
const torrents = await torrentsRes.json() as Array<{ hash: string; name: string }>;
|
||||||
@ -432,7 +444,7 @@ export class QBittorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getInfoHashV2(torrentName: string): Promise<string> {
|
async getInfoHashV2(torrentName: string): Promise<string> {
|
||||||
console.log(`getInfoHashV2 using torrentName=${torrentName}`)
|
this.log(`getInfoHashV2 using torrentName=${torrentName}`)
|
||||||
|
|
||||||
|
|
||||||
const torrent = await this.getTorrentInfos(torrentName);
|
const torrent = await this.getTorrentInfos(torrentName);
|
||||||
@ -456,7 +468,7 @@ export class QBittorrentClient {
|
|||||||
*/
|
*/
|
||||||
private async __addTorrent(localFilePath: string): Promise<void> {
|
private async __addTorrent(localFilePath: string): Promise<void> {
|
||||||
|
|
||||||
console.log(`__addTorrent using localFilePath=${localFilePath}`)
|
this.log(`__addTorrent using localFilePath=${localFilePath}`)
|
||||||
|
|
||||||
if (!this.sidCookie) {
|
if (!this.sidCookie) {
|
||||||
throw new Error("Not connected. (SID cookie missing.)");
|
throw new Error("Not connected. (SID cookie missing.)");
|
||||||
@ -487,7 +499,7 @@ export class QBittorrentClient {
|
|||||||
throw new Error(`__addTorrent failed: ${res.status} ${res.statusText} ${body}`);
|
throw new Error(`__addTorrent failed: ${res.status} ${res.statusText} ${body}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('__addTorrent success.');
|
this.log('__addTorrent success.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -498,7 +510,7 @@ export class QBittorrentClient {
|
|||||||
*/
|
*/
|
||||||
async deleteTorrent(id: string): Promise<void> {
|
async deleteTorrent(id: string): Promise<void> {
|
||||||
await this.connect();
|
await this.connect();
|
||||||
console.log(`Deleting torrent ${id}...`);
|
this.log(`Deleting torrent ${id}...`);
|
||||||
|
|
||||||
if (!this.sidCookie) {
|
if (!this.sidCookie) {
|
||||||
throw new Error('Not logged in. sidCookie missing.');
|
throw new Error('Not logged in. sidCookie missing.');
|
||||||
@ -515,14 +527,14 @@ export class QBittorrentClient {
|
|||||||
} else {
|
} else {
|
||||||
// Not a hash → treat as name → look up hash
|
// Not a hash → treat as name → look up hash
|
||||||
const info = await this.getTorrentInfos(id);
|
const info = await this.getTorrentInfos(id);
|
||||||
console.log('info', info);
|
this.log('info', info);
|
||||||
hashToDelete = info.hash;
|
hashToDelete = info.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`deleting ${id} (${hashToDelete})`);
|
this.log(`deleting ${id} (${hashToDelete})`);
|
||||||
await this.__deleteTorrent(hashToDelete);
|
await this.__deleteTorrent(hashToDelete);
|
||||||
|
|
||||||
console.log(`deleteTorrent success for: ${hashToDelete}`);
|
this.log(`deleteTorrent success for: ${hashToDelete}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -533,7 +545,7 @@ export class QBittorrentClient {
|
|||||||
if (!hashes) throw new Error('__deleteTorrent hashes arg missing');
|
if (!hashes) throw new Error('__deleteTorrent hashes arg missing');
|
||||||
if (!this.sidCookie) throw new Error('__deleteTorrent missing sidCookie');
|
if (!this.sidCookie) throw new Error('__deleteTorrent missing sidCookie');
|
||||||
|
|
||||||
console.log(`deleting hashes`, hashes)
|
this.log(`deleting hashes`, hashes)
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
hashes,
|
hashes,
|
||||||
@ -559,7 +571,7 @@ export class QBittorrentClient {
|
|||||||
* @deprecated use getTorrentInfos instead
|
* @deprecated use getTorrentInfos instead
|
||||||
*/
|
*/
|
||||||
async getMagnetLink(fileName: string): Promise<string> {
|
async getMagnetLink(fileName: string): Promise<string> {
|
||||||
console.log(`getMagnetLink using fileName=${fileName}`)
|
this.log(`getMagnetLink using fileName=${fileName}`)
|
||||||
|
|
||||||
// qBittorrent does NOT return infoHash directly here
|
// qBittorrent does NOT return infoHash directly here
|
||||||
// we have to get it by querying the torrents list
|
// we have to get it by querying the torrents list
|
||||||
@ -573,7 +585,7 @@ export class QBittorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createTorrent(localFilePath: string): Promise<{ torrentFilePath: string; magnetLink: string, info: QBTorrentInfo }> {
|
async createTorrent(localFilePath: string): Promise<{ torrentFilePath: string; magnetLink: string, info: QBTorrentInfo }> {
|
||||||
console.log(`Creating torrent from file: ${localFilePath}`);
|
this.log(`Creating torrent from file: ${localFilePath}`);
|
||||||
await this.connect();
|
await this.connect();
|
||||||
|
|
||||||
if (!this.sidCookie) {
|
if (!this.sidCookie) {
|
||||||
@ -582,7 +594,7 @@ export class QBittorrentClient {
|
|||||||
|
|
||||||
// 1. start task
|
// 1. start task
|
||||||
const taskId = await this.addTorrentCreationTask(localFilePath);
|
const taskId = await this.addTorrentCreationTask(localFilePath);
|
||||||
console.log(`Created torrent task ${taskId}`);
|
this.log(`Created torrent task ${taskId}`);
|
||||||
|
|
||||||
// 2. poll until finished
|
// 2. poll until finished
|
||||||
await this.pollTorrentStatus(taskId);
|
await this.pollTorrentStatus(taskId);
|
||||||
@ -597,7 +609,7 @@ export class QBittorrentClient {
|
|||||||
await this.__addTorrent(torrentFilePath);
|
await this.__addTorrent(torrentFilePath);
|
||||||
|
|
||||||
// 5. Get magnet link
|
// 5. Get magnet link
|
||||||
console.log('lets get the torrent infos');
|
this.log('lets get the torrent infos');
|
||||||
const info = await this.getTorrentInfos(basename(localFilePath))
|
const info = await this.getTorrentInfos(basename(localFilePath))
|
||||||
const magnetLink = info.magnet_uri;
|
const magnetLink = info.magnet_uri;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { test, describe, expect } from 'vitest';
|
import { test, describe, expect } from 'vitest';
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { sshClient } from './sftp.ts';
|
import { seedboxSSHClient } from './sftp.ts';
|
||||||
|
|
||||||
const fixturesDir = join(import.meta.dirname, '..', 'fixtures');
|
const fixturesDir = join(import.meta.dirname, '..', 'fixtures');
|
||||||
const remoteUploadDir = "/upload"
|
const remoteUploadDir = "/upload"
|
||||||
@ -11,13 +11,13 @@ describe('sftp integration', () => {
|
|||||||
let filePath = join(fixturesDir, 'pizza.avif');
|
let filePath = join(fixturesDir, 'pizza.avif');
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sshClient.uploadFile(filePath, remoteUploadDir)
|
seedboxSSHClient.uploadFileToDir(filePath, remoteUploadDir)
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uploadFile rejects on missing local file", async () => {
|
test("uploadFile rejects on missing local file", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
sshClient.uploadFile("/does/not/exist.jpg", remoteUploadDir)
|
seedboxSSHClient.uploadFileToDir("/does/not/exist.jpg", remoteUploadDir)
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -2,13 +2,13 @@
|
|||||||
import { Client, ConnectConfig, SFTPWrapper } from 'ssh2';
|
import { Client, ConnectConfig, SFTPWrapper } from 'ssh2';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import env from '../../.config/env';
|
import env from '../../.config/env';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
interface SSHClientOptions {
|
interface SSHClientOptions {
|
||||||
host: string;
|
host: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
username: string;
|
username: string;
|
||||||
password?: string;
|
privateKey: string | Buffer;
|
||||||
privateKey?: Buffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SSHClient {
|
export class SSHClient {
|
||||||
@ -18,6 +18,12 @@ export class SSHClient {
|
|||||||
|
|
||||||
constructor(private options: SSHClientOptions) { }
|
constructor(private options: SSHClientOptions) { }
|
||||||
|
|
||||||
|
static getSFTPPrivateKey(keyFile: string) {
|
||||||
|
if (!keyFile) throw new Error('no keyFile passed to getSFTPPrivateKey');
|
||||||
|
console.log(`we are loading keyFile=${keyFile}`);
|
||||||
|
return readFileSync(keyFile);
|
||||||
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (this.connected) return;
|
if (this.connected) return;
|
||||||
|
|
||||||
@ -29,7 +35,6 @@ export class SSHClient {
|
|||||||
host: this.options.host,
|
host: this.options.host,
|
||||||
port: this.options.port || 22,
|
port: this.options.port || 22,
|
||||||
username: this.options.username,
|
username: this.options.username,
|
||||||
password: this.options.password,
|
|
||||||
privateKey: this.options.privateKey,
|
privateKey: this.options.privateKey,
|
||||||
} as ConnectConfig);
|
} as ConnectConfig);
|
||||||
});
|
});
|
||||||
@ -86,7 +91,7 @@ export class SSHClient {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the upload fails.
|
* @throws {Error} If the upload fails.
|
||||||
*/
|
*/
|
||||||
async uploadFile(
|
async uploadFileToDir(
|
||||||
localFilePath: string,
|
localFilePath: string,
|
||||||
remoteDir: string,
|
remoteDir: string,
|
||||||
onProgress?: (info: { transferred: number; total: number; percent: number }) => void
|
onProgress?: (info: { transferred: number; total: number; percent: number }) => void
|
||||||
@ -105,7 +110,7 @@ export class SSHClient {
|
|||||||
console.log(`fileName=${fileName}`)
|
console.log(`fileName=${fileName}`)
|
||||||
|
|
||||||
const remoteFilePath = path.posix.join(remoteDir, fileName);
|
const remoteFilePath = path.posix.join(remoteDir, fileName);
|
||||||
console.log(`remoteFilePath=${remoteFilePath}`)
|
console.log(`remoteFilePath=${remoteFilePath}`);
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
sftp.fastPut(
|
sftp.fastPut(
|
||||||
@ -126,6 +131,43 @@ export class SSHClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a local file to a specific path on the remote server via SFTP.
|
||||||
|
* @param {string} localFilePath - Path to the local file.
|
||||||
|
* @param {string} remoteFullPath - Full remote file path including filename.
|
||||||
|
* @param {(info: { transferred: number; total: number; percent: number }) => void} [onProgress] - Optional progress callback.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If the upload fails.
|
||||||
|
*/
|
||||||
|
async uploadFileAs(
|
||||||
|
localFilePath: string,
|
||||||
|
remoteFullPath: string,
|
||||||
|
onProgress?: (info: { transferred: number; total: number; percent: number }) => void
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`Uploading localFilePath=${localFilePath} to remoteFullPath=${remoteFullPath}...`);
|
||||||
|
|
||||||
|
await this.connect();
|
||||||
|
const sftp = await this.getSFTP();
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
sftp.fastPut(
|
||||||
|
localFilePath,
|
||||||
|
remoteFullPath,
|
||||||
|
{
|
||||||
|
step: (transferred, chunk, total) => {
|
||||||
|
const percent = (transferred / total) * 100;
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({ transferred, total, percent });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => (err ? reject(err) : resolve())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a file from the remote server via SFTP.
|
* Downloads a file from the remote server via SFTP.
|
||||||
* @param {string} remoteFilePath - Path to the file on the remote server.
|
* @param {string} remoteFilePath - Path to the file on the remote server.
|
||||||
@ -194,9 +236,17 @@ export class SSHClient {
|
|||||||
/**
|
/**
|
||||||
* Preconfigured SSHClient instance using environment-defined credentials.
|
* Preconfigured SSHClient instance using environment-defined credentials.
|
||||||
*/
|
*/
|
||||||
export const sshClient = new SSHClient({
|
export const seedboxSSHClient = new SSHClient({
|
||||||
host: env.SEEDBOX_SFTP_HOST,
|
host: env.SEEDBOX_SFTP_HOST,
|
||||||
port: parseInt(env.SEEDBOX_SFTP_PORT),
|
port: parseInt(env.SEEDBOX_SFTP_PORT),
|
||||||
username: env.SEEDBOX_SFTP_USERNAME,
|
username: env.SEEDBOX_SFTP_USERNAME,
|
||||||
password: env.SEEDBOX_SFTP_PASSWORD,
|
privateKey: SSHClient.getSFTPPrivateKey(env.SEEDBOX_SFTP_KEY_FILE),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const opentrackerSSHClient = new SSHClient({
|
||||||
|
host: env.TRACKER_SFTP_HOST,
|
||||||
|
port: Number(env.TRACKER_SFTP_PORT),
|
||||||
|
username: env.TRACKER_SFTP_USERNAME,
|
||||||
|
privateKey: SSHClient.getSFTPPrivateKey(env.TRACKER_SFTP_KEY_FILE),
|
||||||
});
|
});
|
||||||
@ -3,7 +3,7 @@ import { join } from "node:path";
|
|||||||
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import spawn from 'nano-spawn';
|
import spawn from 'nano-spawn';
|
||||||
import env from '../../env';
|
import env from '../../.config/env.ts';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { Tensor, InferenceSession } from "onnxruntime-web";
|
import { Tensor, InferenceSession } from "onnxruntime-web";
|
||||||
|
|
||||||
@ -131,14 +131,20 @@ export async function loadDataYaml(yamlPath: string): Promise<DataYaml> {
|
|||||||
*/
|
*/
|
||||||
export async function inference(videoFilePath: string): Promise<string> {
|
export async function inference(videoFilePath: string): Promise<string> {
|
||||||
|
|
||||||
const modelPath = join(env.VIBEUI_DIR, 'vibeui.pt')
|
const modelPath = join(env.VIBEUI_DIR, "vibeui/runs/detect/vibeui/weights/best.pt");
|
||||||
|
|
||||||
// Generate a unique name based on video name + UUID
|
const uniqueName = nanoid();
|
||||||
const uniqueName = nanoid()
|
|
||||||
const customProjectDir = 'vibeui/runs' // or any custom folder
|
|
||||||
const outputPath = join(env.APP_DIR, customProjectDir, uniqueName)
|
|
||||||
|
|
||||||
await spawn('yolo', [
|
// absolute path
|
||||||
|
const customProjectDir = join(env.VIBEUI_DIR, "vibeui/runs");
|
||||||
|
|
||||||
|
// correct output path
|
||||||
|
const outputPath = join(customProjectDir, uniqueName);
|
||||||
|
|
||||||
|
console.log(`vibeui inference with modelPath=${modelPath} and uniqueName=${uniqueName} and cwd=${env.VIBEUI_DIR} and outputPath=${outputPath}`);
|
||||||
|
const yoloPath = join(env.VIBEUI_DIR, '.venv/bin/yolo')
|
||||||
|
|
||||||
|
const proc = await spawn(yoloPath, [
|
||||||
'predict',
|
'predict',
|
||||||
`model=${modelPath}`,
|
`model=${modelPath}`,
|
||||||
`source=${videoFilePath}`,
|
`source=${videoFilePath}`,
|
||||||
@ -148,9 +154,12 @@ export async function inference(videoFilePath: string): Promise<string> {
|
|||||||
`project=${customProjectDir}`,
|
`project=${customProjectDir}`,
|
||||||
`name=${uniqueName}`,
|
`name=${uniqueName}`,
|
||||||
], {
|
], {
|
||||||
cwd: env.APP_DIR,
|
cwd: env.VIBEUI_DIR,
|
||||||
stdio: 'inherit',
|
});
|
||||||
})
|
|
||||||
|
console.log(`yolo stdout: ${proc.stdout}`);
|
||||||
|
console.log(`yolo stderr: ${proc.stderr}`);
|
||||||
|
|
||||||
|
|
||||||
return outputPath // contains labels/ folder and predictions
|
return outputPath // contains labels/ folder and predictions
|
||||||
}
|
}
|
||||||
@ -207,14 +216,14 @@ export async function ffprobe(videoPath: string): Promise<{ fps: number; frames:
|
|||||||
*/
|
*/
|
||||||
export async function loadVideoMetadata(videoPath: string) {
|
export async function loadVideoMetadata(videoPath: string) {
|
||||||
const { fps, frames: totalFrames } = await ffprobe(videoPath);
|
const { fps, frames: totalFrames } = await ffprobe(videoPath);
|
||||||
// job.log(`Video metadata: fps=${fps}, frames=${totalFrames}`);
|
// console.log(`Video metadata: fps=${fps}, frames=${totalFrames}`);
|
||||||
return { fps, totalFrames };
|
return { fps, totalFrames };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processLabelFiles(labelDir: string, data: DataYaml): Promise<Detection[]> {
|
export async function processLabelFiles(labelDir: string, data: DataYaml): Promise<Detection[]> {
|
||||||
const labelFiles = (await readdir(labelDir)).filter(file => file.endsWith('.txt'));
|
const labelFiles = (await readdir(labelDir)).filter(file => file.endsWith('.txt'));
|
||||||
job.log(`[processLabelFiles] Found label files: ${labelFiles.length}`);
|
console.log(`[processLabelFiles] Found label files: ${labelFiles.length}`);
|
||||||
if (labelFiles.length === 0) job.log(`⚠️⚠️⚠️ no label files found! this should normally NOT happen unless the video contained no lovense overlay. ⚠️⚠️⚠️`);
|
if (labelFiles.length === 0) console.log(`⚠️⚠️⚠️ no label files found! this should normally NOT happen unless the video contained no lovense overlay. ⚠️⚠️⚠️`);
|
||||||
|
|
||||||
const detections: Map<number, Detection[]> = new Map();
|
const detections: Map<number, Detection[]> = new Map();
|
||||||
const names = data.names;
|
const names = data.names;
|
||||||
@ -222,7 +231,7 @@ export async function processLabelFiles(labelDir: string, data: DataYaml): Promi
|
|||||||
for (const file of labelFiles) {
|
for (const file of labelFiles) {
|
||||||
const match = file.match(/(\d+)\.txt$/);
|
const match = file.match(/(\d+)\.txt$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
job.log(`[processLabelFiles] Skipping invalid filename: ${file}`);
|
console.log(`[processLabelFiles] Skipping invalid filename: ${file}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!match[1]) {
|
if (!match[1]) {
|
||||||
@ -231,7 +240,7 @@ export async function processLabelFiles(labelDir: string, data: DataYaml): Promi
|
|||||||
|
|
||||||
const frameIndex = parseInt(match[1], 10);
|
const frameIndex = parseInt(match[1], 10);
|
||||||
if (isNaN(frameIndex)) {
|
if (isNaN(frameIndex)) {
|
||||||
job.log(`[processLabelFiles] Skipping invalid frame index: ${file}`);
|
console.log(`[processLabelFiles] Skipping invalid frame index: ${file}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,10 +272,10 @@ export async function processLabelFiles(labelDir: string, data: DataYaml): Promi
|
|||||||
if (maxConfidence > 0 && selectedClassIndex !== -1) {
|
if (maxConfidence > 0 && selectedClassIndex !== -1) {
|
||||||
const className = names[selectedClassIndex.toString()];
|
const className = names[selectedClassIndex.toString()];
|
||||||
if (className) {
|
if (className) {
|
||||||
job.log(`[processLabelFiles] Frame ${frameIndex}: detected class "${className}" with confidence ${maxConfidence}`);
|
console.log(`[processLabelFiles] Frame ${frameIndex}: detected class "${className}" with confidence ${maxConfidence}`);
|
||||||
frameDetections.push({ startFrame: frameIndex, endFrame: frameIndex, className });
|
frameDetections.push({ startFrame: frameIndex, endFrame: frameIndex, className });
|
||||||
} else {
|
} else {
|
||||||
job.log(`[processLabelFiles] Frame ${frameIndex}: class index ${selectedClassIndex} has no name`);
|
console.log(`[processLabelFiles] Frame ${frameIndex}: class index ${selectedClassIndex} has no name`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,9 +301,9 @@ export async function processLabelFiles(labelDir: string, data: DataYaml): Promi
|
|||||||
|
|
||||||
if (currentDetection) detectionSegments.push(currentDetection);
|
if (currentDetection) detectionSegments.push(currentDetection);
|
||||||
|
|
||||||
job.log(`[processLabelFiles] Total detection segments: ${detectionSegments.length}`);
|
console.log(`[processLabelFiles] Total detection segments: ${detectionSegments.length}`);
|
||||||
for (const segment of detectionSegments) {
|
for (const segment of detectionSegments) {
|
||||||
job.log(` - Class "${segment.className}": frames ${segment.startFrame}–${segment.endFrame}`);
|
console.log(` - Class "${segment.className}": frames ${segment.startFrame}–${segment.endFrame}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return detectionSegments;
|
return detectionSegments;
|
||||||
|
|||||||
@ -17,7 +17,7 @@ new Worker(
|
|||||||
throw new Error(`${workerName} Unknown job name: ${job.name}`);
|
throw new Error(`${workerName} Unknown job name: ${job.name}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ connection }
|
{ connection, concurrency: 1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`${workerName} is running...`);
|
console.log(`${workerName} is running...`);
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
// gpuWorker
|
// gpuWorker
|
||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import { connection } from '../../.config/bullmq.config.ts';
|
import { connection } from '../../.config/bullmq.config.ts';
|
||||||
|
import { createFunscript } from '../processors/createFunscript.ts';
|
||||||
|
import { createHlsPlaylist } from '../processors/createHlsPlaylist.ts';
|
||||||
|
|
||||||
new Worker(
|
new Worker(
|
||||||
'gpuQueue',
|
'gpuQueue',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
console.log('gpuWorker. we got a job on the gpuQueue.', job.data, job.name);
|
console.log('gpuWorker. we got a job on the gpuQueue.', job.data, job.name);
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
// @todo implement
|
case 'createFunscript':
|
||||||
|
return await createFunscript(job);
|
||||||
|
|
||||||
|
case 'createHlsPlaylist':
|
||||||
|
return await createHlsPlaylist(job);
|
||||||
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`gpuWorker Unknown job name: ${job.name}`);
|
throw new Error(`gpuWorker Unknown job name: ${job.name}`);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { syncronizePatreon } from '../processors/syncronizePatreon.ts'
|
|||||||
import { getAnnounceUrlDetails } from '../processors/getAnnounceUrlDetails.ts'
|
import { getAnnounceUrlDetails } from '../processors/getAnnounceUrlDetails.ts'
|
||||||
import { createTorrent } from '../processors/createTorrent.ts';
|
import { createTorrent } from '../processors/createTorrent.ts';
|
||||||
import { copyV1VideoToV3 } from '../processors/copyV1VideoToV3.ts';
|
import { copyV1VideoToV3 } from '../processors/copyV1VideoToV3.ts';
|
||||||
|
import { updateTrackerWhitelist } from '../processors/updateTrackerWhitelist.ts';
|
||||||
|
|
||||||
new Worker(
|
new Worker(
|
||||||
'highPriorityQueue',
|
'highPriorityQueue',
|
||||||
@ -26,6 +27,9 @@ new Worker(
|
|||||||
case 'copyV1VideoToV3':
|
case 'copyV1VideoToV3':
|
||||||
await copyV1VideoToV3(job);
|
await copyV1VideoToV3(job);
|
||||||
break;
|
break;
|
||||||
|
case 'updateTrackerWhitelist':
|
||||||
|
await updateTrackerWhitelist(job);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown job name: ${job.name}`);
|
throw new Error(`Unknown job name: ${job.name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
loginctl enable-linger
|
loginctl enable-linger
|
||||||
sudo cp worker.service /etc/systemd/user/worker.service
|
sudo cp worker.service /etc/systemd/user/worker.service
|
||||||
sudo cp qbittorrent-nox.service /etc/systemd/user/worker.service
|
sudo cp qbittorrent-nox.service /etc/systemd/user/qbittorrent-nox.service
|
||||||
|
|
||||||
|
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user