aboutsummaryrefslogtreecommitdiff
path: root/cmd/_old/sanguisuga
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2024-10-25 14:06:42 -0400
committerXe Iaso <me@xeiaso.net>2024-10-25 14:06:42 -0400
commitafa4bc6c01297af78885bf0562e2dae7ff83605b (patch)
tree97a1149d5646cf9b1c7aa2892d6e849c589219cc /cmd/_old/sanguisuga
parent797eec6d94e193ae684db977179ea4a422b2499f (diff)
downloadx-afa4bc6c01297af78885bf0562e2dae7ff83605b.tar.xz
x-afa4bc6c01297af78885bf0562e2dae7ff83605b.zip
cmd: add amano and stealthmountain
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd/_old/sanguisuga')
-rw-r--r--cmd/_old/sanguisuga/.gitignore2
-rw-r--r--cmd/_old/sanguisuga/admin.go10
-rw-r--r--cmd/_old/sanguisuga/config.default.ts114
-rw-r--r--cmd/_old/sanguisuga/config.go99
-rw-r--r--cmd/_old/sanguisuga/config_test.go14
-rw-r--r--cmd/_old/sanguisuga/dcc.go426
-rw-r--r--cmd/_old/sanguisuga/internal/dcc/dcc.go190
-rw-r--r--cmd/_old/sanguisuga/js/scripts/iptorrents.js39
-rw-r--r--cmd/_old/sanguisuga/js/scripts/subsplease.js50
-rw-r--r--cmd/_old/sanguisuga/js/scripts/torrentleech.js39
-rw-r--r--cmd/_old/sanguisuga/main.go443
-rw-r--r--cmd/_old/sanguisuga/plex/plex.go106
-rw-r--r--cmd/_old/sanguisuga/sanguisuga.templ346
-rw-r--r--cmd/_old/sanguisuga/sanguisuga_templ.go199
-rw-r--r--cmd/_old/sanguisuga/static/alpine.js5
-rw-r--r--cmd/_old/sanguisuga/static/styles.css1
-rw-r--r--cmd/_old/sanguisuga/tailwind.config.js14
-rw-r--r--cmd/_old/sanguisuga/tv.go74
18 files changed, 2171 insertions, 0 deletions
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<fname>\[SubsPlease\] (?P<showName>.*) - (?P<episode>[0-9]+) \((?P<resolution>[0-9]{3,4})p\) \[(?P<crc32>[0-9A-Fa-f]{8})\]\.mkv) \* /MSG (?P<botName>[^ ]+) XDCC SEND (?P<packID>[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],
+ }
+<