From afa4bc6c01297af78885bf0562e2dae7ff83605b Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Fri, 25 Oct 2024 14:06:42 -0400 Subject: cmd: add amano and stealthmountain Signed-off-by: Xe Iaso --- cmd/_old/sanguisuga/.gitignore | 2 + cmd/_old/sanguisuga/admin.go | 10 + cmd/_old/sanguisuga/config.default.ts | 114 ++++++ cmd/_old/sanguisuga/config.go | 99 ++++++ cmd/_old/sanguisuga/config_test.go | 14 + cmd/_old/sanguisuga/dcc.go | 426 ++++++++++++++++++++++ cmd/_old/sanguisuga/internal/dcc/dcc.go | 190 ++++++++++ cmd/_old/sanguisuga/js/scripts/iptorrents.js | 39 ++ cmd/_old/sanguisuga/js/scripts/subsplease.js | 50 +++ cmd/_old/sanguisuga/js/scripts/torrentleech.js | 39 ++ cmd/_old/sanguisuga/main.go | 443 +++++++++++++++++++++++ cmd/_old/sanguisuga/plex/plex.go | 106 ++++++ cmd/_old/sanguisuga/sanguisuga.templ | 346 ++++++++++++++++++ cmd/_old/sanguisuga/sanguisuga_templ.go | 199 +++++++++++ cmd/_old/sanguisuga/static/alpine.js | 5 + cmd/_old/sanguisuga/static/styles.css | 1 + cmd/_old/sanguisuga/tailwind.config.js | 14 + cmd/_old/sanguisuga/tv.go | 74 ++++ cmd/amano/main.go | 112 ++++++ cmd/sanguisuga/.gitignore | 2 - cmd/sanguisuga/admin.go | 10 - cmd/sanguisuga/config.default.ts | 114 ------ cmd/sanguisuga/config.go | 99 ------ cmd/sanguisuga/config_test.go | 14 - cmd/sanguisuga/dcc.go | 426 ---------------------- cmd/sanguisuga/internal/dcc/dcc.go | 190 ---------- cmd/sanguisuga/js/scripts/iptorrents.js | 39 -- cmd/sanguisuga/js/scripts/subsplease.js | 50 --- cmd/sanguisuga/js/scripts/torrentleech.js | 39 -- cmd/sanguisuga/main.go | 471 ------------------------- cmd/sanguisuga/plex/plex.go | 106 ------ cmd/sanguisuga/sanguisuga.templ | 346 ------------------ cmd/sanguisuga/sanguisuga_templ.go | 199 ----------- cmd/sanguisuga/static/alpine.js | 5 - cmd/sanguisuga/static/styles.css | 1 - cmd/sanguisuga/tailwind.config.js | 14 - cmd/sanguisuga/tv.go | 74 ---- cmd/stealthmountain/main.go | 152 ++++++++ cmd/within.website/main.go | 2 - cmd/xedn/cache.go | 3 - cmd/xedn/imgoptimize.go | 9 - cmd/xedn/main.go | 57 --- 42 files changed, 2435 insertions(+), 2270 deletions(-) create mode 100644 cmd/_old/sanguisuga/.gitignore create mode 100644 cmd/_old/sanguisuga/admin.go create mode 100644 cmd/_old/sanguisuga/config.default.ts create mode 100644 cmd/_old/sanguisuga/config.go create mode 100644 cmd/_old/sanguisuga/config_test.go create mode 100644 cmd/_old/sanguisuga/dcc.go create mode 100644 cmd/_old/sanguisuga/internal/dcc/dcc.go create mode 100644 cmd/_old/sanguisuga/js/scripts/iptorrents.js create mode 100644 cmd/_old/sanguisuga/js/scripts/subsplease.js create mode 100644 cmd/_old/sanguisuga/js/scripts/torrentleech.js create mode 100644 cmd/_old/sanguisuga/main.go create mode 100644 cmd/_old/sanguisuga/plex/plex.go create mode 100644 cmd/_old/sanguisuga/sanguisuga.templ create mode 100644 cmd/_old/sanguisuga/sanguisuga_templ.go create mode 100644 cmd/_old/sanguisuga/static/alpine.js create mode 100644 cmd/_old/sanguisuga/static/styles.css create mode 100644 cmd/_old/sanguisuga/tailwind.config.js create mode 100644 cmd/_old/sanguisuga/tv.go create mode 100644 cmd/amano/main.go delete mode 100644 cmd/sanguisuga/.gitignore delete mode 100644 cmd/sanguisuga/admin.go delete mode 100644 cmd/sanguisuga/config.default.ts delete mode 100644 cmd/sanguisuga/config.go delete mode 100644 cmd/sanguisuga/config_test.go delete mode 100644 cmd/sanguisuga/dcc.go delete mode 100644 cmd/sanguisuga/internal/dcc/dcc.go delete mode 100644 cmd/sanguisuga/js/scripts/iptorrents.js delete mode 100644 cmd/sanguisuga/js/scripts/subsplease.js delete mode 100644 cmd/sanguisuga/js/scripts/torrentleech.js delete mode 100644 cmd/sanguisuga/main.go delete mode 100644 cmd/sanguisuga/plex/plex.go delete mode 100644 cmd/sanguisuga/sanguisuga.templ delete mode 100644 cmd/sanguisuga/sanguisuga_templ.go delete mode 100644 cmd/sanguisuga/static/alpine.js delete mode 100644 cmd/sanguisuga/static/styles.css delete mode 100644 cmd/sanguisuga/tailwind.config.js delete mode 100644 cmd/sanguisuga/tv.go create mode 100644 cmd/stealthmountain/main.go (limited to 'cmd') diff --git a/cmd/_old/sanguisuga/.gitignore b/cmd/_old/sanguisuga/.gitignore new file mode 100644 index 0000000..5841fb6 --- /dev/null +++ b/cmd/_old/sanguisuga/.gitignore @@ -0,0 +1,2 @@ +config.ts +data.json diff --git a/cmd/_old/sanguisuga/admin.go b/cmd/_old/sanguisuga/admin.go new file mode 100644 index 0000000..65f231f --- /dev/null +++ b/cmd/_old/sanguisuga/admin.go @@ -0,0 +1,10 @@ +package main + +import ( + "embed" +) + +var ( + //go:embed static + static embed.FS +) diff --git a/cmd/_old/sanguisuga/config.default.ts b/cmd/_old/sanguisuga/config.default.ts new file mode 100644 index 0000000..77d887e --- /dev/null +++ b/cmd/_old/sanguisuga/config.default.ts @@ -0,0 +1,114 @@ +export type IRC = { + server: string; + password: string; + channel: string; + nick: string; + user: string; + real: string; +}; + +export type Show = { + title: string; + diskPath: string; + quality: string; +}; + +export type Transmission = { + host: string; + user: string; + password: string; + https: boolean; + rpcURI: string; +}; + +export type Tailscale = { + hostname: string; + authkey: string; + dataDir?: string; +}; + +export type Telegram = { + token: string; + mentionUser: number; +}; + +export type WireGuardPeer = { + publicKey: string; + endpoint: string; + allowedIPs: string[]; +}; + +export type WireGuard = { + privateKey: string; + address: string[]; + dns: string; + peers: WireGuardPeer[]; +}; + +export type Config = { + irc: IRC; + xdcc: IRC; + transmission: Transmission; + shows: Show[]; + rssKey: string; + tailscale: Tailscale; + baseDiskPath: string; + telegram: Telegram; + wireguard: WireGuard; +}; + +export default { + irc: { + server: "", + password: "", + channel: "", + nick: "", + user: "", + real: "" + }, + xdcc: { + server: "", + password: "", + channel: "", + nick: "", + user: "", + real: "" + }, + transmission: { + host: "", + user: "", + password: "", + https: true, + rpcURI: "/transmission/rpc" + }, + shows: [ + { + title: "Show Name", + diskPath: "/data/TV/Show Name", + quality: "1080p" + } + ], + rssKey: "", + tailscale: { + hostname: "sanguisuga-dev", + authkey: "", + dataDir: undefined, + }, + baseDiskPath: "/data/TV/", + telegram: { + token: "", + mentionUser: 0, + }, + wireguard: { // for downloading files over DCC (XDCC) + privateKey: "", + address: [], + dns: "", + peers: [ + { + publicKey: "", + allowedIPs: [], + endpoint: "", + }, + ], + }, +} satisfies Config; diff --git a/cmd/_old/sanguisuga/config.go b/cmd/_old/sanguisuga/config.go new file mode 100644 index 0000000..29e6b07 --- /dev/null +++ b/cmd/_old/sanguisuga/config.go @@ -0,0 +1,99 @@ +package main + +import ( + "fmt" + "io" + "log/slog" + "net/netip" + + "within.website/x/internal/key2hex" +) + +type IRC struct { + Server string `json:"server"` + Password string `json:"password"` + Channel string `json:"channel"` + Regex string `json:"regex"` + Nick string `json:"nick"` + User string `json:"user"` + Real string `json:"real"` +} + +type Show struct { + Title string `json:"title"` + DiskPath string `json:"diskPath"` + 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"` + Password string `json:"password"` +} + +type Tailscale struct { + Hostname string `json:"hostname"` + Authkey string `json:"authkey"` + DataDir *string `json:"dataDir,omitempty"` +} + +type Telegram struct { + Token string `json:"token"` + MentionUser int64 `json:"mentionUser"` +} + +type WireGuard struct { + PrivateKey string `json:"privateKey"` + Address []netip.Addr `json:"address"` + DNS netip.Addr `json:"dns"` + Peers []WireGuardPeer `json:"peers"` +} + +type WireGuardPeer struct { + PublicKey string `json:"publicKey"` + AllowedIPs []string `json:"allowedIPs"` + Endpoint string `json:"endpoint"` +} + +func (w WireGuard) UAPI(out io.Writer) error { + pkey, err := key2hex.Convert(w.PrivateKey) + if err != nil { + return err + } + fmt.Fprintf(out, "private_key=%s\n", pkey) + fmt.Fprintln(out, "listen_port=0") + fmt.Fprintln(out, "replace_peers=true") + for _, peer := range w.Peers { + pkey, err := key2hex.Convert(peer.PublicKey) + if err != nil { + return err + } + fmt.Fprintf(out, "public_key=%s\n", pkey) + fmt.Fprintf(out, "endpoint=%s\n", peer.Endpoint) + for _, ip := range peer.AllowedIPs { + fmt.Fprintf(out, "allowed_ip=%s\n", ip) + } + fmt.Fprintln(out, "persistent_keepalive_interval=25") + } + return nil +} + +type Config struct { + IRC IRC `json:"irc"` + XDCC IRC `json:"xdcc"` + Transmission Transmission `json:"transmission"` + Shows []Show `json:"shows"` + RSSKey string `json:"rssKey"` + Tailscale Tailscale `json:"tailscale"` + BaseDiskPath string `json:"baseDiskPath"` + Telegram Telegram `json:"telegram"` + WireGuard WireGuard `json:"wireguard"` +} diff --git a/cmd/_old/sanguisuga/config_test.go b/cmd/_old/sanguisuga/config_test.go new file mode 100644 index 0000000..f7d0f8c --- /dev/null +++ b/cmd/_old/sanguisuga/config_test.go @@ -0,0 +1,14 @@ +package main + +import ( + "testing" + + "go.jetpack.io/tyson" +) + +func TestDefaultConfig(t *testing.T) { + var c Config + if err := tyson.Unmarshal("./config.default.ts", &c); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/_old/sanguisuga/dcc.go b/cmd/_old/sanguisuga/dcc.go new file mode 100644 index 0000000..defab31 --- /dev/null +++ b/cmd/_old/sanguisuga/dcc.go @@ -0,0 +1,426 @@ +package main + +import ( + "context" + "encoding/binary" + "encoding/json" + "errors" + "expvar" + "fmt" + "hash/crc32" + "io" + "log" + "log/slog" + "net" + "net/http" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + irc "github.com/thoj/go-ircevent" + "tailscale.com/metrics" + "within.website/x/cmd/sanguisuga/internal/dcc" +) + +var ( + subspleaseAnnounceRegex = regexp.MustCompile(`^.*\* (?P\[SubsPlease\] (?P.*) - (?P[0-9]+) \((?P[0-9]{3,4})p\) \[(?P[0-9A-Fa-f]{8})\]\.mkv) \* /MSG (?P[^ ]+) XDCC SEND (?P[0-9]+)$`) + dccCommand = regexp.MustCompile(`^DCC SEND "(.*)" ([0-9]+) ([0-9]+) ([0-9]+)$`) + + bytesDownloaded = &metrics.LabelMap{Label: "filename"} +) + +func init() { + expvar.Publish("gauge_sanguisuga_bytes_downloaded", bytesDownloaded) +} + +type SubspleaseAnnouncement struct { + Filename string `json:"fname"` + ShowName string `json:"showName"` + Episode string `json:"episode"` + Resolution string `json:"resolution"` + CRC32 string `json:"crc32"` + BotName string `json:"botName"` + PackID string `json:"packID"` +} + +func (sa SubspleaseAnnouncement) Key() string { + return fmt.Sprintf("%s %s %s", sa.BotName, sa.ShowName, sa.Episode) +} + +func (sa SubspleaseAnnouncement) LogValue() slog.Value { + return slog.GroupValue( + slog.String("showname", sa.ShowName), + slog.String("episode", sa.Episode), + slog.String("resolution", sa.Resolution), + slog.String("botName", sa.BotName), + slog.String("crc32", sa.CRC32), + ) +} + +func ParseSubspleaseAnnouncement(input string) (*SubspleaseAnnouncement, error) { + re := subspleaseAnnounceRegex + matches := subspleaseAnnounceRegex.FindStringSubmatch(input) + + if matches == nil { + return nil, errors.New("invalid annoucement format") + } + + return &SubspleaseAnnouncement{ + Filename: matches[re.SubexpIndex("fname")], + ShowName: matches[re.SubexpIndex("showName")], + Episode: matches[re.SubexpIndex("episode")], + Resolution: matches[re.SubexpIndex("resolution")], + CRC32: matches[re.SubexpIndex("crc32")], + BotName: matches[re.SubexpIndex("botName")], + PackID: matches[re.SubexpIndex("packID")], + }, nil +} + +func int2ip(nn uint32) string { + ip := make(net.IP, 4) + binary.BigEndian.PutUint32(ip, nn) + return ip.String() +} + +func (s *Sanguisuga) XDCC() { + ircCli := irc.IRC(s.Config.XDCC.Nick, s.Config.XDCC.User) + ircCli.Password = s.Config.XDCC.Password + ircCli.RealName = s.Config.XDCC.Real + ircCli.AddCallback("001", func(ev *irc.Event) { + ircCli.Join(s.Config.XDCC.Channel) + }) + ircCli.AddCallback("PRIVMSG", s.ScrapeSubsplease) + ircCli.AddCallback("CTCP", s.SubspleaseDCC) + + ircCli.Log = slog.NewLogLogger(slog.Default().Handler().WithAttrs([]slog.Attr{slog.String("from", "ircevent"), slog.String("for", "anime")}), slog.LevelInfo) + ircCli.Timeout = 5 * time.Second + + if err := ircCli.Connect(s.Config.XDCC.Server); err != nil { + log.Fatalf("can't connect to XDCC server %s: %v", s.Config.XDCC.Server, err) + } + + 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() + + 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 = append(s.db.Data.AnimeWatch, show) + if err := s.db.Save(); err != nil { + slog.Error("can't save database", "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(s.db.Data.AnimeWatch) +} + +func (s *Sanguisuga) ListAnimeSnatches(w http.ResponseWriter, r *http.Request) { + s.dbLock.Lock() + defer s.dbLock.Unlock() + + json.NewEncoder(w).Encode(s.db.Data.AnimeSnatches) +} + +func (s *Sanguisuga) ListAnime(w http.ResponseWriter, r *http.Request) { + s.dbLock.Lock() + defer s.dbLock.Unlock() + + json.NewEncoder(w).Encode(s.db.Data.AnimeWatch) +} + +func (s *Sanguisuga) ScrapeSubsplease(ev *irc.Event) { + slog.Debug("chat line", "code", ev.Code, "channel", ev.Arguments[0], "msg", ev.MessageWithoutFormat()) + if ev.Code != "PRIVMSG" { + return + } + + if ev.Arguments[0] != s.Config.XDCC.Channel { + return + } + + switch ev.Nick { + case "CR-ARUTHA|NEW", "CR-HOLLAND|NEW", "Belath": + default: + return + } + + ann, err := ParseSubspleaseAnnouncement(ev.MessageWithoutFormat()) + if err != nil { + slog.Debug("can't parse announcement", "input", ev.MessageWithoutFormat(), "err", err) + return + } + + if ann.Resolution != "1080" { + return + } + + lg := slog.Default().With("announcement", ann) + lg.Info("found announcement") + + s.dbLock.Lock() + defer s.dbLock.Unlock() + + s.aifLock.Lock() + defer s.aifLock.Unlock() + + if _, ok := s.db.Data.AnimeSnatches[ann.Filename]; ok { + return + } + + found := false + for _, show := range s.db.Data.AnimeWatch { + if ann.ShowName == show.Title { + found = true + } + } + + if !found { + return + } + + // if already being fetched, don't fetch again + if _, ok := s.animeInFlight[ann.Filename]; ok { + return + } + + ev.Connection.Privmsgf(ann.BotName, "XDCC SEND %s", ann.PackID) + + s.animeInFlight[ann.Filename] = ann + + s.db.Data.AnimeSnatches[ann.Filename] = *ann + if err := s.db.Save(); err != nil { + lg.Error("can't save database", "err", err) + return + } +} +func (s *Sanguisuga) SubspleaseDCC(ev *irc.Event) { + go s.subspleaseDCC(ev) +} + +func (s *Sanguisuga) subspleaseDCC(ev *irc.Event) { + matches := dccCommand.FindStringSubmatch(ev.MessageWithoutFormat()) + if matches == nil { + return + } + + s.aifLock.Lock() + defer s.aifLock.Unlock() + + if len(matches) != 5 { + slog.Error("wrong message from DCC bot", "botName", ev.Nick, "message", ev.Message()) + return + } + + fname := matches[1] + ipString := matches[2] + port := matches[3] + sizeString := matches[4] + + if strings.HasSuffix(fname, "\"") { + fname = fname[:len(fname)-2] + } + + var ann *SubspleaseAnnouncement + t := time.NewTicker(25 * time.Millisecond) + defer t.Stop() + i := 0 +waitLoop: + for { + select { + case <-t.C: + ann, _ = s.animeInFlight[fname] + + if ann == nil { + continue + } else { + slog.Debug("found announcement", "ann", ann) + break waitLoop + } + + default: + if i >= 30 { + slog.Error("wanted to download file but we aren't watching for it", "fname", fname) + return + } + + } + } + + // TODO(Xe): fix for IPv6 + ipUint, err := strconv.ParseUint(ipString, 10, 32) + if err != nil { + slog.Error("can't parse IP address", "addr", ipString, "err", err) + return + } + + ip := int2ip(uint32(ipUint)) + addr := net.JoinHostPort(ip, port) + + size, err := strconv.Atoi(sizeString) + if err != nil { + slog.Error("can't parse size", "size", sizeString, "err", err) + return + } + + lg := slog.Default().With("fname", fname, "botName", ev.Nick, "addr", addr) + lg.Info("fetching episode") + + baseDir := "" + for _, show := range s.db.Data.AnimeWatch { + if ann.ShowName == show.Title { + baseDir = show.DiskPath + } + } + + outFname := filepath.Join(baseDir, fname) + + os.MkdirAll(baseDir, 0777) + + fout, err := os.Create(outFname) + if err != nil { + lg.Error("can't create output file", "outFname", outFname, "err", err) + return + } + defer fout.Close() + + d := dcc.NewDCC(addr, size, fout, s.tnet.DialContext) + + ctx, cancel := context.WithTimeout(ev.Ctx, 120*time.Minute) + defer cancel() + + start := time.Now() + progc, errc := d.Run(ctx) + +outer: + for { + select { + case p := <-progc: + curr := bytesDownloaded.GetFloat(fname) + curr.Set(p.CurrentFileSize) + + if p.CurrentFileSize == p.FileSize { + break outer + } + + if p.Percentage >= 100 { + break outer + } + + lg.Debug("download progress", "progress", p) + case err := <-errc: + lg.Error("error in DCC thread, giving up", "err", err) + delete(s.animeInFlight, fname) + return + } + } + + delete(s.animeInFlight, fname) + dur := time.Since(start) + + lg.Info("finished downloading", "dur", dur.String()) + + s.Notify(fmt.Sprintf("Fetched %s episode %s", ann.ShowName, ann.Episode)) + + _, err = crcCheck(outFname, ann.CRC32) + if err != nil { + lg.Error("got wrong hash", "err", err) + } + + lg.Debug("hash check passed") +} + +func crcCheck(fname, wantHash string) (bool, error) { + fin, err := os.Open(fname) + if err != nil { + return false, err + } + defer fin.Close() + + h := crc32.NewIEEE() + + if _, err := io.Copy(h, fin); err != nil { + return false, err + } + + gotHash := fmt.Sprintf("%X", h.Sum32()) + + if wantHash != gotHash { + return false, crcError{ + Want: wantHash, + Got: gotHash, + } + } + + return true, nil +} + +type crcError struct { + Want string + Got string +} + +func (c crcError) Error() string { + return fmt.Sprintf("crc32 didn't match: want %s, got %s", c.Want, c.Got) +} + +func (c crcError) LogValue() slog.Value { + return slog.GroupValue( + slog.String("type", "crc_error"), + slog.String("want", c.Want), + slog.String("got", c.Got), + ) +} diff --git a/cmd/_old/sanguisuga/internal/dcc/dcc.go b/cmd/_old/sanguisuga/internal/dcc/dcc.go new file mode 100644 index 0000000..82d2ff3 --- /dev/null +++ b/cmd/_old/sanguisuga/internal/dcc/dcc.go @@ -0,0 +1,190 @@ +package dcc + +import ( + "context" + "encoding/binary" + "io" + "log/slog" + "net" + "time" +) + +// Progress contains the progression of the +// download handled by the DCC client socket +type Progress struct { + Speed float64 + Percentage float64 + CurrentFileSize float64 + FileSize float64 +} + +func (p Progress) LogValue() slog.Value { + return slog.GroupValue( + slog.Float64("speed", p.Speed), + slog.Float64("percentage", p.Percentage), + slog.Float64("curr", p.CurrentFileSize), + slog.Float64("total", p.FileSize), + ) +} + +// DCC creates a new socket client instance where +// it'll download the DCC transaction into the +// specified io.Writer destination +type DCC struct { + // important properties + address string + size int + + // output channels used for the Run and the receiver methods() + // to avoid parameter passing + progressc chan Progress + done chan error + + // internal DCC socket connection + conn net.Conn + + // assigned context passed from the Run() method + ctx context.Context + + // destination writer + writer io.Writer + + // dial function + dialFunc func(ctx context.Context, network, address string) (net.Conn, error) +} + +// NewDCC creates a new DCC instance. +// the host, port are needed for the socket client connection +// the size is required so the download progress is calculated +// the writer is required to store the transaction fragments into +// the specified io.Writer +func NewDCC( + address string, + size int, + writer io.Writer, + dialFunc func(ctx context.Context, network, address string) (net.Conn, error), +) *DCC { + return &DCC{ + address: address, + size: size, + progressc: make(chan Progress, 1), + done: make(chan error, 1), + writer: writer, + dialFunc: dialFunc, + } +} + +func (d *DCC) progress(written float64, speed *float64) time.Time { + d.progressc <- Progress{ + Speed: written - *speed, + Percentage: (written / float64(d.size)) * 100, + CurrentFileSize: written, + FileSize: float64(d.size), + } + + *speed = float64(written) + + return time.Now() +} + +func (d *DCC) receive() { + defer func() { // close channels + close(d.done) + + // close the connection afterwards.. + d.conn.Close() + }() + + var ( + written int + speed float64 + buf = make([]byte, 30720) + reader = io.LimitReader(d.conn, int64(d.size)) + ticker = time.NewTicker(time.Second) + ) + + defer ticker.Stop() + +D: + for { + select { + case <-d.ctx.Done(): + d.done <- nil // send empty to notify the watchers that we're done + return // terminated.. + case <-ticker.C: + d.progress(float64(written), &speed) + // notify the other side about the state of the connection + writtenNetworkOrder := uint32(written) + if err := binary.Write(d.conn, binary.BigEndian, writtenNetworkOrder); err != nil { + if err == io.EOF { + err = nil + } + + d.progress(float64(written), &speed) + d.done <- err + + return + } + default: + n, err := reader.Read(buf) + + if err != nil { + if err == io.EOF { // empty out the error + err = nil + } + + d.progress(float64(written), &speed) + d.done <- err + + return + } + + if n > 0 { + _, err = d.writer.Write(buf[0:n]) + + if err != nil { + d.done <- err + return + } else if written >= d.size { // finished + break D + } + + written += n + } + } + } +} + +// Run established the connection with the DCC TCP socket +// and returns two channels, where one is used for the download progress +// and the other is used to return exceptions during our transaction. +// A context is required, where you have the ability to cancel and timeout +// a download. +// One should check the second value for the progress/error channels when +// receiving data as if the channels are closed, it means that the transaction +// is finished or got interrupted. +func (d *DCC) Run(ctx context.Context) ( + progressc <-chan Progress, + done <-chan error, +) { + // assign the output to the struct properties + progressc = d.progressc + done = d.done + + // assign the passed context + d.ctx = ctx + + conn, err := d.dialFunc(d.ctx, "tcp", d.address) + + if err != nil { + d.done <- err + return + } + + // setup the connection for the receiver + d.conn = conn + + go d.receive() + + return +} diff --git a/cmd/_old/sanguisuga/js/scripts/iptorrents.js b/cmd/_old/sanguisuga/js/scripts/iptorrents.js new file mode 100644 index 0000000..fd45abe --- /dev/null +++ b/cmd/_old/sanguisuga/js/scripts/iptorrents.js @@ -0,0 +1,39 @@ +const regex = + /^\[([^\]]*)]\s+(.*?(FREELEECH)?)\s+-\s+https?\:\/\/([^\/]+).*[&;\?]id=(\d+)\s*- (.*)$/; + +const genURL = (torrentName, baseURL, id, passkey) => + `https://${baseURL}/download.php/${id}/${torrentName}.torrent?torrent_pass=${passkey}`; + +export const allowLine = (nick, channel) => { + if (channel != "#ipt.announce") { + return false; + } + + if (nick !== "IPT") { + return false; + } + + return true; +}; + +export const parseLine = (msg) => { + const [ + _blank, + category, + torrentName, + freeleech, + baseURL, + id, + size, + ] = msg.split(regex); + + return { + torrent: { + category, + name: torrentName, + freeleech: freeleech !== "", + id: id, + url: genURL(torrentName, baseURL, id), + }, + }; +}; diff --git a/cmd/_old/sanguisuga/js/scripts/subsplease.js b/cmd/_old/sanguisuga/js/scripts/subsplease.js new file mode 100644 index 0000000..dd9566e --- /dev/null +++ b/cmd/_old/sanguisuga/js/scripts/subsplease.js @@ -0,0 +1,50 @@ +const regex = + /^.*\* (\[SubsPlease\] (.*) - ([0-9]+) \(([0-9]{3,4})p\) \[([0-9A-Fa-f]{8})\]\.mkv) \* .MSG ([^ ]+) XDCC SEND ([0-9]+)$/; + +const bots = [ + "CR-ARUTHA|NEW", + "CR-HOLLAND|NEW", +]; + +export const ircInfo = { + server: "irc.rizon.net:6697", + channel: "#subsplease", + downloadType: "DCC", +}; + +export const allowLine = (nick, channel) => { + if (channel != "#subsplease") { + return false; + } + + if (!bots.includes(nick)) { + return false; + } + + return true; +}; + +export const parseLine = (msg) => { + const [ + _blank, + fname, + showName, + episode, + resolution, + crc32, + botName, + packID, + ] = msg.split(regex); + + const result = { + fname, + showName, + episode, + resolution, + crc32, + botName, + packID, + }; + + return result; +}; diff --git a/cmd/_old/sanguisuga/js/scripts/torrentleech.js b/cmd/_old/sanguisuga/js/scripts/torrentleech.js new file mode 100644 index 0000000..7ddfa9e --- /dev/null +++ b/cmd/_old/sanguisuga/js/scripts/torrentleech.js @@ -0,0 +1,39 @@ +const regex = + /^New Torrent Announcement: <([^>]*)>\s+Name:'(.*)' uploaded by '.*' ?(freeleech)?\s+-\s+https:..\w+.\w+.\w+\/.\w+\/([0-9]+)$/; + +const genURL = (torrentName, baseURL, id, passkey) => + `https://www.torrentleech.org/rss/download/${id}/${passkey}/${torrentName}`; + +export const allowLine = (nick, channel) => { + if (channel !== "#tlannounces") { + return false; + } + + if (nick !== "_AnnounceBot_") { + return false; + } + + return true; +}; + +export const parseLine = (msg) => { + const [ + _blank, + category, + torrentName, + freeleech, + baseURL, + id, + size, + ] = msg.split(regex); + + return { + torrent: { + category, + name: torrentName, + freeleech: freeleech !== "", + id: id, + url: genURL(torrentName, baseURL, id), + }, + }; +}; diff --git a/cmd/_old/sanguisuga/main.go b/cmd/_old/sanguisuga/main.go new file mode 100644 index 0000000..06efced --- /dev/null +++ b/cmd/_old/sanguisuga/main.go @@ -0,0 +1,443 @@ +package main + +import ( + "bytes" + "encoding/base64" + "errors" + "expvar" + "flag" + "fmt" + "io" + "log" + "log/slog" + "net/http" + "net/netip" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "github.com/a-h/templ" + "github.com/mymmrac/telego" + tu "github.com/mymmrac/telego/telegoutil" + "github.com/tailscale/wireguard-go/conn" + "github.com/tailscale/wireguard-go/device" + "github.com/tailscale/wireguard-go/tun/netstack" + irc "github.com/thoj/go-ircevent" + "go.jetpack.io/tyson" + "honnef.co/go/transmission" + "tailscale.com/jsondb" + "tailscale.com/tsnet" + "within.website/x/internal" + "within.website/x/web/parsetorrentname" +) + +//go:generate tailwindcss --output static/styles.css --minify +//go:generate go run github.com/a-h/templ/cmd/templ@latest generate + +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?") + + crcCheckCLI = flag.Bool("crc-check", false, "if true, check args[0] against hash args[1]") + + annRegex = regexp.MustCompile(`^New Torrent Announcement: <([^>]*)>\s+Name:'(.*)' uploaded by '.*' ?(freeleech)?\s+-\s+https://\w+.\w+.\w+./\w+./([0-9]+)$`) + + snatches = expvar.NewInt("gauge_sanguisuga_snatches") +) + +func ConvertURL(torrentID, rssKey, name string) string { + name = strings.ReplaceAll(name, " ", ".") + ".torrent" + return fmt.Sprintf("https://www.torrentleech.org/rss/download/%s/%s/%s", torrentID, rssKey, name) +} + +type TorrentAnnouncement struct { + Category string + Name string + Freeleech bool + TorrentID string +} + +func ParseTorrentAnnouncement(input string) (*TorrentAnnouncement, error) { + match := annRegex.FindStringSubmatch(input) + + if match == nil { + return nil, errors.New("invalid torrent announcement format") + } + + torrent := &TorrentAnnouncement{ + Category: match[1], + Name: strings.TrimSpace(match[2]), + Freeleech: match[3] != "", + TorrentID: match[4], + } + + return torrent, nil +} + +func main() { + internal.HandleStartup() + + if *crcCheckCLI { + if flag.NArg() != 2 { + log.Fatalf("usage: %s ", os.Args[0]) + } + + fname := flag.Arg(0) + hash := flag.Arg(1) + + ok, err := crcCheck(fname, hash) + if err != nil { + log.Fatal(err) + } + log.Printf("hash status: %v", ok) + return + } + + var c Config + if err := tyson.Unmarshal(*tysonConfig, &c); err != nil { + log.Fatalf("can't unmarshal config: %v", err) + } + + db, err := jsondb.Open[State](*dbLoc) + if err != nil { + log.Fatalf("can't set up database: %v", err) + } + if db.Data == nil { + db.Data = &State{ + 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) + } + + var dataDir string + if c.Tailscale.DataDir != nil { + dataDir = *c.Tailscale.DataDir + } + _ = dataDir + + cl := &transmission.Client{ + Client: http.DefaultClient, + Endpoint: c.Transmission.URL, + Username: c.Transmission.User, + Password: c.Transmission.Password, + } + + if _, err := cl.SessionStats(); err != nil { + log.Fatalf("can't connect to transmission: %v", err) + } + + bot, err := telego.NewBot(c.Telegram.Token) + if err != nil { + log.Fatalf("can't connect to telegram: %v", err) + } + + defer bot.StopLongPolling() + + tun, tnet, err := netstack.CreateNetTUN( + c.WireGuard.Address, + []netip.Addr{c.WireGuard.DNS}, + 1280, + ) + if err != nil { + log.Fatalf("can't create tun: %v", err) + } + + var confSB bytes.Buffer + if err := c.WireGuard.UAPI(&confSB); err != nil { + log.Fatalf("can't write wireguard config: %v", err) + } + + dev := device.NewDevice(tun, conn.NewStdNetBind(), device.NewLogger(device.LogLevelError, "wireguard: ")) + if err := dev.IpcSetOperation(&confSB); err != nil { + log.Fatalf("can't set wireguard config: %v", err) + } + + s := &Sanguisuga{ + Config: c, + cl: cl, + db: db, + bot: bot, + tnet: tnet, + + animeInFlight: map[string]*SubspleaseAnnouncement{}, + } + + http.Handle("/{$}", templ.Handler(base("sanguisuga", indexPage()))) + http.Handle("/", templ.Handler(base("Not found", notFoundPage()))) + http.Handle("/anime", templ.Handler(base("Anime", animePage()))) + http.Handle("/tv", templ.Handler(base("TV", tvPage()))) + + 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.FileServer(http.FS(static))) + + go s.XDCC() + + ircCli := irc.IRC(c.IRC.Nick, c.IRC.User) + ircCli.Password = c.IRC.Password + ircCli.RealName = c.IRC.Real + ircCli.AddCallback("PRIVMSG", s.HandleIRCMessage) + ircCli.AddCallback("001", func(ev *irc.Event) { + ircCli.Join(c.IRC.Channel) + }) + ircCli.Log = slog.NewLogLogger(slog.Default().Handler().WithAttrs([]slog.Attr{slog.String("from", "ircevent")}), slog.LevelInfo) + ircCli.Timeout = 5 * time.Second + + if err := ircCli.Connect(c.IRC.Server); err != nil { + log.Fatalf("can't connect to IRC server %s: %v", c.IRC.Server, err) + } + + ircCli.Loop() +} + +type Sanguisuga struct { + Config Config + cl *transmission.Client + db *jsondb.DB[State] + dbLock sync.Mutex + bot *telego.Bot + tnet *netstack.Net + srv *tsnet.Server + + animeInFlight map[string]*SubspleaseAnnouncement + aifLock sync.Mutex +} + +func (s *Sanguisuga) Notify(msg string) { + s.bot.SendMessage(tu.Message(tu.ID(s.Config.Telegram.MentionUser), msg)) +} + +type State struct { + TVSnatches map[string]TorrentAnnouncement + TVWatch []Show + + AnimeSnatches map[string]SubspleaseAnnouncement + AnimeWatch []Show +} + +func (s *Sanguisuga) ExternalSeedAnime(ta *TorrentAnnouncement, lg *slog.Logger) { + fname := fmt.Sprintf("%s.mkv", ta.Category) + _ = fname + // make goroutine to delay until download is done, set up directory structure, hard link, and download torrent + + for { + s.aifLock.Lock() + _, ok := s.animeInFlight[fname] + s.aifLock.Unlock() + if ok { + time.Sleep(5 * time.Second) + continue + } else { + break + } + } + + s.dbLock.Lock() + ann, ok := s.db.Data.AnimeSnatches[fname] + s.dbLock.Unlock() + + if !ok { + lg.Debug("can't opportunistically external seed", "why", "episode not already snatched") + return + } + + s.dbLock.Lock() + var show Show + for _, trackShow := range s.db.Data.AnimeWatch { + if trackShow.Title == ann.ShowName { + show = trackShow + } + } + s.dbLock.Unlock() + + if show.Title == "" { + lg.Debug("can't opportunistically external seed", "why", "can't find show in database but we have a snatch?") + return + } + + torrentURL := ConvertURL(ta.TorrentID, s.Config.RSSKey, ta.Name) + + dirName := filepath.Join("/data", "Torrents", "seedHacking", "tl", ta.TorrentID) + + if err := os.Link(filepath.Join(show.DiskPath, fname), filepath.Join(dirName, fname)); err != nil { + lg.Error("can't set up seedhacking directory", "err", err) + return + } + + var buf bytes.Buffer + resp, err := http.Get(torrentURL) + if err != nil { + lg.Error("can't download torrent", "url", torrentURL, "err", err, "torrentID", ta.TorrentID) + return + } + + if resp.StatusCode != http.StatusOK { + lg.Error("got wrong status code", "want", http.StatusOK, "got", resp.StatusCode, "url", torrentURL, "torrentID", ta.TorrentID) + return + } + + defer resp.Body.Close() + if n, err := io.Copy(&buf, resp.Body); err != nil { + lg.Error("can't fetch torrent body", "url", torrentURL, "err", err, "torrentID", ta.TorrentID) + return + } else { + lg.Info("downloaded bytes", "n", n, "url", torrentURL) + } + + metaInfo := base64.StdEncoding.EncodeToString(buf.Bytes()) + + _, _, err = s.cl.AddTorrent(&transmission.NewTorrent{ + DownloadDir: dirName, + Metainfo: metaInfo, + Paused: false, + }) + if err != nil { + lg.Error("error adding torrent", "url", torrentURL, "err", err, "torrentID", ta.TorrentID) + return + } + + lg.Info("opportunistically external seeding") + snatches.Add(1) +} + +func (s *Sanguisuga) HandleIRCMessage(ev *irc.Event) { + // check if in channel + if ev.Code != "PRIVMSG" { + return + } + + if ev.Arguments[0] != s.Config.IRC.Channel { + return + } + + ta, err := ParseTorrentAnnouncement(ev.MessageWithoutFormat()) + if err != nil { + slog.Debug("can't parse torrent announcment", "err", err, "msg", ev.MessageWithoutFormat()) + return + } + + lg := slog.Default().With("category", ta.Category, "freeleech", ta.Freeleech, "name", ta.Name) + + lg.Debug("found torrent announcment") + + switch ta.Category { + case "Animation :: Anime": + if !*externalSeed { + return + } + + go s.ExternalSeedAnime(ta, lg) + case "TV :: Episodes HD": + ti, err := parsetorrentname.Parse(ta.Name) + if err != nil { + lg.Error("can't parse ShowMeta", "err", err) + return + } + id := fmt.Sprintf("S%02dE%02d", ti.Season, ti.Episode) + lg := lg.With("title", ti.Title, "id", id, "quality", ti.Resolution, "group", ti.Group) + lg.Debug("found ShowMeta") + + stateKey := fmt.Sprintf("%s %s", ti.Title, id) + + s.dbLock.Lock() + defer s.dbLock.Unlock() + + for _, show := range s.db.Data.TVWatch { + if s.db.Data == nil { + s.db.Data = &State{ + TVSnatches: map[string]TorrentAnnouncement{}, + } + } + if _, found := s.db.Data.TVSnatches[stateKey]; found { + lg.Info("already snatched", "title", ti.Title, "id", id) + return + } + + if show.Title != ti.Title { + lg.Debug("wrong name") + continue + } + + if show.Quality != ti.Resolution { + lg.Debug("wrong resolution") + continue + } + + torrentURL := ConvertURL(ta.TorrentID, s.Config.RSSKey, ta.Name) + + lg.Debug("found url", "url", torrentURL) + downloadDir := filepath.Join(show.DiskPath, fmt.Sprintf("Season %02d", ti.Season)) + + var buf bytes.Buffer + resp, err := http.Get(torrentURL) + if err != nil { + lg.Error("can't download torrent", "url", torrentURL, "err", err, "torrentID", ta.TorrentID) + continue + } + + if resp.StatusCode != http.StatusOK { + lg.Error("got wrong status code", "want", http.StatusOK, "got", resp.StatusCode, "url", torrentURL, "torrentID", ta.TorrentID) + continue + } + + defer resp.Body.Close() + if n, err := io.Copy(&buf, resp.Body); err != nil { + lg.Error("can't fetch torrent body", "url", torrentURL, "err", err, "torrentID", ta.TorrentID) + continue + } else { + lg.Info("downloaded bytes", "n", n, "url", torrentURL) + } + + metaInfo := base64.StdEncoding.EncodeToString(buf.Bytes()) + + t, dupe, err := s.cl.AddTorrent(&transmission.NewTorrent{ + DownloadDir: downloadDir, + Metainfo: metaInfo, + Paused: false, + }) + if err != nil { + lg.Error("error adding torrent", "url", torrentURL, "err", err, "torrentID", ta.TorrentID) + return + } + + lg.Info("added torrent", "title", ti.Title, "id", id, "path", downloadDir, "infohash", t.Hash, "tid", t.ID, "dupe", dupe) + snatches.Add(1) + + s.Notify(fmt.Sprintf("added torrent for %s %s", ti.Title, id)) + + s.db.Data.TVSnatches[stateKey] = *ta + if err := s.db.Save(); err != nil { + lg.Error("error saving state", "err", err) + } + } + } +} diff --git a/cmd/_old/sanguisuga/plex/plex.go b/cmd/_old/sanguisuga/plex/plex.go new file mode 100644 index 0000000..21b5afb --- /dev/null +++ b/cmd/_old/sanguisuga/plex/plex.go @@ -0,0 +1,106 @@ +package plex + +type Webhook struct { + Event string `json:"event"` + User bool `json:"user"` + Owner bool `json:"owner"` + Account Account `json:"Account"` + Server *Server `json:"Server"` + Player *Player `json:"Player"` + Metadata *Metadata `json:"Metadata"` +} + +type Account struct { + ID int `json:"id"` + Thumb string `json:"thumb"` + Title string `json:"title"` +} + +type Server struct { + Title string `json:"title"` + UUID string `json:"uuid"` +} + +type Player struct { + Local bool `json:"local"` + PublicAddress string `json:"publicAddress"` + Title string `json:"title"` + UUID string `json:"uuid"` +} + +type GUID0 struct { + ID string `json:"id"` +} + +type Rating struct { + Image string `json:"image"` + Value float64 `json:"value"` + Type string `json:"type"` +} + +type Director struct { + ID int `json:"id"` + Filter string `json:"filter"` + Tag string `json:"tag"` + TagKey string `json:"tagKey"` +} + +type Writer struct { + ID int `json:"id"` + Filter string `json:"filter"` + Tag string `json:"tag"` + TagKey string `json:"tagKey"` +} + +type Role struct { + ID int `json:"id"` + Filter string `json:"filter"` + Tag string `json:"tag"` + TagKey string `json:"tagKey"` + Role string `json:"role"` + Thumb string `json:"thumb"` +} + +type Metadata struct { + LibrarySectionType string `json:"librarySectionType"` + RatingKey string `json:"ratingKey"` + Key string `json:"key"` + ParentRatingKey string `json:"parentRatingKey"` + GrandparentRatingKey string `json:"grandparentRatingKey"` + GUID string `json:"guid"` + ParentGUID string `json:"parentGuid"` + GrandparentGUID string `json:"grandparentGuid"` + Type string `json:"type"` + Title string `json:"title"` + GrandparentKey string `json:"grandparentKey"` + ParentKey string `json:"parentKey"` + LibrarySectionTitle string `json:"librarySectionTitle"` + LibrarySectionID int `json:"librarySectionID"` + LibrarySectionKey string `json:"librarySectionKey"` + GrandparentTitle string `json:"grandparentTitle"` + ParentTitle string `json:"parentTitle"` + OriginalTitle string `json:"originalTitle"` + ContentRating string `json:"contentRating"` + Summary string `json:"summary"` + Index int `json:"index"` + ParentIndex int `json:"parentIndex"` + AudienceRating float64 `json:"audienceRating"` + ViewOffset int `json:"viewOffset"` + LastViewedAt int `json:"lastViewedAt"` + Year int `json:"year"` + Thumb string `json:"thumb"` + Art string `json:"art"` + ParentThumb string `json:"parentThumb"` + GrandparentThumb string `json:"grandparentThumb"` + GrandparentArt string `json:"grandparentArt"` + Duration int `json:"duration"` + OriginallyAvailableAt string `json:"originallyAvailableAt"` + AddedAt int `json:"addedAt"` + UpdatedAt int `json:"updatedAt"` + AudienceRatingImage string `json:"audienceRatingImage"` + GUID0 []GUID0 `json:"Guid"` + Rating []Rating `json:"Rating"` + Director []Director `json:"Director"` + Writer []Writer `json:"Writer"` + Role []Role `json:"Role"` +} diff --git a/cmd/_old/sanguisuga/sanguisuga.templ b/cmd/_old/sanguisuga/sanguisuga.templ new file mode 100644 index 0000000..55833bb --- /dev/null +++ b/cmd/_old/sanguisuga/sanguisuga.templ @@ -0,0 +1,346 @@ +package main + +templ base(title string, body templ.Component) { + + + + + + + + + { title } + + + + +
+ +

+ { title } +

+ @body + +
+ + +} + +templ notFoundPage() { +

If you expected to find a page here, this ain't it chief. Try going home.

+} + +templ indexPage() { +

+ Welcome to sanguisuga. This is a tool that will help you leech content from private trackers + and XDCC bots. The following options are available: +

+ +

Thank you for following the development of sanguisuga.

+} + +templ animePage() { +

Shows

+
+ + + + + + + + + +
TitleDisk Path
+
+

Downloads

+
+ +
+

{ "Every one of these files is saved in the snatchlist and should be available on Plex. If a file is not available, you may need to check if someone put RAR files in a torrent. Again." }

+
+ + + + + + + + + +
Show + Episode + Method
+
+
+
+

Track new show

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

+
+} + +templ tvPage() { +

Shows

+
+ + + + + + + + + +
+ Title + + Disk Path +
+
+

Downloads

+
+ +
+

+ { "Every one of these files is saved in the snatchlist and should be available on Plex. If a file is not available, you may need to check if someone put RAR files in a torrent. Again." } +

+
+ + + + + + + + +
+ Show + Episode + + Torrent Link +
+
+
+
+

+ Track new show +

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

+
+} diff --git a/cmd/_old/sanguisuga/sanguisuga_templ.go b/cmd/_old/sanguisuga/sanguisuga_templ.go new file mode 100644 index 0000000..f2bbaea --- /dev/null +++ b/cmd/_old/sanguisuga/sanguisuga_templ.go @@ -0,0 +1,199 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.731 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func base(title string, body templ.Component) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `sanguisuga.templ`, Line: 12, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `sanguisuga.templ`, Line: 33, Col: 12} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = body.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +func notFoundPage() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

If you expected to find a page here, this ain't it chief. Try going home.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +func indexPage() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Welcome to sanguisuga. This is a tool that will help you leech content from private trackers and XDCC bots. The following options are available:

Thank you for following the development of sanguisuga.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +func animePage() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Shows

TitleDisk Path