migrated to boltdb from sqlite + session files

This commit is contained in:
2025-08-31 20:58:54 +02:00
parent 4c35b0b41f
commit 658d43f9ea
15 changed files with 448 additions and 400 deletions

30
go.mod
View File

@@ -4,29 +4,21 @@ go 1.24
require ( require (
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
github.com/coreos/go-oidc/v3 v3.12.0 github.com/coreos/go-oidc/v3 v3.15.0
github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/robfig/cron/v3 v3.0.0 github.com/robfig/cron/v3 v3.0.1
golang.org/x/oauth2 v0.25.0 go.etcd.io/bbolt v1.4.3
golang.org/x/sync v0.10.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.29.0 golang.org/x/sync v0.16.0
golang.org/x/sys v0.35.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.34.5
) )
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect golang.org/x/crypto v0.41.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
modernc.org/libc v1.61.11 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
) )

89
go.sum
View File

@@ -1,79 +1,38 @@
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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 v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw=
modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM=
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.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
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.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
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=

View File

@@ -2,31 +2,58 @@ package kv
import ( import (
"encoding/gob" "encoding/gob"
"encoding/json"
"errors" "errors"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync" "sync"
"time"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/downloaders" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/downloaders"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue"
bolt "go.etcd.io/bbolt"
) )
var memDbEvents = make(chan downloaders.Downloader, runtime.NumCPU()) var (
bucket = []byte("downloads")
memDbEvents = make(chan downloaders.Downloader, runtime.NumCPU())
)
// In-Memory Thread-Safe Key-Value Storage with optional persistence // In-Memory Thread-Safe Key-Value Storage with optional persistence
type Store struct { type Store struct {
db *bolt.DB
table map[string]downloaders.Downloader table map[string]downloaders.Downloader
mu sync.RWMutex mu sync.RWMutex
} }
func NewStore() *Store { func NewStore(db *bolt.DB, snaptshotInteval time.Duration) (*Store, error) {
return &Store{ // init bucket
err := db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(bucket)
return err
})
if err != nil {
return nil, err
}
s := &Store{
db: db,
table: make(map[string]downloaders.Downloader), table: make(map[string]downloaders.Downloader),
} }
go func() {
ticker := time.NewTicker(snaptshotInteval)
for range ticker.C {
s.Snapshot()
}
}()
return s, err
} }
// Get a process pointer given its id // Get a process pointer given its id
@@ -108,25 +135,25 @@ func (m *Store) Persist() error {
// Restore a persisted state // Restore a persisted state
func (m *Store) Restore(mq *queue.MessageQueue) { func (m *Store) Restore(mq *queue.MessageQueue) {
sf := filepath.Join(config.Instance().SessionFilePath, "session.dat")
fd, err := os.Open(sf)
if err != nil {
return
}
var session Session
if err := gob.NewDecoder(fd).Decode(&session); err != nil {
return
}
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
for _, snap := range session.Processes { var snapshot []internal.ProcessSnapshot
var restored downloaders.Downloader
m.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
return b.ForEach(func(k, v []byte) error {
var snap internal.ProcessSnapshot
if err := json.Unmarshal(v, &snap); err != nil {
return err
}
snapshot = append(snapshot, snap)
return nil
})
})
for _, snap := range snapshot {
var restored downloaders.Downloader
if snap.DownloaderName == "generic" { if snap.DownloaderName == "generic" {
d := downloaders.NewGenericDownload("", []string{}) d := downloaders.NewGenericDownload("", []string{})
err := d.RestoreFromSnapshot(&snap) err := d.RestoreFromSnapshot(&snap)
@@ -134,9 +161,7 @@ func (m *Store) Restore(mq *queue.MessageQueue) {
continue continue
} }
restored = d restored = d
m.table[snap.Id] = restored m.table[snap.Id] = restored
if !restored.(*downloaders.GenericDownloader).DownloaderBase.Completed { if !restored.(*downloaders.GenericDownloader).DownloaderBase.Completed {
mq.Publish(restored) mq.Publish(restored)
} }
@@ -152,3 +177,23 @@ func (m *Store) EventListener() {
} }
} }
} }
func (m *Store) Snapshot() error {
slog.Debug("snapshotting downloads state")
running := m.All()
return m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
for _, v := range *running {
data, err := json.Marshal(v)
if err != nil {
return err
}
if err := b.Put([]byte(v.Id), data); err != nil {
return err
}
}
return nil
})
}

