mirror of
https://github.com/jimeh/go-midjourney.git
synced 2026-02-19 01:46:41 +00:00
feat: initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.env*
|
||||||
|
bin/*
|
||||||
|
mje
|
||||||
93
.golangci.yml
Normal file
93
.golangci.yml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
linters-settings:
|
||||||
|
funlen:
|
||||||
|
lines: 100
|
||||||
|
statements: 150
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 20
|
||||||
|
golint:
|
||||||
|
min-confidence: 0
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- fieldalignment
|
||||||
|
lll:
|
||||||
|
line-length: 80
|
||||||
|
tab-width: 4
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- asciicheck
|
||||||
|
- bodyclose
|
||||||
|
- depguard
|
||||||
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
|
- errorlint
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- funlen
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- godot
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- importas
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nilerr
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- staticcheck
|
||||||
|
- tparallel
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude:
|
||||||
|
- Using the variable on range scope `tt` in function literal
|
||||||
|
- Using the variable on range scope `tc` in function literal
|
||||||
|
exclude-rules:
|
||||||
|
- path: "_test\\.go"
|
||||||
|
linters:
|
||||||
|
- funlen
|
||||||
|
- dupl
|
||||||
|
- source: "^//go:generate "
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
- source: "`json:"
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
- source: "`xml:"
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
- source: "`yaml:"
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 2m
|
||||||
|
allow-parallel-runners: true
|
||||||
|
modules-download-mode: readonly
|
||||||
194
Makefile
Normal file
194
Makefile
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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))
|
||||||
137
client.go
Normal file
137
client.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
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.0-dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
17
go.mod
Normal file
17
go.mod
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module github.com/jimeh/go-midjourney
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/rs/zerolog v1.28.0
|
||||||
|
github.com/stretchr/testify v1.8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
28
go.sum
Normal file
28
go.sum
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
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=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
77
job.go
Normal file
77
job.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package midjourney
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type JobType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
JobTypeNull JobType = "null"
|
||||||
|
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 Time `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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) DiscordURL() string {
|
||||||
|
if j.Platform != "discord" || j.GuildID == "" ||
|
||||||
|
j.PlatformChannelID == "" || j.PlatformMessageID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("https://discord.com/channels/%s/%s/%s",
|
||||||
|
j.GuildID,
|
||||||
|
j.PlatformChannelID,
|
||||||
|
j.PlatformMessageID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) MainImageURL() string {
|
||||||
|
return fmt.Sprintf("https://mj-gallery.com/%s/grid_0.png", j.ID)
|
||||||
|
}
|
||||||
124
job_test.go
Normal file
124
job_test.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package midjourney
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJob_UnmarshalJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
json string
|
||||||
|
want *Job
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full",
|
||||||
|
//nolint: lll
|
||||||
|
json: `{
|
||||||
|
"current_status": "completed",
|
||||||
|
"enqueue_time": "2022-09-07 06:58:02.200753",
|
||||||
|
"event": {
|
||||||
|
"height": 512,
|
||||||
|
"textPrompt": [
|
||||||
|
"earth, landscape, picturesque, photo, photorealistic"
|
||||||
|
],
|
||||||
|
"imagePrompts": [],
|
||||||
|
"width": 768,
|
||||||
|
"batchSize": 1,
|
||||||
|
"seedImageURL": null
|
||||||
|
},
|
||||||
|
"flagged": false,
|
||||||
|
"followed_by_user": false,
|
||||||
|
"grid_id": null,
|
||||||
|
"grid_num": null,
|
||||||
|
"guild_id": null,
|
||||||
|
"hidden": false,
|
||||||
|
"id": "a3052616-372b-42a1-a72b-eb86fa0be633",
|
||||||
|
"image_paths": [
|
||||||
|
"https://storage.googleapis.com/dream-machines-output/a3052616-372b-42a1-a72b-eb86fa0be633/0_0.png"
|
||||||
|
],
|
||||||
|
"is_published": true,
|
||||||
|
"liked_by_user": false,
|
||||||
|
"low_priority": true,
|
||||||
|
"metered": false,
|
||||||
|
"mod_hidden": false,
|
||||||
|
"platform": "discord",
|
||||||
|
"platform_channel": "DM",
|
||||||
|
"platform_channel_id": "991150132894638170",
|
||||||
|
"platform_message_id": "1016966680586506291",
|
||||||
|
"platform_thread_id": null,
|
||||||
|
"prompt": "earth, landscape, picturesque, photo, photorealistic",
|
||||||
|
"ranked_by_user": false,
|
||||||
|
"ranking_by_user": null,
|
||||||
|
"type": "grid",
|
||||||
|
"user_id": "146914681683050496",
|
||||||
|
"username": "jimeh",
|
||||||
|
"full_command": "earth, landscape, picturesque, photo, photorealistic --testp --ar 16:10 --video",
|
||||||
|
"reference_job_id": null,
|
||||||
|
"reference_image_num": null
|
||||||
|
}`,
|
||||||
|
want: &Job{
|
||||||
|
CurrentStatus: "completed",
|
||||||
|
EnqueueTime: Time{
|
||||||
|
time.Date(2022, 9, 7, 6, 58, 2, 200753000, time.UTC),
|
||||||
|
},
|
||||||
|
Event: &Event{
|
||||||
|
Height: 512,
|
||||||
|
TextPrompt: []string{
|
||||||
|
"earth, landscape, picturesque, photo, photorealistic",
|
||||||
|
},
|
||||||
|
ImagePrompts: []string{},
|
||||||
|
Width: 768,
|
||||||
|
BatchSize: 1,
|
||||||
|
SeedImageURL: "",
|
||||||
|
},
|
||||||
|
Flagged: false,
|
||||||
|
FollowedByUser: false,
|
||||||
|
GridID: "",
|
||||||
|
GridNum: "",
|
||||||
|
GuildID: "",
|
||||||
|
Hidden: false,
|
||||||
|
ID: "a3052616-372b-42a1-a72b-eb86fa0be633",
|
||||||
|
ImagePaths: []string{
|
||||||
|
"https://storage.googleapis.com/dream-machines-output/" +
|
||||||
|
"a3052616-372b-42a1-a72b-eb86fa0be633/0_0.png",
|
||||||
|
},
|
||||||
|
IsPublished: true,
|
||||||
|
LikedByUser: false,
|
||||||
|
LowPriority: true,
|
||||||
|
Metered: false,
|
||||||
|
ModHidden: false,
|
||||||
|
Platform: "discord",
|
||||||
|
PlatformChannel: "DM",
|
||||||
|
PlatformChannelID: "991150132894638170",
|
||||||
|
PlatformMessageID: "1016966680586506291",
|
||||||
|
PlatformThreadID: "",
|
||||||
|
Prompt: "earth, landscape, picturesque, photo, " +
|
||||||
|
"photorealistic",
|
||||||
|
RankedByUser: false,
|
||||||
|
RankingByUser: 0,
|
||||||
|
Type: "grid",
|
||||||
|
UserID: "146914681683050496",
|
||||||
|
Username: "jimeh",
|
||||||
|
FullCommand: "earth, landscape, picturesque, photo, " +
|
||||||
|
"photorealistic --testp --ar 16:10 --video",
|
||||||
|
ReferenceJobID: "",
|
||||||
|
ReferenceImageNum: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := &Job{}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(tt.json), got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
midjourney.go
Normal file
5
midjourney.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// 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, meaning that it is subject to change at any time, breaking this
|
||||||
|
// package.
|
||||||
|
package midjourney
|
||||||
203
recent_jobs.go
Normal file
203
recent_jobs.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package midjourney
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const FromDateFormat = "2006-01-02 15:04:05.999999"
|
||||||
|
|
||||||
|
var ErrUserIDRequired = fmt.Errorf("%w: user id required", Err)
|
||||||
|
|
||||||
|
type Order string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderHot Order = "hot"
|
||||||
|
OrderNew Order = "new"
|
||||||
|
OrderOldest Order = "oldest"
|
||||||
|
OrderTopToday Order = "top-today"
|
||||||
|
OrderTopWeekly Order = "top-weekly"
|
||||||
|
OrderTopMonth Order = "top-month"
|
||||||
|
OrderTopAll Order = "top-all"
|
||||||
|
OrderLikedTime Order = "liked_timestamp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecentJobsQuery struct {
|
||||||
|
Amount int
|
||||||
|
JobType JobType
|
||||||
|
OrderBy Order
|
||||||
|
JobStatus JobStatus
|
||||||
|
UserID string
|
||||||
|
UserIDLiked string
|
||||||
|
FromDate time.Time
|
||||||
|
Page int
|
||||||
|
Prompt string
|
||||||
|
Personal bool
|
||||||
|
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.UserIDLiked != "" {
|
||||||
|
v.Set("userIdLiked", rjq.UserIDLiked)
|
||||||
|
}
|
||||||
|
if !rjq.FromDate.IsZero() {
|
||||||
|
v.Set("fromDate", rjq.FromDate.Format(FromDateFormat))
|
||||||
|
}
|
||||||
|
if rjq.Page != 0 {
|
||||||
|
v.Set("page", strconv.Itoa(rjq.Page))
|
||||||
|
}
|
||||||
|
if rjq.Prompt != "" {
|
||||||
|
v.Set("prompt", rjq.Prompt)
|
||||||
|
}
|
||||||
|
if rjq.Personal {
|
||||||
|
v.Set("personal", "true")
|
||||||
|
}
|
||||||
|
if rjq.Dedupe {
|
||||||
|
v.Set("dedupe", "true")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
now := 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 = now
|
||||||
|
}
|
||||||
|
|
||||||
|
return rj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Home(
|
||||||
|
ctx context.Context,
|
||||||
|
userID string,
|
||||||
|
) (*RecentJobs, error) {
|
||||||
|
if userID == "" {
|
||||||
|
return nil, ErrUserIDRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.RecentJobs(ctx, &RecentJobsQuery{
|
||||||
|
Amount: 50,
|
||||||
|
JobType: JobTypeNull,
|
||||||
|
OrderBy: OrderNew,
|
||||||
|
JobStatus: JobStatusCompleted,
|
||||||
|
UserID: userID,
|
||||||
|
Dedupe: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CommunityFeed(ctx context.Context) (*RecentJobs, error) {
|
||||||
|
return c.RecentJobs(ctx, &RecentJobsQuery{
|
||||||
|
Amount: 50,
|
||||||
|
JobType: JobTypeUpscale,
|
||||||
|
OrderBy: OrderHot,
|
||||||
|
JobStatus: JobStatusCompleted,
|
||||||
|
Dedupe: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PersonalFeed(ctx context.Context) (*RecentJobs, error) {
|
||||||
|
return c.RecentJobs(ctx, &RecentJobsQuery{
|
||||||
|
Amount: 50,
|
||||||
|
JobType: JobTypeUpscale,
|
||||||
|
OrderBy: OrderNew,
|
||||||
|
JobStatus: JobStatusCompleted,
|
||||||
|
Personal: true,
|
||||||
|
Dedupe: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Bookmarks(
|
||||||
|
ctx context.Context,
|
||||||
|
userID string,
|
||||||
|
) (*RecentJobs, error) {
|
||||||
|
if userID == "" {
|
||||||
|
return nil, ErrUserIDRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.RecentJobs(ctx, &RecentJobsQuery{
|
||||||
|
Amount: 50,
|
||||||
|
JobType: JobTypeNull,
|
||||||
|
OrderBy: OrderLikedTime,
|
||||||
|
JobStatus: JobStatusCompleted,
|
||||||
|
UserIDLiked: userID,
|
||||||
|
Dedupe: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
34
time.go
Normal file
34
time.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package midjourney
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TimeFormat = "2006-01-02 15:04:05.999999"
|
||||||
|
|
||||||
|
type Time struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *Time) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
s := strings.Trim(string(b), "\"")
|
||||||
|
if s == "null" {
|
||||||
|
ct.Time = time.Time{}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.Time, err = time.Parse(TimeFormat, s)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *Time) MarshalJSON() ([]byte, error) {
|
||||||
|
if ct.Time.IsZero() {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(fmt.Sprintf("\"%s\"", ct.Time.Format(TimeFormat))), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user