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: migrate-redo:
@goose redo @goose redo
migrate-up:
@goose up

View file

@ -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

View file

@ -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,

View file

@ -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
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 { 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)

View file

@ -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

View file

@ -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)
} }

View file

@ -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)

View file

@ -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",

View file

@ -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,

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 modd
nodePackages_latest.nodejs nodePackages_latest.nodejs
goose goose
sqlc
air air
]; ];
}; };

6
go.mod
View file

@ -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
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/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=

View file

@ -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
} }