View File

@@ -36,10 +36,10 @@ type LiveStream struct {
liveDate time.Time liveDate time.Time
mq *queue.MessageQueue mq *queue.MessageQueue
db *kv.Store store *kv.Store
} }
func New(url string, done chan *LiveStream, mq *queue.MessageQueue, db *kv.Store) *LiveStream { func New(url string, done chan *LiveStream, mq *queue.MessageQueue, store *kv.Store) *LiveStream {
return &LiveStream{ return &LiveStream{
url: url, url: url,
done: done, done: done,
@@ -47,7 +47,7 @@ func New(url string, done chan *LiveStream, mq *queue.MessageQueue, db *kv.Store
waitTime: time.Second * 0, waitTime: time.Second * 0,
waitTimeChan: make(chan time.Duration), waitTimeChan: make(chan time.Duration),
mq: mq, mq: mq,
db: db, store: store,
} }
} }
@@ -94,7 +94,7 @@ func (l *LiveStream) Start() error {
//TODO: add pipes //TODO: add pipes
d := downloaders.NewLiveStreamDownloader(l.url, []pipes.Pipe{}) d := downloaders.NewLiveStreamDownloader(l.url, []pipes.Pipe{})
l.db.Set(d) l.store.Set(d)
l.mq.Publish(d) l.mq.Publish(d)
return nil return nil

View File

@@ -1,28 +1,26 @@
package livestream package livestream
import ( import (
"encoding/gob"
"log/slog"
"maps"
"os"
"path/filepath"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/kv" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/kv"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue"
bolt "go.etcd.io/bbolt"
) )
var bucket = []byte("livestreams")
type Monitor struct { type Monitor struct {
db *kv.Store // where the just started livestream will be published db *bolt.DB
store *kv.Store // where the just started livestream will be published
mq *queue.MessageQueue // where the just started livestream will be published mq *queue.MessageQueue // where the just started livestream will be published
streams map[string]*LiveStream // keeps track of the livestreams streams map[string]*LiveStream // keeps track of the livestreams
done chan *LiveStream // to signal individual processes completition done chan *LiveStream // to signal individual processes completition
} }
func NewMonitor(mq *queue.MessageQueue, db *kv.Store) *Monitor { func NewMonitor(mq *queue.MessageQueue, store *kv.Store, db *bolt.DB) *Monitor {
return &Monitor{ return &Monitor{
mq: mq, mq: mq,
db: db, db: db,
store: store,
streams: make(map[string]*LiveStream), streams: make(map[string]*LiveStream),
done: make(chan *LiveStream), done: make(chan *LiveStream),
} }
@@ -32,14 +30,24 @@ func NewMonitor(mq *queue.MessageQueue, db *kv.Store) *Monitor {
func (m *Monitor) Schedule() { func (m *Monitor) Schedule() {
for l := range m.done { for l := range m.done {
delete(m.streams, l.url) delete(m.streams, l.url)
m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
return b.Delete([]byte(l.url))
})
} }
} }
func (m *Monitor) Add(url string) { func (m *Monitor) Add(url string) {
ls := New(url, m.done, m.mq, m.db) ls := New(url, m.done, m.mq, m.store)
go ls.Start() go ls.Start()
m.streams[url] = ls m.streams[url] = ls
m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
return b.Put([]byte(url), []byte{})
})
} }
func (m *Monitor) Remove(url string) error { func (m *Monitor) Remove(url string) error {
@@ -59,11 +67,6 @@ func (m *Monitor) Status() LiveStreamStatus {
status := make(LiveStreamStatus) status := make(LiveStreamStatus)
for k, v := range m.streams { for k, v := range m.streams {
// wt, ok := <-v.WaitTime()
// if !ok {
// continue
// }
status[k] = Status{ status[k] = Status{
Status: v.status, Status: v.status,
WaitTime: v.waitTime, WaitTime: v.waitTime,
@@ -74,46 +77,13 @@ func (m *Monitor) Status() LiveStreamStatus {
return status return status
} }
// Persist the monitor current state to a file.
// The file is located in the configured config directory
func (m *Monitor) Persist() error {
fd, err := os.Create(filepath.Join(config.Instance().SessionFilePath, "livestreams.dat"))
if err != nil {
return err
}
defer fd.Close()
slog.Debug("persisting livestream monitor state")
var toPersist []string
for url := range maps.Keys(m.streams) {
toPersist = append(toPersist, url)
}
return gob.NewEncoder(fd).Encode(toPersist)
}
// Restore a saved state and resume the monitored livestreams // Restore a saved state and resume the monitored livestreams
func (m *Monitor) Restore() error { func (m *Monitor) Restore() error {
fd, err := os.Open(filepath.Join(config.Instance().SessionFilePath, "livestreams.dat")) return m.db.View(func(tx *bolt.Tx) error {
if err != nil { b := tx.Bucket(bucket)
return err return b.ForEach(func(k, v []byte) error {
} m.Add(string(k))
defer fd.Close()
var toRestore []string
if err := gob.NewDecoder(fd).Decode(&toRestore); err != nil {
return err
}
for _, url := range toRestore {
m.Add(url)
}
slog.Debug("restored livestream monitor state")
return nil return nil
})
})
} }

View File

@@ -0,0 +1,95 @@
package pipeline
import (
"encoding/json"
"fmt"
bolt "go.etcd.io/bbolt"
)
var bucket = []byte("pipelines")
type Step struct {
Type string `json:"type"` // es. "transcoder", "filewriter"
FFmpegArgs []string `json:"ffmpeg_args,omitempty"` // args da passare a ffmpeg
Path string `json:"path,omitempty"` // solo per filewriter
}
type Pipeline struct {
ID string `json:"id"`
Name string `json:"name"`
Steps []Step `json:"steps"`
}
type Store struct {
db *bolt.DB
}
func NewStore(path string) (*Store, error) {
db, err := bolt.Open(path, 0600, nil)
if err != nil {
return nil, err
}
// init bucket
err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(bucket)
return err
})
if err != nil {
return nil, err
}
return &Store{db: db}, nil
}
func (s *Store) Save(p Pipeline) error {
data, err := json.Marshal(p)
if err != nil {
return err
}
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
return b.Put([]byte(p.ID), data)
})
}
func (s *Store) Get(id string) (*Pipeline, error) {
var p Pipeline
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
v := b.Get([]byte(id))
if v == nil {
return fmt.Errorf("pipeline %s not found", id)
}
return json.Unmarshal(v, &p)
})
if err != nil {
return nil, err
}
return &p, nil
}
func (s *Store) List() ([]Pipeline, error) {
var result []Pipeline
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
return b.ForEach(func(k, v []byte) error {
var p Pipeline
if err := json.Unmarshal(v, &p); err != nil {
return err
}
result = append(result, p)
return nil
})
})
if err != nil {
return nil, err
}
return result, nil
}

View File

@@ -11,9 +11,11 @@ func ApplyAuthenticationByConfig(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.Instance().RequireAuth { if config.Instance().RequireAuth {
Authenticated(next) Authenticated(next)
return
} }
if config.Instance().UseOpenId { if config.Instance().UseOpenId {
openid.Middleware(next) openid.Middleware(next)
return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })

View File

@@ -1,14 +1,16 @@
package rest package rest
import ( import (
"database/sql"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/kv" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/kv"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue"
bolt "go.etcd.io/bbolt"
) )
type ContainerArgs struct { type ContainerArgs struct {
DB *sql.DB DB *bolt.DB
MDB *kv.Store MDB *kv.Store
MQ *queue.MessageQueue MQ *queue.MessageQueue
LM *livestream.Monitor
} }

View File

@@ -14,11 +14,7 @@ var (
func ProvideService(args *ContainerArgs) *Service { func ProvideService(args *ContainerArgs) *Service {
serviceOnce.Do(func() { serviceOnce.Do(func() {
service = &Service{ service = NewService(args.MDB, args.DB, args.MQ, args.LM)
mdb: args.MDB,
db: args.DB,
mq: args.MQ,
}
}) })
return service return service
} }

View File

@@ -2,8 +2,9 @@ package rest
import ( import (
"context" "context"
"database/sql" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@@ -17,15 +18,35 @@ import (
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/playlist" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/playlist"
bolt "go.etcd.io/bbolt"
) )
type Service struct { type Service struct {
mdb *kv.Store mdb *kv.Store
db *sql.DB db *bolt.DB
mq *queue.MessageQueue mq *queue.MessageQueue
lm *livestream.Monitor lm *livestream.Monitor
} }
func NewService(
mdb *kv.Store,
db *bolt.DB,
mq *queue.MessageQueue,
lm *livestream.Monitor,
) *Service {
db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("templates"))
return err
})
return &Service{
mdb: mdb,
db: db,
mq: mq,
lm: lm,
}
}
func (s *Service) Exec(req internal.DownloadRequest) (string, error) { func (s *Service) Exec(req internal.DownloadRequest) (string, error) {
d := downloaders.NewGenericDownload(req.URL, req.Params) d := downloaders.NewGenericDownload(req.URL, req.Params)
d.SetOutput(internal.DownloadOutput{ d.SetOutput(internal.DownloadOutput{
@@ -85,64 +106,56 @@ func (s *Service) SetCookies(ctx context.Context, cookies string) error {
} }
func (s *Service) SaveTemplate(ctx context.Context, template *internal.CustomTemplate) error { func (s *Service) SaveTemplate(ctx context.Context, template *internal.CustomTemplate) error {
conn, err := s.db.Conn(ctx) return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("templates"))
v, err := json.Marshal(template)
if err != nil { if err != nil {
return err return err
} }
return b.Put([]byte(uuid.NewString()), v)
defer conn.Close() })
_, err = conn.ExecContext(
ctx,
"INSERT INTO templates (id, name, content) VALUES (?, ?, ?)",
uuid.NewString(),
template.Name,
template.Content,
)
return err
} }
func (s *Service) GetTemplates(ctx context.Context) (*[]internal.CustomTemplate, error) { func (s *Service) GetTemplates(ctx context.Context) (*[]internal.CustomTemplate, error) {
conn, err := s.db.Conn(ctx)
if err != nil {
return nil, err
}
defer conn.Close()
rows, err := conn.QueryContext(ctx, "SELECT * FROM templates")
if err != nil {
return nil, err
}
defer rows.Close()
templates := make([]internal.CustomTemplate, 0) templates := make([]internal.CustomTemplate, 0)
for rows.Next() { err := s.db.View(func(tx *bolt.Tx) error {
t := internal.CustomTemplate{} b := tx.Bucket([]byte("templates"))
if b == nil {
err := rows.Scan(&t.Id, &t.Name, &t.Content) return nil // bucket vuoto, restituisco lista vuota
if err != nil {
return nil, err
} }
return b.ForEach(func(k, v []byte) error {
var t internal.CustomTemplate
if err := json.Unmarshal(v, &t); err != nil {
return err
}
templates = append(templates, t) templates = append(templates, t)
return nil
})
})
if err != nil {
return nil, err
} }
return &templates, nil return &templates, nil
} }
func (s *Service) UpdateTemplate(ctx context.Context, t *internal.CustomTemplate) (*internal.CustomTemplate, error) { func (s *Service) UpdateTemplate(ctx context.Context, t *internal.CustomTemplate) (*internal.CustomTemplate, error) {
conn, err := s.db.Conn(ctx) data, err := json.Marshal(t)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer conn.Close() err = s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("templates"))
if b == nil {
return fmt.Errorf("bucket templates not found")
}
return b.Put([]byte(t.Id), data)
})
_, err = conn.ExecContext(ctx, "UPDATE templates SET name = ?, content = ? WHERE id = ?", t.Name, t.Content, t.Id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -151,16 +164,10 @@ func (s *Service) UpdateTemplate(ctx context.Context, t *internal.CustomTemplate
} }
func (s *Service) DeleteTemplate(ctx context.Context, id string) error { func (s *Service) DeleteTemplate(ctx context.Context, id string) error {
conn, err := s.db.Conn(ctx) return s.db.Update(func(tx *bolt.Tx) error {
if err != nil { b := tx.Bucket([]byte("templates"))
return err return b.Delete([]byte(id))
} })
defer conn.Close()
_, err = conn.ExecContext(ctx, "DELETE FROM templates WHERE id = ?", id)
return err
} }
func (s *Service) GetVersion(ctx context.Context) (string, string, error) { func (s *Service) GetVersion(ctx context.Context) (string, string, error) {

View File

@@ -3,7 +3,6 @@ package server
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@@ -13,16 +12,14 @@ import (
"net/rpc" "net/rpc"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strings" "strings"
"syscall" "syscall"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archiver"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/dbutil"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/filebrowser" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/filebrowser"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/kv" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/kv"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/livestream"
@@ -38,7 +35,7 @@ import (
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/twitch" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/twitch"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/user" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/user"
_ "modernc.org/sqlite" bolt "go.etcd.io/bbolt"
) )
type RunConfig struct { type RunConfig struct {
@@ -50,7 +47,7 @@ type serverConfig struct {
frontend fs.FS frontend fs.FS
swagger fs.FS swagger fs.FS
mdb *kv.Store mdb *kv.Store
db *sql.DB db *bolt.DB
mq *queue.MessageQueue mq *queue.MessageQueue
lm *livestream.Monitor lm *livestream.Monitor
tm *twitch.Monitor tm *twitch.Monitor
@@ -60,7 +57,17 @@ type serverConfig struct {
var observableLogger = logging.NewObservableLogger() var observableLogger = logging.NewObservableLogger()
func RunBlocking(rc *RunConfig) { func RunBlocking(rc *RunConfig) {
mdb := kv.NewStore() dbPath := filepath.Join(config.Instance().SessionFilePath, "bolt.db")
boltdb, err := bolt.Open(dbPath, 0600, nil)
if err != nil {
panic(err)
}
mdb, err := kv.NewStore(boltdb, time.Second*15)
if err != nil {
panic(err)
}
// ---- LOGGING --------------------------------------------------- // ---- LOGGING ---------------------------------------------------
logWriters := []io.Writer{ logWriters := []io.Writer{
@@ -97,15 +104,6 @@ func RunBlocking(rc *RunConfig) {
slog.SetDefault(logger) slog.SetDefault(logger)
// ---------------------------------------------------------------- // ----------------------------------------------------------------
db, err := sql.Open("sqlite", conf.LocalDatabasePath)
if err != nil {
slog.Error("failed to open database", slog.String("err", err.Error()))
}
if err := dbutil.Migrate(context.Background(), db); err != nil {
slog.Error("failed to init database", slog.String("err", err.Error()))
}
mq, err := queue.NewMessageQueue() mq, err := queue.NewMessageQueue()
if err != nil { if err != nil {
panic(err) panic(err)
@@ -114,7 +112,7 @@ func RunBlocking(rc *RunConfig) {
go mdb.Restore(mq) go mdb.Restore(mq)
go mdb.EventListener() go mdb.EventListener()
lm := livestream.NewMonitor(mq, mdb) lm := livestream.NewMonitor(mq, mdb, boltdb)
go lm.Schedule() go lm.Schedule()
go lm.Restore() go lm.Restore()
@@ -123,6 +121,7 @@ func RunBlocking(rc *RunConfig) {
config.Instance().Twitch.ClientId, config.Instance().Twitch.ClientId,
config.Instance().Twitch.ClientSecret, config.Instance().Twitch.ClientSecret,
), ),
boltdb,
) )
go tm.Monitor( go tm.Monitor(
context.TODO(), context.TODO(),
@@ -135,8 +134,8 @@ func RunBlocking(rc *RunConfig) {
frontend: rc.App, frontend: rc.App,
swagger: rc.Swagger, swagger: rc.Swagger,
mdb: mdb, mdb: mdb,
db: boltdb,
mq: mq, mq: mq,
db: db,
lm: lm, lm: lm,
tm: tm, tm: tm,
} }
@@ -144,7 +143,6 @@ func RunBlocking(rc *RunConfig) {
srv := newServer(scfg) srv := newServer(scfg)
go gracefulShutdown(srv, &scfg) go gracefulShutdown(srv, &scfg)
go autoPersist(time.Minute*5, mdb, lm, tm)
var ( var (
network = "tcp" network = "tcp"
@@ -171,7 +169,7 @@ func RunBlocking(rc *RunConfig) {
} }
func newServer(c serverConfig) *http.Server { func newServer(c serverConfig) *http.Server {
archiver.Register(c.db) // archiver.Register(c.db)
cronTaskRunner := task.NewCronTaskRunner(c.mq, c.mdb) cronTaskRunner := task.NewCronTaskRunner(c.mq, c.mdb)
go cronTaskRunner.Spawner(context.TODO()) go cronTaskRunner.Spawner(context.TODO())
@@ -216,7 +214,7 @@ func newServer(c serverConfig) *http.Server {
}) })
// Archive routes // Archive routes
r.Route("/archive", archive.ApplyRouter(c.db)) // r.Route("/archive", archive.ApplyRouter(c.db))
// Authentication routes // Authentication routes
r.Route("/auth", func(r chi.Router) { r.Route("/auth", func(r chi.Router) {
@@ -238,6 +236,7 @@ func newServer(c serverConfig) *http.Server {
DB: c.db, DB: c.db,
MDB: c.mdb, MDB: c.mdb,
MQ: c.mq, MQ: c.mq,
LM: c.lm,
})) }))
// Logging // Logging
@@ -273,34 +272,10 @@ func gracefulShutdown(srv *http.Server, cfg *serverConfig) {
defer func() { defer func() {
cfg.mdb.Persist() cfg.mdb.Persist()
cfg.lm.Persist() cfg.db.Close()
cfg.tm.Persist()
stop() stop()
srv.Shutdown(context.Background()) srv.Shutdown(context.Background())
}() }()
}() }()
} }
func autoPersist(
d time.Duration,
db *kv.Store,
lm *livestream.Monitor,
tm *twitch.Monitor,
) {
for {
time.Sleep(d)
if err := db.Persist(); err != nil {
slog.Warn("failed to persisted session", slog.Any("err", err))
}
if err := lm.Persist(); err != nil {
slog.Warn(
"failed to persisted livestreams monitor session", slog.Any("err", err.Error()))
}
if err := tm.Persist(); err != nil {
slog.Warn(
"failed to persisted twitch monitor session", slog.Any("err", err.Error()))
}
slog.Debug("sucessfully persisted session")
}
}

View File

@@ -1,13 +1,13 @@
package subscription package subscription
import ( import (
"database/sql"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/domain" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/domain"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/task" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/task"
bolt "go.etcd.io/bbolt"
) )
func Container(db *sql.DB, runner task.TaskRunner) domain.RestHandler { func Container(db *bolt.DB, runner task.TaskRunner) domain.RestHandler {
var ( var (
r = provideRepository(db) r = provideRepository(db)
s = provideService(r, runner) s = provideService(r, runner)

View File

@@ -1,7 +1,6 @@
package subscription package subscription
import ( import (
"database/sql"
"sync" "sync"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/domain" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/domain"
@@ -9,6 +8,8 @@ import (
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/rest" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/rest"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/service" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/service"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/task" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/task"
bolt "go.etcd.io/bbolt"
) )
var ( var (
@@ -21,7 +22,7 @@ var (
handOnce sync.Once handOnce sync.Once
) )
func provideRepository(db *sql.DB) domain.Repository { func provideRepository(db *bolt.DB) domain.Repository {
repoOnce.Do(func() { repoOnce.Do(func() {
repo = repository.New(db) repo = repository.New(db)
}) })

View File

@@ -2,131 +2,142 @@ package repository
import ( import (
"context" "context"
"database/sql" "encoding/json"
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/data" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/data"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/domain" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/subscription/domain"
bolt "go.etcd.io/bbolt"
) )
var bucketName = []byte("subscriptions")
type Repository struct { type Repository struct {
db *sql.DB db *bolt.DB
} }
// Delete implements domain.Repository. // Delete implements domain.Repository.
func (r *Repository) Delete(ctx context.Context, id string) error { func (r *Repository) Delete(ctx context.Context, id string) error {
conn, err := r.db.Conn(ctx) return r.db.Update(func(tx *bolt.Tx) error {
if err != nil { b := tx.Bucket(bucketName)
return err return b.Delete([]byte(id))
} })
defer conn.Close()
_, err = conn.ExecContext(ctx, "DELETE FROM subscriptions WHERE id = ?", id)
return err
} }
// GetCursor implements domain.Repository. // GetCursor implements domain.Repository.
func (r *Repository) GetCursor(ctx context.Context, id string) (int64, error) { func (s *Repository) GetCursor(ctx context.Context, id string) (int64, error) {
conn, err := r.db.Conn(ctx) var cursor int64
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("subscriptions"))
v := b.Get([]byte(id))
if v == nil {
return fmt.Errorf("subscription %s not found", id)
}
var data struct {
Cursor int64 `json:"cursor"`
}
if err := json.Unmarshal(v, &data); err != nil {
return err
}
cursor = data.Cursor
return nil
})
if err != nil { if err != nil {
return -1, err return -1, err
} }
defer conn.Close() return cursor, nil
row := conn.QueryRowContext(ctx, "SELECT rowid FROM subscriptions WHERE id = ?", id)
var rowId int64
if err := row.Scan(&rowId); err != nil {
return -1, err
}
return rowId, nil
} }
// List implements domain.Repository. // List implements domain.Repository.
func (r *Repository) List(ctx context.Context, start int64, limit int) (*[]data.Subscription, error) { func (r *Repository) List(ctx context.Context, start int64, limit int) (*[]data.Subscription, error) {
conn, err := r.db.Conn(ctx) var subs []data.Subscription
err := r.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketName)
return b.ForEach(func(k, v []byte) error {
var sub data.Subscription
if err := json.Unmarshal(v, &sub); err != nil {
return err
}
subs = append(subs, sub)
return nil
})
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer conn.Close() return &subs, nil
var elements []data.Subscription
rows, err := conn.QueryContext(ctx, "SELECT rowid, * FROM subscriptions WHERE rowid > ? LIMIT ?", start, limit)
if err != nil {
return nil, err
}
for rows.Next() {
var rowId int64
var element data.Subscription
if err := rows.Scan(
&rowId,
&element.Id,
&element.URL,
&element.Params,
&element.CronExpr,
); err != nil {
return &elements, err
}
elements = append(elements, element)
}
return &elements, nil
} }
// Submit implements domain.Repository. // Submit implements domain.Repository.
func (r *Repository) Submit(ctx context.Context, sub *data.Subscription) (*data.Subscription, error) { func (s *Repository) Submit(ctx context.Context, sub *data.Subscription) (*data.Subscription, error) {
conn, err := r.db.Conn(ctx) if sub.Id == "" {
sub.Id = uuid.NewString()
}
data, err := json.Marshal(sub)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer conn.Close() err = s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("subscriptions"))
return b.Put([]byte(sub.Id), data)
})
_, err = conn.ExecContext( if err != nil {
ctx, return nil, err
"INSERT INTO subscriptions (id, url, params, cron) VALUES (?, ?, ?, ?)", }
uuid.NewString(),
sub.URL,
sub.Params,
sub.CronExpr,
)
return sub, err return sub, nil
} }
// UpdateByExample implements domain.Repository. // UpdateByExample implements domain.Repository.
func (r *Repository) UpdateByExample(ctx context.Context, example *data.Subscription) error { func (s *Repository) UpdateByExample(ctx context.Context, example *data.Subscription) error {
conn, err := r.db.Conn(ctx) return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("subscriptions"))
return b.ForEach(func(k, v []byte) error {
var sub data.Subscription
if err := json.Unmarshal(v, &sub); err != nil {
return err
}
if sub.Id == example.Id || sub.URL == example.URL {
// aggiorna i campi
sub.URL = example.URL
sub.Params = example.Params
sub.CronExpr = example.CronExpr
data, err := json.Marshal(sub)
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() if err := b.Put(k, data); err != nil {
_, err = conn.ExecContext(
ctx,
"UPDATE subscriptions SET url = ?, params = ?, cron = ? WHERE id = ? OR url = ?",
example.URL,
example.Params,
example.CronExpr,
example.Id,
example.URL,
)
return err return err
}
}
return nil
})
})
} }
func New(db *sql.DB) domain.Repository { func New(db *bolt.DB) domain.Repository {
db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(bucketName)
return err
})
return &Repository{ return &Repository{
db: db, db: db,
} }

View File

@@ -2,12 +2,10 @@ package twitch
import ( import (
"context" "context"
"encoding/gob"
"fmt" "fmt"
"iter" "iter"
"log/slog" "log/slog"
"maps" "maps"
"os"
"path/filepath" "path/filepath"
"sync" "sync"
"time" "time"
@@ -17,29 +15,48 @@ import (
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/kv" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/kv"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/pipes" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/pipes"
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue" "github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal/queue"
bolt "go.etcd.io/bbolt"
) )
var bucket = []byte("twitch-monitor")
type Monitor struct { type Monitor struct {
liveChannel chan *StreamInfo liveChannel chan *StreamInfo
monitored map[string]*Client monitored map[string]*Client
lastState map[string]bool lastState map[string]bool
mu sync.RWMutex mu sync.RWMutex
db *bolt.DB
authenticationManager *AuthenticationManager authenticationManager *AuthenticationManager
} }
func NewMonitor(authenticationManager *AuthenticationManager) *Monitor { func NewMonitor(authenticationManager *AuthenticationManager, db *bolt.DB) *Monitor {
db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(bucket)
return err
})
return &Monitor{ return &Monitor{
liveChannel: make(chan *StreamInfo, 16), liveChannel: make(chan *StreamInfo, 16),
monitored: make(map[string]*Client), monitored: make(map[string]*Client),
lastState: make(map[string]bool), lastState: make(map[string]bool),
authenticationManager: authenticationManager, authenticationManager: authenticationManager,
db: db,
} }
} }
func (m *Monitor) Add(user string) { func (m *Monitor) Add(user string) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock()
m.monitored[user] = NewTwitchClient(m.authenticationManager) m.monitored[user] = NewTwitchClient(m.authenticationManager)
m.mu.Unlock()
m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
//TODO: the empty byte array will be replaced with configs per user
err := b.Put([]byte(user), []byte(""))
return err
})
slog.Info("added user to twitch monitor", slog.String("user", user)) slog.Info("added user to twitch monitor", slog.String("user", user))
} }
@@ -88,9 +105,15 @@ func (m *Monitor) GetMonitoredUsers() iter.Seq[string] {
func (m *Monitor) DeleteUser(user string) { func (m *Monitor) DeleteUser(user string) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock()
delete(m.monitored, user) delete(m.monitored, user)
delete(m.lastState, user) delete(m.lastState, user)
m.mu.Unlock()
m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
err := b.Delete([]byte(user))
return err
})
} }
func DEFAULT_DOWNLOAD_HANDLER(db *kv.Store, mq *queue.MessageQueue) func(user string) error { func DEFAULT_DOWNLOAD_HANDLER(db *kv.Store, mq *queue.MessageQueue) func(user string) error {
@@ -106,10 +129,6 @@ func DEFAULT_DOWNLOAD_HANDLER(db *kv.Store, mq *queue.MessageQueue) func(user st
) )
d := downloaders.NewLiveStreamDownloader(url, []pipes.Pipe{ d := downloaders.NewLiveStreamDownloader(url, []pipes.Pipe{
// &pipes.FileWriter{
// Path: filename + ".mp4",
// IsFinal: false,
// },
&pipes.Transcoder{ &pipes.Transcoder{
Args: []string{ Args: []string{
"-c:a", "libopus", "-c:a", "libopus",
@@ -130,42 +149,16 @@ func DEFAULT_DOWNLOAD_HANDLER(db *kv.Store, mq *queue.MessageQueue) func(user st
} }
} }
func (m *Monitor) Persist() error {
filename := filepath.Join(config.Instance().SessionFilePath, "twitch-monitor.dat")
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
enc := gob.NewEncoder(f)
users := make([]string, 0, len(m.monitored))
for user := range m.monitored {
users = append(users, user)
}
return enc.Encode(users)
}
func (m *Monitor) Restore() error { func (m *Monitor) Restore() error {
filename := filepath.Join(config.Instance().SessionFilePath, "twitch-monitor.dat")
f, err := os.Open(filename)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer f.Close()
dec := gob.NewDecoder(f)
var users []string var users []string
if err := dec.Decode(&users); err != nil {
return err m.db.View(func(tx *bolt.Tx) error {
} b := tx.Bucket(bucket)
return b.ForEach(func(k, v []byte) error {
users = append(users, string(k))
return nil
})
})
m.monitored = make(map[string]*Client) m.monitored = make(map[string]*Client)
for _, user := range users { for _, user := range users {