removed sqlc, moved to bob

This commit is contained in:
Tigor Hutasuhut 2024-04-25 12:31:20 +07:00
parent fd29c35b1a
commit db432f4dc9
18 changed files with 202 additions and 118 deletions

View file

@ -55,3 +55,6 @@ migrate-new:
migrate-redo:
@goose redo
migrate-up:
@goose up

View file

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"os"
"github.com/robfig/cron/v3"
"github.com/stephenafamo/bob"
@ -11,18 +12,21 @@ import (
"github.com/tigorlazuardi/redmage/api/bmessage"
"github.com/tigorlazuardi/redmage/api/reddit"
"github.com/tigorlazuardi/redmage/config"
"github.com/tigorlazuardi/redmage/db/queries"
"github.com/tigorlazuardi/redmage/models"
"github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/ThreeDotsLabs/watermill"
watermillSql "github.com/ThreeDotsLabs/watermill-sql/v3/pkg/sql"
"github.com/ThreeDotsLabs/watermill/message"
)
type API struct {
queries *queries.Queries
db *sql.DB
exec bob.Executor
scheduler *cron.Cron
scheduleMap map[cron.EntryID]queries.Subreddit
scheduleMap map[cron.EntryID]*models.Subreddit
downloadBroadcast *broadcast.Relay[bmessage.ImageDownloadMessage]
@ -32,32 +36,54 @@ type API struct {
subredditSemaphore chan struct{}
reddit *reddit.Reddit
subscriber message.Subscriber
publisher message.Publisher
}
type Dependencies struct {
Queries *queries.Queries
DB *sql.DB
Config *config.Config
Reddit *reddit.Reddit
}
const downloadTopic = "subreddit.download"
func New(deps Dependencies) *API {
ackDeadline := deps.Config.Duration("download.pubsub.ack.deadline")
subscriber, err := watermillSql.NewSubscriber(deps.DB, watermillSql.SubscriberConfig{
AckDeadline: &ackDeadline,
SchemaAdapter: watermillSql.DefaultPostgreSQLSchema{},
OffsetsAdapter: watermillSql.DefaultPostgreSQLOffsetsAdapter{},
InitializeSchema: true,
}, watermill.NewStdLoggerWithOut(os.Stderr, true, true))
if err != nil {
panic(err)
}
publisher, err := watermillSql.NewPublisher(deps.DB, watermillSql.PublisherConfig{
SchemaAdapter: watermillSql.DefaultPostgreSQLSchema{},
AutoInitializeSchema: true,
}, watermill.NewStdLoggerWithOut(os.Stderr, true, true))
if err != nil {
panic(err)
}
return &API{
queries: deps.Queries,
db: deps.DB,
exec: bob.New(deps.DB),
scheduler: cron.New(),
scheduleMap: make(map[cron.EntryID]queries.Subreddit, 8),
scheduleMap: make(map[cron.EntryID]*models.Subreddit, 8),
downloadBroadcast: broadcast.NewRelay[bmessage.ImageDownloadMessage](),
config: deps.Config,
imageSemaphore: make(chan struct{}, deps.Config.Int("download.concurrency.images")),
subredditSemaphore: make(chan struct{}, deps.Config.Int("download.concurrency.subreddits")),
reddit: deps.Reddit,
subscriber: subscriber,
publisher: publisher,
}
}
func (api *API) StartScheduler(ctx context.Context) error {
subreddits, err := api.queries.SubredditsGetAll(ctx)
subreddits, err := models.Subreddits.Query(ctx, api.exec, nil).All()
if err != nil {
return errs.Wrapw(err, "failed to get all subreddits")
}
@ -76,13 +102,12 @@ func (api *API) StartScheduler(ctx context.Context) error {
return nil
}
func (api *API) scheduleSubreddit(subreddit queries.Subreddit) error {
func (api *API) scheduleSubreddit(subreddit *models.Subreddit) error {
id, err := api.scheduler.AddFunc(subreddit.Schedule, func() {
})
if err != nil {
return errs.Wrap(err)
}
api.scheduleMap[id] = subreddit
return nil

View file

@ -15,17 +15,18 @@ import (
"github.com/disintegration/imaging"
"github.com/tigorlazuardi/redmage/api/reddit"
"github.com/tigorlazuardi/redmage/db/queries"
"github.com/tigorlazuardi/redmage/models"
"github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/pkg/telemetry"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
)
type DownloadSubredditParams struct {
Countback int
Devices []queries.Device
Devices models.DeviceSlice
SubredditType reddit.SubredditType
}
@ -51,15 +52,19 @@ func (api *API) DownloadSubredditImages(ctx context.Context, subredditName strin
countback := params.Countback
for page := 1; countback > 0; page += 1 {
limit := countback
if limit > 100 {
limit = 100
var (
list reddit.Listing
err error
)
for countback > 0 {
limit := 100
if limit > countback {
limit = countback
}
list, err := api.reddit.GetPosts(ctx, reddit.GetPostsParam{
list, err = api.reddit.GetPosts(ctx, reddit.GetPostsParam{
Subreddit: subredditName,
Limit: limit,
Page: page,
After: list.GetLastAfter(),
SubredditType: params.SubredditType,
})
if err != nil {
@ -73,6 +78,9 @@ func (api *API) DownloadSubredditImages(ctx context.Context, subredditName strin
log.New(ctx).Err(err).Error("failed to download image")
}
}(ctx, list)
if len(list.GetPosts()) == 0 {
break
}
countback -= len(list.GetPosts())
}
@ -91,7 +99,7 @@ func (api *API) downloadSubredditListImage(ctx context.Context, list reddit.List
if !post.IsImagePost() {
continue
}
devices := getDevicesThatAcceptPost(post, params.Devices)
devices := api.getDevicesThatAcceptPost(ctx, post, params.Devices)
if len(devices) == 0 {
continue
}
@ -114,7 +122,7 @@ func (api *API) downloadSubredditListImage(ctx context.Context, list reddit.List
return nil
}
func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, devices []queries.Device) error {
func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, devices models.DeviceSlice) error {
ctx, span := tracer.Start(ctx, "*API.downloadSubredditImage")
defer span.End()
@ -170,7 +178,7 @@ func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, de
return nil
}
func (api *API) createDeviceImageWriters(post reddit.Post, devices []queries.Device) (writer io.Writer, close func(), err error) {
func (api *API) createDeviceImageWriters(post reddit.Post, devices models.DeviceSlice) (writer io.Writer, close func(), err error) {
// open file for each device
var files []*os.File
var writers []io.Writer
@ -181,7 +189,7 @@ func (api *API) createDeviceImageWriters(post reddit.Post, devices []queries.Dev
} else {
filename = post.GetImageTargetPath(api.config, device)
}
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
for _, f := range files {
_ = f.Close()
@ -203,40 +211,67 @@ func (api *API) createDeviceImageWriters(post reddit.Post, devices []queries.Dev
}, nil
}
func getDevicesThatAcceptPost(post reddit.Post, devices []queries.Device) []queries.Device {
var devs []queries.Device
func (api *API) getDevicesThatAcceptPost(ctx context.Context, post reddit.Post, devices models.DeviceSlice) (devs models.DeviceSlice) {
var mu sync.Mutex
errgrp, ctx := errgroup.WithContext(ctx)
for _, device := range devices {
if shouldDownloadPostForDevice(post, device) {
device := device
errgrp.Go(func() error {
if !api.isImageExists(ctx, post, device) {
mu.Lock()
defer mu.Unlock()
devs = append(devices, device)
}
return nil
})
}
}
_ = errgrp.Wait()
return devs
}
func shouldDownloadPostForDevice(post reddit.Post, device queries.Device) bool {
if post.IsNSFW() && device.Nsfw == 0 {
func (api *API) isImageExists(ctx context.Context, post reddit.Post, device *models.Device) (found bool) {
ctx, span := tracer.Start(ctx, "*API.IsImageExists")
defer span.End()
// Image does not exist in target image.
if _, err := os.Stat(post.GetImageTargetPath(api.config, device)); err != nil {
return false
}
_, err := models.Images.Query(ctx, api.exec,
models.SelectWhere.Images.DeviceID.EQ(device.ID),
models.SelectWhere.Images.PostID.EQ(post.GetID()),
).One()
return err == nil
}
func shouldDownloadPostForDevice(post reddit.Post, device *models.Device) bool {
if post.IsNSFW() && device.NSFW == 0 {
return false
}
if math.Abs(deviceAspectRatio(device)-post.GetImageAspectRatio()) > device.AspectRatioTolerance { // outside of aspect ratio tolerance
return false
}
width, height := post.GetImageSize()
if device.MaxX > 0 && width > device.MaxX {
if device.MaxX > 0 && width > int64(device.MaxX) {
return false
}
if device.MaxY > 0 && height > device.MaxY {
if device.MaxY > 0 && height > int64(device.MaxY) {
return false
}
if device.MinX > 0 && width < device.MinX {
if device.MinX > 0 && width < int64(device.MinX) {
return false
}
if device.MinY > 0 && height < device.MinY {
if device.MinY > 0 && height < int64(device.MinY) {
return false
}
return true
}
func deviceAspectRatio(device queries.Device) float64 {
func deviceAspectRatio(device *models.Device) float64 {
return float64(device.ResolutionX) / float64(device.ResolutionY)
}
@ -269,10 +304,10 @@ func (api *API) copyImageToTempDir(ctx context.Context, img reddit.PostImage) (t
split := strings.Split(url.Path, "/")
imageFilename := split[len(split)-1]
tmpDirname := path.Join(os.TempDir(), "redmage")
_ = os.MkdirAll(tmpDirname, 0644)
_ = os.MkdirAll(tmpDirname, 0o644)
tmpFilename := path.Join(tmpDirname, imageFilename)
file, err := os.OpenFile(tmpFilename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
file, err := os.OpenFile(tmpFilename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return nil, errs.Wrapw(err, "failed to open temp image file",
"temp_file_path", tmpFilename,

View file

@ -11,6 +11,6 @@ type DownloadSubredditPostsParams struct {
Limit int
}
func (api *API) DownloadSubredditPosts(ctx context.Context, subredditName string, params DownloadSubredditParams) (posts []reddit.Listing, err error) {
func (api *API) DownloadSubredditPosts(ctx context.Context, subredditName string, params DownloadSubredditPostsParams) (posts reddit.Listing, err error) {
return posts, err
}

52
api/pubsub_download.go Normal file
View file

@ -0,0 +1,52 @@
package api
import (
"context"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/tigorlazuardi/redmage/api/reddit"
"github.com/tigorlazuardi/redmage/models"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/pkg/telemetry"
"go.opentelemetry.io/otel/attribute"
)
func (api *API) startSubredditDownloadPubsub(messages <-chan *message.Message) {
for msg := range messages {
api.subredditSemaphore <- struct{}{}
go func(msg *message.Message) {
defer func() {
msg.Ack()
<-api.subredditSemaphore
}()
var err error
ctx, span := tracer.Start(context.Background(), "Download Subreddit Pubsub")
defer func() { telemetry.EndWithStatus(span, err) }()
span.AddEvent("pubsub." + downloadTopic)
subredditName := string(msg.Payload)
span.SetAttributes(attribute.String("subreddit", subredditName))
subreddit, err := models.Subreddits.Query(ctx, api.exec, models.SelectWhere.Subreddits.Name.EQ(subredditName)).One()
if err != nil {
log.New(ctx).Err(err).Error("failed to find subreddit", "subreddit", subredditName)
return
}
devices, err := models.Devices.Query(ctx, api.exec).All()
if err != nil {
log.New(ctx).Err(err).Error("failed to query devices")
return
}
err = api.DownloadSubredditImages(ctx, subredditName, DownloadSubredditParams{
Countback: int(subreddit.Countback),
Devices: devices,
SubredditType: reddit.SubredditType(subreddit.Subtype),
})
if err != nil {
log.New(ctx).Err(err).Error("failed to download subreddit images", "subreddit", subredditName)
return
}
}(msg)
}
}

View file

@ -31,12 +31,12 @@ func (s SubredditType) Code() string {
type GetPostsParam struct {
Subreddit string
Limit int
Page int
After string
SubredditType SubredditType
}
func (reddit *Reddit) GetPosts(ctx context.Context, params GetPostsParam) (posts Listing, err error) {
url := fmt.Sprintf("https://reddit.com/%s/%s.json?limit=%d&page=%d", params.SubredditType.Code(), params.Subreddit, params.Limit, params.Page)
url := fmt.Sprintf("https://reddit.com/%s/%s.json?limit=%d&after=%s", params.SubredditType.Code(), params.Subreddit, params.Limit, params.After)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return posts, errs.Wrapw(err, "reddit: failed to create http request instance", "url", url, "params", params)

View file

@ -29,8 +29,6 @@ type ImageDownloadReader struct {
deltastart time.Time
deltavalue atomic.Int64
end time.Time
exit chan struct{}
mu sync.Mutex

View file

@ -7,7 +7,7 @@ import (
"strings"
"github.com/tigorlazuardi/redmage/config"
"github.com/tigorlazuardi/redmage/db/queries"
"github.com/tigorlazuardi/redmage/models"
)
type Listing struct {
@ -19,6 +19,17 @@ func (l *Listing) GetPosts() []Post {
return l.Data.Children
}
// GetLastAfter returns the last post namee for pagination.
//
// Returns empty string if there is no more posts to look up.
func (l *Listing) GetLastAfter() string {
posts := l.GetPosts()
if len(posts) == 0 {
return ""
}
return posts[len(posts)-1].GetName()
}
type (
MediaEmbed struct{}
SecureMediaEmbed struct{}
@ -219,12 +230,16 @@ func (post *Post) GetImageAspectRatio() float64 {
return float64(width) / float64(height)
}
func (post *Post) GetImageTargetPath(cfg *config.Config, device queries.Device) string {
func (post *Post) GetName() string {
return post.Data.Name
}
func (post *Post) GetImageTargetPath(cfg *config.Config, device *models.Device) string {
baseDownloadDir := cfg.String("download.directory")
return path.Join(baseDownloadDir, device.Name, post.GetSubreddit(), post.GetImageFilename())
}
func (post *Post) GetWindowsWallpaperImageTargetPath(cfg *config.Config, device queries.Device) string {
func (post *Post) GetWindowsWallpaperImageTargetPath(cfg *config.Config, device *models.Device) string {
baseDownloadDir := cfg.String("download.directory")
filename := fmt.Sprintf("%s_%s", post.GetSubreddit(), post.GetImageFilename())
return path.Join(baseDownloadDir, device.Name, filename)
@ -239,11 +254,11 @@ func (post *Post) GetThumbnailRelativePath() string {
return path.Join("_thumbnails", post.GetSubreddit(), post.GetImageFilename())
}
func (post *Post) GetImageRelativePath(device queries.Device) string {
func (post *Post) GetImageRelativePath(device *models.Device) string {
return path.Join(device.Slug, post.GetSubreddit(), post.GetImageFilename())
}
func (post *Post) GetWindowsWallpaperImageRelativePath(device queries.Device) string {
func (post *Post) GetWindowsWallpaperImageRelativePath(device *models.Device) string {
filename := fmt.Sprintf("%s_%s", post.GetSubreddit(), post.GetImageFilename())
return path.Join(device.Slug, filename)
}

View file

@ -9,7 +9,6 @@ import (
"github.com/tigorlazuardi/redmage/api"
"github.com/tigorlazuardi/redmage/api/reddit"
"github.com/tigorlazuardi/redmage/db"
"github.com/tigorlazuardi/redmage/db/queries"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/pkg/telemetry"
"github.com/tigorlazuardi/redmage/server"
@ -35,15 +34,12 @@ var serveCmd = &cobra.Command{
os.Exit(1)
}
queries := queries.New(db)
red := &reddit.Reddit{
Client: http.DefaultClient,
Config: cfg,
}
api := api.New(api.Dependencies{
Queries: queries,
DB: db,
Config: cfg,
Reddit: red,

View file

@ -22,6 +22,8 @@ var DefaultConfig = map[string]any{
"download.timeout.idle": "5s",
"download.timeout.idlespeed": "10KB",
"download.pubsub.ack.deadline": "3h",
"http.port": "8080",
"http.host": "0.0.0.0",
"http.shutdown_timeout": "5s",

View file

@ -8,6 +8,7 @@ CREATE TABLE images(
post_id VARCHAR(50) NOT NULL,
post_url VARCHAR(255) NOT NULL,
post_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_name VARCHAR(255) NOT NULL,
poster VARCHAR(50) NOT NULL,
poster_url VARCHAR(255) NOT NULL,
image_relative_path VARCHAR(255) NOT NULL,

View file

@ -1,28 +0,0 @@
-- name: DeviceGetAll :many
SELECT * FROM devices
ORDER BY name;
-- name: DeviceCount :one
SELECT COUNT(*) FROM devices;
-- name: DeviceList :many
SELECT * FROM devices
ORDER BY name
LIMIT ? OFFSET ?;
-- name: DeviceSearch :many
SELECT * FROM devices
WHERE (name LIKE ? OR slug LIKE ?)
ORDER BY name
LIMIT ? OFFSET ?;
-- name: DeviceSearchCount :one
SELECT COUNT(*) FROM devices
WHERE (name LIKE ? OR slug LIKE ?)
ORDER BY name
LIMIT ? OFFSET ?;
-- name: DeviceCreate :one
INSERT INTO devices (name, slug, resolution_x, resolution_y, aspect_ratio_tolerance, min_x, min_y, max_x, max_y, nsfw, windows_wallpaper_mode)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;

View file

@ -1,5 +0,0 @@
-- name: RecentlyAddedImages :many
SELECT * FROM images
WHERE created_at > ?
ORDER BY created_at
LIMIT ? OFFSET ?;

View file

@ -1,28 +0,0 @@
-- name: SubredditsGetAll :many
SELECT * FROM subreddits;
-- name: SubredditsList :many
SELECT * FROM subreddits
ORDER BY name
LIMIT ? OFFSET ?;
-- name: SubredditsListCount :one
SELECT COUNT(*) From subreddits;
-- name: SubredditsSearch :many
SELECT * FROM subreddits
WHERE name LIKE ?
ORDER BY name
LIMIT ? OFFSET ?;
-- name: SubredditsSearchCount :one
SELECT COUNT(*) FROM subreddits
WHERE name LIKE ?
ORDER BY name
LIMIT ? OFFSET ?;
-- name: SubredditCreate :one
INSERT INTO subreddits (name, subtype, schedule)
VALUES (?, ?, ?)
RETURNING *;

View file

@ -19,7 +19,6 @@
modd
nodePackages_latest.nodejs
goose
sqlc
air
];
};

6
go.mod
View file

@ -3,6 +3,7 @@ module github.com/tigorlazuardi/redmage
go 1.22.1
require (
github.com/ThreeDotsLabs/watermill v1.3.5
github.com/a-h/templ v0.2.648
github.com/aarondl/opt v0.0.0-20240108180805-338d04d857dc
github.com/adrg/xdg v0.4.0
@ -33,9 +34,14 @@ require (
)
require (
github.com/ThreeDotsLabs/watermill-sql/v3 v3.0.1 // indirect
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/stephenafamo/scan v0.4.2 // indirect

13
go.sum
View file

@ -16,6 +16,10 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/ThreeDotsLabs/watermill v1.3.5 h1:50JEPEhMGZQMh08ct0tfO1PsgMOAOhV3zxK2WofkbXg=
github.com/ThreeDotsLabs/watermill v1.3.5/go.mod h1:O/u/Ptyrk5MPTxSeWM5vzTtZcZfxXfO9PK9eXTYiFZY=
github.com/ThreeDotsLabs/watermill-sql/v3 v3.0.1 h1:+uW9Db+7Ep4uon7enOq1cozCRua3REH7zdmtXIuGQ7c=
github.com/ThreeDotsLabs/watermill-sql/v3 v3.0.1/go.mod h1:iYZqlHt0tJPQIFwQSXoI6GnxDhTZhAzxVR1/EIS3DOw=
github.com/XSAM/otelsql v0.29.0 h1:pEw9YXXs8ZrGRYfDc0cmArIz9lci5b42gmP5+tA1Huc=
github.com/XSAM/otelsql v0.29.0/go.mod h1:d3/0xGIGC5RVEE+Ld7KotwaLy6zDeaF3fLJHOPpdN2w=
github.com/a-h/templ v0.2.648 h1:A1ggHGIE7AONOHrFaDTM8SrqgqHL6fWgWCijQ21Zy9I=
@ -87,10 +91,15 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
@ -143,6 +152,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 h1:6PfEMwfInASh9hkN83aR0j4W/eKaAZt/AURtXAXlas0=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475/go.mod h1:20nXSmcf0nAscrzqsXeC2/tA3KkV2eCiJqYuyAgl+ss=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -164,6 +175,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=

View file

@ -4,7 +4,7 @@ Content-Type: application/json
Content-Length: 78
{
"slug": "sync-l",
"slug": "laptop",
"aspect_ratio_tolerance": 0.2,
"nsfw": 1
}