major update

This commit is contained in:
Tigor Hutasuhut 2024-04-09 21:49:23 +07:00
parent a0403539ae
commit 7fd9d17f40
16 changed files with 155 additions and 24 deletions

View file

@ -3,6 +3,9 @@
# Variables # Variables
export PATH := $(shell pwd)/node_modules/.bin:$(shell pwd)/bin:$(PATH) export PATH := $(shell pwd)/node_modules/.bin:$(shell pwd)/bin:$(PATH)
export GOBIN := $(shell pwd)/bin export GOBIN := $(shell pwd)/bin
export GOOSE_DRIVER=sqlite3
export GOOSE_DBSTRING=./data.db
export GOOSE_MIGRATION_DIR=db/migrations
start: dev-dependencies start: dev-dependencies
@air @air
@ -40,3 +43,10 @@ build: build-dependencies
tailwindcss -i src/styles.css -o public/styles.css tailwindcss -i src/styles.css -o public/styles.css
templ generate templ generate
go build -o redmage go build -o redmage
migrate-new:
@read -p "Name new migration: " name
if [[ $$name ]]; then goose create "$$name" sql; fi
migrate-redo:
@goose redo

View file

@ -1,12 +1,59 @@
package api package api
import ( import (
"context"
"database/sql" "database/sql"
"fmt"
"github.com/robfig/cron/v3"
"github.com/teivah/broadcast"
"github.com/tigorlazuardi/redmage/db/queries" "github.com/tigorlazuardi/redmage/db/queries"
"github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log"
) )
type API struct { type API struct {
Queries *queries.Queries queries *queries.Queries
DB *sql.DB db *sql.DB
scheduler *cron.Cron
scheduleMap map[cron.EntryID]queries.Subreddit
downloadBroadcast *broadcast.Relay[DownloadStatusMessage]
}
func New(q *queries.Queries, db *sql.DB) *API {
return &API{
queries: q,
db: db,
scheduler: cron.New(),
scheduleMap: make(map[cron.EntryID]queries.Subreddit, 8),
downloadBroadcast: broadcast.NewRelay[DownloadStatusMessage](),
}
}
func (api *API) StartScheduler(ctx context.Context) error {
subreddits, err := api.queries.SubredditsGetAll(ctx)
if err != nil {
return errs.Wrapw(err, "failed to get all subreddits")
}
for _, subreddit := range subreddits {
id, err := api.scheduler.AddFunc(subreddit.Schedule, func() {
// TODO: Add download
})
if err != nil {
log.
New(ctx).
Err(err).
Error(
fmt.Sprintf("failed to start scheduler for subreddit '%s'", subreddit.Name),
"subreddit", subreddit,
)
continue
}
api.scheduleMap[id] = subreddit
}
return nil
} }

View file

@ -0,0 +1,9 @@
package api
import "github.com/tigorlazuardi/redmage/db/queries"
type DownloadStatusMessage struct {
Data queries.Image
Progress float64
Subreddit string
}

View file

@ -20,7 +20,7 @@ type ListSubredditsResult struct {
func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (result ListSubredditsResult, err error) { func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (result ListSubredditsResult, err error) {
if arg.Name != "" { if arg.Name != "" {
result.Data, err = api.Queries.SubredditsSearch(ctx, queries.SubredditsSearchParams{ result.Data, err = api.queries.SubredditsSearch(ctx, queries.SubredditsSearchParams{
Name: "%" + arg.Name + "%", Name: "%" + arg.Name + "%",
Limit: arg.Limit, Limit: arg.Limit,
Offset: arg.Offset, Offset: arg.Offset,
@ -28,7 +28,7 @@ func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (r
if err != nil { if err != nil {
return result, errs.Wrapw(err, "failed to search subreddit", "query", arg) return result, errs.Wrapw(err, "failed to search subreddit", "query", arg)
} }
result.Total, err = api.Queries.SubredditsSearchCount(ctx, queries.SubredditsSearchCountParams{ result.Total, err = api.queries.SubredditsSearchCount(ctx, queries.SubredditsSearchCountParams{
Name: "%" + arg.Name + "%", Name: "%" + arg.Name + "%",
Limit: arg.Limit, Limit: arg.Limit,
Offset: arg.Offset, Offset: arg.Offset,
@ -39,14 +39,14 @@ func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (r
return result, err return result, err
} }
result.Data, err = api.Queries.SubredditsList(ctx, queries.SubredditsListParams{ result.Data, err = api.queries.SubredditsList(ctx, queries.SubredditsListParams{
Limit: arg.Limit, Limit: arg.Limit,
Offset: arg.Offset, Offset: arg.Offset,
}) })
if err != nil { if err != nil {
return result, errs.Wrapw(err, "failed to list subreddit", "query", arg) return result, errs.Wrapw(err, "failed to list subreddit", "query", arg)
} }
result.Total, err = api.Queries.SubredditsListCount(ctx) result.Total, err = api.queries.SubredditsListCount(ctx)
if err != nil { if err != nil {
return result, errs.Wrapw(err, "failed to count subreddit list", "query", arg) return result, errs.Wrapw(err, "failed to count subreddit list", "query", arg)
} }

View file

@ -28,10 +28,7 @@ var serveCmd = &cobra.Command{
queries := queries.New(db) queries := queries.New(db)
api := &api.API{ api := api.New(queries, db)
Queries: queries,
DB: db,
}
server := server.New(cfg, api, PublicDir) server := server.New(cfg, api, PublicDir)

View file

@ -8,6 +8,13 @@ CREATE TABLE subreddits (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
); );
CREATE UNIQUE INDEX idx_subreddits_name ON subreddits (name);
CREATE TRIGGER update_subreddits_timestamp AFTER UPDATE ON subreddits FOR EACH ROW
BEGIN
UPDATE subreddits SET updated_at = CURRENT_TIMESTAMP WHERE id = old.id;
END;
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down

View file

@ -1,9 +0,0 @@
-- +goose Up
-- +goose StatementBegin
CREATE UNIQUE INDEX idx_subreddits_name ON subreddits (name);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX idx_subreddits_name;
-- +goose StatementEnd

View file

@ -0,0 +1,32 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE images(
id INTEGER PRIMARY KEY,
subreddit_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
post_id VARCHAR(50) NOT NULL,
poster VARCHAR(50) NOT NULL,
image_relative_path VARCHAR(255) NOT NULL,
thumbnail_relative_path VARCHAR(255) NOT NULL DEFAULT '',
image_original_url VARCHAR(255) NOT NULL,
thumbnail_original_url VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
CONSTRAINT fk_subreddit_id
FOREIGN KEY (subreddit_id)
REFERENCES subreddits(id)
ON DELETE CASCADE
);
CREATE TRIGGER update_images_timestamp AFTER UPDATE ON images FOR EACH ROW
BEGIN
UPDATE images SET updated_at = CURRENT_TIMESTAMP WHERE id = old.id;
END;
CREATE INDEX idx_subreddit_id ON images(subreddit_id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS images;
-- +goose StatementEnd

5
db/queries/images.sql Normal file
View file

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

View file

@ -1,3 +1,6 @@
-- name: SubredditsGetAll :many
SELECT * FROM subreddits;
-- name: SubredditsList :many -- name: SubredditsList :many
SELECT * FROM subreddits SELECT * FROM subreddits
ORDER BY name ORDER BY name

View file

@ -17,7 +17,7 @@
templPkg templPkg
go go
modd modd
nodejs_21 nodePackages_latest.nodejs
goose goose
sqlc sqlc
air air

5
go.mod
View file

@ -28,14 +28,19 @@ require (
require ( require (
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/robfig/cron/v3 v3.0.0 // indirect
github.com/sethvargo/go-retry v0.2.4 // indirect github.com/sethvargo/go-retry v0.2.4 // indirect
github.com/teivah/broadcast v0.1.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
golang.org/x/sync v0.6.0 // indirect golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

10
go.sum
View file

@ -47,6 +47,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-co-op/gocron/v2 v2.2.9 h1:aoKosYWSSdXFLecjFWX1i8+R6V7XdZb8sB2ZKAY5Yis=
github.com/go-co-op/gocron/v2 v2.2.9/go.mod h1:mZx3gMSlFnb97k3hRqX3+GdlG3+DUwTh6B8fnsTScXg=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI= github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
@ -154,6 +156,10 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
@ -175,6 +181,8 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/teivah/broadcast v0.1.0 h1:UMs1tn8w20Xlnod+VbLbwH3dzEH2zfJy4lxdzZjQLL0=
github.com/teivah/broadcast v0.1.0/go.mod h1:mXEgvXdYz2xUkQFARxI+jyX1MfCBwMDiGjIKSAsEq1g=
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898 h1:1MvEhzI5pvP27e9Dzz861mxk9WzXZLSJwzOU67cKTbU= github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898 h1:1MvEhzI5pvP27e9Dzz861mxk9WzXZLSJwzOU67cKTbU=
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898/go.mod h1:9bKuHS7eZh/0mJndbUOrCx8Ej3PlsRDszj4L7oVYMPQ= github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898/go.mod h1:9bKuHS7eZh/0mJndbUOrCx8Ej3PlsRDszj4L7oVYMPQ=
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw= github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
@ -199,6 +207,8 @@ golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=

View file

@ -21,7 +21,7 @@ func (routes *Routes) PageHome(rw http.ResponseWriter, r *http.Request) {
data := homeview.Data{ data := homeview.Data{
SubredditsList: list, SubredditsList: list,
Error: err, Error: err,
} }
if err := homeview.Home(vc, data).Render(ctx, rw); err != nil { if err := homeview.Home(vc, data).Render(ctx, rw); err != nil {

View file

@ -2,7 +2,7 @@ package components
import "github.com/tigorlazuardi/redmage/views" import "github.com/tigorlazuardi/redmage/views"
templ Head(vc *views.Context) { templ Head(vc *views.Context, extras ...templ.Component) {
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="keywords" content="Reddit, Image, Downloader"/> <meta name="keywords" content="Reddit, Image, Downloader"/>
@ -13,5 +13,12 @@ templ Head(vc *views.Context) {
if vc.Config.Bool("http.hotreload") { if vc.Config.Bool("http.hotreload") {
<script src="/public/hot_reload.js"></script> <script src="/public/hot_reload.js"></script>
} }
for _, extra := range extras {
@extra
}
</head> </head>
} }
templ HeadTitle(name string) {
<title>{name}</title>
}

View file

@ -11,7 +11,7 @@ type Data struct {
templ Home(c *views.Context, data Data) { templ Home(c *views.Context, data Data) {
@components.Doctype() { @components.Doctype() {
@components.Head(c) @components.Head(c, components.HeadTitle("Redmage - Home"))
@components.Body(c) { @components.Body(c) {
@components.Container() { @components.Container() {
if data.Error != nil { if data.Error != nil {
@ -26,6 +26,14 @@ templ Home(c *views.Context, data Data) {
templ home(c *views.Context, data Data) { templ home(c *views.Context, data Data) {
<div class="prose"> <div class="prose">
<h1>Recently Added</h1> <section>
<h1>Recently Added</h1>
</section>
<section>
<h1>Subreddits</h1>
for _, subreddit := range data.SubredditsList.Data {
<h3>{ subreddit.Name } - { subreddit.Schedule }</h3>
}
</section>
</div> </div>
} }