removed sqlc, moved to bob
This commit is contained in:
parent
fd29c35b1a
commit
db432f4dc9
3
Makefile
3
Makefile
|
@ -55,3 +55,6 @@ migrate-new:
|
||||||
|
|
||||||
migrate-redo:
|
migrate-redo:
|
||||||
@goose redo
|
@goose redo
|
||||||
|
|
||||||
|
migrate-up:
|
||||||
|
@goose up
|
53
api/api.go
53
api/api.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/stephenafamo/bob"
|
"github.com/stephenafamo/bob"
|
||||||
|
@ -11,18 +12,21 @@ import (
|
||||||
"github.com/tigorlazuardi/redmage/api/bmessage"
|
"github.com/tigorlazuardi/redmage/api/bmessage"
|
||||||
"github.com/tigorlazuardi/redmage/api/reddit"
|
"github.com/tigorlazuardi/redmage/api/reddit"
|
||||||
"github.com/tigorlazuardi/redmage/config"
|
"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/errs"
|
||||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
"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 {
|
type API struct {
|
||||||
queries *queries.Queries
|
db *sql.DB
|
||||||
db *sql.DB
|
exec bob.Executor
|
||||||
exec bob.Executor
|
|
||||||
|
|
||||||
scheduler *cron.Cron
|
scheduler *cron.Cron
|
||||||
scheduleMap map[cron.EntryID]queries.Subreddit
|
scheduleMap map[cron.EntryID]*models.Subreddit
|
||||||
|
|
||||||
downloadBroadcast *broadcast.Relay[bmessage.ImageDownloadMessage]
|
downloadBroadcast *broadcast.Relay[bmessage.ImageDownloadMessage]
|
||||||
|
|
||||||
|
@ -32,32 +36,54 @@ type API struct {
|
||||||
subredditSemaphore chan struct{}
|
subredditSemaphore chan struct{}
|
||||||
|
|
||||||
reddit *reddit.Reddit
|
reddit *reddit.Reddit
|
||||||
|
|
||||||
|
subscriber message.Subscriber
|
||||||
|
publisher message.Publisher
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dependencies struct {
|
type Dependencies struct {
|
||||||
Queries *queries.Queries
|
DB *sql.DB
|
||||||
DB *sql.DB
|
Config *config.Config
|
||||||
Config *config.Config
|
Reddit *reddit.Reddit
|
||||||
Reddit *reddit.Reddit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadTopic = "subreddit.download"
|
||||||
|
|
||||||
func New(deps Dependencies) *API {
|
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{
|
return &API{
|
||||||
queries: deps.Queries,
|
|
||||||
db: deps.DB,
|
db: deps.DB,
|
||||||
exec: bob.New(deps.DB),
|
exec: bob.New(deps.DB),
|
||||||
scheduler: cron.New(),
|
scheduler: cron.New(),
|
||||||
scheduleMap: make(map[cron.EntryID]queries.Subreddit, 8),
|
scheduleMap: make(map[cron.EntryID]*models.Subreddit, 8),
|
||||||
downloadBroadcast: broadcast.NewRelay[bmessage.ImageDownloadMessage](),
|
downloadBroadcast: broadcast.NewRelay[bmessage.ImageDownloadMessage](),
|
||||||
config: deps.Config,
|
config: deps.Config,
|
||||||
imageSemaphore: make(chan struct{}, deps.Config.Int("download.concurrency.images")),
|
imageSemaphore: make(chan struct{}, deps.Config.Int("download.concurrency.images")),
|
||||||
subredditSemaphore: make(chan struct{}, deps.Config.Int("download.concurrency.subreddits")),
|
subredditSemaphore: make(chan struct{}, deps.Config.Int("download.concurrency.subreddits")),
|
||||||
reddit: deps.Reddit,
|
reddit: deps.Reddit,
|
||||||
|
subscriber: subscriber,
|
||||||
|
publisher: publisher,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) StartScheduler(ctx context.Context) error {
|
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 {
|
if err != nil {
|
||||||
return errs.Wrapw(err, "failed to get all subreddits")
|
return errs.Wrapw(err, "failed to get all subreddits")
|
||||||
}
|
}
|
||||||
|
@ -76,13 +102,12 @@ func (api *API) StartScheduler(ctx context.Context) error {
|
||||||
return nil
|
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() {
|
id, err := api.scheduler.AddFunc(subreddit.Schedule, func() {
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.Wrap(err)
|
return errs.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.scheduleMap[id] = subreddit
|
api.scheduleMap[id] = subreddit
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -15,17 +15,18 @@ import (
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/tigorlazuardi/redmage/api/reddit"
|
"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/errs"
|
||||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||||
"github.com/tigorlazuardi/redmage/pkg/telemetry"
|
"github.com/tigorlazuardi/redmage/pkg/telemetry"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DownloadSubredditParams struct {
|
type DownloadSubredditParams struct {
|
||||||
Countback int
|
Countback int
|
||||||
Devices []queries.Device
|
Devices models.DeviceSlice
|
||||||
SubredditType reddit.SubredditType
|
SubredditType reddit.SubredditType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,15 +52,19 @@ func (api *API) DownloadSubredditImages(ctx context.Context, subredditName strin
|
||||||
|
|
||||||
countback := params.Countback
|
countback := params.Countback
|
||||||
|
|
||||||
for page := 1; countback > 0; page += 1 {
|
var (
|
||||||
limit := countback
|
list reddit.Listing
|
||||||
if limit > 100 {
|
err error
|
||||||
limit = 100
|
)
|
||||||
|
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,
|
Subreddit: subredditName,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
Page: page,
|
After: list.GetLastAfter(),
|
||||||
SubredditType: params.SubredditType,
|
SubredditType: params.SubredditType,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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")
|
log.New(ctx).Err(err).Error("failed to download image")
|
||||||
}
|
}
|
||||||
}(ctx, list)
|
}(ctx, list)
|
||||||
|
if len(list.GetPosts()) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
countback -= len(list.GetPosts())
|
countback -= len(list.GetPosts())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +99,7 @@ func (api *API) downloadSubredditListImage(ctx context.Context, list reddit.List
|
||||||
if !post.IsImagePost() {
|
if !post.IsImagePost() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
devices := getDevicesThatAcceptPost(post, params.Devices)
|
devices := api.getDevicesThatAcceptPost(ctx, post, params.Devices)
|
||||||
if len(devices) == 0 {
|
if len(devices) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -114,7 +122,7 @@ func (api *API) downloadSubredditListImage(ctx context.Context, list reddit.List
|
||||||
return nil
|
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")
|
ctx, span := tracer.Start(ctx, "*API.downloadSubredditImage")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
@ -170,7 +178,7 @@ func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, de
|
||||||
return nil
|
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
|
// open file for each device
|
||||||
var files []*os.File
|
var files []*os.File
|
||||||
var writers []io.Writer
|
var writers []io.Writer
|
||||||
|
@ -181,7 +189,7 @@ func (api *API) createDeviceImageWriters(post reddit.Post, devices []queries.Dev
|
||||||
} else {
|
} else {
|
||||||
filename = post.GetImageTargetPath(api.config, device)
|
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 {
|
if err != nil {
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
|
@ -203,40 +211,67 @@ func (api *API) createDeviceImageWriters(post reddit.Post, devices []queries.Dev
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDevicesThatAcceptPost(post reddit.Post, devices []queries.Device) []queries.Device {
|
func (api *API) getDevicesThatAcceptPost(ctx context.Context, post reddit.Post, devices models.DeviceSlice) (devs models.DeviceSlice) {
|
||||||
var devs []queries.Device
|
var mu sync.Mutex
|
||||||
|
errgrp, ctx := errgroup.WithContext(ctx)
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
if shouldDownloadPostForDevice(post, device) {
|
if shouldDownloadPostForDevice(post, device) {
|
||||||
devs = append(devices, 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
|
return devs
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldDownloadPostForDevice(post reddit.Post, device queries.Device) bool {
|
func (api *API) isImageExists(ctx context.Context, post reddit.Post, device *models.Device) (found bool) {
|
||||||
if post.IsNSFW() && device.Nsfw == 0 {
|
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
|
return false
|
||||||
}
|
}
|
||||||
if math.Abs(deviceAspectRatio(device)-post.GetImageAspectRatio()) > device.AspectRatioTolerance { // outside of aspect ratio tolerance
|
if math.Abs(deviceAspectRatio(device)-post.GetImageAspectRatio()) > device.AspectRatioTolerance { // outside of aspect ratio tolerance
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
width, height := post.GetImageSize()
|
width, height := post.GetImageSize()
|
||||||
if device.MaxX > 0 && width > device.MaxX {
|
if device.MaxX > 0 && width > int64(device.MaxX) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if device.MaxY > 0 && height > device.MaxY {
|
if device.MaxY > 0 && height > int64(device.MaxY) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if device.MinX > 0 && width < device.MinX {
|
if device.MinX > 0 && width < int64(device.MinX) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if device.MinY > 0 && height < device.MinY {
|
if device.MinY > 0 && height < int64(device.MinY) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func deviceAspectRatio(device queries.Device) float64 {
|
func deviceAspectRatio(device *models.Device) float64 {
|
||||||
return float64(device.ResolutionX) / float64(device.ResolutionY)
|
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, "/")
|
split := strings.Split(url.Path, "/")
|
||||||
imageFilename := split[len(split)-1]
|
imageFilename := split[len(split)-1]
|
||||||
tmpDirname := path.Join(os.TempDir(), "redmage")
|
tmpDirname := path.Join(os.TempDir(), "redmage")
|
||||||
_ = os.MkdirAll(tmpDirname, 0644)
|
_ = os.MkdirAll(tmpDirname, 0o644)
|
||||||
tmpFilename := path.Join(tmpDirname, imageFilename)
|
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 {
|
if err != nil {
|
||||||
return nil, errs.Wrapw(err, "failed to open temp image file",
|
return nil, errs.Wrapw(err, "failed to open temp image file",
|
||||||
"temp_file_path", tmpFilename,
|
"temp_file_path", tmpFilename,
|
||||||
|
|
|
@ -11,6 +11,6 @@ type DownloadSubredditPostsParams struct {
|
||||||
Limit int
|
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
|
return posts, err
|
||||||
}
|
}
|
||||||
|
|
52
api/pubsub_download.go
Normal file
52
api/pubsub_download.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,12 +31,12 @@ func (s SubredditType) Code() string {
|
||||||
type GetPostsParam struct {
|
type GetPostsParam struct {
|
||||||
Subreddit string
|
Subreddit string
|
||||||
Limit int
|
Limit int
|
||||||
Page int
|
After string
|
||||||
SubredditType SubredditType
|
SubredditType SubredditType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (reddit *Reddit) GetPosts(ctx context.Context, params GetPostsParam) (posts Listing, err error) {
|
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)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return posts, errs.Wrapw(err, "reddit: failed to create http request instance", "url", url, "params", params)
|
return posts, errs.Wrapw(err, "reddit: failed to create http request instance", "url", url, "params", params)
|
||||||
|
|
|
@ -29,8 +29,6 @@ type ImageDownloadReader struct {
|
||||||
deltastart time.Time
|
deltastart time.Time
|
||||||
deltavalue atomic.Int64
|
deltavalue atomic.Int64
|
||||||
|
|
||||||
end time.Time
|
|
||||||
|
|
||||||
exit chan struct{}
|
exit chan struct{}
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tigorlazuardi/redmage/config"
|
"github.com/tigorlazuardi/redmage/config"
|
||||||
"github.com/tigorlazuardi/redmage/db/queries"
|
"github.com/tigorlazuardi/redmage/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Listing struct {
|
type Listing struct {
|
||||||
|
@ -19,6 +19,17 @@ func (l *Listing) GetPosts() []Post {
|
||||||
return l.Data.Children
|
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 (
|
type (
|
||||||
MediaEmbed struct{}
|
MediaEmbed struct{}
|
||||||
SecureMediaEmbed struct{}
|
SecureMediaEmbed struct{}
|
||||||
|
@ -219,12 +230,16 @@ func (post *Post) GetImageAspectRatio() float64 {
|
||||||
return float64(width) / float64(height)
|
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")
|
baseDownloadDir := cfg.String("download.directory")
|
||||||
return path.Join(baseDownloadDir, device.Name, post.GetSubreddit(), post.GetImageFilename())
|
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")
|
baseDownloadDir := cfg.String("download.directory")
|
||||||
filename := fmt.Sprintf("%s_%s", post.GetSubreddit(), post.GetImageFilename())
|
filename := fmt.Sprintf("%s_%s", post.GetSubreddit(), post.GetImageFilename())
|
||||||
return path.Join(baseDownloadDir, device.Name, filename)
|
return path.Join(baseDownloadDir, device.Name, filename)
|
||||||
|
@ -239,11 +254,11 @@ func (post *Post) GetThumbnailRelativePath() string {
|
||||||
return path.Join("_thumbnails", post.GetSubreddit(), post.GetImageFilename())
|
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())
|
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())
|
filename := fmt.Sprintf("%s_%s", post.GetSubreddit(), post.GetImageFilename())
|
||||||
return path.Join(device.Slug, filename)
|
return path.Join(device.Slug, filename)
|
||||||
}
|
}
|
||||||
|
|
10
cli/serve.go
10
cli/serve.go
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/tigorlazuardi/redmage/api"
|
"github.com/tigorlazuardi/redmage/api"
|
||||||
"github.com/tigorlazuardi/redmage/api/reddit"
|
"github.com/tigorlazuardi/redmage/api/reddit"
|
||||||
"github.com/tigorlazuardi/redmage/db"
|
"github.com/tigorlazuardi/redmage/db"
|
||||||
"github.com/tigorlazuardi/redmage/db/queries"
|
|
||||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||||
"github.com/tigorlazuardi/redmage/pkg/telemetry"
|
"github.com/tigorlazuardi/redmage/pkg/telemetry"
|
||||||
"github.com/tigorlazuardi/redmage/server"
|
"github.com/tigorlazuardi/redmage/server"
|
||||||
|
@ -35,18 +34,15 @@ var serveCmd = &cobra.Command{
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
queries := queries.New(db)
|
|
||||||
|
|
||||||
red := &reddit.Reddit{
|
red := &reddit.Reddit{
|
||||||
Client: http.DefaultClient,
|
Client: http.DefaultClient,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
api := api.New(api.Dependencies{
|
api := api.New(api.Dependencies{
|
||||||
Queries: queries,
|
DB: db,
|
||||||
DB: db,
|
Config: cfg,
|
||||||
Config: cfg,
|
Reddit: red,
|
||||||
Reddit: red,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
server := server.New(cfg, api, PublicDir)
|
server := server.New(cfg, api, PublicDir)
|
||||||
|
|
|
@ -22,6 +22,8 @@ var DefaultConfig = map[string]any{
|
||||||
"download.timeout.idle": "5s",
|
"download.timeout.idle": "5s",
|
||||||
"download.timeout.idlespeed": "10KB",
|
"download.timeout.idlespeed": "10KB",
|
||||||
|
|
||||||
|
"download.pubsub.ack.deadline": "3h",
|
||||||
|
|
||||||
"http.port": "8080",
|
"http.port": "8080",
|
||||||
"http.host": "0.0.0.0",
|
"http.host": "0.0.0.0",
|
||||||
"http.shutdown_timeout": "5s",
|
"http.shutdown_timeout": "5s",
|
||||||
|
|
|
@ -8,6 +8,7 @@ CREATE TABLE images(
|
||||||
post_id VARCHAR(50) NOT NULL,
|
post_id VARCHAR(50) NOT NULL,
|
||||||
post_url VARCHAR(255) NOT NULL,
|
post_url VARCHAR(255) NOT NULL,
|
||||||
post_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
post_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
post_name VARCHAR(255) NOT NULL,
|
||||||
poster VARCHAR(50) NOT NULL,
|
poster VARCHAR(50) NOT NULL,
|
||||||
poster_url VARCHAR(255) NOT NULL,
|
poster_url VARCHAR(255) NOT NULL,
|
||||||
image_relative_path VARCHAR(255) NOT NULL,
|
image_relative_path VARCHAR(255) NOT NULL,
|
||||||
|
|
|
@ -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 *;
|
|
|
@ -1,5 +0,0 @@
|
||||||
-- name: RecentlyAddedImages :many
|
|
||||||
SELECT * FROM images
|
|
||||||
WHERE created_at > ?
|
|
||||||
ORDER BY created_at
|
|
||||||
LIMIT ? OFFSET ?;
|
|
|
@ -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 *;
|
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
modd
|
modd
|
||||||
nodePackages_latest.nodejs
|
nodePackages_latest.nodejs
|
||||||
goose
|
goose
|
||||||
sqlc
|
|
||||||
air
|
air
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -3,6 +3,7 @@ module github.com/tigorlazuardi/redmage
|
||||||
go 1.22.1
|
go 1.22.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ThreeDotsLabs/watermill v1.3.5
|
||||||
github.com/a-h/templ v0.2.648
|
github.com/a-h/templ v0.2.648
|
||||||
github.com/aarondl/opt v0.0.0-20240108180805-338d04d857dc
|
github.com/aarondl/opt v0.0.0-20240108180805-338d04d857dc
|
||||||
github.com/adrg/xdg v0.4.0
|
github.com/adrg/xdg v0.4.0
|
||||||
|
@ -33,9 +34,14 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ThreeDotsLabs/watermill-sql/v3 v3.0.1 // indirect
|
||||||
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
|
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // 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/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/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
|
||||||
github.com/samber/lo v1.38.1 // indirect
|
github.com/samber/lo v1.38.1 // indirect
|
||||||
github.com/stephenafamo/scan v0.4.2 // indirect
|
github.com/stephenafamo/scan v0.4.2 // indirect
|
||||||
|
|
13
go.sum
13
go.sum
|
@ -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/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 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
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 h1:pEw9YXXs8ZrGRYfDc0cmArIz9lci5b42gmP5+tA1Huc=
|
||||||
github.com/XSAM/otelsql v0.29.0/go.mod h1:d3/0xGIGC5RVEE+Ld7KotwaLy6zDeaF3fLJHOPpdN2w=
|
github.com/XSAM/otelsql v0.29.0/go.mod h1:d3/0xGIGC5RVEE+Ld7KotwaLy6zDeaF3fLJHOPpdN2w=
|
||||||
github.com/a-h/templ v0.2.648 h1:A1ggHGIE7AONOHrFaDTM8SrqgqHL6fWgWCijQ21Zy9I=
|
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/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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
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 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
|
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/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 h1:6PfEMwfInASh9hkN83aR0j4W/eKaAZt/AURtXAXlas0=
|
||||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475/go.mod h1:20nXSmcf0nAscrzqsXeC2/tA3KkV2eCiJqYuyAgl+ss=
|
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
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=
|
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/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 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
|
||||||
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
|
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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
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=
|
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||||
|
|
|
@ -4,7 +4,7 @@ Content-Type: application/json
|
||||||
Content-Length: 78
|
Content-Length: 78
|
||||||
|
|
||||||
{
|
{
|
||||||
"slug": "sync-l",
|
"slug": "laptop",
|
||||||
"aspect_ratio_tolerance": 0.2,
|
"aspect_ratio_tolerance": 0.2,
|
||||||
"nsfw": 1
|
"nsfw": 1
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue