diff options
| author | Xe Iaso <me@xeiaso.net> | 2025-01-07 23:41:53 -0500 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2025-01-11 16:46:28 -0500 |
| commit | 6bc2938856792f851b18fb3550fdb4c2c8040fce (patch) | |
| tree | e7a77aa35d9c94ae5ea7609c08f00b2b3ffca6eb /cmd | |
| parent | c2187ae96bf081aba31a965d0b58e5934a42a93f (diff) | |
| download | x-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.jpg | bin | 0 -> 95741 bytes | |||
| -rw-r--r-- | cmd/patchouli/diagrams/architecture.png | bin | 0 -> 61811 bytes | |||
| -rw-r--r-- | cmd/patchouli/main.go | 134 | ||||
| -rw-r--r-- | cmd/patchouli/var/.gitignore | 2 | ||||
| -rw-r--r-- | cmd/patchouli/ytdlp/ytdlp.go | 113 |
5 files changed, 249 insertions, 0 deletions
diff --git a/cmd/orodyagzou/diagram.jpg b/cmd/orodyagzou/diagram.jpg Binary files differnew file mode 100644 index 0000000..e557a73 --- /dev/null +++ b/cmd/orodyagzou/diagram.jpg diff --git a/cmd/patchouli/diagrams/architecture.png b/cmd/patchouli/diagrams/architecture.png Binary files differnew file mode 100644 index 0000000..f53c074 --- /dev/null +++ b/cmd/patchouli/diagrams/architecture.png 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 +} |
