Compare commits

...

10 Commits

20 changed files with 3786 additions and 406 deletions

View File

@ -1,52 +1,53 @@
# Rating Orama # Rating Orama
Rating Orama is a web application for displaying TV show ratings and statistics. It is composed of 3 main parts: Rating Orama is a web application for displaying TV show ratings and statistics.
It is composed of 2 main parts:
1. **Core**: Written in Go and Fiber, responsible for orchestrating everything and displaying the data using a template engine. 1. **Core**: Written in Go and Fiber, responsible for orchestrating everything
2. **Harvester**: Written in Python, Flask, and Cinemagoer, responsible for collecting data for the core. and displaying the data using a template engine.
3. **Database**: PostgreSQL for storing data. 3. **Database**: PostgreSQL for storing data.
## Running the project ## Running the project
There are two ways to run the project: launching each part individually or building the Dockerfile and running it using Docker Compose. Here's an example of the `docker-compose.yml` file for the latter option: There are two ways to run the project: launching each part individually or
building the Dockerfile and running it using Docker Compose. Here's an example
of the `docker-compose.yml` file for the latter option:
```yaml ```yaml
version: '3' version: '3'
services: services:
harvester:
container_name: harvester-ratingorama
image: harvester:0.1.0
networks:
- ratingorama
core: core:
container_name: core-ratingorama container_name: core-ratingorama
image: core:0.1.0 image: core:latest
environment: environment:
DATASOURCE: ${DATASOURCE} DATASOURCE: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable
HARVESTER_API: ${HARVESTER_API}
IS_PRODUCTION: ${IS_PRODUCTION}
ports: ports:
- "3000:3000" - "8080:8080"
networks: networks:
- ratingorama - ratingorama
db: db:
container_name: db-ratingorama container_name: db-ratingorama
image: postgres:15.2-alpine image: postgres:16.3-alpine3.20
environment: environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports: ports:
- "5433:5432" - "5432:5432"
volumes: volumes:
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql - rating-orama_data:/var/lib/postgresql/data
- ./data:/var/lib/postgresql/data
networks: networks:
- ratingorama - ratingorama
networks: networks:
ratingorama: ratingorama:
volumes:
rating-orama_data:
``` ```
## Contributions ## Contributions
If you have ideas for improvements or bug fixes, feel free to contribute! To do so, simply clone the repository, create a new branch, and submit a pull request. If you have ideas for improvements or bug fixes, feel free to contribute! To do
so, simply clone the repository, create a new branch, and submit a pull request.

5
core/.env.example Normal file
View File

@ -0,0 +1,5 @@
DRIVERNAME=pgx
MIGRATE=true
DATASOURCE=postgresql://developer:secret@localhost:5432/db?sslmode=disable
ASYMMETRICKEY=
DURATION=

View File

@ -1,13 +1,8 @@
FROM golang:1.20.3-alpine FROM alpine:3.20
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./ COPY ./database ./database
COPY ./tmp/rating .
RUN go mod download CMD ["/app/rating"]
COPY . .
RUN go build -o main
CMD ["/app/main"]

View File

@ -1,40 +1,145 @@
GO ?= go
GOFMT ?= gofmt "-s"
GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
PACKAGES ?= $(shell $(GO) list ./...)
VETPACKAGES ?= $(shell $(GO) list ./...)
GOFILES := $(shell find . -name "*.go")
.PHONY: sayhello
# Print Hello World
sayhello: sayhello:
@echo "Hello World" @echo "Hello World"
.PHONY: dockerize
# Creates a development database.
dockerize: dockerize:
docker run --name rating-db-dev -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=developer -e POSTGRES_DB=rating -p 5432:5432 -d postgres:16.3-alpine3.20 docker run --name rating-db-dev -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=developer -e POSTGRES_DB=rating -p 5432:5432 -d postgres:16.3-alpine3.20
.PHONY: undockerize
# Destroy a development database.
undockerize: undockerize:
docker rm -f rating-db-dev docker rm -f rating-db-dev
.PHONY: migrateup
# Migrate all schemas, triggers and data located in cmd/database/migrations.
migrateup: migrateup:
migrate -path database/migrations -database "postgresql://developer:secret@localhost:5432/rating?sslmode=disable" -verbose up migrate -path cmd/database/migrations -database "postgresql://developer:secret@localhost:5432/rating?sslmode=disable" -verbose up
migratedown:
migrate -path database/migrations -database "postgresql://developer:secret@localhost:5432/rating?sslmode=disable" -verbose down
.PHONY: sqlc
# Generate or recreate SQLC queries.
sqlc: sqlc:
sqlc generate sqlc generate
.PHONY: test
# Test all files and generate coverage file.
test: test:
go test -v -cover ./... $(GO) test -v -covermode=count -coverprofile=coverage.out $(PACKAGES)
.PHONY: gomock
# Generate mock files.
gomock: gomock:
mockgen -package mock -destination internal/repository/mock/querier.go github.com/zepyrshut/rating-orama/internal/repository ExtendedQuerier mockgen -package mock -destination internal/repository/mock/querier.go rating-orama/internal/repository ExtendedQuerier
.PHONY: run
# Run project.
run: run:
go run ./cmd/. $(GO) run ./cmd/.
.PHONY: recreate
# Destroy development DB and generate ones.
recreate: recreate:
make undockerize make undockerize
make dockerize make dockerize
sleep 2 sleep 2
make migrateup make migrateup
.PHONY: tidy
# Runs a go mod tidy
tidy:
$(GO) mod tidy
.PHONY: build-linux
# Build and generate linux executable.
build-linux: build-linux:
make tidy
make remove-debug
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./tmp/arena ./cmd/. CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./tmp/arena ./cmd/.
.PHONY: pack-docker
# Run docker build for pack binary and assets to Docker container.
pack-docker: pack-docker:
make test make test
make build-linux make build-linux
docker build -t rating:${version} -t rating:latest . docker build -t rating-orama:${version} -t rating-orama:latest .
.PHONY: remove-debug
# Remove all debug entries for reduce size binary.
remove-debug:
find . -name "*.go" -type f -exec sed -i '/slog\.Debug/d' {} +
.PHONY: fmt
# Ensure consistent code formatting.
fmt:
$(GOFMT) -w $(GOFILES)
.PHONY: fmt-check
# format (check only).
fmt-check:
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \
exit 1; \
fi;
.PHONY: vet
# Examine packages and report suspicious constructs if any.
vet:
$(GO) vet $(VETPACKAGES)
.PHONY: tools
# Install tools (migrate and sqlc).
tools:
@if [ $(GO_VERSION) -gt 16 ]; then \
$(GO) install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest; \
$(GO) install github.com/sqlc-dev/sqlc/cmd/sqlc@latest; \
fi
.PHONY: env
# Copy .env.example to .env if .env does not already exist
env:
@if [ ! -f .env ]; then \
cp .env.example .env; \
echo ".env file created from .env.example"; \
else \
echo ".env file already exists"; \
fi
.PHONY: first-run
# Runs for the first time
first-run:
make tools
make env
make recreate
make run
.PHONY: help
# Help.
help:
@echo ''
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-\0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf " - \033[36m%-20s\033[0m %s\n", helpCommand, helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help

View File

@ -14,7 +14,8 @@ where tt_imdb = $1;
-- name: GetEpisodes :many -- name: GetEpisodes :many
select * from "episodes" select * from "episodes"
where tv_show_id = $1; where tv_show_id = $1
order by season, episode asc;
-- name: IncreasePopularity :exec -- name: IncreasePopularity :exec
update "tv_show" set popularity = popularity + 1 update "tv_show" set popularity = popularity + 1
@ -33,5 +34,5 @@ select avg(avg_rating) from "episodes"
where tv_show_id = $1 and season = $2; where tv_show_id = $1 and season = $2;
-- name: SeasonMedianRating :one -- name: SeasonMedianRating :one
-- select percentile_cont(0.5) within group (order by avg_rating) from "episodes" select percentile_cont(0.5) within group (order by avg_rating) from "episodes"
-- where tv_show_id = $1 and season = $2; where tv_show_id = $1 and season = $2;

View File

@ -1,82 +1,42 @@
package main package main
import ( import (
"embed"
"encoding/gob" "encoding/gob"
"gopher-toolbox/config" "gopher-toolbox/app"
"log/slog"
"net/http"
"os"
"gopher-toolbox/db" "gopher-toolbox/db"
"gopher-toolbox/utils" "log/slog"
"github.com/golang-migrate/migrate/v4" "github.com/gofiber/fiber/v2"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/joho/godotenv"
"github.com/zepyrshut/rating-orama/internal/handlers" "github.com/zepyrshut/rating-orama/internal/handlers"
"github.com/zepyrshut/rating-orama/internal/repository" "github.com/zepyrshut/rating-orama/internal/repository"
) )
const version = "0.2.0-beta.20241116-4" //go:embed database/migrations
var database embed.FS
var app *config.App const version = "0.2.0-beta.20241116-4"
const appName = "rating-orama"
func init() { func init() {
gob.Register(map[string]string{}) gob.Register(map[string]string{})
err := godotenv.Load()
if err != nil {
slog.Error("cannot load .env file", "error", err)
}
config.NewLogger(config.LogLevel(os.Getenv("LOG_LEVEL")))
slog.Info("starting server")
if os.Getenv("MIGRATE") == "true" {
migrateDB()
}
}
func migrateDB() {
slog.Info("migrating database")
m, err := migrate.New("file://database/migrations", os.Getenv("DATASOURCE"))
if err != nil {
slog.Error("cannot create migration", "error", err)
}
err = m.Up()
if err != nil && err != migrate.ErrNoChange {
slog.Error("cannot migrate", "error", err)
panic(err)
}
if err == migrate.ErrNoChange {
slog.Info("migration has no changes")
}
slog.Info("migration done")
} }
func main() { func main() {
app := app.New(version)
app = &config.App{ r := fiber.New(fiber.Config{
DataSource: os.Getenv("DATASOURCE"), AppName: appName,
UseCache: utils.GetBool(os.Getenv("USE_CACHE")), })
AppInfo: config.AppInfo{
GinMode: os.Getenv("GIN_MODE"), dbPool := db.NewPGXPool(app.Database.DataSource)
Version: version,
},
}
dbPool := db.NewPostgresPool(app.DataSource)
defer dbPool.Close() defer dbPool.Close()
q := repository.NewPGXRepo(dbPool) q := repository.NewPGXRepo(dbPool)
h := handlers.New(q, app) h := handlers.New(app, q)
r := Router(h, app) router(h, r)
slog.Info("server started", "port", "8080", "version", version) slog.Info("server started", "port", "8080", "version", version)
err := http.ListenAndServe(":8080", r) if err := r.Listen(":8080"); err != nil {
if err != nil {
slog.Error("cannot start server", "error", err) slog.Error("cannot start server", "error", err)
} }
} }

View File

@ -1,17 +1,12 @@
package main package main
import ( import (
"github.com/gin-gonic/gin" "github.com/gofiber/fiber/v2"
"github.com/zepyrshut/rating-orama/internal/handlers" "github.com/zepyrshut/rating-orama/internal/handlers"
"gopher-toolbox/config"
) )
func Router(h *handlers.Handlers, app *config.App) *gin.Engine { func router(h *handlers.Handlers, r *fiber.App) {
gin.SetMode(app.AppInfo.GinMode)
r := gin.New()
r.GET("/tvshow", h.GetTVShow) r.Get("/tvshow", h.GetTVShow)
return r
} }

View File

@ -2,69 +2,56 @@ module github.com/zepyrshut/rating-orama
go 1.23.2 go 1.23.2
require github.com/jackc/pgx/v5 v5.7.1 require (
github.com/PuerkitoBio/goquery v1.10.0
github.com/gofiber/fiber/v2 v2.52.5
github.com/jackc/pgx/v5 v5.7.1
)
require ( require (
github.com/PuerkitoBio/goquery v1.10.0 // indirect aidanwoods.dev/go-paseto v1.5.2 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect aidanwoods.dev/go-result v0.1.0 // indirect
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/antchfx/htmlquery v1.3.3 // indirect github.com/antchfx/htmlquery v1.3.3 // indirect
github.com/antchfx/xmlquery v1.4.2 // indirect github.com/antchfx/xmlquery v1.4.2 // indirect
github.com/antchfx/xpath v1.3.2 // indirect github.com/antchfx/xpath v1.3.2 // indirect
github.com/bytedance/sonic v1.12.4 // indirect github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgconn v1.14.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/klauspost/compress v1.17.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect github.com/lib/pq v1.10.9 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/o1egl/paseto v1.0.0 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/temoto/robotstxt v1.1.2 // indirect github.com/temoto/robotstxt v1.1.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/valyala/fasthttp v1.51.0 // indirect
go.uber.org/atomic v1.11.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/arch v0.11.0 // indirect go.uber.org/atomic v1.7.0 // indirect
golang.org/x/net v0.30.0 // indirect golang.org/x/net v0.30.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.35.1 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( require (
github.com/gin-gonic/gin v1.10.0
github.com/gocolly/colly v1.2.0 github.com/gocolly/colly v1.2.0
github.com/golang-migrate/migrate/v4 v4.18.1 github.com/golang-migrate/migrate/v4 v4.18.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/crypto v0.28.0 // indirect golang.org/x/crypto v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/sys v0.26.0 // indirect

View File

@ -1,16 +1,15 @@
aidanwoods.dev/go-paseto v1.5.2 h1:9aKbCQQUeHCqis9Y6WPpJpM9MhEOEI5XBmfTkFMSF/o=
aidanwoods.dev/go-paseto v1.5.2/go.mod h1:7eEJZ98h2wFi5mavCcbKfv9h86oQwut4fLVeL/UBFnw=
aidanwoods.dev/go-result v0.1.0 h1:y/BMIRX6q3HwaorX1Wzrjo3WUdiYeyWbvGe18hKS3K8=
aidanwoods.dev/go-result v0.1.0/go.mod h1:yridkWghM7AXSFA6wzx0IbsurIm1Lhuro3rYef8FBHM=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU=
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE=
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antchfx/htmlquery v1.3.3 h1:x6tVzrRhVNfECDaVxnZi1mEGrQg3mjE/rxbH2Pe6dNE= github.com/antchfx/htmlquery v1.3.3 h1:x6tVzrRhVNfECDaVxnZi1mEGrQg3mjE/rxbH2Pe6dNE=
@ -19,15 +18,6 @@ github.com/antchfx/xmlquery v1.4.2 h1:MZKd9+wblwxfQ1zd1AdrTsqVaMjMCwow3IqkCSe00K
github.com/antchfx/xmlquery v1.4.2/go.mod h1:QXhvf5ldTuGqhd1SHNvvtlhhdQLks4dD0awIVhXIDTA= github.com/antchfx/xmlquery v1.4.2/go.mod h1:QXhvf5ldTuGqhd1SHNvvtlhhdQLks4dD0awIVhXIDTA=
github.com/antchfx/xpath v1.3.2 h1:LNjzlsSjinu3bQpw9hWMY9ocB80oLOWuQqFvO6xt51U= github.com/antchfx/xpath v1.3.2 h1:LNjzlsSjinu3bQpw9hWMY9ocB80oLOWuQqFvO6xt51U=
github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -43,30 +33,18 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
@ -80,7 +58,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -107,75 +84,52 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0=
github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU=
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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zepyrshut/esfaker v0.0.0-20241017072233-b4a5efb1f24d h1:o52tUkQBIDD6s2v2OHmXIsZQIKTEiVMPeov2SmvXJWk=
github.com/zepyrshut/esfaker v0.0.0-20241017072233-b4a5efb1f24d/go.mod h1:HgsPkO8n/XumWNHKfMZNV9UgC9/sUghpxVFuQcPJd2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
@ -184,11 +138,8 @@ go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
@ -208,12 +159,12 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -241,13 +192,10 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -1,90 +1,30 @@
package handlers package handlers
import ( import (
"log/slog"
"net/http" "net/http"
"strings"
"gopher-toolbox/config" "gopher-toolbox/app"
"github.com/gin-gonic/gin" "github.com/gofiber/fiber/v2"
"github.com/zepyrshut/rating-orama/internal/repository" "github.com/zepyrshut/rating-orama/internal/repository"
) )
// TODO: Extract to toolbox
const (
InvalidRequest string = "invalid_request"
InternalError string = "internal_error"
RequestID string = "request_id"
NotFound string = "not_found"
Created string = "created"
Updated string = "updated"
Deleted string = "deleted"
Enabled string = "enabled"
Disabled string = "disabled"
Retrieved string = "retrieved"
ErrorCreating string = "error_creating"
ErrorUpdating string = "error_updating"
ErrorEnabling string = "error_enabling"
ErrorDisabling string = "error_disabling"
ErrorGetting string = "error_getting"
ErrorGettingAll string = "error_getting_all"
InvalidEntityID string = "invalid_entity_id"
NotImplemented string = "not_implemented"
UserUsernameKey string = "user_username_key"
UserEmailKey string = "user_email_key"
UsernameAlReadyExists string = "username_already_exists"
EmailAlreadyExists string = "email_already_exists"
IncorrectPassword string = "incorrect_password"
ErrorGeneratingToken string = "error_generating_token"
LoggedIn string = "logged_in"
CategoryNameKey string = "category_name_key"
CategoryAlreadyExists string = "category_already_exists"
ItemsNameKey string = "items_name_key"
NameAlreadyExists string = "name_already_exists"
)
type Handlers struct { type Handlers struct {
App *config.App app *app.App
Queries repository.ExtendedQuerier queries repository.ExtendedQuerier
} }
func New(q repository.ExtendedQuerier, app *config.App) *Handlers { func New(app *app.App, q repository.ExtendedQuerier) *Handlers {
return &Handlers{ return &Handlers{
Queries: q, app: app,
App: app, queries: q,
} }
} }
func (hq *Handlers) ToBeImplemented(c *gin.Context) { func (hq *Handlers) ToBeImplemented(c *fiber.Ctx) error {
c.JSON(http.StatusOK, gin.H{ return c.Status(http.StatusNotImplemented).JSON("not implemented")
"message": "Not implemented yet",
})
} }
func (hq *Handlers) Ping(c *gin.Context) { func (hq *Handlers) Ping(c *fiber.Ctx) error {
c.JSON(http.StatusOK, gin.H{ return c.JSON("pong")
"message": "pong",
})
}
// TODO: Extract to toolbox
func handleQueryError(c *gin.Context, err error, errorMap map[string]string, logMessage string, defaultErrorMessage string) bool {
if err != nil {
for key, message := range errorMap {
if strings.Contains(err.Error(), key) {
slog.Error(logMessage, "error", message, RequestID, c.Request.Context().Value(RequestID))
c.JSON(http.StatusConflict, gin.H{"error": message})
return true
}
}
slog.Error(logMessage, "error", err.Error(), RequestID, c.Request.Context().Value(RequestID))
c.JSON(http.StatusInternalServerError, gin.H{"error": defaultErrorMessage})
return true
}
return false
} }

View File

@ -4,53 +4,58 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gofiber/fiber/v2"
"github.com/zepyrshut/rating-orama/internal/scraper" "github.com/zepyrshut/rating-orama/internal/scraper"
"github.com/zepyrshut/rating-orama/internal/sqlc" "github.com/zepyrshut/rating-orama/internal/sqlc"
) )
func (hq *Handlers) GetTVShow(c *gin.Context) { func (hq *Handlers) GetTVShow(c *fiber.Ctx) error {
ttShowID := c.Query("ttid") ttShowID := c.Query("ttid")
slog.Info("", "ttid", ttShowID, RequestID, c.Request.Context().Value(RequestID))
var title string var title string
var scraperEpisodes []scraper.Episode var scraperEpisodes []scraper.Episode
var sqlcEpisodes []sqlc.Episode var sqlcEpisodes []sqlc.Episode
tvShow, err := hq.Queries.CheckTVShowExists(c, ttShowID) tvShow, err := hq.queries.CheckTVShowExists(c.Context(), ttShowID)
if err != nil { if err != nil {
title, scraperEpisodes = scraper.ScrapeEpisodes(ttShowID) title, scraperEpisodes = scraper.ScrapeEpisodes(ttShowID)
// TODO: make transactional
sqlcEpisodes, err = hq.Queries.CreateTvShowWithEpisodes(c, sqlc.CreateTVShowParams{ ttShow, err := hq.queries.CreateTVShow(c.Context(), sqlc.CreateTVShowParams{
TtImdb: ttShowID, TtImdb: ttShowID,
Name: title, Name: title,
}, scraperEpisodes) })
if err != nil { if err != nil {
slog.Error("failed to create tv show with episodes", "ttid", ttShowID, "error", err) slog.Error("failed to create tv show", "ttid", ttShowID, "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrorCreating}) return c.SendStatus(http.StatusInternalServerError)
return }
slog.Info("ttshowid", "id", ttShow.ID)
for _, episode := range scraperEpisodes {
sqlcEpisodesParams := episode.ToEpisodeParams(ttShow.ID)
sqlcEpisode, err := hq.queries.CreateEpisodes(c.Context(), sqlcEpisodesParams)
if err != nil {
slog.Error("failed to create episodes", "ttid", ttShowID, "error", err)
return c.SendStatus(http.StatusInternalServerError)
}
sqlcEpisodes = append(sqlcEpisodes, sqlcEpisode)
} }
slog.Info("scraped seasons", "ttid", ttShowID, "title", title) slog.Info("scraped seasons", "ttid", ttShowID, "title", title)
} else { } else {
title = tvShow.Name title = tvShow.Name
sqlcEpisodes, err = hq.Queries.GetEpisodes(c, tvShow.ID) sqlcEpisodes, err = hq.queries.GetEpisodes(c.Context(), tvShow.ID)
if err != nil { if err != nil {
slog.Error("failed to get episodes", "ttid", ttShowID, "error", err) slog.Error("failed to get episodes", "ttid", ttShowID, "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrorGetting}) return c.SendStatus(http.StatusInternalServerError)
return
}
if err := hq.Queries.IncreasePopularity(c, ttShowID); err != nil {
slog.Error("failed to increase popularity", "ttid", ttShowID, "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrorUpdating})
return
} }
hq.queries.IncreasePopularity(c.Context(), ttShowID)
slog.Info("tv show exists", "ttid", ttShowID, "title", tvShow.Name) slog.Info("tv show exists", "ttid", ttShowID, "title", tvShow.Name)
} }
c.JSON(http.StatusOK, gin.H{ return c.JSON(fiber.Map{
"popularity": tvShow.Popularity, "popularity": tvShow.Popularity,
"title": title, "title": title,
"seasons": sqlcEpisodes, "seasons": sqlcEpisodes,

View File

@ -3,8 +3,6 @@ package repository
import ( import (
"context" "context"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"log/slog"
"github.com/zepyrshut/rating-orama/internal/scraper" "github.com/zepyrshut/rating-orama/internal/scraper"
"github.com/zepyrshut/rating-orama/internal/sqlc" "github.com/zepyrshut/rating-orama/internal/sqlc"
) )
@ -18,11 +16,8 @@ func (r *pgxRepository) CreateTvShowWithEpisodes(ctx context.Context, tvShow sql
return err return err
} }
slog.Info("episodes lenght", "episodes", len(episodes))
for _, episode := range episodes { for _, episode := range episodes {
sqlcEpisodeParams := episode.ToEpisodeParams(tvShow.ID) sqlcEpisodeParams := episode.ToEpisodeParams(tvShow.ID)
slog.Info("creating episode", "episode", sqlcEpisodeParams)
episode, err := qtx.CreateEpisodes(ctx, sqlcEpisodeParams) episode, err := qtx.CreateEpisodes(ctx, sqlcEpisodeParams)
if err != nil { if err != nil {
return err return err

View File

@ -9,6 +9,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/PuerkitoBio/goquery"
"github.com/gocolly/colly" "github.com/gocolly/colly"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"github.com/zepyrshut/rating-orama/internal/sqlc" "github.com/zepyrshut/rating-orama/internal/sqlc"
@ -27,7 +29,7 @@ type Episode struct {
func (e Episode) ToEpisodeParams(tvShowID int32) sqlc.CreateEpisodesParams { func (e Episode) ToEpisodeParams(tvShowID int32) sqlc.CreateEpisodesParams {
var date pgtype.Date var date pgtype.Date
date.Scan(e.Released) _ = date.Scan(e.Released)
return sqlc.CreateEpisodesParams{ return sqlc.CreateEpisodesParams{
TvShowID: tvShowID, TvShowID: tvShowID,
@ -42,14 +44,19 @@ func (e Episode) ToEpisodeParams(tvShowID int32) sqlc.CreateEpisodesParams {
} }
const ( const (
titleSelector = "h2.sc-b8cc654b-9.dmvgRY" titleSelector = "h2.sc-b8cc654b-9.dmvgRY"
seasonsSelector = "ul.ipc-tabs a[data-testid='tab-season-entry']" seasonsSelector = "ul.ipc-tabs a[data-testid='tab-season-entry']"
episodesSelector = "section.sc-1e7f96be-0.ZaQIL" episodeCardSelector = "article.sc-f8507e90-1.cHtpvn.episode-item-wrapper"
nextSeasonButtonSelector = "#next-season-btn" seasonEpisodeAndTitleSelector = "div.ipc-title__text"
imdbEpisodesURL = "https://www.imdb.com/title/%s/episodes?season=%d" releasedDateSelector = "span.sc-f2169d65-10.bYaARM"
visitURL = "https://www.imdb.com/title/%s/episodes" plotSelector = "div.ipc-html-content-inner-div"
starRatingSelector = "span.ipc-rating-star--rating"
voteCountSelector = "span.ipc-rating-star--voteCount"
imdbEpisodesURL = "https://www.imdb.com/title/%s/episodes/?season=%d"
visitURL = "https://www.imdb.com/title/%s/episodes"
) )
func ScrapeEpisodes(ttImdb string) (string, []Episode) { func ScrapeEpisodes(ttImdb string) (string, []Episode) {
c := colly.NewCollector( c := colly.NewCollector(
colly.AllowedDomains("imdb.com", "www.imdb.com"), colly.AllowedDomains("imdb.com", "www.imdb.com"),
@ -77,7 +84,7 @@ func ScrapeEpisodes(ttImdb string) (string, []Episode) {
c.OnScraped(func(r *colly.Response) { c.OnScraped(func(r *colly.Response) {
seasonMap := make(map[int]bool) seasonMap := make(map[int]bool)
uniqueSeasons := []int{} var uniqueSeasons []int
slog.Info("scraped seasons", "seasons", seasons) slog.Info("scraped seasons", "seasons", seasons)
for _, seasonNum := range seasons { for _, seasonNum := range seasons {
if !seasonMap[seasonNum] { if !seasonMap[seasonNum] {
@ -87,77 +94,121 @@ func ScrapeEpisodes(ttImdb string) (string, []Episode) {
} }
sort.Ints(uniqueSeasons) sort.Ints(uniqueSeasons)
episodeCollector := c.Clone() episodeCollector := c.Clone()
episodeCollector.OnHTML(episodesSelector, func(e *colly.HTMLElement) { episodeCollector.OnResponse(func(r *colly.Response) {
seasonEpisodes := extractEpisodesFromSeason(e.Text) slog.Info("response", "url", r.Request.URL)
allSeasons = append(allSeasons, seasonEpisodes...) season := extractEpisodesFromSeason(string(r.Body))
allSeasons = append(allSeasons, season...)
}) })
for _, seasonNum := range uniqueSeasons { for _, seasonNum := range uniqueSeasons {
seasonURL := fmt.Sprintf(imdbEpisodesURL, ttImdb, seasonNum) seasonURL := fmt.Sprintf(imdbEpisodesURL, ttImdb, seasonNum)
slog.Info("visiting season", "url", seasonURL) slog.Info("visiting season", "url", seasonURL)
episodeCollector.Visit(seasonURL) _ = episodeCollector.Visit(seasonURL)
} }
episodeCollector.Wait() episodeCollector.Wait()
}) })
c.Visit(fmt.Sprintf(visitURL, ttImdb)) _ = c.Visit(fmt.Sprintf(visitURL, ttImdb))
c.Wait() c.Wait()
slog.Info("scraped all seasons", "seasons", allSeasons) slog.Info("scraped all seasons", "length", len(allSeasons))
return title, allSeasons return title, allSeasons
} }
func extractEpisodesFromSeason(data string) []Episode { func extractEpisodesFromSeason(data string) []Episode {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
slog.Info("extracting episodes", "data", data) if err != nil {
slog.Error("error parsing html")
const pattern = `(S\d+\.E\d+)\s∙\s(.*?)` + return []Episode{}
`(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s` +
`(.*?),\s(\d{4})(.*?)` +
`(\d\.\d{1,2}\/10) \((\d+K)\)Rate`
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(data, -1)
episodes := make([]Episode, 0, len(matches))
slog.Info("matches", "num", len(matches))
for _, match := range matches {
var episode Episode
seasonEpisode := match[1]
name := strings.TrimSpace(match[2])
day := match[3]
dateRest := strings.TrimSpace(match[4])
year := match[5]
plot := strings.TrimSpace(match[6])
rate := match[7]
voteCount := match[8]
seasonNum := strings.TrimPrefix(strings.Split(seasonEpisode, ".")[0], "S")
episodeNum := strings.TrimPrefix(strings.Split(seasonEpisode, ".")[1], "E")
votesInt, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimSuffix(voteCount, "K"), "K"))
rateFloat, _ := strconv.ParseFloat(strings.TrimSuffix(rate, "/10"), 32)
episode.Name = name
episode.Episode, _ = strconv.Atoi(episodeNum)
episode.Season, _ = strconv.Atoi(seasonNum)
episode.Released, _ = time.Parse("Mon, Jan 2, 2006", fmt.Sprintf("%s, %s, %s", day, dateRest, year))
episode.Plot = plot
episode.Rate = float32(rateFloat)
episode.VoteCount = votesInt * 1000
episodes = append(episodes, episode)
} }
var episodes []Episode
doc.Find(episodeCardSelector).Each(func(i int, s *goquery.Selection) {
var episode Episode
seasonEpisodeTitle := s.Find(seasonEpisodeAndTitleSelector).Text()
episode.Season, episode.Episode, episode.Name = parseSeasonEpisodeTitle(seasonEpisodeTitle)
releasedDate := s.Find(releasedDateSelector).Text()
episode.Released = parseReleasedDate(releasedDate)
plot := s.Find(plotSelector).Text()
if plot == "Add a plot" {
episode.Plot = ""
} else {
episode.Plot = plot
}
starRating := s.Find(starRatingSelector).Text()
episode.Rate = parseStarRating(starRating)
voteCount := s.Find(voteCountSelector).Text()
episode.VoteCount = parseVoteCount(voteCount)
episodes = append(episodes, episode)
})
slog.Info("extracted episodes", "length", len(episodes))
return episodes return episodes
} }
func parseSeasonEpisodeTitle(input string) (int, int, string) {
re := regexp.MustCompile(`S(\d+)\.E(\d+)\s*∙\s*(.+)`)
matches := re.FindStringSubmatch(input)
if len(matches) != 4 {
return 0, 0, ""
}
seasonNum, err1 := strconv.Atoi(matches[1])
episodeNum, err2 := strconv.Atoi(matches[2])
name := strings.TrimSpace(matches[3])
if err1 != nil || err2 != nil {
return 0, 0, ""
}
return seasonNum, episodeNum, name
}
func parseReleasedDate(releasedDate string) time.Time {
const layout = "Mon, Jan 2, 2006"
parsedDate, err := time.Parse(layout, releasedDate)
if err != nil {
slog.Error("error parsing date", "date", releasedDate)
return time.Time{}
}
return parsedDate
}
func parseStarRating(starRating string) float32 {
rating, err := strconv.ParseFloat(starRating, 32)
if err != nil || rating < 0 || rating > 10 {
slog.Warn("error parsing rating, out of limits", "rating", starRating)
return 0
}
return float32(rating)
}
func parseVoteCount(voteCount string) int {
re := regexp.MustCompile(`\(([\d.]+)(K?)\)`)
matches := re.FindStringSubmatch(voteCount)
if len(matches) != 3 {
slog.Error("error parsing vote count", "count", voteCount)
return 0
}
num, err := strconv.ParseFloat(matches[1], 64)
if err != nil {
slog.Error("error parsing vote count", "count", voteCount)
return 0
}
if matches[2] == "K" {
num *= 1000
}
return int(num)
}

View File

@ -0,0 +1,117 @@
package scraper
import (
"testing"
"time"
)
func Test_parseSeasonEpisodeTitle(t *testing.T) {
var tests = []struct {
given string
expected struct {
seasonNum int
episodeNum int
name string
}
}{
{"S5.E1 ∙ Live Free or Die", struct {
seasonNum int
episodeNum int
name string
}{5, 1, "Live Free or Die"}},
{"S5.E13 ∙ To'hajiilee", struct {
seasonNum int
episodeNum int
name string
}{5, 13, "To'hajiilee"}},
}
for _, tt := range tests {
t.Run(tt.given, func(t *testing.T) {
seasonNum, episodeNum, name := parseSeasonEpisodeTitle(tt.given)
if seasonNum != tt.expected.seasonNum || episodeNum != tt.expected.episodeNum || name != tt.expected.name {
t.Errorf("parseSeasonEpisodeTitle(%s): expected %d, %d, %s, actual %d, %d, %s", tt.given, tt.expected.seasonNum, tt.expected.episodeNum, tt.expected.name, seasonNum, episodeNum, name)
}
})
}
}
func Test_parseReleasedDate(t *testing.T) {
var tests = []struct {
given string
expected time.Time
}{
{"", time.Time{}},
{"1", time.Time{}},
{"Sun, Feb 3, 2005", time.Date(2005, time.February, 3, 0, 0, 0, 0, time.UTC)},
{"Mon, Jan 2, 2006", time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC)},
{"Tue, Mar 4, 2007", time.Date(2007, time.March, 4, 0, 0, 0, 0, time.UTC)},
{"Wed, Apr 5, 2008", time.Date(2008, time.April, 5, 0, 0, 0, 0, time.UTC)},
{"Thu, May 6, 2009", time.Date(2009, time.May, 6, 0, 0, 0, 0, time.UTC)},
{"Fri, Jun 7, 2010", time.Date(2010, time.June, 7, 0, 0, 0, 0, time.UTC)},
{"Sat, Jul 8, 2011", time.Date(2011, time.July, 8, 0, 0, 0, 0, time.UTC)},
{"Sun, Aug 9, 2012", time.Date(2012, time.August, 9, 0, 0, 0, 0, time.UTC)},
{"Mon, Sep 10, 2013", time.Date(2013, time.September, 10, 0, 0, 0, 0, time.UTC)},
{"Tue, Oct 11, 2014", time.Date(2014, time.October, 11, 0, 0, 0, 0, time.UTC)},
{"Wed, Nov 12, 2015", time.Date(2015, time.November, 12, 0, 0, 0, 0, time.UTC)},
{"Thu, Dec 13, 2016", time.Date(2016, time.December, 13, 0, 0, 0, 0, time.UTC)},
}
for _, tt := range tests {
t.Run(tt.given, func(t *testing.T) {
actual := parseReleasedDate(tt.given)
if actual != tt.expected {
t.Errorf("parseReleasedDate(%s): expected %v, actual %v", tt.given, tt.expected, actual)
}
})
}
}
func Test_parseStarRating(t *testing.T) {
var tests = []struct {
given string
expected float32
}{
{"1", 1},
{"1.5", 1.5},
{"10", 10},
{"10.5", 0},
{"0", 0},
{"999", 0},
{"hello", 0},
}
for _, tt := range tests {
t.Run(tt.given, func(t *testing.T) {
actual := parseStarRating(tt.given)
if actual != tt.expected {
t.Errorf("parseStarRating(%s): expected %f, actual %f", tt.given, tt.expected, actual)
}
})
}
}
func Test_parseVoteCount(t *testing.T) {
var tests = []struct {
given string
expected int
}{
{" (148K)", 148000},
{" (8K)", 8000},
{" (12K)", 12000},
{" (1)", 1},
{" (10)", 10},
{" (100)", 100},
{" (1K)", 1000},
{" (1.9K)", 1900},
}
for _, tt := range tests {
t.Run(tt.given, func(t *testing.T) {
actual := parseVoteCount(tt.given)
if actual != tt.expected {
t.Errorf("parseVoteCount(%s): expected %d, actual %d", tt.given, tt.expected, actual)
}
})
}
}

View File

@ -15,6 +15,7 @@ type Querier interface {
GetEpisodes(ctx context.Context, tvShowID int32) ([]Episode, error) GetEpisodes(ctx context.Context, tvShowID int32) ([]Episode, error)
IncreasePopularity(ctx context.Context, ttImdb string) error IncreasePopularity(ctx context.Context, ttImdb string) error
SeasonAverageRating(ctx context.Context, arg SeasonAverageRatingParams) (float64, error) SeasonAverageRating(ctx context.Context, arg SeasonAverageRatingParams) (float64, error)
SeasonMedianRating(ctx context.Context, arg SeasonMedianRatingParams) (float64, error)
TvShowAverageRating(ctx context.Context, tvShowID int32) (float64, error) TvShowAverageRating(ctx context.Context, tvShowID int32) (float64, error)
TvShowMedianRating(ctx context.Context, tvShowID int32) (float64, error) TvShowMedianRating(ctx context.Context, tvShowID int32) (float64, error)
} }

View File

@ -101,6 +101,7 @@ func (q *Queries) CreateTVShow(ctx context.Context, arg CreateTVShowParams) (TvS
const getEpisodes = `-- name: GetEpisodes :many const getEpisodes = `-- name: GetEpisodes :many
select id, tv_show_id, season, episode, released, name, plot, avg_rating, vote_count from "episodes" select id, tv_show_id, season, episode, released, name, plot, avg_rating, vote_count from "episodes"
where tv_show_id = $1 where tv_show_id = $1
order by season, episode asc
` `
func (q *Queries) GetEpisodes(ctx context.Context, tvShowID int32) ([]Episode, error) { func (q *Queries) GetEpisodes(ctx context.Context, tvShowID int32) ([]Episode, error) {
@ -160,6 +161,23 @@ func (q *Queries) SeasonAverageRating(ctx context.Context, arg SeasonAverageRati
return avg, err return avg, err
} }
const seasonMedianRating = `-- name: SeasonMedianRating :one
select percentile_cont(0.5) within group (order by avg_rating) from "episodes"
where tv_show_id = $1 and season = $2
`
type SeasonMedianRatingParams struct {
TvShowID int32 `json:"tv_show_id"`
Season int32 `json:"season"`
}
func (q *Queries) SeasonMedianRating(ctx context.Context, arg SeasonMedianRatingParams) (float64, error) {
row := q.db.QueryRow(ctx, seasonMedianRating, arg.TvShowID, arg.Season)
var percentile_cont float64
err := row.Scan(&percentile_cont)
return percentile_cont, err
}
const tvShowAverageRating = `-- name: TvShowAverageRating :one const tvShowAverageRating = `-- name: TvShowAverageRating :one
select avg(avg_rating) from "episodes" select avg(avg_rating) from "episodes"
where tv_show_id = $1 where tv_show_id = $1

View File

@ -1,8 +1,8 @@
version: "2" version: "2"
sql: sql:
- engine: "postgresql" - engine: "postgresql"
schema: "./database/migrations/*" schema: "./cmd/database/migrations/*"
queries: "./database/queries/*" queries: "./cmd/database/queries/*"
gen: gen:
go: go:
package: "sqlc" package: "sqlc"

View File

@ -1,28 +0,0 @@
package utils
import "time"
// TODO: Move to toolbox
func TimeParser(timeString string) (time.Time, error) {
if len(timeString) == 1 {
return time.Time{}, nil
}
if len(timeString) == 4 {
return time.Parse("2006", timeString)
}
if len(timeString) == 9 {
return time.Parse("Jan. 2006", timeString)
}
if len(timeString) == 10 {
return time.Parse("2 Jan 2006", timeString)
}
if len(timeString) == 11 {
if timeString[5:6] == "." {
return time.Parse("2 Jan. 2006", timeString)
} else {
return time.Parse("2 Jan 2006", timeString)
}
}
return time.Parse("2 Jan. 2006", timeString)
}

3284
views-dev/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff