aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2025-01-07 23:41:53 -0500
committerXe Iaso <me@xeiaso.net>2025-01-11 16:46:28 -0500
commit6bc2938856792f851b18fb3550fdb4c2c8040fce (patch)
treee7a77aa35d9c94ae5ea7609c08f00b2b3ffca6eb /cmd
parentc2187ae96bf081aba31a965d0b58e5934a42a93f (diff)
downloadx-6bc2938856792f851b18fb3550fdb4c2c8040fce.tar.xz
x-6bc2938856792f851b18fb3550fdb4c2c8040fce.zip
kube/alrest: fix
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/orodyagzou/diagram.jpgbin0 -> 95741 bytes
-rw-r--r--cmd/patchouli/diagrams/architecture.pngbin0 -> 61811 bytes
-rw-r--r--cmd/patchouli/main.go134
-rw-r--r--cmd/patchouli/var/.gitignore2
-rw-r--r--cmd/patchouli/ytdlp/ytdlp.go113
5 files changed, 249 insertions, 0 deletions
diff --git a/cmd/orodyagzou/diagram.jpg b/cmd/orodyagzou/diagram.jpg
new file mode 100644
index 0000000..e557a73
--- /dev/null
+++ b/cmd/orodyagzou/diagram.jpg
Binary files differ
diff --git a/cmd/patchouli/diagrams/architecture.png b/cmd/patchouli/diagrams/architecture.png
new file mode 100644
index 0000000..f53c074
--- /dev/null
+++ b/cmd/patchouli/diagrams/architecture.png
Binary files differ
diff --git a/cmd/patchouli/main.go b/cmd/patchouli/main.go
new file mode 100644
index 0000000..a316a0c
--- /dev/null
+++ b/cmd/patchouli/main.go
@@ -0,0 +1,134 @@
+package main
+
+import (
+ "context"
+ "database/sql"
+ "flag"
+ "fmt"
+ "log"
+ "log/slog"
+ "net/http"
+ "path/filepath"
+
+ "connectrpc.com/connect"
+ _ "github.com/ncruces/go-sqlite3/embed"
+ "github.com/ncruces/go-sqlite3/gormlite"
+ slogGorm "github.com/orandin/slog-gorm"
+ "google.golang.org/protobuf/types/known/timestamppb"
+ "gorm.io/gorm"
+ gormPrometheus "gorm.io/plugin/prometheus"
+ "within.website/x/buf/patchouli"
+ "within.website/x/buf/patchouli/patchouliconnect"
+ "within.website/x/cmd/patchouli/ytdlp"
+ "within.website/x/internal"
+)
+
+var (
+ bind = flag.String("bind", ":2934", "HTTP bind address")
+ dataDir = flag.String("data-dir", "./var", "location to store data persistently")
+)
+
+func main() {
+ internal.HandleStartup()
+
+ slog.Info("starting up", "bind", *bind, "data-dir", *dataDir)
+
+ db, err := connectDB()
+ if err != nil {
+ log.Fatalf("can't connect to DB: %w", err)
+ }
+ _ = db
+
+ s := &Server{db: db}
+
+ compress1KB := connect.WithCompressMinBytes(1024)
+
+ mux := http.NewServeMux()
+ mux.Handle(patchouliconnect.NewSyndicateHandler(s, compress1KB))
+
+ slog.Info("listening", "url", "http://localhost"+*bind)
+ log.Fatal(http.ListenAndServe(*bind, mux))
+}
+
+func connectDB() (*gorm.DB, error) {
+ db, err := gorm.Open(gormlite.Open(filepath.Join(*dataDir, "data.db")), &gorm.Config{
+ Logger: slogGorm.New(
+ slogGorm.WithErrorField("err"),
+ slogGorm.WithRecordNotFoundError(),
+ ),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to database: %w", err)
+ }
+
+ if err := db.AutoMigrate(
+ &Video{},
+ ); err != nil {
+ return nil, fmt.Errorf("failed to migrate schema: %w", err)
+ }
+
+ db.Use(gormPrometheus.New(gormPrometheus.Config{
+ DBName: "mi",
+ }))
+
+ return db, nil
+}
+
+type Server struct {
+ db *gorm.DB
+}
+
+func (s *Server) Info(
+ ctx context.Context,
+ req *connect.Request[patchouli.TwitchInfoReq],
+) (
+ *connect.Response[patchouli.TwitchInfoResp],
+ error,
+) {
+ metadata, err := ytdlp.Metadata(ctx, req.Msg.GetUrl())
+ if err != nil {
+ slog.Error("can't fetch metadata for video", "url", req.Msg.GetUrl(), "err", err)
+ return nil, connect.NewError(connect.CodeInternal, err)
+ }
+
+ result := &patchouli.TwitchInfoResp{
+ Id: metadata.ID,
+ Title: metadata.Title,
+ ThumbnailUrl: metadata.Thumbnail,
+ Duration: metadata.DurationString,
+ UploadDate: timestamppb.New(metadata.UploadDate.Time),
+ Url: req.Msg.GetUrl(),
+ }
+
+ return connect.NewResponse(result), nil
+}
+
+func (s *Server) Download(
+ ctx context.Context,
+ req *connect.Request[patchouli.TwitchDownloadReq],
+) (
+ *connect.Response[patchouli.TwitchDownloadResp],
+ error,
+) {
+ dir := filepath.Join(*dataDir, "video")
+
+ if err := ytdlp.Download(ctx, req.Msg.GetUrl(), dir); err != nil {
+ return nil, err
+ }
+
+ result := &patchouli.TwitchDownloadResp{
+ Url: req.Msg.Url,
+ Location: dir,
+ }
+
+ return connect.NewResponse(result), nil
+}
+
+type Video struct {
+ gorm.Model
+ TwitchID string `gorm:"uniqueIndex"`
+ TwitchURL string `gorm:"uniqueIndex"`
+ Title string
+ State string
+ BlogPath sql.NullString
+}
diff --git a/cmd/patchouli/var/.gitignore b/cmd/patchouli/var/.gitignore
new file mode 100644
index 0000000..c96a04f
--- /dev/null
+++ b/cmd/patchouli/var/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore \ No newline at end of file
diff --git a/cmd/patchouli/ytdlp/ytdlp.go b/cmd/patchouli/ytdlp/ytdlp.go
new file mode 100644
index 0000000..096ea2b
--- /dev/null
+++ b/cmd/patchouli/ytdlp/ytdlp.go
@@ -0,0 +1,113 @@
+package ytdlp
+
+import (
+ "bytes"
+ "context"
+ "database/sql/driver"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "time"
+
+ "within.website/x/internal"
+)
+
+const dateFormat = "20060102"
+
+type VideoMetadata struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Thumbnail string `json:"thumbnail"`
+ DurationString string `json:"duration_string"`
+ UploadDate Date `json:"upload_date"`
+ URL string `json:"url" gorm:"uniqueIndex"`
+}
+
+type Date struct {
+ time.Time
+}
+
+func (d *Date) MarshalJSON() ([]byte, error) {
+ result := d.Format(dateFormat)
+
+ return []byte(fmt.Sprintf("%q", result)), nil
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ str := string(data)
+ str = str[1 : len(str)-1]
+
+ parsedTime, err := time.Parse(dateFormat, str)
+ if err != nil {
+ return err
+ }
+
+ d.Time = parsedTime
+ return nil
+}
+
+// Value implements the driver.Valuer interface
+func (d Date) Value() (driver.Value, error) {
+ return d.Format(dateFormat), nil
+}
+
+// Scan implements the sql.Scanner interface
+func (d *Date) Scan(value interface{}) error {
+ if value == nil {
+ *d = Date{Time: time.Time{}}
+ return nil
+ }
+
+ switch v := value.(type) {
+ case time.Time:
+ *d = Date{Time: v}
+ case []byte:
+ parsedTime, err := time.Parse(dateFormat, string(v))
+ if err != nil {
+ return err
+ }
+ *d = Date{Time: parsedTime}
+ case string:
+ parsedTime, err := time.Parse(dateFormat, v)
+ if err != nil {
+ return err
+ }
+ *d = Date{Time: parsedTime}
+ default:
+ return fmt.Errorf("cannot scan type %T into Date", value)
+ }
+
+ return nil
+}
+
+func Metadata(ctx context.Context, url string) (*VideoMetadata, error) {
+ result, err := internal.RunJSON[VideoMetadata](ctx, "yt-dlp", "--dump-json", url)
+ if err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
+
+func Download(ctx context.Context, url, to string) error {
+ exePath, err := exec.LookPath("yt-dlp")
+ if err != nil {
+ return fmt.Errorf("can't find yt-dlp: %w", err)
+ }
+
+ cmd := exec.CommandContext(ctx, exePath, "-o", filepath.Join(to, "%(id)s.%(ext)s"), "--write-info-json", url)
+
+ var stdout, stderr bytes.Buffer
+
+ cmd.Stdout = io.MultiWriter(&stdout, os.Stdout)
+ cmd.Stderr = io.MultiWriter(&stderr, os.Stderr)
+
+ if err := cmd.Run(); err != nil {
+ // TODO(Xe): return stdout/err?
+ return fmt.Errorf("can't download %s: %w", url, err)
+ }
+
+ return nil
+}