refactor: rename from mj2n to mje

This commit is contained in:
2022-09-15 23:44:32 +01:00
parent b0e52fc498
commit 7fdd61b54c
14 changed files with 389 additions and 376 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,3 @@
.env*
bin/*
mj2n
mje

215
Makefile Normal file
View File

@@ -0,0 +1,215 @@
NAME = mje
BINARY = bin/${NAME}
GOMODNAME := $(shell grep 'module' go.mod | sed -e 's/^module //')
SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \
-or -name "Makefile")
# Verbose output
ifdef VERBOSE
V = -v
endif
#
# Environment
#
BINDIR := bin
TOOLDIR := $(BINDIR)/tools
# Global environment variables for all targets
SHELL ?= /bin/bash
SHELL := env \
GO111MODULE=on \
GOBIN=$(CURDIR)/$(TOOLDIR) \
CGO_ENABLED=1 \
PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \
$(SHELL)
#
# Defaults
#
# Default target
.DEFAULT_GOAL := test
#
# Tools
#
# external tool
define tool # 1: binary-name, 2: go-import-path
TOOLS += $(TOOLDIR)/$(1)
$(TOOLDIR)/$(1): Makefile
GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)"
endef
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc@latest))
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@latest))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.49))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
$(eval $(call tool,mockgen,github.com/golang/mock/mockgen@v1.6.0))
.PHONY: tools
tools: $(TOOLS)
#
# Build
#
VERSION ?= $(shell git describe --tags --exact 2>/dev/null)
ifeq ($(VERSION),)
VERSION = 0.0.0-dev
endif
$(BINARY): $(SOURCES)
CGO_ENABLED=0 go build -a -o ${BINARY} -ldflags \ "\
-s -w \
-X main.version=${VERSION} \
-X main.commit=$(shell git show --format="%h" --no-patch) \
-X main.date=$(shell date +%Y-%m-%dT%T%z)"
.PHONY: build
build: $(BINARY)
#
# Development
#
BENCH ?= .
TESTARGS ?=
.PHONY: clean
clean:
rm -f $(TOOLS)
rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check
.PHONY: test
test:
go test $(V) -count=1 -race $(TESTARGS) ./...
.PHONY: test-integration
test-integration:
env USE_ZFS=1 go test $(V) -count=1 $(TESTARGS) -run=^TestIntegration .
.PHONY: test-deps
test-deps:
go test all
.PHONY: lint
lint: $(TOOLDIR)/golangci-lint
golangci-lint $(V) run
.PHONY: format
format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt
goimports -w . && gofumpt -w .
.SILENT: bench
.PHONY: bench
bench:
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./...
#
# Code Generation
#
.PHONY: generate
generate: $(TOOLDIR)/mockgen
go generate ./...
.PHONY: check-generate
check-generate: $(TOOLDIR)/mockgen
$(eval CHKDIR := $(shell mktemp -d))
cp -av . "$(CHKDIR)"
make -C "$(CHKDIR)/" generate
( diff -rN . "$(CHKDIR)" && rm -rf "$(CHKDIR)" ) || \
( rm -rf "$(CHKDIR)" && exit 1 )
#
# Coverage
#
.PHONY: cov
cov: coverage.out
.PHONY: cov-html
cov-html: coverage.out
go tool cover -html=./coverage.out
.PHONY: cov-func
cov-func: coverage.out
go tool cover -func=./coverage.out
coverage.out: $(SOURCES)
go test $(V) -covermode=count -coverprofile=./coverage.out ./...
#
# Dependencies
#
.PHONY: deps
deps:
go mod download
.PHONY: deps-update
deps-update:
go get -u -t ./...
.PHONY: deps-analyze
deps-analyze: $(TOOLDIR)/gomod
gomod analyze
.PHONY: tidy
tidy:
go mod tidy $(V)
.PHONY: verify
verify:
go mod verify
.SILENT: check-tidy
.PHONY: check-tidy
check-tidy:
cp go.mod go.mod.tidy-check
cp go.sum go.sum.tidy-check
go mod tidy
( \
diff go.mod go.mod.tidy-check && \
diff go.sum go.sum.tidy-check && \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum \
) || ( \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum; \
exit 1 \
)
#
# Documentation
#
# Serve docs
.PHONY: docs
docs: $(TOOLDIR)/godoc
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
@godoc -http=127.0.0.1:6060
#
# Release
#
.PHONY: new-version
new-version: check-npx
npx standard-version
.PHONY: next-version
next-version: check-npx
npx standard-version --dry-run
.PHONY: check-npx
check-npx:
$(if $(shell which npx),,\
$(error No npx found in PATH, please install NodeJS))

View File

@@ -1,7 +1,7 @@
package commands
import (
"github.com/jimeh/mj2n/midjourney"
"github.com/jimeh/go-midjourney"
"github.com/spf13/cobra"
)
@@ -9,15 +9,22 @@ func NewMidjourney(mc *midjourney.Client) (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "midjourney",
Aliases: []string{"mj"},
Short: "MidJourney specific commands",
Short: "Query the Midjourney API directly",
}
recentJobsCmd, err := NewMidjourneyRecentJobs(mc)
if err != nil {
return nil, err
}
wordsCmd, err := NewMidjourneyWords(mc)
if err != nil {
return nil, err
}
cmd.AddCommand(recentJobsCmd)
cmd.AddCommand(
recentJobsCmd,
wordsCmd,
)
return cmd, nil
}

View File

@@ -1,7 +1,9 @@
package commands
import (
"github.com/jimeh/mj2n/midjourney"
"time"
"github.com/jimeh/go-midjourney"
"github.com/spf13/cobra"
)
@@ -18,7 +20,8 @@ func NewMidjourneyRecentJobs(mc *midjourney.Client) (*cobra.Command, error) {
cmd.Flags().StringP("type", "t", "", "type of jobs to list")
cmd.Flags().StringP("order", "o", "new", "either \"new\" or \"oldest\"")
cmd.Flags().StringP("user-id", "u", "", "user ID to list jobs for")
cmd.Flags().StringP("page", "p", "", "page to fetch")
cmd.Flags().IntP("page", "p", 0, "page to fetch")
cmd.Flags().StringP("prompt", "s", "", "prompt text to search for")
cmd.Flags().Bool("dedupe", true, "dedupe results")
return cmd, nil
@@ -44,6 +47,9 @@ func midjourneyRecentJobsRunE(mc *midjourney.Client) runEFunc {
if v, err := fs.GetInt("page"); err == nil && v != 0 {
q.Page = v
}
if v, err := fs.GetString("prompt"); err == nil && v != "" {
q.Prompt = v
}
if v, err := fs.GetBool("dedupe"); err == nil {
q.Dedupe = v
}
@@ -59,7 +65,7 @@ func midjourneyRecentJobsRunE(mc *midjourney.Client) runEFunc {
ID: j.ID,
Status: string(j.CurrentStatus),
Type: string(j.Type),
EnqueueTime: j.EnqueueTime,
EnqueueTime: j.EnqueueTime.Time,
Prompt: j.Prompt,
ImagePaths: j.ImagePaths,
IsPublished: j.IsPublished,
@@ -76,15 +82,15 @@ func midjourneyRecentJobsRunE(mc *midjourney.Client) runEFunc {
}
type MidjourneyJob struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Status string `json:"current_status,omitempty" yaml:"current_status,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
EnqueueTime string `json:"enqueue_time,omitempty" yaml:"enqueue_time,omitempty"`
Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty"`
ImagePaths []string `json:"image_paths,omitempty" yaml:"image_paths,omitempty"`
IsPublished bool `json:"is_published,omitempty" yaml:"is_published,omitempty"`
UserID string `json:"user_id,omitempty" yaml:"user_id,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
FullCommand string `json:"full_command,omitempty" yaml:"full_command,omitempty"`
ReferenceJobID string `json:"reference_job_id,omitempty" yaml:"reference_job_id,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Status string `json:"current_status,omitempty" yaml:"current_status,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
EnqueueTime time.Time `json:"enqueue_time,omitempty" yaml:"enqueue_time,omitempty"`
Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty"`
ImagePaths []string `json:"image_paths,omitempty" yaml:"image_paths,omitempty"`
IsPublished bool `json:"is_published,omitempty" yaml:"is_published,omitempty"`
UserID string `json:"user_id,omitempty" yaml:"user_id,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
FullCommand string `json:"full_command,omitempty" yaml:"full_command,omitempty"`
ReferenceJobID string `json:"reference_job_id,omitempty" yaml:"reference_job_id,omitempty"`
}

View File

@@ -0,0 +1,70 @@
package commands
import (
"sort"
"github.com/jimeh/go-midjourney"
"github.com/spf13/cobra"
)
func NewMidjourneyWords(mc *midjourney.Client) (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "words",
Aliases: []string{"w"},
Short: "Get dictionary words",
RunE: midjourneyWordsRunE(mc),
}
cmd.Flags().StringP("format", "f", "", "output format (yaml or json)")
cmd.Flags().StringP("query", "q", "", "query to search for")
cmd.Flags().IntP("amount", "a", 50, "amount of words to fetch")
cmd.Flags().IntP("page", "p", 0, "page to fetch")
cmd.Flags().IntP("seed", "s", 0, "seed")
return cmd, nil
}
func midjourneyWordsRunE(mc *midjourney.Client) runEFunc {
return func(cmd *cobra.Command, _ []string) error {
fs := cmd.Flags()
q := &midjourney.WordsQuery{}
if v, err := fs.GetString("query"); err == nil && v != "" {
q.Query = v
}
if v, err := fs.GetInt("amount"); err == nil && v > 0 {
q.Amount = v
}
if v, err := fs.GetInt("page"); err == nil && v != 0 {
q.Page = v
}
if v, err := fs.GetInt("seed"); err == nil && v != 0 {
q.Seed = v
}
words, err := mc.Words(cmd.Context(), q)
if err != nil {
return err
}
r := []*MidjourneyWord{}
for _, w := range words {
r = append(r, &MidjourneyWord{
Word: w.Word,
ImageURL: w.ImageURL(),
})
}
format := flagString(cmd, "format")
sort.SliceStable(r, func(i, j int) bool {
return r[i].Word < r[j].Word
})
return render(cmd.OutOrStdout(), format, r)
}
}
type MidjourneyWord struct {
Word string `json:"word"`
ImageURL string `json:"image_url"`
}

View File

@@ -1,10 +1,11 @@
package commands
import (
"fmt"
"io"
"os"
"github.com/jimeh/mj2n/midjourney"
"github.com/jimeh/go-midjourney"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
@@ -12,27 +13,43 @@ import (
type runEFunc func(cmd *cobra.Command, _ []string) error
func NewMJ2N() (*cobra.Command, error) {
mc, err := midjourney.New(midjourney.WithUserAgent("mj2n/0.0.1-dev"))
type Info struct {
Version string
Commit string
Date string
}
func New(info Info) (*cobra.Command, error) {
if info.Version == "" {
info.Version = "0.0.0-dev"
}
mc, err := midjourney.New(midjourney.WithUserAgent("mje/" + info.Version))
if err != nil {
return nil, err
}
cmd := &cobra.Command{
Use: "mj2n",
Short: "MidJourney to Notion importer",
Use: "mje",
Short: "MidJourney exporter",
Version: info.Version,
PersistentPreRunE: persistentPreRunE(mc),
}
cmd.PersistentFlags().StringP(
"log-level", "l", "info",
cmd.PersistentFlags().String(
"log-level", "info",
"one of: trace, debug, info, warn, error, fatal, panic",
)
cmd.PersistentFlags().StringP(
"mj-token", "m", "", "MidJourney API token",
cmd.PersistentFlags().String(
"log-format", "plain",
"one of: plain, json",
)
cmd.PersistentFlags().String(
"mj-api-url", midjourney.DefaultAPIURL.String(), "MidJourney API URL",
"token", "", "MidJourney token",
)
cmd.PersistentFlags().String(
"api-url", midjourney.DefaultAPIURL.String(),
"MidJourney API URL",
)
midjourneyCmd, err := NewMidjourney(mc)
@@ -66,13 +83,13 @@ func setupMidJourney(cmd *cobra.Command, mc *midjourney.Client) error {
midjourney.WithLogger(log.Logger),
}
if f := cmd.Flag("mj-token"); f.Changed {
if f := cmd.Flag("token"); f != nil && f.Changed {
opts = append(opts, midjourney.WithAuthToken(f.Value.String()))
} else if v := os.Getenv("MIDJOURNEY_TOKEN"); v != "" {
opts = append(opts, midjourney.WithAuthToken(v))
}
apiURL := flagString(cmd, "mj-api-url")
apiURL := flagString(cmd, "api-url")
if apiURL == "" {
apiURL = os.Getenv("MIDJOURNEY_API_URL")
}
@@ -85,11 +102,16 @@ func setupMidJourney(cmd *cobra.Command, mc *midjourney.Client) error {
func setupZerolog(cmd *cobra.Command) error {
var levelStr string
if v := os.Getenv("MJ2N_DEBUG"); v != "" {
var logFormat string
if v := os.Getenv("MJE_DEBUG"); v != "" {
levelStr = "debug"
} else if v := os.Getenv("MJ2N_LOG_LEVEL"); v != "" {
} else if v := os.Getenv("MJE_LOG_LEVEL"); v != "" {
levelStr = v
}
if v := os.Getenv("MJE_LOG_FORMAT"); v != "" {
logFormat = v
}
var out io.Writer = os.Stderr
@@ -99,27 +121,33 @@ func setupZerolog(cmd *cobra.Command) error {
if fl != nil && (fl.Changed || levelStr == "") {
levelStr = fl.Value.String()
}
fl = cmd.Flag("log-format")
if fl != nil && (fl.Changed || logFormat == "") {
logFormat = fl.Value.String()
}
}
if levelStr == "" {
levelStr = "info"
}
level, err := zerolog.ParseLevel(levelStr)
if err != nil {
return err
}
zerolog.SetGlobalLevel(level)
zerolog.TimeFieldFormat = ""
output := zerolog.ConsoleWriter{Out: out}
output.FormatTimestamp = func(i interface{}) string {
return ""
switch logFormat {
case "plain":
output := zerolog.ConsoleWriter{Out: out}
output.FormatTimestamp = func(i interface{}) string { return "" }
log.Logger = zerolog.New(output).With().Logger()
case "json":
log.Logger = zerolog.New(out).With().Timestamp().Logger()
default:
return fmt.Errorf("unknown log-format: %s", logFormat)
}
log.Logger = zerolog.New(output).With().Timestamp().Logger()
return nil
}

View File

@@ -34,7 +34,7 @@ func render(w io.Writer, format string, v interface{}) error {
return err
}
return renderYAML(w, v)
return renderJSON(w, v)
}
func renderYAML(w io.Writer, v interface{}) error {

5
go.mod
View File

@@ -1,8 +1,9 @@
module github.com/jimeh/mj2n
module github.com/jimeh/mje
go 1.19
require (
github.com/jimeh/go-midjourney v0.0.0-20220915223103-5d89204e21f0
github.com/rs/zerolog v1.28.0
github.com/spf13/cobra v1.5.0
gopkg.in/yaml.v3 v3.0.1
@@ -13,5 +14,5 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 // indirect
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect
)

9
go.sum
View File

@@ -1,9 +1,12 @@
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jimeh/go-midjourney v0.0.0-20220915223103-5d89204e21f0 h1:xPHw47dnd//qDeUCBQ9XpJK6F+5XQFQJ2VP2AGldZto=
github.com/jimeh/go-midjourney v0.0.0-20220915223103-5d89204e21f0/go.mod h1:pQqr224bXA8yuiXxb5P1V/aNIiuhq2s8jBlT6P5tcMA=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -11,6 +14,7 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
@@ -19,11 +23,12 @@ github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 h1:qSa+Hg9oBe6UJXrznE+yYvW51V9UbyIj/nj/KpDigo8=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc=
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

14
main.go
View File

@@ -7,11 +7,21 @@ import (
"os/signal"
"syscall"
"github.com/jimeh/mj2n/commands"
"github.com/jimeh/mje/commands"
)
var (
version = ""
commit = ""
date = ""
)
func main() {
cmd, err := commands.NewMJ2N()
cmd, err := commands.New(commands.Info{
Version: version,
Commit: commit,
Date: date,
})
if err != nil {
fatal(err)
}

View File

@@ -1,144 +0,0 @@
package midjourney
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/rs/zerolog"
)
var (
Err = errors.New("midjourney")
ErrNoAuthToken = fmt.Errorf("%w: no auth token", Err)
ErrInvalidAPIURL = fmt.Errorf("%w: invalid API URL", Err)
ErrInvalidHTTPClient = fmt.Errorf("%w: invalid HTTP client", Err)
ErrResponseStatus = fmt.Errorf("%w: response status", Err)
DefaultAPIURL = url.URL{
Scheme: "https",
Host: "www.midjourney.com",
Path: "/api/",
}
DefaultUserAgent = "go-midjourney/0.0.1-dev"
)
type Order string
const (
OrderNew Order = "new"
OrderOldest Order = "oldest"
)
type Option interface {
apply(*Client) error
}
type optionFunc func(*Client) error
func (fn optionFunc) apply(o *Client) error {
return fn(o)
}
func WithAuthToken(authToken string) Option {
return optionFunc(func(c *Client) error {
c.AuthToken = authToken
return nil
})
}
func WithAPIURL(baseURL string) Option {
return optionFunc(func(c *Client) error {
if !strings.HasSuffix(baseURL, "/") {
baseURL += "/"
}
u, err := url.Parse(baseURL)
if err != nil {
return err
}
c.APIURL = u
return nil
})
}
func WithHTTPClient(httpClient *http.Client) Option {
return optionFunc(func(c *Client) error {
c.HTTPClient = httpClient
return nil
})
}
func WithUserAgent(userAgent string) Option {
return optionFunc(func(c *Client) error {
c.UserAgent = userAgent
return nil
})
}
func WithLogger(logger zerolog.Logger) Option {
return optionFunc(func(c *Client) error {
c.Logger = logger
return nil
})
}
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type Client struct {
HTTPClient HTTPClient
APIURL *url.URL
AuthToken string
UserAgent string
Logger zerolog.Logger
}
func New(options ...Option) (*Client, error) {
c := &Client{
HTTPClient: http.DefaultClient,
APIURL: &DefaultAPIURL,
UserAgent: DefaultUserAgent,
Logger: zerolog.Nop(),
}
err := c.Set(options...)
return c, err
}
func (c *Client) Set(options ...Option) error {
for _, opt := range options {
err := opt.apply(c)
if err != nil {
return err
}
}
return nil
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
req.URL = c.APIURL.ResolveReference(req.URL)
c.Logger.Debug().Str("url", req.URL.String()).Msg("request")
req.Header.Set("Accept", "application/json")
if c.AuthToken != "" {
req.Header.Set(
"Cookie", "__Secure-next-auth.session-token="+c.AuthToken,
)
}
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
return c.HTTPClient.Do(req)
}

View File

@@ -1,57 +0,0 @@
package midjourney
type JobType string
const (
JobTypeGrid JobType = "grid"
JobTypeUpscale JobType = "upscale"
)
type JobStatus string
const (
JobStatusRunning JobStatus = "running"
JobStatusCompleted JobStatus = "completed"
)
type Event struct {
Height int `json:"height,omitempty"`
TextPrompt []string `json:"textPrompt,omitempty"`
ImagePrompts []string `json:"imagePrompts,omitempty"`
Width int `json:"width,omitempty"`
BatchSize int `json:"batchSize,omitempty"`
SeedImageURL string `json:"seedImageURL,omitempty"`
}
type Job struct {
CurrentStatus JobStatus `json:"current_status,omitempty"`
EnqueueTime string `json:"enqueue_time,omitempty"`
Event *Event `json:"event,omitempty"`
Flagged bool `json:"flagged,omitempty"`
FollowedByUser bool `json:"followed_by_user,omitempty"`
GridID string `json:"grid_id,omitempty"`
GridNum string `json:"grid_num,omitempty"`
GuildID string `json:"guild_id,omitempty"`
Hidden bool `json:"hidden,omitempty"`
ID string `json:"id,omitempty"`
ImagePaths []string `json:"image_paths,omitempty"`
IsPublished bool `json:"is_published,omitempty"`
LikedByUser bool `json:"liked_by_user,omitempty"`
LowPriority bool `json:"low_priority,omitempty"`
Metered bool `json:"metered,omitempty"`
ModHidden bool `json:"mod_hidden,omitempty"`
Platform string `json:"platform,omitempty"`
PlatformChannel string `json:"platform_channel,omitempty"`
PlatformChannelID string `json:"platform_channel_id,omitempty"`
PlatformMessageID string `json:"platform_message_id,omitempty"`
PlatformThreadID string `json:"platform_thread_id,omitempty"`
Prompt string `json:"prompt,omitempty"`
RankedByUser bool `json:"ranked_by_user,omitempty"`
RankingByUser int `json:"ranking_by_user,omitempty"`
Type JobType `json:"type,omitempty"`
UserID string `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
FullCommand string `json:"full_command,omitempty"`
ReferenceJobID string `json:"reference_job_id,omitempty"`
ReferenceImageNum string `json:"reference_image_num,omitempty"`
}

View File

@@ -1,4 +0,0 @@
// Package midjourney provides a basic read-only API client for MidJourney. As
// there is no official API, it uses the same API as the MidJourney website
// uses.
package midjourney

View File

@@ -1,124 +0,0 @@
package midjourney
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)
const FromDateFormat = "2006-01-02 15:04:05.999999"
type RecentJobsQuery struct {
Amount int
JobType JobType
OrderBy Order
JobStatus JobStatus
UserID string
FromDate time.Time
Page int
Dedupe bool
RefreshAPI int
}
func (rjq *RecentJobsQuery) Values() url.Values {
v := url.Values{}
if rjq.Amount != 0 {
v.Set("amount", strconv.Itoa(rjq.Amount))
}
if rjq.JobType != "" {
v.Set("jobType", string(rjq.JobType))
}
if rjq.OrderBy != "" {
v.Set("orderBy", string(rjq.OrderBy))
}
if rjq.JobStatus != "" {
v.Set("jobStatus", string(rjq.JobStatus))
}
if rjq.UserID != "" {
v.Set("userId", rjq.UserID)
}
if !rjq.FromDate.IsZero() {
v.Set("fromDate", rjq.FromDate.Format(FromDateFormat))
}
if rjq.Page != 0 {
v.Set("page", strconv.Itoa(rjq.Page))
}
if rjq.Dedupe {
v.Set("dedupe", "true")
}
if rjq.RefreshAPI != 0 {
v.Set("refreshApi", strconv.Itoa(rjq.RefreshAPI))
}
return v
}
func (rjq *RecentJobsQuery) NextPage() *RecentJobsQuery {
q := *rjq
if q.OrderBy == OrderNew && q.FromDate.IsZero() {
q.FromDate = time.Now().UTC()
}
if q.Page == 0 {
q.Page = 1
}
q.Page = rjq.Page + 1
return &q
}
type RecentJobs struct {
Query RecentJobsQuery
Jobs []*Job
Page int
}
func (c *Client) RecentJobs(
ctx context.Context,
q *RecentJobsQuery,
) (*RecentJobs, error) {
u := &url.URL{
Path: "app/recent-jobs",
RawQuery: q.Values().Encode(),
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
fromDate := q.FromDate
if fromDate.IsZero() {
fromDate = time.Now().UTC()
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %s", ErrResponseStatus, resp.Status)
}
rj := &RecentJobs{
Query: *q,
Jobs: []*Job{},
Page: q.Page,
}
err = json.NewDecoder(resp.Body).Decode(&rj.Jobs)
if err != nil {
return nil, err
}
if rj.Query.OrderBy == OrderNew && rj.Query.FromDate.IsZero() {
rj.Query.FromDate = fromDate
}
return rj, nil
}