diff --git a/Makefile b/Makefile index 253c8b8..cf8aea4 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,9 @@ # Variables export PATH := $(shell pwd)/node_modules/.bin:$(shell pwd)/bin:$(PATH) export GOBIN := $(shell pwd)/bin +export GOOSE_DRIVER=sqlite3 +export GOOSE_DBSTRING=./data.db +export GOOSE_MIGRATION_DIR=db/migrations start: dev-dependencies @air @@ -40,3 +43,10 @@ build: build-dependencies tailwindcss -i src/styles.css -o public/styles.css templ generate go build -o redmage + +migrate-new: + @read -p "Name new migration: " name + if [[ $$name ]]; then goose create "$$name" sql; fi + +migrate-redo: + @goose redo \ No newline at end of file diff --git a/api/api.go b/api/api.go index a845bef..1b62487 100644 --- a/api/api.go +++ b/api/api.go @@ -1,12 +1,59 @@ package api import ( + "context" "database/sql" + "fmt" + "github.com/robfig/cron/v3" + "github.com/teivah/broadcast" "github.com/tigorlazuardi/redmage/db/queries" + "github.com/tigorlazuardi/redmage/pkg/errs" + "github.com/tigorlazuardi/redmage/pkg/log" ) type API struct { - Queries *queries.Queries - DB *sql.DB + queries *queries.Queries + 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 } diff --git a/api/broadcast_download_message.go b/api/broadcast_download_message.go new file mode 100644 index 0000000..cf273e4 --- /dev/null +++ b/api/broadcast_download_message.go @@ -0,0 +1,9 @@ +package api + +import "github.com/tigorlazuardi/redmage/db/queries" + +type DownloadStatusMessage struct { + Data queries.Image + Progress float64 + Subreddit string +} diff --git a/api/subreddits_list.go b/api/subreddits_list.go index 73dd453..2a35e1a 100644 --- a/api/subreddits_list.go +++ b/api/subreddits_list.go @@ -20,7 +20,7 @@ type ListSubredditsResult struct { func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (result ListSubredditsResult, err error) { if arg.Name != "" { - result.Data, err = api.Queries.SubredditsSearch(ctx, queries.SubredditsSearchParams{ + result.Data, err = api.queries.SubredditsSearch(ctx, queries.SubredditsSearchParams{ Name: "%" + arg.Name + "%", Limit: arg.Limit, Offset: arg.Offset, @@ -28,7 +28,7 @@ func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (r if err != nil { 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 + "%", Limit: arg.Limit, Offset: arg.Offset, @@ -39,14 +39,14 @@ func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (r return result, err } - result.Data, err = api.Queries.SubredditsList(ctx, queries.SubredditsListParams{ + result.Data, err = api.queries.SubredditsList(ctx, queries.SubredditsListParams{ Limit: arg.Limit, Offset: arg.Offset, }) if err != nil { 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 { return result, errs.Wrapw(err, "failed to count subreddit list", "query", arg) } diff --git a/cli/serve.go b/cli/serve.go index c562a50..54ada16 100644 --- a/cli/serve.go +++ b/cli/serve.go @@ -28,10 +28,7 @@ var serveCmd = &cobra.Command{ queries := queries.New(db) - api := &api.API{ - Queries: queries, - DB: db, - } + api := api.New(queries, db) server := server.New(cfg, api, PublicDir) diff --git a/db/migrations/20240406220949_create_subreddits_table.sql b/db/migrations/20240406220949_create_subreddits_table.sql index 658274f..fed1c6f 100644 --- a/db/migrations/20240406220949_create_subreddits_table.sql +++ b/db/migrations/20240406220949_create_subreddits_table.sql @@ -8,6 +8,13 @@ CREATE TABLE subreddits ( created_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 Down diff --git a/db/migrations/20240406235716_create_unique_index_on_subreddits_table.sql b/db/migrations/20240406235716_create_unique_index_on_subreddits_table.sql deleted file mode 100644 index 3db53bb..0000000 --- a/db/migrations/20240406235716_create_unique_index_on_subreddits_table.sql +++ /dev/null @@ -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 diff --git a/db/migrations/20240409162347_create_images_table.sql b/db/migrations/20240409162347_create_images_table.sql new file mode 100644 index 0000000..b334dc2 --- /dev/null +++ b/db/migrations/20240409162347_create_images_table.sql @@ -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 diff --git a/db/queries/images.sql b/db/queries/images.sql new file mode 100644 index 0000000..67cfa0e --- /dev/null +++ b/db/queries/images.sql @@ -0,0 +1,5 @@ +-- name: RecentlyAddedImages :many +SELECT * FROM images +WHERE created_at > ? +ORDER BY created_at +LIMIT ? OFFSET ?; \ No newline at end of file diff --git a/db/queries/subreddits.sql b/db/queries/subreddits.sql index 6151b78..701d680 100644 --- a/db/queries/subreddits.sql +++ b/db/queries/subreddits.sql @@ -1,3 +1,6 @@ +-- name: SubredditsGetAll :many +SELECT * FROM subreddits; + -- name: SubredditsList :many SELECT * FROM subreddits ORDER BY name diff --git a/flake.nix b/flake.nix index 9c282c9..db58542 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ templPkg go modd - nodejs_21 + nodePackages_latest.nodejs goose sqlc air diff --git a/go.mod b/go.mod index 43acd5e..fa7f14e 100644 --- a/go.mod +++ b/go.mod @@ -28,14 +28,19 @@ require ( require ( github.com/fsnotify/fsnotify v1.7.0 // 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/jonboulle/clockwork v0.4.0 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/copystructure v1.2.0 // 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/teivah/broadcast v0.1.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/sys v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 973d71f..0611c78 100644 --- a/go.sum +++ b/go.sum @@ -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/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-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/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 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/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 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/go.mod h1:9bKuHS7eZh/0mJndbUOrCx8Ej3PlsRDszj4L7oVYMPQ= 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/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-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/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= diff --git a/server/routes/page_home.go b/server/routes/page_home.go index 5aa8dce..fd21a17 100644 --- a/server/routes/page_home.go +++ b/server/routes/page_home.go @@ -21,7 +21,7 @@ func (routes *Routes) PageHome(rw http.ResponseWriter, r *http.Request) { data := homeview.Data{ SubredditsList: list, - Error: err, + Error: err, } if err := homeview.Home(vc, data).Render(ctx, rw); err != nil { diff --git a/views/components/head.templ b/views/components/head.templ index 6518f26..2ddb3e1 100644 --- a/views/components/head.templ +++ b/views/components/head.templ @@ -2,7 +2,7 @@ package components import "github.com/tigorlazuardi/redmage/views" -templ Head(vc *views.Context) { +templ Head(vc *views.Context, extras ...templ.Component) { @@ -13,5 +13,12 @@ templ Head(vc *views.Context) { if vc.Config.Bool("http.hotreload") { } + for _, extra := range extras { + @extra + } } + +templ HeadTitle(name string) { + {name} +} \ No newline at end of file diff --git a/views/homeview/homeview.templ b/views/homeview/homeview.templ index a7f5a3f..f96933b 100644 --- a/views/homeview/homeview.templ +++ b/views/homeview/homeview.templ @@ -11,7 +11,7 @@ type Data struct { templ Home(c *views.Context, data Data) { @components.Doctype() { - @components.Head(c) + @components.Head(c, components.HeadTitle("Redmage - Home")) @components.Body(c) { @components.Container() { if data.Error != nil { @@ -26,6 +26,14 @@ templ Home(c *views.Context, data Data) { templ home(c *views.Context, data Data) {
-

Recently Added

+
+

Recently Added

+
+
+

Subreddits

+ for _, subreddit := range data.SubredditsList.Data { +

{ subreddit.Name } - { subreddit.Schedule }

+ } +
}