add vibeui player
This commit is contained in:
parent
a5433e7bd5
commit
9a708fce1d
31
services/pocketbase/.gitignore
vendored
31
services/pocketbase/.gitignore
vendored
@ -2,4 +2,33 @@
|
||||
node_modules
|
||||
pb_data
|
||||
.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",
|
||||
"version": "4.0.0",
|
||||
"version": "4.2.0",
|
||||
"private": true,
|
||||
"description": "Dedication to the preservation of lewdtuber history",
|
||||
"license": "Unlicense",
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
/// <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.
|
||||
* @see https://pocketbase.io/docs/js-event-hooks/#onfiledownloadrequest
|
||||
@ -13,7 +11,12 @@
|
||||
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.collection
|
||||
// e.record
|
||||
@ -24,7 +27,7 @@ onFileDownloadRequest((event) => {
|
||||
const securityKey = process.env?.BUNNY_TOKEN_KEY;
|
||||
const baseUrl = process.env?.BUNNY_ZONE_URL;
|
||||
|
||||
// console.log(`securityKey=${securityKey}, baseUrl=${baseUrl}`)
|
||||
console.log(`securityKey=${securityKey}, baseUrl=${baseUrl}`)
|
||||
|
||||
if (!securityKey) {
|
||||
console.error('BUNNY_TOKEN_KEY was missing from env');
|
||||
@ -37,34 +40,52 @@ onFileDownloadRequest((event) => {
|
||||
|
||||
/**
|
||||
* 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} baseUrl - The base URL (protocol + host)
|
||||
* @param {string} path - Path to the file (starting with /)
|
||||
* @param {string} rawQuery - Raw query string, e.g., "width=500&quality=5"
|
||||
* @param {string} inputUrl - The base URL (protocol + host + path + (optional) qs)
|
||||
* @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 (baseUrl.endsWith('/')) throw new Error(`baseUrl must not end with a slash. got baseUrl=${baseUrl}`);
|
||||
if (inputUrl.endsWith('/')) throw new Error(`url must not end with a slash. got inputUrl=${inputUrl}`);
|
||||
|
||||
|
||||
// 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)
|
||||
let parameterData = "";
|
||||
if (rawQuery) {
|
||||
const params = rawQuery
|
||||
.split("&")
|
||||
.map(p => p.split("="))
|
||||
.filter(([key]) => key && key !== "token" && key !== "expires")
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
// let parameterData = "";
|
||||
// if (rawQuery) {
|
||||
// const params = rawQuery
|
||||
// .split("&")
|
||||
// .map(p => p.split("="))
|
||||
// .filter(([key]) => key && key !== "token" && key !== "expires")
|
||||
// .sort(([a], [b]) => a.localeCompare(b));
|
||||
|
||||
if (params.length) {
|
||||
parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
|
||||
}
|
||||
}
|
||||
// if (params.length) {
|
||||
// parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
|
||||
// }
|
||||
// }
|
||||
|
||||
// Build hashable base
|
||||
const hashableBase = securityKey + path + expires + parameterData;
|
||||
// console.log(`hashableBase`, hashableBase)
|
||||
console.log(`hashableBase`, hashableBase)
|
||||
|
||||
// Compute token using your $security.sha256 workflow
|
||||
const tokenH = $security.sha256(hashableBase);
|
||||
@ -79,6 +100,7 @@ onFileDownloadRequest((event) => {
|
||||
let tokenUrl = baseUrl + path + "?token=" + token;
|
||||
if (parameterData) tokenUrl += "&" + parameterData;
|
||||
tokenUrl += "&expires=" + expires;
|
||||
if (tokenPath) tokenUrl += "&token_path=" + tokenPath;
|
||||
|
||||
return tokenUrl;
|
||||
}
|
||||
@ -87,12 +109,12 @@ onFileDownloadRequest((event) => {
|
||||
|
||||
const rawQuery = event.requestEvent.request.url.rawQuery;
|
||||
|
||||
// console.log(`record: ${JSON.stringify(event.record)}`)
|
||||
// // console.log(`collection: ${JSON.stringify(event.collection)}`)
|
||||
// console.log(`app: ${JSON.stringify(event.app)}`)
|
||||
// console.log(`fileField: ${JSON.stringify(event.fileField)}`)
|
||||
// console.log(`servedPath: ${JSON.stringify(event.servedPath)}`)
|
||||
// console.log(`servedName: ${JSON.stringify(event.servedName)}`)
|
||||
console.log(`record: ${JSON.stringify(event.record)}`)
|
||||
// console.log(`collection: ${JSON.stringify(event.collection)}`)
|
||||
console.log(`app: ${JSON.stringify(event.app)}`)
|
||||
console.log(`fileField: ${JSON.stringify(event.fileField)}`)
|
||||
console.log(`servedPath: ${JSON.stringify(event.servedPath)}`)
|
||||
console.log(`servedName: ${JSON.stringify(event.servedName)}`)
|
||||
|
||||
// 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
|
||||
@ -100,8 +122,8 @@ onFileDownloadRequest((event) => {
|
||||
const path = event.servedPath;
|
||||
const expires = Math.round(Date.now() / 1000) + 7 * 24 * 3600; // 7 days
|
||||
const signedUrl = signUrlCool(securityKey, baseUrl, path, rawQuery, expires);
|
||||
// console.log(`rawQUery`, rawQuery, 'path', path);
|
||||
// console.log(`signedUrl=${signedUrl}`);
|
||||
console.log(`rawQuery`, rawQuery, 'path', path);
|
||||
console.log(`signedUrl=${signedUrl}`);
|
||||
|
||||
// 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.
|
||||
@ -71,8 +71,9 @@
|
||||
<footer class="footer mt-5">
|
||||
<div class="content has-text-centered">
|
||||
<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><i><a href="/about">Dedication to the preservation of lewdtuber history</a></i></p>
|
||||
|
||||
|
||||
<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 => { %>
|
||||
<form method="POST" action="/auth/oauth/login">
|
||||
<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>
|
||||
<% }) %>
|
||||
<% 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) { %>
|
||||
<div class="notification is-warning">
|
||||
<span class="icon-text">
|
||||
@ -16,9 +19,9 @@
|
||||
</div>
|
||||
<% } 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'">
|
||||
<video controls <% if (!data?.user?.get('patron')) { %> preload="none" <% } %>>
|
||||
<% if (data.vod?.get('videoSrcB2')) { %>
|
||||
@ -29,22 +32,115 @@
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- CDN1 Player (Mux, patrons only) -->
|
||||
<% if (data?.user?.get('patron')) { %>
|
||||
<%# CDN1 Player (Mux, patrons only) %>
|
||||
<div class="mux-player" data-show="$selected == 'cdn1'">
|
||||
<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>
|
||||
</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>
|
||||
<!-- Player toggle buttons -->
|
||||
<%# Player toggle buttons %>
|
||||
<nav class="level mt-5">
|
||||
<div class="level-left">
|
||||
<% if (data?.vod?.get('muxAssetId')) { %>
|
||||
<% 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>
|
||||
<% } else { %>
|
||||
<button disabled class="button is-danger">vibe 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>
|
||||
<% } %>
|
||||
|
||||
<% if (data.vod?.get('thumbnail')) { %>
|
||||
<p><b id="thumbnail">Thumbnail:</b></p>
|
||||
<figure class="image">
|
||||
<img src="/api/files/vods/<%= data.vod?.get('id') %>/<%= data.vod?.get('thumbnail') %>" />
|
||||
</figure>
|
||||
|
||||
<% if (data.vod?.get('funscriptThrust') || data.vod?.get('funscriptVibrate')) { %>
|
||||
<p><b>Funscripts</b> <i class="icon is-small"><%- buttplugSVG %></i></p>
|
||||
|
||||
<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 class="mb-5"></div>
|
||||
@ -4,5 +4,7 @@
|
||||
</script>
|
||||
|
||||
|
||||
<h3>Vtubers</h3>
|
||||
<%- include('vtuber-list.ejs', { vtubers }) %>
|
||||
<div class="content">
|
||||
<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) { %>
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
@ -8,6 +10,7 @@
|
||||
<th>Thumbnail</th>
|
||||
<th>Torrent</th>
|
||||
<th>Magnet Link</th>
|
||||
<th>Funscript</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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" />
|
||||
</g>
|
||||
</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>
|
||||
</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.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.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_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');
|
||||
@ -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_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_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_PORT) throw new Error('QBT_PORT 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.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.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;
|
||||
|
||||
@ -55,7 +60,7 @@ const env = (() => {
|
||||
MUX_SIGNING_KEY_PRIVATE_KEY,
|
||||
PATREON_CREATOR_ACCESS_TOKEN,
|
||||
VIBEUI_DIR,
|
||||
APP_DIR,
|
||||
WORKER_DIR,
|
||||
AWS_BUCKET,
|
||||
AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY,
|
||||
@ -74,6 +79,12 @@ const env = (() => {
|
||||
SEEDBOX_SFTP_PORT,
|
||||
SEEDBOX_SFTP_USERNAME,
|
||||
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,
|
||||
QBT_HOST,
|
||||
QBT_USERNAME,
|
||||
@ -96,7 +107,7 @@ const env = (() => {
|
||||
MUX_SIGNING_KEY_PRIVATE_KEY,
|
||||
PATREON_CREATOR_ACCESS_TOKEN,
|
||||
VIBEUI_DIR,
|
||||
APP_DIR,
|
||||
WORKER_DIR,
|
||||
AWS_BUCKET,
|
||||
AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY,
|
||||
@ -115,6 +126,12 @@ const env = (() => {
|
||||
SEEDBOX_SFTP_PORT,
|
||||
SEEDBOX_SFTP_USERNAME,
|
||||
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,
|
||||
QBT_HOST,
|
||||
QBT_USERNAME,
|
||||
|
||||
102
services/worker/package-lock.json
generated
102
services/worker/package-lock.json
generated
@ -1,17 +1,19 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "worker",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"@bull-board/express": "^6.14.0",
|
||||
"@mux/mux-node": "^12.8.0",
|
||||
"@types/express": "^5.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/magnet-uri": "^5.1.5",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
@ -20,6 +22,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"fs-extra": "^11.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"magnet-uri": "^7.0.7",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"onnxruntime-web": "^1.23.2",
|
||||
@ -1586,6 +1589,18 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
|
||||
@ -1668,6 +1683,16 @@
|
||||
"@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": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
@ -1680,6 +1705,24 @@
|
||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||
"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": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
@ -2191,6 +2234,15 @@
|
||||
"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": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||
@ -2209,6 +2261,15 @@
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
@ -3754,6 +3815,34 @@
|
||||
"@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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -5274,6 +5363,15 @@
|
||||
"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": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
"@bull-board/express": "^6.14.0",
|
||||
"@mux/mux-node": "^12.8.0",
|
||||
"@types/express": "^5.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/magnet-uri": "^5.1.5",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
@ -20,6 +22,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"fs-extra": "^11.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"magnet-uri": "^7.0.7",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"onnxruntime-web": "^1.23.2",
|
||||
@ -40,4 +43,4 @@
|
||||
"scripts": {
|
||||
"start": "tsx src/index.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import env from '../.config/env.ts';
|
||||
import { version } from '../package.json';
|
||||
import { createBullBoard } from '@bull-board/api';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
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 { gpuQueue } from './queues/gpuQueue.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 { cacheQueue } from './queues/cacheQueue.ts';
|
||||
import { muxQueue } from './queues/muxQueue.ts';
|
||||
import { b2Queue } from './queues/b2Queue.ts';
|
||||
|
||||
|
||||
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=createMuxAsset&vodId=">Task: createMuxAsset</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>
|
||||
`)
|
||||
})
|
||||
@ -103,6 +107,15 @@ const run = async () => {
|
||||
case 'download':
|
||||
await downloadQueue.add(name, data);
|
||||
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:
|
||||
await highPriorityQueue.add(name, data);
|
||||
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 { join } from "node:path";
|
||||
import env from "../../.config/env";
|
||||
import { subDays } from "date-fns";
|
||||
|
||||
const retainmentDayCount = 2;
|
||||
const retainmentDayCount = 365;
|
||||
|
||||
/**
|
||||
* cacheCleanup
|
||||
*
|
||||
* Deletes files in the cache directory that are older than retainmentDayCount days
|
||||
* Recursively delete files older than retainmentDayCount days
|
||||
*/
|
||||
export default async function cacheCleanup(job: Job) {
|
||||
const cacheDir = join(env.CACHE_ROOT, 'worker');
|
||||
let cleanedCount = 0;
|
||||
|
||||
try {
|
||||
// read all files in the cache directory
|
||||
const files = await fs.readdir(cacheDir);
|
||||
const cutoffDate = subDays(new Date(), retainmentDayCount);
|
||||
const cutoffMs = cutoffDate.getTime();
|
||||
|
||||
const now = Date.now();
|
||||
const retainMs = retainmentDayCount * 24 * 60 * 60 * 1000; // days → ms
|
||||
job.log(`Starting recursive cache cleanup in directory: ${cacheDir}`);
|
||||
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) {
|
||||
const filePath = join(cacheDir, file);
|
||||
const filePath = join(dir, file);
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
// only delete files older than retainment
|
||||
if (now - stat.mtimeMs > retainMs) {
|
||||
await fs.unlink(filePath);
|
||||
cleanedCount++;
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
job.log(`Entering directory: ${filePath}`);
|
||||
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) {
|
||||
// skip errors per-file, but log them
|
||||
job.log(`failed to check/delete ${file}: ${(err as Error).message}`);
|
||||
job.log(`Failed to check/delete ${filePath}: ${(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) {
|
||||
job.log(`cache cleanup failed: ${(err as Error).message}`);
|
||||
throw err; // allow BullMQ to handle retry/failure
|
||||
job.log(`Cache cleanup failed: ${(err as Error).message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +1,104 @@
|
||||
import { getOrDownloadAsset } from "../util/cache.ts";
|
||||
import env from "../../env.ts";
|
||||
import { getS3Client, uploadFile } from "../util/s3.ts";
|
||||
import env from "../../.config/env.ts";
|
||||
import { inference } from "../util/vibeui.ts";
|
||||
import { buildFunscript } from "../util/funscripts.ts";
|
||||
import { generateS3Path } from "../util/formatters.ts";
|
||||
import { type Job } from "bullmq";
|
||||
import { getPocketBaseClient } from '../util/pocketbase';
|
||||
import { cacheQueue, cacheQueueEvents } from "../queues/cacheQueue.ts";
|
||||
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 {
|
||||
vodId: string;
|
||||
}
|
||||
|
||||
interface Funscripts { vibratePath: string, thrustPath: 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");
|
||||
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");
|
||||
}
|
||||
|
||||
async function getVod(vodId: string) {
|
||||
// return prisma.vod.findFirstOrThrow({
|
||||
// where: { id: vodId },
|
||||
// include: { vtubers: true },
|
||||
// });
|
||||
// funscriptVibrate: funscriptKeys.vibrateKey,
|
||||
// funscriptThrust: funscriptKeys.thrustKey,
|
||||
|
||||
|
||||
/**
|
||||
* createFunscript
|
||||
*/
|
||||
export async function createFunscript(job: Job) {
|
||||
assertPayload(job.data);
|
||||
const vodId = job.data.vodId;
|
||||
const pb = await getPocketBaseClient();
|
||||
const vod = await pb.collection('vods').getOne(vodId, {
|
||||
expand: 'vtubers'
|
||||
});
|
||||
return vod;
|
||||
const vod = await pb.collection('vods').getOne(vodId, { expand: 'vtubers' });
|
||||
|
||||
job.log(`createFunscript running on vodId ${vodId}`);
|
||||
|
||||
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) {
|
||||
job.log(`Doing nothing-- vod ${vod.id} already has funscripts.`);
|
||||
return false;
|
||||
@ -48,28 +113,16 @@ function ensureVodReady(vod: Awaited<ReturnType<typeof getVod>>) {
|
||||
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) {
|
||||
job.log(`Running inference on video...`);
|
||||
const predictionOutputPath = await inference(videoFilePath);
|
||||
job.log(`Prediction output at ${predictionOutputPath}`);
|
||||
return predictionOutputPath;
|
||||
}
|
||||
|
||||
async function buildFunscripts(
|
||||
job: Job,
|
||||
predictionOutputPath: string,
|
||||
videoFilePath: string
|
||||
) {
|
||||
): Promise<Funscripts> {
|
||||
job.log(`Building funscripts...`);
|
||||
const vibratePath = await buildFunscript(
|
||||
predictionOutputPath,
|
||||
@ -87,95 +140,97 @@ async function buildFunscripts(
|
||||
return { vibratePath, thrustPath };
|
||||
}
|
||||
|
||||
async function uploadFunscripts(
|
||||
s3Client: ReturnType<typeof getS3Client>,
|
||||
slug: string,
|
||||
streamDate: Date,
|
||||
vodId: string,
|
||||
funscripts: { vibratePath: string; thrustPath: string }
|
||||
) {
|
||||
const vibrateKey = generateS3Path(
|
||||
slug,
|
||||
streamDate,
|
||||
vodId,
|
||||
`funscripts/vibrate.funscript`
|
||||
);
|
||||
const vibrateUrl = await uploadFile(
|
||||
s3Client,
|
||||
env.S3_BUCKET,
|
||||
vibrateKey,
|
||||
funscripts.vibratePath,
|
||||
"application/json"
|
||||
);
|
||||
// async function uploadFunscripts(
|
||||
// job: Job,
|
||||
// s3Client: ReturnType<typeof getS3Client>,
|
||||
// slug: string,
|
||||
// streamDate: Date,
|
||||
// vodId: string,
|
||||
// funscripts: { vibratePath: string; thrustPath: string }
|
||||
// ) {
|
||||
// const vibrateKey = generateS3Path(
|
||||
// slug,
|
||||
// streamDate,
|
||||
// vodId,
|
||||
// `funscripts/vibrate.funscript`
|
||||
// );
|
||||
// const vibrateUrl = await uploadFile(
|
||||
// s3Client,
|
||||
// env.S3_BUCKET,
|
||||
// vibrateKey,
|
||||
// funscripts.vibratePath,
|
||||
// "application/json"
|
||||
// );
|
||||
|
||||
const thrustKey = generateS3Path(
|
||||
slug,
|
||||
streamDate,
|
||||
vodId,
|
||||
`funscripts/thrust.funscript`
|
||||
);
|
||||
const thrustUrl = await uploadFile(
|
||||
s3Client,
|
||||
env.S3_BUCKET,
|
||||
thrustKey,
|
||||
funscripts.thrustPath,
|
||||
"application/json"
|
||||
);
|
||||
// const thrustKey = generateS3Path(
|
||||
// slug,
|
||||
// streamDate,
|
||||
// vodId,
|
||||
// `funscripts/thrust.funscript`
|
||||
// );
|
||||
// const thrustUrl = await uploadFile(
|
||||
// s3Client,
|
||||
// env.S3_BUCKET,
|
||||
// thrustKey,
|
||||
// funscripts.thrustPath,
|
||||
// "application/json"
|
||||
// );
|
||||
|
||||
job.log(`Uploaded funscriptVibrate to S3: ${vibrateUrl}`);
|
||||
job.log(`Uploaded funscriptThrust to S3: ${thrustUrl}`);
|
||||
// job.log(`Uploaded funscriptVibrate to S3: ${vibrateUrl}`);
|
||||
// job.log(`Uploaded funscriptThrust to S3: ${thrustUrl}`);
|
||||
|
||||
return { vibrateKey, thrustKey };
|
||||
}
|
||||
// return { vibrateKey, thrustKey };
|
||||
// }
|
||||
|
||||
async function saveToDatabase(
|
||||
vodId: string,
|
||||
funscriptKeys: { vibrateKey: string; thrustKey: string }
|
||||
) {
|
||||
const pb = await getPocketBaseClient();
|
||||
// async function saveToDatabase(
|
||||
// job: Job,
|
||||
// vodId: string,
|
||||
// funscriptKeys: { vibrateKey: string; thrustKey: string }
|
||||
// ) {
|
||||
// const pb = await getPocketBaseClient();
|
||||
|
||||
await pb.collection('users').update(vodId, {
|
||||
funscriptVibrate: funscriptKeys.vibrateKey,
|
||||
funscriptThrust: funscriptKeys.thrustKey,
|
||||
});
|
||||
// await pb.collection('users').update(vodId, {
|
||||
// funscriptVibrate: funscriptKeys.vibrateKey,
|
||||
// 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;
|
||||
job.log(`createFunscript called with vodId=${vodId}`);
|
||||
// const { vodId } = job.data.vodId;
|
||||
// job.log(`createFunscript called with vodId=${vodId}`);
|
||||
|
||||
const vod = await getVod(vodId);
|
||||
if (!ensureVodReady(vod)) return;
|
||||
// const vod = await getVod(vodId);
|
||||
// if (!ensureVodReady(vod)) return;
|
||||
|
||||
const { s3Client, videoFilePath } = await downloadVideo(vod.sourceVideo!);
|
||||
const predictionOutputPath = await runInference(videoFilePath);
|
||||
// const { s3Client, videoFilePath } = await downloadVideo(vod.sourceVideo!);
|
||||
// const predictionOutputPath = await runInference(videoFilePath);
|
||||
|
||||
const slug = vod.vtubers[0].slug;
|
||||
if (!slug)
|
||||
throw new Error(`vod.vtubers[0].slug for vod ${vod.id} was falsy.`);
|
||||
// const slug = vod.vtubers[0].slug;
|
||||
// if (!slug)
|
||||
// throw new Error(`vod.vtubers[0].slug for vod ${vod.id} was falsy.`);
|
||||
|
||||
const funscripts = await buildFunscripts(
|
||||
predictionOutputPath,
|
||||
videoFilePath
|
||||
);
|
||||
// const funscripts = await buildFunscripts(
|
||||
// predictionOutputPath,
|
||||
// videoFilePath
|
||||
// );
|
||||
|
||||
const funscriptKeys = await uploadFunscripts(
|
||||
s3Client,
|
||||
slug,
|
||||
vod.streamDate,
|
||||
vod.id,
|
||||
funscripts
|
||||
);
|
||||
// const funscriptKeys = await uploadFunscripts(
|
||||
// s3Client,
|
||||
// slug,
|
||||
// vod.streamDate,
|
||||
// vod.id,
|
||||
// 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 { 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 env from "../../.config/env.ts";
|
||||
import { nanoid } from "nanoid";
|
||||
import { basename, join, dirname } from "node:path";
|
||||
import { basename, join } from "node:path";
|
||||
import { mkdirp } from "fs-extra";
|
||||
import { listFilesRecursive } from "../utils/filesystem";
|
||||
import { getMimeType } from "../utils/mimetype";
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
|
||||
import { listFilesRecursive } from "../util/fsExtra.ts";
|
||||
import spawn from "nano-spawn";
|
||||
import { Job } from "bullmq";
|
||||
import { getSourceVideo } from "../util/cache.ts";
|
||||
import { getPocketBaseClient } from "../util/pocketbase";
|
||||
import PocketBase, { RecordModel } from 'pocketbase';
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { getS3KeyTarget } from "../util/b2.ts";
|
||||
|
||||
interface Payload {
|
||||
vodId: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface HlsVariant {
|
||||
videoPath: string;
|
||||
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`)
|
||||
// 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[]> {
|
||||
export async function createVariants(job: Job, inputFilePath: string): Promise<HlsVariant[]> {
|
||||
const workdir = join(env.CACHE_ROOT, nanoid());
|
||||
await mkdirp(workdir);
|
||||
const baseName = basename(inputFilePath, '.mp4');
|
||||
const spawn = await getNanoSpawn()
|
||||
|
||||
const resolutions = [
|
||||
{ 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(
|
||||
helpers: Helpers,
|
||||
job: Job,
|
||||
variants: HlsVariant[],
|
||||
outputDir: string
|
||||
): Promise<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)
|
||||
variants.sort((a, b) => b.bandwidth - a.bandwidth);
|
||||
|
||||
@ -130,15 +108,33 @@ export async function packageHls(
|
||||
args.push("--generate_static_live_mpd");
|
||||
args.push("--segment_duration=2");
|
||||
|
||||
await spawn("packager", args, {
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
await spawn("packager", args);
|
||||
|
||||
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 {
|
||||
@ -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
|
||||
const vod = await prisma.vod.findFirstOrThrow({
|
||||
where: {
|
||||
id: vodId
|
||||
}
|
||||
})
|
||||
// * [x] load vod
|
||||
|
||||
|
||||
export async function createHlsPlaylist(job: Job) {
|
||||
assertPayload(job.data)
|
||||
const { vodId } = job.data;
|
||||
|
||||
const pb = await getPocketBaseClient();
|
||||
const vod = await pb.collection('vods').getOne(vodId);
|
||||
|
||||
|
||||
// * [x] exit if video.hlsPlaylist already defined
|
||||
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
|
||||
}
|
||||
|
||||
@ -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.`);
|
||||
}
|
||||
|
||||
logger.info(`Creating HLS Playlist.`)
|
||||
const s3Client = getS3Client()
|
||||
job.log(`Creating HLS Playlist.`)
|
||||
const taskId = nanoid()
|
||||
const workDirPath = join(env.CACHE_ROOT, taskId)
|
||||
const workDirPath = join(env.CACHE_ROOT, 'worker', taskId)
|
||||
const packageDirPath = join(workDirPath, 'package', 'hls')
|
||||
await mkdirp(packageDirPath)
|
||||
|
||||
logger.info("download source video from pull-thru cache")
|
||||
const videoFilePath = await getOrDownloadAsset(s3Client, env.S3_BUCKET, vod.sourceVideo)
|
||||
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))
|
||||
job.log("download source video from pull-thru cache")
|
||||
const videoFilePath = await getSourceVideo(job);
|
||||
|
||||
|
||||
logger.info("run shaka packager")
|
||||
const masterPlaylistPath = await packageHls(helpers, variants, packageDirPath)
|
||||
logger.debug(`masterPlaylistPath=${masterPlaylistPath}`)
|
||||
job.log(`videoFilePath=${videoFilePath}`)
|
||||
|
||||
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)
|
||||
logger.info('assets as follows')
|
||||
logger.info(JSON.stringify(assets))
|
||||
job.log('assets as follows')
|
||||
job.log(JSON.stringify(assets))
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
const asset = assets[i]
|
||||
const s3Key = `package/${taskId}/hls/${basename(asset)}`
|
||||
const mimetype = getMimeType(asset)
|
||||
await uploadFile(s3Client, env.S3_BUCKET, s3Key, asset, mimetype)
|
||||
const asset = assets[i];
|
||||
// const s3Key = `package/${taskId}/hls/${basename(asset)}`;
|
||||
// const mimetype = mime.lookup(asset);
|
||||
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]);
|
||||
job.log("generate thumbnail s3 key");
|
||||
await uploadHlsAsset(job, pb, vod, masterPlaylistPath);
|
||||
await job.log('Finished creating HLS playlist');
|
||||
|
||||
logger.info("generate thumbnail s3 key")
|
||||
const s3Key = `package/${taskId}/hls/master.m3u8`
|
||||
}
|
||||
|
||||
|
||||
// * [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 { Job } from "bullmq";
|
||||
import { getPocketBaseClient } from "../util/pocketbase";
|
||||
import { basename } from 'node:path';
|
||||
import { cacheQueue, cacheQueueEvents } from "../queues/cacheQueue";
|
||||
import { highPriorityQueue } from "../queues/highPriorityQueue";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
|
||||
@ -163,7 +164,7 @@ async function uploadTorrentToSeedbox(job: Job, videoFilePath: string, torrentFi
|
||||
job.log(`Uploading ${videoFilePath} to seedbox...`);
|
||||
let lastLog = 0; // timestamp in ms
|
||||
|
||||
await sshClient.uploadFile(videoFilePath, './data', async ({ percent }) => {
|
||||
await seedboxSSHClient.uploadFileToDir(videoFilePath, './data', async ({ percent }) => {
|
||||
const now = Date.now();
|
||||
if (now - lastLog >= 1_000) { // 10 seconds
|
||||
lastLog = now;
|
||||
@ -172,7 +173,7 @@ async function uploadTorrentToSeedbox(job: Job, videoFilePath: string, torrentFi
|
||||
});
|
||||
|
||||
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 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);
|
||||
|
||||
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 { b2Queue } from "../queues/b2Queue.ts";
|
||||
import { shuffle } from "../util/random.ts";
|
||||
import { gpuQueue } from "../queues/gpuQueue.ts";
|
||||
|
||||
const queues: Record<string, Queue> = {
|
||||
generalQueue: generalQueue,
|
||||
muxQueue: muxQueue,
|
||||
b2Queue: b2Queue,
|
||||
gpuQueue: gpuQueue,
|
||||
};
|
||||
|
||||
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.
|
||||
const results = await pb.collection('vods').getList(1, 3, {
|
||||
filter: config.filter,
|
||||
sort: '-created',
|
||||
sort: '-created', // newest first
|
||||
});
|
||||
|
||||
const vods = results.items;
|
||||
@ -53,6 +55,8 @@ async function handleMissing(job: Job, pb: Client, config: VodJobConfig) {
|
||||
const attempts = 3;
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
return handleMissing(job, pb, {
|
||||
filter: "sourceVideo != '' && magnetLink = ''",
|
||||
@ -195,9 +210,9 @@ export async function findWork(job: Job) {
|
||||
await handleMissingSourceVideo(job, pb);
|
||||
await handleMissingMuxAsset(job, pb);
|
||||
await handleMissingThumbnail(job, pb);
|
||||
await handleMissingFunscripts(job, pb);
|
||||
|
||||
|
||||
// findMissingThumbnail
|
||||
// findMissingAudioAnalysis
|
||||
// ... 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
|
||||
});
|
||||
|
||||
await downloadQueue.setGlobalConcurrency(1);
|
||||
@ -13,3 +13,15 @@ await highPriorityQueue.upsertJobScheduler(
|
||||
opts: {}
|
||||
},
|
||||
)
|
||||
|
||||
await highPriorityQueue.upsertJobScheduler(
|
||||
'updateTrackerWhitelist-recurring',
|
||||
{
|
||||
pattern: '0 * * * *'
|
||||
},
|
||||
{
|
||||
name: 'updateTrackerWhitelist',
|
||||
data: {},
|
||||
opts: {}
|
||||
}
|
||||
)
|
||||
@ -1,7 +1,28 @@
|
||||
import spawn from 'nano-spawn';
|
||||
import env from '../../.config/env';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
export async function b2Download(fromS3Key: string, tmpFilePath: string): Promise<string> {
|
||||
await spawn('b2', ['file', 'download', `b2://${env.AWS_BUCKET}/${fromS3Key}`, tmpFilePath]);
|
||||
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 { signUrl } from "./bunnyCDN.ts";
|
||||
import env from "../../.config/env.ts";
|
||||
|
||||
describe("signUrl", () => {
|
||||
describe("signUrl unit", () => {
|
||||
it("generates a correct signed BunnyCDN URL", () => {
|
||||
const securityKey = "my-secret";
|
||||
const baseUrl = "https://cdn.example.com";
|
||||
const path = "/videos/test.mp4";
|
||||
const rawQuery = "width=500&quality=80";
|
||||
const url = "https://cdn.example.com/videos/test.mp4?width=500&quality=80";
|
||||
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
|
||||
expect(signed).toContain(baseUrl + path);
|
||||
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}`);
|
||||
});
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 { escape } from 'node:querystring';
|
||||
|
||||
/**
|
||||
* Generate a signed BunnyCDN URL.
|
||||
*
|
||||
* @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) {
|
||||
if (!path) throw new Error('signUrl requires a path argument, but it was falsy.');
|
||||
if (!path.startsWith('/')) path = '/' + path;
|
||||
if (baseUrl.endsWith('/')) throw new Error(`baseUrl must not end with a slash. got baseUrl=${baseUrl}`);
|
||||
export function signUrl(
|
||||
securityKey: string,
|
||||
url: string,
|
||||
expires: number,
|
||||
pathAllowed: string = '',
|
||||
) {
|
||||
if (url.endsWith('/')) throw new Error('url must not end with a slash.');
|
||||
|
||||
// Build parameter string (sort keys alphabetically)
|
||||
let parameterData = "";
|
||||
if (rawQuery) {
|
||||
const params = rawQuery
|
||||
.split("&")
|
||||
.map(p => p.split("="))
|
||||
.filter(([key]) => key && key !== "token" && key !== "expires")
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
let parsedURL = new URL(url);
|
||||
let params = (new URL(parsedURL)).searchParams;
|
||||
let signaturePath = '';
|
||||
let parameterData = '';
|
||||
let parameterDataUrl = '';
|
||||
|
||||
if (params.length) {
|
||||
parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
|
||||
}
|
||||
if (pathAllowed) {
|
||||
signaturePath = pathAllowed;
|
||||
params.set('token_path', signaturePath);
|
||||
} else {
|
||||
signaturePath = decodeURIComponent(parsedURL.pathname);
|
||||
}
|
||||
|
||||
// Build hashable base
|
||||
const hashableBase = securityKey + path + expires + parameterData;
|
||||
// console.log(`hashableBase`, hashableBase)
|
||||
params.sort();
|
||||
if (Array.from(params).length > 0) {
|
||||
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 token = Buffer.from(tokenH, "hex")
|
||||
@ -41,9 +58,7 @@ export function signUrl(securityKey: string, baseUrl: string, path: string, rawQ
|
||||
.replace(/=/g, "");
|
||||
|
||||
// Build final signed URL
|
||||
let tokenUrl = baseUrl + path + "?token=" + token;
|
||||
if (parameterData) tokenUrl += "&" + parameterData;
|
||||
tokenUrl += "&expires=" + expires;
|
||||
return `${parsedURL.protocol}//${parsedURL.host}${parsedURL.pathname}?token=${token}${parameterDataUrl}&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
|
||||
|
||||
import { join } from "node:path";
|
||||
import { writeJson } from "fs-extra";
|
||||
import { env } from "../config/env";
|
||||
import env from "../../.config/env.ts";
|
||||
import { nanoid } from "nanoid";
|
||||
import { loadDataYaml, loadVideoMetadata, processLabelFiles } from "./vibeui";
|
||||
import logger from "./logger";
|
||||
import { writeJson } from "./fsExtra.ts";
|
||||
import { mkdirp } from "fs-extra";
|
||||
|
||||
|
||||
export interface FunscriptAction {
|
||||
at: number;
|
||||
@ -168,7 +169,7 @@ export function generateActions(
|
||||
export async function writeFunscript(outputPath: string, actions: FunscriptAction[]) {
|
||||
const funscript: Funscript = { version: '1.0', actions };
|
||||
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'");
|
||||
|
||||
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 {
|
||||
const data = await loadDataYaml(join(env.VIBEUI_DIR, 'data.yaml'));
|
||||
@ -195,7 +198,7 @@ export async function buildFunscript(
|
||||
|
||||
return outputPath;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,13 +22,14 @@ import { nanoid } from 'nanoid';
|
||||
import semverParse from 'semver/functions/parse';
|
||||
import { type SemVer } from 'semver';
|
||||
import retry from "./retry";
|
||||
|
||||
import { Job } from "bullmq";
|
||||
|
||||
interface QBittorrentClientOptions {
|
||||
host?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
job?: Job;
|
||||
}
|
||||
|
||||
|
||||
@ -123,6 +124,7 @@ export class QBittorrentClient {
|
||||
private readonly username: string;
|
||||
private readonly password: string;
|
||||
private readonly baseUrl: string;
|
||||
private readonly job: Job | undefined;
|
||||
private sidCookie: string | null = null;
|
||||
|
||||
constructor(options: QBittorrentClientOptions = {}) {
|
||||
@ -140,7 +142,7 @@ export class QBittorrentClient {
|
||||
password: env.QBT_PASSWORD!,
|
||||
};
|
||||
|
||||
const { host, port, username, password } = {
|
||||
const { host, port, username, password, job } = {
|
||||
...defaults,
|
||||
...envOptions,
|
||||
...options,
|
||||
@ -149,10 +151,20 @@ export class QBittorrentClient {
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.job = job;
|
||||
|
||||
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.
|
||||
*
|
||||
@ -160,7 +172,7 @@ export class QBittorrentClient {
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (!this.sidCookie) {
|
||||
console.log(`Connecting to qBittorrent at ${this.baseUrl}`);
|
||||
this.log(`Connecting to qBittorrent at ${this.baseUrl}`);
|
||||
await this.__login();
|
||||
}
|
||||
}
|
||||
@ -199,7 +211,7 @@ export class QBittorrentClient {
|
||||
* Then use the returned SID cookie for subsequent requests.
|
||||
*/
|
||||
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`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -216,25 +228,25 @@ export class QBittorrentClient {
|
||||
|
||||
if (!response.ok) {
|
||||
const msg = `Login request failed (${response.status} ${response.statusText}). body=${responseBody}`;
|
||||
console.error(msg);
|
||||
this.log(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
console.log(`Login response: status=${response.status} ${response.statusText}`);
|
||||
console.log(`Headers: ${JSON.stringify([...response.headers.entries()])}`);
|
||||
this.log(`Login response: status=${response.status} ${response.statusText}`);
|
||||
this.log(`Headers: ${JSON.stringify([...response.headers.entries()])}`);
|
||||
|
||||
// Extract SID cookie
|
||||
const setCookie = response.headers.get("set-cookie");
|
||||
if (!setCookie) {
|
||||
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);
|
||||
}
|
||||
|
||||
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> {
|
||||
const torrentFilePath = join(tmpdir(), `${nanoid()}.torrent`);
|
||||
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) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
console.log('addTorrent success.');
|
||||
this.log('addTorrent success.');
|
||||
|
||||
|
||||
|
||||
@ -309,14 +321,14 @@ export class QBittorrentClient {
|
||||
}
|
||||
|
||||
const data = JSON.parse(text);
|
||||
console.log({ addTaskResponse: data });
|
||||
this.log(JSON.stringify({ addTaskResponse: data }));
|
||||
return data.taskID;
|
||||
}
|
||||
|
||||
|
||||
private async pollTorrentStatus(taskId: string): Promise<TorrentCreatorTaskStatus> {
|
||||
while (true) {
|
||||
console.log(`Polling torrent creation taskID=${taskId}`);
|
||||
this.log(`Polling torrent creation taskID=${taskId}`);
|
||||
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/api/v2/torrentcreator/status?taskId=${taskId}`,
|
||||
@ -326,12 +338,12 @@ export class QBittorrentClient {
|
||||
if (!res.ok) {
|
||||
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;
|
||||
console.log({ statusMap: statusMap })
|
||||
this.log(JSON.stringify({ statusMap: statusMap }))
|
||||
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`);
|
||||
}
|
||||
|
||||
console.log(` Torrent creator task status=${task.status}`);
|
||||
this.log(` Torrent creator task status=${task.status}`);
|
||||
|
||||
switch (task.status) {
|
||||
case "Failed":
|
||||
const msg = `Torrent creation failed: ${task.errorMessage}`;
|
||||
console.error(msg);
|
||||
console.log('here is the task that failed', task);
|
||||
this.log(msg);
|
||||
this.log('here is the task that failed', task);
|
||||
throw new Error(msg);
|
||||
case "Finished":
|
||||
return task;
|
||||
@ -417,9 +429,9 @@ export class QBittorrentClient {
|
||||
});
|
||||
|
||||
if (!torrentsRes.ok) {
|
||||
console.error('__getTorrentInfos failed to fetch() torrent info.');
|
||||
this.log('__getTorrentInfos failed to fetch() torrent info.');
|
||||
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 }>;
|
||||
@ -432,7 +444,7 @@ export class QBittorrentClient {
|
||||
}
|
||||
|
||||
async getInfoHashV2(torrentName: string): Promise<string> {
|
||||
console.log(`getInfoHashV2 using torrentName=${torrentName}`)
|
||||
this.log(`getInfoHashV2 using torrentName=${torrentName}`)
|
||||
|
||||
|
||||
const torrent = await this.getTorrentInfos(torrentName);
|
||||
@ -456,7 +468,7 @@ export class QBittorrentClient {
|
||||
*/
|
||||
private async __addTorrent(localFilePath: string): Promise<void> {
|
||||
|
||||
console.log(`__addTorrent using localFilePath=${localFilePath}`)
|
||||
this.log(`__addTorrent using localFilePath=${localFilePath}`)
|
||||
|
||||
if (!this.sidCookie) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
console.log('__addTorrent success.');
|
||||
this.log('__addTorrent success.');
|
||||
}
|
||||
|
||||
/*
|
||||
@ -498,7 +510,7 @@ export class QBittorrentClient {
|
||||
*/
|
||||
async deleteTorrent(id: string): Promise<void> {
|
||||
await this.connect();
|
||||
console.log(`Deleting torrent ${id}...`);
|
||||
this.log(`Deleting torrent ${id}...`);
|
||||
|
||||
if (!this.sidCookie) {
|
||||
throw new Error('Not logged in. sidCookie missing.');
|
||||
@ -515,14 +527,14 @@ export class QBittorrentClient {
|
||||
} else {
|
||||
// Not a hash → treat as name → look up hash
|
||||
const info = await this.getTorrentInfos(id);
|
||||
console.log('info', info);
|
||||
this.log('info', info);
|
||||
hashToDelete = info.hash;
|
||||
}
|
||||
|
||||
console.log(`deleting ${id} (${hashToDelete})`);
|
||||
this.log(`deleting ${id} (${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 (!this.sidCookie) throw new Error('__deleteTorrent missing sidCookie');
|
||||
|
||||
console.log(`deleting hashes`, hashes)
|
||||
this.log(`deleting hashes`, hashes)
|
||||
|
||||
const body = new URLSearchParams({
|
||||
hashes,
|
||||
@ -559,7 +571,7 @@ export class QBittorrentClient {
|
||||
* @deprecated use getTorrentInfos instead
|
||||
*/
|
||||
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
|
||||
// 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 }> {
|
||||
console.log(`Creating torrent from file: ${localFilePath}`);
|
||||
this.log(`Creating torrent from file: ${localFilePath}`);
|
||||
await this.connect();
|
||||
|
||||
if (!this.sidCookie) {
|
||||
@ -582,7 +594,7 @@ export class QBittorrentClient {
|
||||
|
||||
// 1. start task
|
||||
const taskId = await this.addTorrentCreationTask(localFilePath);
|
||||
console.log(`Created torrent task ${taskId}`);
|
||||
this.log(`Created torrent task ${taskId}`);
|
||||
|
||||
// 2. poll until finished
|
||||
await this.pollTorrentStatus(taskId);
|
||||
@ -597,7 +609,7 @@ export class QBittorrentClient {
|
||||
await this.__addTorrent(torrentFilePath);
|
||||
|
||||
// 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 magnetLink = info.magnet_uri;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { test, describe, expect } from 'vitest';
|
||||
import { join } from "node:path";
|
||||
import { sshClient } from './sftp.ts';
|
||||
import { seedboxSSHClient } from './sftp.ts';
|
||||
|
||||
const fixturesDir = join(import.meta.dirname, '..', 'fixtures');
|
||||
const remoteUploadDir = "/upload"
|
||||
@ -11,13 +11,13 @@ describe('sftp integration', () => {
|
||||
let filePath = join(fixturesDir, 'pizza.avif');
|
||||
|
||||
await expect(
|
||||
sshClient.uploadFile(filePath, remoteUploadDir)
|
||||
seedboxSSHClient.uploadFileToDir(filePath, remoteUploadDir)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("uploadFile rejects on missing local file", async () => {
|
||||
await expect(
|
||||
sshClient.uploadFile("/does/not/exist.jpg", remoteUploadDir)
|
||||
seedboxSSHClient.uploadFileToDir("/does/not/exist.jpg", remoteUploadDir)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
})
|
||||
@ -2,13 +2,13 @@
|
||||
import { Client, ConnectConfig, SFTPWrapper } from 'ssh2';
|
||||
import path from 'path';
|
||||
import env from '../../.config/env';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
interface SSHClientOptions {
|
||||
host: string;
|
||||
port?: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
privateKey?: Buffer;
|
||||
privateKey: string | Buffer;
|
||||
}
|
||||
|
||||
export class SSHClient {
|
||||
@ -18,6 +18,12 @@ export class SSHClient {
|
||||
|
||||
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> {
|
||||
if (this.connected) return;
|
||||
|
||||
@ -29,7 +35,6 @@ export class SSHClient {
|
||||
host: this.options.host,
|
||||
port: this.options.port || 22,
|
||||
username: this.options.username,
|
||||
password: this.options.password,
|
||||
privateKey: this.options.privateKey,
|
||||
} as ConnectConfig);
|
||||
});
|
||||
@ -86,7 +91,7 @@ export class SSHClient {
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the upload fails.
|
||||
*/
|
||||
async uploadFile(
|
||||
async uploadFileToDir(
|
||||
localFilePath: string,
|
||||
remoteDir: string,
|
||||
onProgress?: (info: { transferred: number; total: number; percent: number }) => void
|
||||
@ -105,7 +110,7 @@ export class SSHClient {
|
||||
console.log(`fileName=${fileName}`)
|
||||
|
||||
const remoteFilePath = path.posix.join(remoteDir, fileName);
|
||||
console.log(`remoteFilePath=${remoteFilePath}`)
|
||||
console.log(`remoteFilePath=${remoteFilePath}`);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
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.
|
||||
* @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.
|
||||
*/
|
||||
export const sshClient = new SSHClient({
|
||||
export const seedboxSSHClient = new SSHClient({
|
||||
host: env.SEEDBOX_SFTP_HOST,
|
||||
port: parseInt(env.SEEDBOX_SFTP_PORT),
|
||||
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 yaml from 'js-yaml';
|
||||
import spawn from 'nano-spawn';
|
||||
import env from '../../env';
|
||||
import env from '../../.config/env.ts';
|
||||
import sharp from 'sharp';
|
||||
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> {
|
||||
|
||||
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 customProjectDir = 'vibeui/runs' // or any custom folder
|
||||
const outputPath = join(env.APP_DIR, customProjectDir, uniqueName)
|
||||
const uniqueName = nanoid();
|
||||
|
||||
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',
|
||||
`model=${modelPath}`,
|
||||
`source=${videoFilePath}`,
|
||||
@ -148,9 +154,12 @@ export async function inference(videoFilePath: string): Promise<string> {
|
||||
`project=${customProjectDir}`,
|
||||
`name=${uniqueName}`,
|
||||
], {
|
||||
cwd: env.APP_DIR,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
cwd: env.VIBEUI_DIR,
|
||||
});
|
||||
|
||||
console.log(`yolo stdout: ${proc.stdout}`);
|
||||
console.log(`yolo stderr: ${proc.stderr}`);
|
||||
|
||||
|
||||
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) {
|
||||
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 };
|
||||
}
|
||||
|
||||
export async function processLabelFiles(labelDir: string, data: DataYaml): Promise<Detection[]> {
|
||||
const labelFiles = (await readdir(labelDir)).filter(file => file.endsWith('.txt'));
|
||||
job.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. ⚠️⚠️⚠️`);
|
||||
console.log(`[processLabelFiles] Found label files: ${labelFiles.length}`);
|
||||
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 names = data.names;
|
||||
@ -222,7 +231,7 @@ export async function processLabelFiles(labelDir: string, data: DataYaml): Promi
|
||||
for (const file of labelFiles) {
|
||||
const match = file.match(/(\d+)\.txt$/);
|
||||
if (!match) {
|
||||
job.log(`[processLabelFiles] Skipping invalid filename: ${file}`);
|
||||
console.log(`[processLabelFiles] Skipping invalid filename: ${file}`);
|
||||
continue;
|
||||
}
|
||||
if (!match[1]) {
|
||||
@ -231,7 +240,7 @@ export async function processLabelFiles(labelDir: string, data: DataYaml): Promi
|
||||
|
||||
const frameIndex = parseInt(match[1], 10);
|
||||
if (isNaN(frameIndex)) {
|
||||
job.log(`[processLabelFiles] Skipping invalid frame index: ${file}`);
|
||||
console.log(`[processLabelFiles] Skipping invalid frame index: ${file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -263,10 +272,10 @@ export async function processLabelFiles(labelDir: string, data: DataYaml): Promi
|
||||
if (maxConfidence > 0 && selectedClassIndex !== -1) {
|
||||
const className = names[selectedClassIndex.toString()];
|
||||
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 });
|
||||
} 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);
|
||||
|
||||
job.log(`[processLabelFiles] Total detection segments: ${detectionSegments.length}`);
|
||||
console.log(`[processLabelFiles] Total detection segments: ${detectionSegments.length}`);
|
||||
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;
|
||||
|
||||
@ -17,7 +17,7 @@ new Worker(
|
||||
throw new Error(`${workerName} Unknown job name: ${job.name}`);
|
||||
}
|
||||
},
|
||||
{ connection }
|
||||
{ connection, concurrency: 1 }
|
||||
);
|
||||
|
||||
console.log(`${workerName} is running...`);
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
// gpuWorker
|
||||
import { Worker } from 'bullmq';
|
||||
import { connection } from '../../.config/bullmq.config.ts';
|
||||
|
||||
import { createFunscript } from '../processors/createFunscript.ts';
|
||||
import { createHlsPlaylist } from '../processors/createHlsPlaylist.ts';
|
||||
|
||||
new Worker(
|
||||
'gpuQueue',
|
||||
async (job) => {
|
||||
console.log('gpuWorker. we got a job on the gpuQueue.', job.data, job.name);
|
||||
switch (job.name) {
|
||||
// @todo implement
|
||||
case 'createFunscript':
|
||||
return await createFunscript(job);
|
||||
|
||||
case 'createHlsPlaylist':
|
||||
return await createHlsPlaylist(job);
|
||||
|
||||
|
||||
default:
|
||||
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 { createTorrent } from '../processors/createTorrent.ts';
|
||||
import { copyV1VideoToV3 } from '../processors/copyV1VideoToV3.ts';
|
||||
import { updateTrackerWhitelist } from '../processors/updateTrackerWhitelist.ts';
|
||||
|
||||
new Worker(
|
||||
'highPriorityQueue',
|
||||
@ -26,6 +27,9 @@ new Worker(
|
||||
case 'copyV1VideoToV3':
|
||||
await copyV1VideoToV3(job);
|
||||
break;
|
||||
case 'updateTrackerWhitelist':
|
||||
await updateTrackerWhitelist(job);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown job name: ${job.name}`);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
loginctl enable-linger
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user