From 5d4aa1072f310640c8f06f815f5a51ab50c9f267 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Sat, 26 Aug 2023 18:43:24 -0400 Subject: cmd/sanguisuga: add UI with Pines Signed-off-by: Xe Iaso --- cmd/sanguisuga/admin.go | 55 +++++++++++++++++ cmd/sanguisuga/config.go | 10 +++ cmd/sanguisuga/dcc.go | 39 ++++++++++++ cmd/sanguisuga/main.go | 45 +++++++++++--- cmd/sanguisuga/static/alpine.js | 5 ++ cmd/sanguisuga/static/tailwind.js | 63 +++++++++++++++++++ cmd/sanguisuga/tmpl/404.html | 7 +++ cmd/sanguisuga/tmpl/anime_index.html | 114 ++++++++++++++++++++++++++++++++++ cmd/sanguisuga/tmpl/base.html | 38 ++++++++++++ cmd/sanguisuga/tmpl/index.html | 18 ++++++ cmd/sanguisuga/tmpl/tv_index.html | 116 +++++++++++++++++++++++++++++++++++ cmd/sanguisuga/tv.go | 74 ++++++++++++++++++++++ 12 files changed, 576 insertions(+), 8 deletions(-) create mode 100644 cmd/sanguisuga/admin.go create mode 100644 cmd/sanguisuga/static/alpine.js create mode 100644 cmd/sanguisuga/static/tailwind.js create mode 100644 cmd/sanguisuga/tmpl/404.html create mode 100644 cmd/sanguisuga/tmpl/anime_index.html create mode 100644 cmd/sanguisuga/tmpl/base.html create mode 100644 cmd/sanguisuga/tmpl/index.html create mode 100644 cmd/sanguisuga/tmpl/tv_index.html create mode 100644 cmd/sanguisuga/tv.go diff --git a/cmd/sanguisuga/admin.go b/cmd/sanguisuga/admin.go new file mode 100644 index 0000000..f66ec7c --- /dev/null +++ b/cmd/sanguisuga/admin.go @@ -0,0 +1,55 @@ +package main + +import ( + "embed" + "log/slog" + "net/http" +) + +var ( + //go:embed tmpl/* + templates embed.FS + + // go:embed static + static embed.FS +) + +func (s *Sanguisuga) AdminIndex(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + w.WriteHeader(http.StatusNotFound) + s.tmpl.ExecuteTemplate(w, "404.tmpl", struct { + Title string + }{ + Title: "Not found: " + r.URL.Path, + }) + + return + } + s.tmpl.ExecuteTemplate(w, "index.html", struct { + Title string + }{ + Title: "sanguisuga", + }) +} + +func (s *Sanguisuga) AdminAnimeList(w http.ResponseWriter, r *http.Request) { + err := s.tmpl.ExecuteTemplate(w, "anime_index.html", struct { + Title string + }{ + Title: "Anime", + }) + if err != nil { + slog.Error("can't render template", "err", err) + } +} + +func (s *Sanguisuga) AdminTVList(w http.ResponseWriter, r *http.Request) { + err := s.tmpl.ExecuteTemplate(w, "tv_index.html", struct { + Title string + }{ + Title: "TV", + }) + if err != nil { + slog.Error("can't render template", "err", err) + } +} diff --git a/cmd/sanguisuga/config.go b/cmd/sanguisuga/config.go index 478dfad..e2e4965 100644 --- a/cmd/sanguisuga/config.go +++ b/cmd/sanguisuga/config.go @@ -1,5 +1,7 @@ package main +import "log/slog" + type IRC struct { Server string `json:"server"` Password string `json:"password"` @@ -16,6 +18,14 @@ type Show struct { Quality string `json:"quality"` } +func (s Show) LogValue() slog.Value { + return slog.GroupValue( + slog.String("title", s.Title), + slog.String("disk_path", s.DiskPath), + slog.String("quality", s.Quality), + ) +} + type Transmission struct { URL string `json:"url"` User string `json:"user"` diff --git a/cmd/sanguisuga/dcc.go b/cmd/sanguisuga/dcc.go index c132ea9..c6ef755 100644 --- a/cmd/sanguisuga/dcc.go +++ b/cmd/sanguisuga/dcc.go @@ -105,6 +105,45 @@ func (s *Sanguisuga) XDCC() { ircCli.Loop() } +func remove[T any](l []T, remove func(T) bool) []T { + out := make([]T, 0) + for _, element := range l { + if !remove(element) { + out = append(out, element) + } + } + return out +} + +func (s *Sanguisuga) UntrackAnime(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + var show Show + err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&show) + if err != nil { + slog.Error("can't read request body", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + s.dbLock.Lock() + defer s.dbLock.Unlock() + + s.db.Data.AnimeWatch = remove(s.db.Data.AnimeWatch, func(s Show) bool { + return s.Title == show.Title + }) + + slog.Info("no longer tracking anime", "show", show) + + if err := s.db.Save(); err != nil { + slog.Error("can't save database", "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + func (s *Sanguisuga) TrackAnime(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() diff --git a/cmd/sanguisuga/main.go b/cmd/sanguisuga/main.go index dc067e6..7fc23d4 100644 --- a/cmd/sanguisuga/main.go +++ b/cmd/sanguisuga/main.go @@ -7,6 +7,7 @@ import ( "expvar" "flag" "fmt" + "html/template" "io" "log" "log/slog" @@ -34,6 +35,7 @@ var ( dbLoc = flag.String("db-loc", "./data.json", "path to data file") tysonConfig = flag.String("tyson-config", "./config.ts", "path to configuration secrets (TySON)") externalSeed = flag.Bool("external-seed", false, "try to external seed?") + tsnetVerbose = flag.Bool("tsnet-verbose", false, "enable verbose tsnet logging") crcCheckCLI = flag.Bool("crc-check", false, "if true, check args[0] against hash args[1]") @@ -102,14 +104,24 @@ func main() { } if db.Data == nil { db.Data = &State{ - Seen: map[string]TorrentAnnouncement{}, + TVSnatches: map[string]TorrentAnnouncement{}, + AnimeSnatches: map[string]SubspleaseAnnouncement{}, + TVWatch: c.Shows, } } + if db.Data.TVSnatches == nil { + db.Data.TVSnatches = map[string]TorrentAnnouncement{} + } + if db.Data.AnimeSnatches == nil { db.Data.AnimeSnatches = map[string]SubspleaseAnnouncement{} } + if len(db.Data.TVWatch) == 0 { + db.Data.TVWatch = c.Shows + } + if err := db.Save(); err != nil { log.Fatalf("can't ping database: %v", err) } @@ -124,7 +136,10 @@ func main() { AuthKey: c.Tailscale.Authkey, Hostname: c.Tailscale.Hostname, Logf: func(string, ...any) {}, - //Logf: slog.NewLogLogger(slog.Default().Handler().WithAttrs([]slog.Attr{slog.String("from", "tsnet")}), slog.LevelDebug).Printf, + } + + if *tsnetVerbose { + srv.Logf = slog.NewLogLogger(slog.Default().Handler().WithAttrs([]slog.Attr{slog.String("from", "tsnet")}), slog.LevelDebug).Printf } if err := srv.Start(); err != nil { @@ -164,13 +179,26 @@ func main() { cl: cl, db: db, bot: bot, + tmpl: template.Must(template.ParseFS(templates, "tmpl/*.html")), animeInFlight: map[string]*SubspleaseAnnouncement{}, } + http.HandleFunc("/", s.AdminIndex) + http.HandleFunc("/anime", s.AdminAnimeList) + http.HandleFunc("/tv", s.AdminTVList) + http.HandleFunc("/api/anime/list", s.ListAnime) http.HandleFunc("/api/anime/snatches", s.ListAnimeSnatches) http.HandleFunc("/api/anime/track", s.TrackAnime) + http.HandleFunc("/api/anime/untrack", s.UntrackAnime) + + http.HandleFunc("/api/tv/list", s.ListTV) + http.HandleFunc("/api/tv/snatches", s.ListTVSnatches) + http.HandleFunc("/api/tv/track", s.TrackTV) + http.HandleFunc("/api/tv/untrack", s.UntrackTV) + + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(static)))) go s.XDCC() @@ -197,6 +225,7 @@ type Sanguisuga struct { db *jsondb.DB[State] dbLock sync.Mutex bot *telego.Bot + tmpl *template.Template animeInFlight map[string]*SubspleaseAnnouncement aifLock sync.Mutex @@ -207,8 +236,8 @@ func (s *Sanguisuga) Notify(msg string) { } type State struct { - // Name + " " + SeasonEpisode -> TorrentAnnouncement - Seen map[string]TorrentAnnouncement + TVSnatches map[string]TorrentAnnouncement + TVWatch []Show AnimeSnatches map[string]SubspleaseAnnouncement AnimeWatch []Show @@ -341,13 +370,13 @@ func (s *Sanguisuga) HandleIRCMessage(ev *irc.Event) { s.dbLock.Lock() defer s.dbLock.Unlock() - for _, show := range s.Config.Shows { + for _, show := range s.db.Data.TVWatch { if s.db.Data == nil { s.db.Data = &State{ - Seen: map[string]TorrentAnnouncement{}, + TVSnatches: map[string]TorrentAnnouncement{}, } } - if _, found := s.db.Data.Seen[stateKey]; found { + if _, found := s.db.Data.TVSnatches[stateKey]; found { lg.Info("already snatched", "title", ti.Title, "id", id) return } @@ -404,7 +433,7 @@ func (s *Sanguisuga) HandleIRCMessage(ev *irc.Event) { s.Notify(fmt.Sprintf("added torrent for %s %s", ti.Title, id)) - s.db.Data.Seen[stateKey] = *ta + s.db.Data.TVSnatches[stateKey] = *ta if err := s.db.Save(); err != nil { lg.Error("error saving state", "err", err) } diff --git a/cmd/sanguisuga/static/alpine.js b/cmd/sanguisuga/static/alpine.js new file mode 100644 index 0000000..e2322d5 --- /dev/null +++ b/cmd/sanguisuga/static/alpine.js @@ -0,0 +1,5 @@ +(()=>{var Ze=!1,Qe=!1,V=[],et=-1;function Kt(e){bn(e)}function bn(e){V.includes(e)||V.push(e),wn()}function we(e){let t=V.indexOf(e);t!==-1&&t>et&&V.splice(t,1)}function wn(){!Qe&&!Ze&&(Ze=!0,queueMicrotask(En))}function En(){Ze=!1,Qe=!0;for(let e=0;ee.effect(t,{scheduler:r=>{tt?Kt(r):r()}}),rt=e.raw}function nt(e){D=e}function Vt(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),L(i))},i},()=>{t()}]}function q(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function O(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>O(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)O(n,t,!1),n=n.nextElementSibling}function S(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var qt=!1;function Ut(){qt&&S("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),qt=!0,document.body||S("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `