Compare commits
20 Commits
e541681dd1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a4cfeb8496 | |||
| 17d4dca0c9 | |||
| d947557373 | |||
| 4418e009e0 | |||
| edaf4583a0 | |||
| 7e5baa96e8 | |||
| 6f78073431 | |||
| 955ac89c1a | |||
| 1786100052 | |||
| 34d1088d9d | |||
| 430892a512 | |||
| 958ef12e91 | |||
| a6f3325842 | |||
| 4dbb47be37 | |||
| 490e610e4e | |||
| c0dd8681aa | |||
| fdee50b574 | |||
| 90c21a1f55 | |||
| 3d71a16633 | |||
| 8c1f642e33 |
@@ -28,3 +28,5 @@ htmlcov/
|
|||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
*,cover
|
*,cover
|
||||||
|
tmp/
|
||||||
|
docker.env
|
||||||
@@ -3,10 +3,13 @@
|
|||||||
All notable change notes in this project will be documented in this file.
|
All notable change notes in this project will be documented in this file.
|
||||||
|
|
||||||
## [0.1.0-2] - 2023-04-09
|
## [0.1.0-2] - 2023-04-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `docker-compose.yml`: Changed the volume path for the `init.sql` file:
|
- `docker-compose.yml`: Changed the volume path for the `init.sql` file:
|
||||||
- From: `./schema.sql:/docker-entrypoint-initdb.d/schema.sql`
|
- From: `./schema.sql:/docker-entrypoint-initdb.d/schema.sql`
|
||||||
- To: `./init.sql:/docker-entrypoint-initdb.d/init.sql`
|
- To: `./init.sql:/docker-entrypoint-initdb.d/init.sql`
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- `docker/schema.sql`: The file has been removed. The schema configuration has been moved to the `init.sql` file.
|
- `docker/schema.sql`: The file has been removed. The schema configuration has been moved to the `init.sql` file.
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
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")
|
||||||
|
CORE_DIR := ./core
|
||||||
|
UI_DIR := ./ui
|
||||||
|
DOCS_DIR := ./docs
|
||||||
|
LIBRARIES_DIR := ./../libraries
|
||||||
|
PG_VERSION := 16.4-alpine3.20
|
||||||
|
DB_NAME := rating
|
||||||
|
MOD_NAME := rating-orama
|
||||||
|
|
||||||
|
.PHONY: sayhello
|
||||||
|
# Print Hello World
|
||||||
|
sayhello:
|
||||||
|
@echo "Hello World"
|
||||||
|
|
||||||
|
.PHONY: dockerize
|
||||||
|
# Creates a development database.
|
||||||
|
dockerize:
|
||||||
|
docker run --name $(DB_NAME)-db-dev -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=developer -e POSTGRES_DB=$(DB_NAME) -p 5432:5432 -d postgres:$(PG_VERSION)
|
||||||
|
|
||||||
|
.PHONY: dockerize-test
|
||||||
|
# Creates a test database.
|
||||||
|
dockerize-test:
|
||||||
|
docker run --name $(DB_NAME)-db-test -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=developer -e POSTGRES_DB=$(DB_NAME) -p 5433:5432 -d postgres:$(PG_VERSION)
|
||||||
|
|
||||||
|
.PHONY: undockerize
|
||||||
|
# Destroy a development database.
|
||||||
|
undockerize:
|
||||||
|
docker rm -f $(DB_NAME)-db-dev
|
||||||
|
|
||||||
|
.PHONY: undockerize-test
|
||||||
|
# Destroy a test database.
|
||||||
|
undockerize-test:
|
||||||
|
docker rm -f $(DB_NAME)-db-test
|
||||||
|
|
||||||
|
.PHONY: restart-db
|
||||||
|
# Restart a development database.
|
||||||
|
restart-db:
|
||||||
|
make undockerize
|
||||||
|
make dockerize
|
||||||
|
|
||||||
|
.PHONY: restart-db-test
|
||||||
|
# Restart a test database.
|
||||||
|
restart-db-test:
|
||||||
|
make undockerize-test
|
||||||
|
make dockerize-test
|
||||||
|
|
||||||
|
.PHONY: migrateup
|
||||||
|
# Migrate all schemas, triggers and data located in database/migrations.
|
||||||
|
migrateup:
|
||||||
|
migrate -path $(CORE_DIR)/cmd/database/migrations -database "postgresql://developer:secret@localhost:5432/$(DB_NAME)?sslmode=disable" -verbose up
|
||||||
|
|
||||||
|
.PHONY: migratedown
|
||||||
|
# Migrate all schemas, triggers and data located in database/migrations.
|
||||||
|
migratedown:
|
||||||
|
migrate -path $(CORE_DIR)/cmd/database/migrations -database "postgresql://developer:secret@localhost:5432/$(DB_NAME)?sslmode=disable" -verbose down
|
||||||
|
|
||||||
|
.PHONY: pg-dump
|
||||||
|
# Dump database to file.
|
||||||
|
pg-dump:
|
||||||
|
docker exec -e PGPASSWORD=secret $(DB_NAME)-db-dev pg_dump -U developer --column-inserts --data-only $(DB_NAME) > $(CORE_DIR)/cmd/database/data/data.sql
|
||||||
|
sed -i '1iSET session_replication_role = '\''replica'\'';' $(CORE_DIR)/cmd/database/data/data.sql
|
||||||
|
sed -i '$$aSET session_replication_role = '\''origin'\'';' $(CORE_DIR)/cmd/database/data/data.sql
|
||||||
|
|
||||||
|
.PHONY: pg-restore
|
||||||
|
# Restore database from file.
|
||||||
|
pg-restore:
|
||||||
|
docker cp $(CORE_DIR)/cmd/database/data/data.sql $(DB_NAME)-db-dev:/data.sql
|
||||||
|
docker exec -e PGPASSWORD=secret $(DB_NAME)-db-dev psql -U developer -d $(DB_NAME) -f data.sql
|
||||||
|
|
||||||
|
.PHONY: pg-docs
|
||||||
|
# Generate docs from database.
|
||||||
|
pg-docs:
|
||||||
|
java -jar $(LIBRARIES_DIR)/schemaspy-6.2.4.jar -t pgsql -dp $(LIBRARIES_DIR)/postgresql-42.7.4.jar -db $(DB_NAME) -host localhost -port 5432 -u developer -p secret -o $(DOCS_DIR)/database -vizjs
|
||||||
|
|
||||||
|
.PHONY: sqlc
|
||||||
|
# Generate or recreate SQLC queries.
|
||||||
|
sqlc:
|
||||||
|
cd $(CORE_DIR) && sqlc generate
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
# Test all files and generate coverage file.
|
||||||
|
test:
|
||||||
|
cd $(CORE_DIR) && $(GO) test ./... -v -covermode=count -coverprofile=./benchmark/coverage.out $(PACKAGES)
|
||||||
|
|
||||||
|
.PHONY: gomock
|
||||||
|
# Generate mock files.
|
||||||
|
gomock:
|
||||||
|
cd $(CORE_DIR) && mockgen -package mock -destination internal/repository/mock/querier.go $(MOD_NAME)/internal/repository ExtendedQuerier
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
# Run project.
|
||||||
|
run:
|
||||||
|
cd $(CORE_DIR) && $(GO) run ./cmd/.
|
||||||
|
|
||||||
|
.PHONY: bench
|
||||||
|
# Run benchmarks.
|
||||||
|
bench:
|
||||||
|
cd $(CORE_DIR) && test -f benchmark/new_benchmark.txt && mv benchmark/new_benchmark.txt benchmark/old_benchmark.txt || true
|
||||||
|
cd $(CORE_DIR) && $(GO) test ./... -bench=. -count=10 -benchmem > benchmark/new_benchmark.txt
|
||||||
|
cd $(CORE_DIR) && benchstat benchmark/old_benchmark.txt benchmark/new_benchmark.txt > benchmark/benchstat.txt
|
||||||
|
|
||||||
|
.PHONY: recreate
|
||||||
|
# Destroy development DB and generate ones.
|
||||||
|
recreate:
|
||||||
|
echo "y" | make migratedown
|
||||||
|
make migrateup
|
||||||
|
|
||||||
|
.PHONY: tidy
|
||||||
|
# Runs a go mod tidy
|
||||||
|
tidy:
|
||||||
|
cd $(CORE_DIR) && $(GO) mod tidy
|
||||||
|
|
||||||
|
.PHONY: build-linux
|
||||||
|
# Build and generate linux executable.
|
||||||
|
build-linux:
|
||||||
|
cd $(CORE_DIR) && go mod tidy
|
||||||
|
cd $(CORE_DIR) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./tmp/$(MOD_NAME) ./cmd/.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: pack-docker
|
||||||
|
# Run docker build for pack binary and assets to Docker container.
|
||||||
|
pack-docker:
|
||||||
|
make build-linux
|
||||||
|
cd $(CORE_DIR) && docker build -t $(MOD_NAME):${version} -t $(MOD_NAME):latest .
|
||||||
|
|
||||||
|
.PHONY: remove-debug
|
||||||
|
# Remove all debug entries for reduce size binary.
|
||||||
|
remove-debug:
|
||||||
|
cd $(CORE_DIR) && find . -name "*.go" -type f -exec sed -i '/slog\.Debug/d' {} +
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
# Ensure consistent code formatting.
|
||||||
|
fmt:
|
||||||
|
cd $(CORE_DIR) && $(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:
|
||||||
|
cd $(CORE_DIR) && $(GO) vet $(VETPACKAGES)
|
||||||
|
|
||||||
|
.PHONY: tools
|
||||||
|
# Install tools (migrate and sqlc).
|
||||||
|
tools:
|
||||||
|
@if [ $(GO_VERSION) -gt 16 ]; then \
|
||||||
|
cd $(CORE_DIR) && $(GO) install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest; \
|
||||||
|
cd $(CORE_DIR) && $(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:
|
||||||
|
cd $(CORE_DIR) && @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
|
||||||
@@ -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.
|
2. **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.
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
DRIVERNAME=pgx
|
||||||
|
MIGRATE=true
|
||||||
|
DATASOURCE=postgresql://developer:secret@localhost:5432/db?sslmode=disable
|
||||||
|
ASYMMETRICKEY=
|
||||||
|
DURATION=
|
||||||
|
|
||||||
|
# scraper
|
||||||
|
TITLE_SELECTOR=
|
||||||
|
SEASON_SELECTOR=
|
||||||
|
EPISODE_CARD_SELECTOR=
|
||||||
|
SEASON_EPISODE_AND_TITLE_SELECTOR=
|
||||||
|
RELEASED_DATE_SELECTOR=
|
||||||
|
PLOT_SELECTOR=
|
||||||
|
STAR_RATING_SELECTOR=
|
||||||
|
VOTE_COUNT_SELECTOR=
|
||||||
|
IMDB_EPISODES_URL=
|
||||||
|
VISIT_URL=
|
||||||
+3
-9
@@ -1,13 +1,7 @@
|
|||||||
FROM golang:1.20.3-alpine
|
FROM alpine:3.20
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY ../tmp/rating-orama .
|
||||||
|
|
||||||
RUN go mod download
|
CMD ["/app/rating-orama"]
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN go build -o main
|
|
||||||
|
|
||||||
CMD ["/app/main"]
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
sayhello:
|
|
||||||
@echo "Hello World"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
undockerize:
|
|
||||||
docker rm -f rating-db-dev
|
|
||||||
|
|
||||||
migrateup:
|
|
||||||
migrate -path 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
|
|
||||||
|
|
||||||
sqlc:
|
|
||||||
sqlc generate
|
|
||||||
|
|
||||||
test:
|
|
||||||
go test -v -cover ./...
|
|
||||||
|
|
||||||
gomock:
|
|
||||||
mockgen -package mock -destination internal/repository/mock/querier.go github.com/zepyrshut/rating-orama/internal/repository ExtendedQuerier
|
|
||||||
|
|
||||||
run:
|
|
||||||
go run ./cmd/.
|
|
||||||
|
|
||||||
recreate:
|
|
||||||
make undockerize
|
|
||||||
make dockerize
|
|
||||||
sleep 2
|
|
||||||
make migrateup
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./tmp/arena ./cmd/.
|
|
||||||
|
|
||||||
pack-docker:
|
|
||||||
make test
|
|
||||||
make build-linux
|
|
||||||
docker build -t rating:${version} -t rating:latest .
|
|
||||||
@@ -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;
|
||||||
+25
-54
@@ -1,81 +1,52 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"gopher-toolbox/config"
|
"gopher-toolbox/db"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
"gopher-toolbox/db"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gopher-toolbox/utils"
|
"github.com/gofiber/template/html/v2"
|
||||||
|
"github.com/zepyrshut/rating-orama/internal/app"
|
||||||
"github.com/golang-migrate/migrate/v4"
|
|
||||||
_ "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"
|
const version = "0.2.3-beta.20250128-27"
|
||||||
|
const appName = "rating-orama"
|
||||||
var app *config.App
|
|
||||||
|
|
||||||
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() {
|
//go:embed database/migrations
|
||||||
slog.Info("migrating database")
|
var database embed.FS
|
||||||
m, err := migrate.New("file://database/migrations", os.Getenv("DATASOURCE"))
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("cannot create migration", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = m.Up()
|
//go:embed templates
|
||||||
if err != nil && err != migrate.ErrNoChange {
|
var templates embed.FS
|
||||||
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() {
|
||||||
|
engine := html.NewFileSystem(http.FS(templates), ".html")
|
||||||
|
engine.Directory = "templates"
|
||||||
|
|
||||||
app = &config.App{
|
app := app.NewExtendedApp(appName, version, ".env")
|
||||||
DataSource: os.Getenv("DATASOURCE"),
|
app.Migrate(database)
|
||||||
UseCache: utils.GetBool(os.Getenv("USE_CACHE")),
|
f := fiber.New(fiber.Config{
|
||||||
AppInfo: config.AppInfo{
|
AppName: appName,
|
||||||
GinMode: os.Getenv("GIN_MODE"),
|
Views: engine,
|
||||||
Version: version,
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
dbPool := db.NewPostgresPool(app.DataSource)
|
pgxPool := db.NewPGXPool(app.Database.DataSource)
|
||||||
defer dbPool.Close()
|
defer pgxPool.Close()
|
||||||
|
|
||||||
q := repository.NewPGXRepo(dbPool)
|
r := repository.NewPGXRepo(pgxPool, app)
|
||||||
h := handlers.New(q, app)
|
h := handlers.New(r, app)
|
||||||
r := Router(h, app)
|
router(h, f)
|
||||||
|
|
||||||
slog.Info("server started", "port", "8080", "version", version)
|
slog.Info("server started", "port", "8080", "version", version)
|
||||||
err := http.ListenAndServe(":8080", r)
|
err := f.Listen(":8080")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("cannot start server", "error", err)
|
slog.Error("cannot start server", "error", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-8
@@ -1,17 +1,13 @@
|
|||||||
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("/", h.GetIndex)
|
||||||
|
r.Get("/tvshow", h.GetTVShow)
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="flex flex-col justify-center min-h-screen">
|
||||||
|
<form class="max-w-md mx-auto w-full" action="/tvshow" method="get">
|
||||||
|
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Buscar</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="lucide lucide-search">
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.3-4.3" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="search" id="default-search"
|
||||||
|
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
placeholder="IMDb ID" required
|
||||||
|
name="ttid"
|
||||||
|
/>
|
||||||
|
<button type="submit"
|
||||||
|
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="min-h-screen bg-slate-50">
|
||||||
|
|
||||||
|
{{ embed }}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
<footer class="bg-slate-200" aria-label="Site Footer">
|
||||||
|
<ul class="mt-12 flex justify-center gap-6 p-12">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-slate-700 transition hover:text-slate-700/75"
|
||||||
|
href="https://www.instagram.com/modelektor/"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Instagram</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-instagram"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"/></svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-slate-700 transition hover:text-slate-700/75"
|
||||||
|
href="https://github.com/zepyrshut"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="sr-only">GitHub</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-slate-700 transition hover:text-slate-700/75"
|
||||||
|
href="https://pedroperez.dev/"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Website</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-slate-700 transition hover:text-slate-700/75"
|
||||||
|
href="https://github.com/zepyrshut/rating-orama"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Source</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
<div class="flex flex-col items-center gap-4 mb-8 p-2">
|
||||||
|
<!-- Card header -->
|
||||||
|
<div class="flex flex-col items-center mt-4 bg-white p-4 rounded-lg shadow-md w-1/2">
|
||||||
|
<div class="flex flex-col items-center space-y-4 w-full">
|
||||||
|
<!-- Title section -->
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-800">{{ .tvshow.Name }}</h1>
|
||||||
|
<div class="mt-1 flex items-center justify-center space-x-2">
|
||||||
|
<a href="https://www.imdb.com/title/{{ .tvshow.TtImdb }}" target="_blank"
|
||||||
|
class="text-amber-500 hover:text-amber-600 flex items-center space-x-1">
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-semibold text-sm">{{ .tvshow.TtImdb }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ratings section -->
|
||||||
|
<div class="grid grid-cols-2 gap-4 w-full">
|
||||||
|
<div class="bg-gray-50 rounded-lg p-2 text-center">
|
||||||
|
<span class="text-xs text-gray-500 uppercase">Average</span>
|
||||||
|
<div class="text-2xl font-bold text-gray-800">{{ printf "%.2f" .avg_rating_show }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 rounded-lg p-2 text-center">
|
||||||
|
<span class="text-xs text-gray-500 uppercase">Median</span>
|
||||||
|
<div class="text-2xl font-bold text-gray-800">{{ printf "%.2f" .median_rating_show }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats section -->
|
||||||
|
<div class="flex justify-around w-full px-3 py-2 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<span class="text-xs text-gray-500">Vote count</span>
|
||||||
|
<div class="text-lg font-bold text-gray-800">{{ .total_vote_count }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<span class="text-xs text-gray-500">Times searched</span>
|
||||||
|
<div class="text-lg font-bold text-gray-800">{{ .tvshow.Popularity }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="w-4/5 mx-6">
|
||||||
|
|
||||||
|
|
||||||
|
<h2 class="mb-6 text-center text-2xl font-bold">Seasons overall</h2>
|
||||||
|
<canvas id="seasons"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="w-4/5">
|
||||||
|
|
||||||
|
<h2 class="mb-6 text-center text-2xl font-bold">Season <span id="seasonNumber">1</span></h2>
|
||||||
|
<div id="seasonButtons" class="mb-4 flex flex-wrap items-center justify-center gap-2"></div>
|
||||||
|
<canvas id="episodes"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{ template "partials/footer" . }}
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"
|
||||||
|
integrity="sha512-ZwR1/gSZM3ai6vCdI+LVF1zSq/5HznD3ZSTk7kajkaj4D292NLuduDCO1c/NT8Id+jE58KYLKT7hXnbtryGmMg=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
const episodes = JSON.parse("{{ .episodes }}")
|
||||||
|
|
||||||
|
function groupEpisodesBySeason(episodes) {
|
||||||
|
const seasons = {};
|
||||||
|
episodes.forEach(episode => {
|
||||||
|
if (!seasons[episode.season]) {
|
||||||
|
seasons[episode.season] = {
|
||||||
|
number: episode.season,
|
||||||
|
episodes: [],
|
||||||
|
avg_rating: 0,
|
||||||
|
median_rating: 0,
|
||||||
|
votes: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
seasons[episode.season].episodes.push({
|
||||||
|
number: episode.episode,
|
||||||
|
title: episode.name,
|
||||||
|
avg_rating: episode.avg_rating,
|
||||||
|
votes: episode.vote_count,
|
||||||
|
aired: episode.released
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(seasons).forEach(season => {
|
||||||
|
const ratings = season.episodes.map(ep => ep.avg_rating);
|
||||||
|
season.avg_rating = ratings.reduce((a, b) => a + b, 0) / ratings.length;
|
||||||
|
season.median_rating = ratings.sort((a, b) => a - b)[Math.floor(ratings.length / 2)];
|
||||||
|
season.votes = season.episodes.reduce((sum, ep) => sum + ep.votes, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
seasons: Object.values(seasons).sort((a, b) => a.number - b.number)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSeasonButtons() {
|
||||||
|
const maxSeason = Math.max(...episodes.map(ep => ep.season));
|
||||||
|
const buttonContainer = document.getElementById('seasonButtons');
|
||||||
|
const seasonNumberSpan = document.getElementById('seasonNumber');
|
||||||
|
|
||||||
|
for (let i = 1; i <= maxSeason; i++) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.textContent = `S${i}`;
|
||||||
|
button.className = 'rounded-md border-2 border-black px-4 py-2 font-semibold transition-colors duration-200 hover:bg-black hover:text-white';
|
||||||
|
if (i === 1) button.classList.add('bg-black', 'text-white');
|
||||||
|
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
buttonContainer.querySelectorAll('button').forEach(btn => {
|
||||||
|
btn.classList.remove('bg-black', 'text-white');
|
||||||
|
btn.classList.add('text-black');
|
||||||
|
});
|
||||||
|
|
||||||
|
button.classList.remove('text-black');
|
||||||
|
button.classList.add('bg-black', 'text-white');
|
||||||
|
|
||||||
|
seasonNumberSpan.textContent = i;
|
||||||
|
|
||||||
|
const formattedData = groupEpisodesBySeason(episodes);
|
||||||
|
loadSpecificSeason(formattedData, i);
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonContainer.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const formattedData = groupEpisodesBySeason(episodes);
|
||||||
|
initCharts(formattedData);
|
||||||
|
createSeasonButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadSeason(seasonNumber) {
|
||||||
|
const formattedData = groupEpisodesBySeason(episodes);
|
||||||
|
loadSpecificSeason(formattedData, seasonNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCharts(tvShowParsed) {
|
||||||
|
new EpisodeChart(tvShowParsed, "episodes", 0);
|
||||||
|
new SeasonsChart(tvShowParsed, "seasons");
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSpecificSeason(tvShowParsed, seasonId) {
|
||||||
|
// Guardamos la posición actual del scroll
|
||||||
|
const currentScroll = window.scrollY;
|
||||||
|
|
||||||
|
new EpisodeChart(tvShowParsed, "episodes", seasonId - 1);
|
||||||
|
|
||||||
|
// Restauramos la posición del scroll
|
||||||
|
window.scrollTo(0, currentScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let chartInstance;
|
||||||
|
class SeasonsChart {
|
||||||
|
constructor(tvShowParsed, canvasId) {
|
||||||
|
this.tvShowParsed = tvShowParsed;
|
||||||
|
this.canvasId = canvasId;
|
||||||
|
this.createChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
createChart() {
|
||||||
|
const seasons = this.tvShowParsed.seasons;
|
||||||
|
const labels = seasons.map((season) => `Season ${season.number}`);
|
||||||
|
const averageRating = seasons.map((season) => season.avg_rating);
|
||||||
|
const medianRating = seasons.map((season) => season.median_rating);
|
||||||
|
const votes = seasons.map((season) => season.votes);
|
||||||
|
const title = "Seasons"
|
||||||
|
const ctx = document.getElementById(this.canvasId);
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Average rating",
|
||||||
|
data: averageRating,
|
||||||
|
backgroundColor: "rgba(75, 192, 192, 0.5)",
|
||||||
|
borderColor: "rgba(75, 192, 192, 1)",
|
||||||
|
borderWidth: 1,
|
||||||
|
Range: 10,
|
||||||
|
yAxisID: "y-axis-ratings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Median rating",
|
||||||
|
data: medianRating,
|
||||||
|
backgroundColor: "rgba(255, 206, 86, 0.5)",
|
||||||
|
borderColor: "rgba(255, 206, 86, 1)",
|
||||||
|
borderWidth: 1,
|
||||||
|
Range: 10,
|
||||||
|
yAxisID: "y-axis-ratings",
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Votes",
|
||||||
|
data: votes,
|
||||||
|
type: "line",
|
||||||
|
tension: 0.4,
|
||||||
|
fill: false,
|
||||||
|
borderColor: "rgba(255, 99, 132, 1)",
|
||||||
|
borderWidth: 2,
|
||||||
|
yAxisID: "y-axis-votes",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
"y-axis-ratings": {
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
type: "linear",
|
||||||
|
display: true,
|
||||||
|
position: "left",
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
"y-axis-votes": {
|
||||||
|
type: "linear",
|
||||||
|
display: true,
|
||||||
|
position: "right",
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: title,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EpisodeChart {
|
||||||
|
constructor(tvShowParsed, canvasId, seasonId) {
|
||||||
|
this.tvShowParsed = tvShowParsed;
|
||||||
|
this.canvasId = canvasId;
|
||||||
|
this.seasonId = seasonId;
|
||||||
|
this.createChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
createChart() {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const episodes = this.tvShowParsed.seasons[this.seasonId].episodes;
|
||||||
|
const labels = episodes.map((episode) => `Ep. ${episode.number}`);
|
||||||
|
const ratings = episodes.map((episode) => episode.avg_rating);
|
||||||
|
const votes = episodes.map((episode) => episode.votes);
|
||||||
|
const title = `Episodes of season ${this.seasonId + 1}`
|
||||||
|
const ctx = document.getElementById(this.canvasId);
|
||||||
|
|
||||||
|
chartInstance = new Chart(ctx, {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Average rating",
|
||||||
|
data: ratings,
|
||||||
|
backgroundColor: "rgba(75, 192, 192, 0.5)",
|
||||||
|
borderColor: "rgba(75, 192, 192, 1)",
|
||||||
|
borderWidth: 1,
|
||||||
|
Range: 10,
|
||||||
|
yAxisID: "y-axis-ratings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Votes",
|
||||||
|
data: votes,
|
||||||
|
type: "line",
|
||||||
|
tension: 0.4,
|
||||||
|
fill: false,
|
||||||
|
borderColor: "rgba(255, 99, 132, 1)",
|
||||||
|
borderWidth: 2,
|
||||||
|
yAxisID: "y-axis-votes",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
"y-axis-ratings": {
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
type: "linear",
|
||||||
|
display: true,
|
||||||
|
position: "left",
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
"y-axis-votes": {
|
||||||
|
type: "linear",
|
||||||
|
display: true,
|
||||||
|
position: "right",
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title: function (context) {
|
||||||
|
const index = context[0].dataIndex;
|
||||||
|
const episode = episodes[index];
|
||||||
|
return `${episode.title} (${episode.aired.split("T")[0]})`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
// TODO: update docker-compose
|
|
||||||
services:
|
|
||||||
core:
|
|
||||||
container_name: core-ratingorama
|
|
||||||
image: core:0.1.0
|
|
||||||
environment:
|
|
||||||
DATASOURCE: ${DATASOURCE}
|
|
||||||
HARVESTER_API: ${HARVESTER_API}
|
|
||||||
IS_PRODUCTION: ${IS_PRODUCTION}
|
|
||||||
ports:
|
|
||||||
- "8086:8080"
|
|
||||||
networks:
|
|
||||||
- ratingorama
|
|
||||||
db:
|
|
||||||
container_name: db-ratingorama
|
|
||||||
image: postgres:15.2-alpine
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
||||||
volumes:
|
|
||||||
- ./init.sql:/docker-entrypoint-initdb.d/schema.sql
|
|
||||||
- ./data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- ratingorama
|
|
||||||
|
|
||||||
networks:
|
|
||||||
ratingorama:
|
|
||||||
+27
-37
@@ -1,73 +1,63 @@
|
|||||||
module github.com/zepyrshut/rating-orama
|
module github.com/zepyrshut/rating-orama
|
||||||
|
|
||||||
go 1.23.2
|
go 1.23.5
|
||||||
|
|
||||||
require github.com/jackc/pgx/v5 v5.7.1
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.0 // indirect
|
github.com/PuerkitoBio/goquery v1.10.0
|
||||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect
|
github.com/jackc/pgx/v5 v5.7.1
|
||||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
aidanwoods.dev/go-paseto v1.5.2 // indirect
|
||||||
|
aidanwoods.dev/go-result v0.1.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.0.5 // 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/gofiber/template v1.8.3 // indirect
|
||||||
|
github.com/gofiber/utils v1.1.0 // 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/gofiber/template/html/v2 v2.1.3
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/golang-migrate/migrate/v4 v4.18.1 // 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.28.0 // indirect
|
||||||
golang.org/x/text v0.19.0 // indirect
|
golang.org/x/text v0.19.0 // indirect
|
||||||
gopher-toolbox v0.0.0-00010101000000-000000000000
|
gopher-toolbox v0.0.0-00010101000000-000000000000
|
||||||
)
|
)
|
||||||
|
|||||||
+40
-86
@@ -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,24 @@ 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/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||||
|
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||||
|
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
|
||||||
|
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
|
||||||
|
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||||
|
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||||
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 +64,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 +90,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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
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/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 +144,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,17 +165,17 @@ 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=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -241,13 +198,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=
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopher-toolbox/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExtendedApp struct {
|
||||||
|
app.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExtendedApp(appName, version, envDirectory string) *ExtendedApp {
|
||||||
|
app := app.New(appName, version, envDirectory)
|
||||||
|
return &ExtendedApp{
|
||||||
|
App: *app,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,90 +1,18 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"github.com/zepyrshut/rating-orama/internal/app"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopher-toolbox/config"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"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.ExtendedApp
|
||||||
Queries repository.ExtendedQuerier
|
queries repository.ExtendedQuerier
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(q repository.ExtendedQuerier, app *config.App) *Handlers {
|
func New(r repository.ExtendedQuerier, app *app.ExtendedApp) *Handlers {
|
||||||
return &Handlers{
|
return &Handlers{
|
||||||
Queries: q,
|
app: app,
|
||||||
App: app,
|
queries: r,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hq *Handlers) ToBeImplemented(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"message": "Not implemented yet",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hq *Handlers) Ping(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,58 +1,111 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"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) GetIndex(c *fiber.Ctx) error {
|
||||||
|
return c.Render("index", fiber.Map{
|
||||||
|
"Title": "Rating Orama",
|
||||||
|
}, "layouts/main")
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
|
||||||
|
if ttShowID == "" {
|
||||||
|
return c.SendStatus(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
var scraperEpisodes []scraper.Episode
|
var scraperEpisodes []scraper.Episode
|
||||||
|
var sqlcTvShow sqlc.TvShow
|
||||||
var sqlcEpisodes []sqlc.Episode
|
var sqlcEpisodes []sqlc.Episode
|
||||||
|
var totalVoteCount int32
|
||||||
|
|
||||||
tvShow, err := hq.Queries.CheckTVShowExists(c, ttShowID)
|
sqlcTvShow, 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{
|
sqlcTvShow, 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", sqlcTvShow.ID)
|
||||||
|
for _, episode := range scraperEpisodes {
|
||||||
|
sqlcEpisodesParams := episode.ToEpisodeParams(sqlcTvShow.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)
|
||||||
|
}
|
||||||
|
totalVoteCount += int32(episode.VoteCount)
|
||||||
|
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 = sqlcTvShow.Name
|
||||||
sqlcEpisodes, err = hq.Queries.GetEpisodes(c, tvShow.ID)
|
sqlcEpisodes, err = hq.queries.GetEpisodes(c.Context(), sqlcTvShow.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 {
|
for _, episode := range sqlcEpisodes {
|
||||||
slog.Error("failed to increase popularity", "ttid", ttShowID, "error", err)
|
totalVoteCount += episode.VoteCount
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrorUpdating})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("tv show exists", "ttid", ttShowID, "title", tvShow.Name)
|
hq.queries.IncreasePopularity(c.Context(), ttShowID)
|
||||||
|
slog.Info("tv show exists", "ttid", ttShowID, "title", sqlcTvShow.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
episodesJSON, err := json.Marshal(sqlcEpisodes)
|
||||||
"popularity": tvShow.Popularity,
|
if err != nil {
|
||||||
"title": title,
|
slog.Error("failed to marshal episodes", "ttid", ttShowID, "error", err)
|
||||||
"seasons": sqlcEpisodes,
|
return c.SendStatus(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
avgRatingShow, err := hq.queries.TvShowAverageRating(c.Context(), sqlcTvShow.ID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to calculate avg rating for the show", "ttid", ttShowID, "error", err)
|
||||||
|
return c.SendStatus(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
medianRatingShow, err := hq.queries.TvShowMedianRating(c.Context(), sqlcTvShow.ID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to calculate median rating for the show", "ttid", ttShowID, "error", err)
|
||||||
|
return c.SendStatus(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
seasongAvgRatings, err := hq.queries.SeasonAverageRating(c.Context(), sqlc.SeasonAverageRatingParams{
|
||||||
|
TvShowID: sqlcTvShow.ID,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
seasonMedianRatings, err := hq.queries.SeasonMedianRating(c.Context(), sqlc.SeasonMedianRatingParams{
|
||||||
|
TvShowID: sqlcTvShow.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.Render("tvshow", fiber.Map{
|
||||||
|
"Title": sqlcTvShow.Name,
|
||||||
|
"tvshow": sqlcTvShow,
|
||||||
|
"episodes": string(episodesJSON),
|
||||||
|
"avg_rating_show": avgRatingShow,
|
||||||
|
"median_rating_show": medianRatingShow,
|
||||||
|
"season_avg_ratings": seasongAvgRatings,
|
||||||
|
"season_median_ratings": seasonMedianRatings,
|
||||||
|
"total_vote_count": totalVoteCount,
|
||||||
|
}, "layouts/main")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,39 +2,44 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/zepyrshut/rating-orama/internal/app"
|
||||||
"github.com/zepyrshut/rating-orama/internal/sqlc"
|
"github.com/zepyrshut/rating-orama/internal/sqlc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pgxRepository struct {
|
type pgxRepository struct {
|
||||||
*sqlc.Queries
|
*sqlc.Queries
|
||||||
db *pgxpool.Pool
|
pool *pgxpool.Pool
|
||||||
|
app *app.ExtendedApp
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPGXRepo(db *pgxpool.Pool) ExtendedQuerier {
|
var _ ExtendedQuerier = &pgxRepository{}
|
||||||
|
|
||||||
|
func NewPGXRepo(pgx *pgxpool.Pool, app *app.ExtendedApp) ExtendedQuerier {
|
||||||
return &pgxRepository{
|
return &pgxRepository{
|
||||||
Queries: sqlc.New(db),
|
Queries: sqlc.New(pgx),
|
||||||
db: db,
|
pool: pgx,
|
||||||
|
app: app,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *pgxRepository) execTx(ctx context.Context, txFunc func(tx pgx.Tx) error) error {
|
func (r *pgxRepository) execTx(ctx context.Context, fn func(*sqlc.Queries) error) error {
|
||||||
slog.Info("starting transaction", "txFunc", txFunc)
|
tx, err := r.pool.Begin(ctx)
|
||||||
tx, err := r.db.Begin(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to start transaction", "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tx.Rollback(ctx)
|
|
||||||
|
|
||||||
if err := txFunc(tx); err != nil {
|
|
||||||
slog.Error("failed to execute transaction", "error", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("committing transaction", "txFunc", txFunc)
|
q := sqlc.New(tx)
|
||||||
|
|
||||||
|
err = fn(q)
|
||||||
|
if err != nil {
|
||||||
|
if rbErr := tx.Rollback(ctx); rbErr != nil {
|
||||||
|
return fmt.Errorf("tx err: %v, rb err: %v", err, rbErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return tx.Commit(ctx)
|
return tx.Commit(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExtendedQuerier interface {
|
type ExtendedQuerier interface {
|
||||||
sqlc.Querier
|
sqlc.Querier
|
||||||
CreateTvShowWithEpisodes(ctx context.Context, tvShow sqlc.CreateTVShowParams, episodes []scraper.Episode) ([]sqlc.Episode, error)
|
CreateTvShowWithEpisodesTX(ctx context.Context, tvShow sqlc.CreateTVShowParams, episodes []scraper.Episode) ([]sqlc.Episode, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"github.com/zepyrshut/rating-orama/internal/scraper"
|
|
||||||
"github.com/zepyrshut/rating-orama/internal/sqlc"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r *pgxRepository) CreateTvShowWithEpisodes(ctx context.Context, tvShow sqlc.CreateTVShowParams, episodes []scraper.Episode) ([]sqlc.Episode, error) {
|
|
||||||
var sqlcEpisodes []sqlc.Episode
|
|
||||||
err := r.execTx(ctx, func(tx pgx.Tx) error {
|
|
||||||
qtx := r.WithTx(tx)
|
|
||||||
tvShow, err := qtx.CreateTVShow(ctx, tvShow)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("episodes lenght", "episodes", len(episodes))
|
|
||||||
|
|
||||||
for _, episode := range episodes {
|
|
||||||
sqlcEpisodeParams := episode.ToEpisodeParams(tvShow.ID)
|
|
||||||
slog.Info("creating episode", "episode", sqlcEpisodeParams)
|
|
||||||
episode, err := qtx.CreateEpisodes(ctx, sqlcEpisodeParams)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlcEpisodes = append(sqlcEpisodes, episode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return sqlcEpisodes, err
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zepyrshut/rating-orama/internal/scraper"
|
||||||
|
"github.com/zepyrshut/rating-orama/internal/sqlc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *pgxRepository) CreateTvShowWithEpisodesTX(ctx context.Context, tvShow sqlc.CreateTVShowParams, episodes []scraper.Episode) ([]sqlc.Episode, error) {
|
||||||
|
var err error
|
||||||
|
var episodesSqlc []sqlc.Episode
|
||||||
|
|
||||||
|
err = r.execTx(ctx, func(tx *sqlc.Queries) error {
|
||||||
|
tvShow, err := tx.CreateTVShow(ctx, tvShow)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, episode := range episodes {
|
||||||
|
sqlcEpisodeParams := episode.ToEpisodeParams(tvShow.ID)
|
||||||
|
episode, err := tx.CreateEpisodes(ctx, sqlcEpisodeParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
episodesSqlc = append(episodesSqlc, episode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return episodesSqlc, err
|
||||||
|
}
|
||||||
@@ -3,12 +3,15 @@ package scraper
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"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 +30,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,
|
||||||
@@ -41,15 +44,6 @@ func (e Episode) ToEpisodeParams(tvShowID int32) sqlc.CreateEpisodesParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
titleSelector = "h2.sc-b8cc654b-9.dmvgRY"
|
|
||||||
seasonsSelector = "ul.ipc-tabs a[data-testid='tab-season-entry']"
|
|
||||||
episodesSelector = "section.sc-1e7f96be-0.ZaQIL"
|
|
||||||
nextSeasonButtonSelector = "#next-season-btn"
|
|
||||||
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"),
|
||||||
@@ -63,7 +57,7 @@ func ScrapeEpisodes(ttImdb string) (string, []Episode) {
|
|||||||
var seasons []int
|
var seasons []int
|
||||||
var title string
|
var title string
|
||||||
|
|
||||||
c.OnHTML(seasonsSelector, func(e *colly.HTMLElement) {
|
c.OnHTML(os.Getenv("SEASON_SELECTOR"), func(e *colly.HTMLElement) {
|
||||||
seasonText := strings.TrimSpace(e.Text)
|
seasonText := strings.TrimSpace(e.Text)
|
||||||
seasonNum, err := strconv.Atoi(seasonText)
|
seasonNum, err := strconv.Atoi(seasonText)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -71,13 +65,13 @@ func ScrapeEpisodes(ttImdb string) (string, []Episode) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
c.OnHTML(titleSelector, func(e *colly.HTMLElement) {
|
c.OnHTML(os.Getenv("TITLE_SELECTOR"), func(e *colly.HTMLElement) {
|
||||||
title = e.Text
|
title = e.Text
|
||||||
})
|
})
|
||||||
|
|
||||||
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 +81,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(os.Getenv("IMDB_EPISODES_URL"), 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(os.Getenv("VISIT_URL"), 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))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error parsing html")
|
||||||
|
return []Episode{}
|
||||||
|
}
|
||||||
|
|
||||||
slog.Info("extracting episodes", "data", data)
|
var episodes []Episode
|
||||||
|
doc.Find(os.Getenv("EPISODE_CARD_SELECTOR")).Each(func(i int, s *goquery.Selection) {
|
||||||
const pattern = `(S\d+\.E\d+)\s∙\s(.*?)` +
|
|
||||||
`(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
|
var episode Episode
|
||||||
|
|
||||||
seasonEpisode := match[1]
|
seasonEpisodeTitle := s.Find(os.Getenv("SEASON_EPISODE_AND_TITLE_SELECTOR")).Text()
|
||||||
|
episode.Season, episode.Episode, episode.Name = parseSeasonEpisodeTitle(seasonEpisodeTitle)
|
||||||
|
|
||||||
name := strings.TrimSpace(match[2])
|
releasedDate := s.Find(os.Getenv("RELEASED_DATE_SELECTOR")).Text()
|
||||||
|
episode.Released = parseReleasedDate(releasedDate)
|
||||||
|
|
||||||
day := match[3]
|
plot := s.Find(os.Getenv("PLOT_SELECTOR")).Text()
|
||||||
dateRest := strings.TrimSpace(match[4])
|
if plot == "Add a plot" {
|
||||||
year := match[5]
|
episode.Plot = ""
|
||||||
|
} else {
|
||||||
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.Plot = plot
|
||||||
episode.Rate = float32(rateFloat)
|
}
|
||||||
episode.VoteCount = votesInt * 1000
|
|
||||||
|
starRating := s.Find(os.Getenv("STAR_RATING_SELECTOR")).Text()
|
||||||
|
episode.Rate = parseStarRating(starRating)
|
||||||
|
|
||||||
|
voteCount := s.Find(os.Getenv("VOTE_COUNT_SELECTOR")).Text()
|
||||||
|
episode.VoteCount = parseVoteCount(voteCount)
|
||||||
|
|
||||||
episodes = append(episodes, episode)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package transfers
|
||||||
|
|
||||||
|
type EpisodePayload struct {
|
||||||
|
Title string
|
||||||
|
Season int
|
||||||
|
Episode int
|
||||||
|
Description string
|
||||||
|
Rating float64
|
||||||
|
}
|
||||||
+2
-3
@@ -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"
|
||||||
@@ -13,4 +13,3 @@ sql:
|
|||||||
emit_json_tags: true
|
emit_json_tags: true
|
||||||
rename:
|
rename:
|
||||||
uuid: "UUID"
|
uuid: "UUID"
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
{{template "partials/header" .}}
|
|
||||||
|
|
||||||
<div class="flex w-full flex-col items-center">
|
|
||||||
<h1 class="mb-6 text-3xl font-bold">{{ .TvShow.Title }}</h1>
|
|
||||||
|
|
||||||
<div class="mb-6 rounded-lg bg-white p-6 shadow-md">
|
|
||||||
<div class="mb-4 text-center">
|
|
||||||
<span class="text-gray-600">IMDb ID:</span>
|
|
||||||
<span class="font-semibold text-gray-800">{{ .TvShow.ShowID }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-8">
|
|
||||||
<ul class="space-y-2">
|
|
||||||
<li>
|
|
||||||
<span class="text-gray-600">Title:</span>
|
|
||||||
<span class="font-semibold text-gray-800">{{ .TvShow.Title }}</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="text-gray-600">Runtime:</span>
|
|
||||||
<span class="font-semibold text-gray-800"
|
|
||||||
>{{ .TvShow.Runtime }} min</span
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="text-gray-600">Seasons:</span>
|
|
||||||
<span class="font-semibold text-gray-800">{{len .TvShow.Seasons }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul class="space-y-2">
|
|
||||||
<li>
|
|
||||||
<span class="text-gray-600">Total votes:</span>
|
|
||||||
<span class="font-semibold text-gray-800">{{ .TvShow.Votes }}</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="text-gray-600">Average rating:</span>
|
|
||||||
<span class="font-semibold text-gray-800"
|
|
||||||
>{{printf "%.2f" .TvShow.AvgRating }}</span
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="text-gray-600">Median rating:</span>
|
|
||||||
<span class="font-semibold text-gray-800"
|
|
||||||
>{{printf "%.2f" .TvShow.MedianRating }}</span
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6 w-2/3">
|
|
||||||
<h2 class="mb-6 text-center text-2xl font-bold">Seasons overall</h2>
|
|
||||||
<canvas id="seasons"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6 w-2/3">
|
|
||||||
<div x-data="{ selectedSeasonNumber: 1}">
|
|
||||||
<h2 class="mb-6 text-center text-2xl font-bold">
|
|
||||||
Season <span x-text="selectedSeasonNumber"></span>
|
|
||||||
</h2>
|
|
||||||
<canvas id="episodes"></canvas>
|
|
||||||
<div class="mt-4 flex items-center justify-center space-x-2">
|
|
||||||
{{ range .TvShow.Seasons }}
|
|
||||||
<button
|
|
||||||
class="rounded-md border-2 border-black px-4 py-2 font-semibold text-black hover:bg-black hover:text-white"
|
|
||||||
:class="selectedSeasonNumber === {{ .Number }} ? 'bg-black text-white' : 'text-black'"
|
|
||||||
x-on:click="selectedSeasonNumber = {{ .Number }}; loadSeason({{ .Number }})"
|
|
||||||
>
|
|
||||||
S{{ .Number }}
|
|
||||||
</button>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{template "partials/footer" .}}
|
|
||||||
<script
|
|
||||||
src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.2.1/chart.umd.js"
|
|
||||||
integrity="sha512-vCUbejtS+HcWYtDHRF2T5B0BKwVG/CLeuew5uT2AiX4SJ2Wff52+kfgONvtdATqkqQMC9Ye5K+Td0OTaz+P7cw=="
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
></script>
|
|
||||||
<script type="module">
|
|
||||||
import { initCharts, loadSpecificSeason } from "/js/components/charts.js";
|
|
||||||
|
|
||||||
let tvShow = "{{ .TvShowJSON }}";
|
|
||||||
let tvShowParsed = JSON.parse(tvShow);
|
|
||||||
|
|
||||||
function loadSeason(season) {
|
|
||||||
const currentScrollY = window.scrollY;
|
|
||||||
loadSpecificSeason(tvShowParsed, season);
|
|
||||||
window.scrollTo(0, currentScrollY);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.loadSeason = loadSeason;
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
initCharts(tvShowParsed);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,55 +0,0 @@
|
|||||||
{{template "partials/header" .}}
|
|
||||||
<div class="flex min-h-screen flex-col items-center justify-center">
|
|
||||||
<h1 class="mb-6 text-3xl font-bold">Rating Orama</h1>
|
|
||||||
<div class="w-full max-w-md">
|
|
||||||
<form x-data="search" @submit.prevent="submit" class="flex items-center gap-3" >
|
|
||||||
<input
|
|
||||||
x-model="ttID"
|
|
||||||
class="rounded-md border-2 border-black bg-white px-4 py-2 w-11/12"
|
|
||||||
id="search"
|
|
||||||
name="search"
|
|
||||||
type="text"
|
|
||||||
placeholder="tt0903747"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
:disabled="isLoading"
|
|
||||||
class="rounded-md border-2 border-black bg-white px-4 py-2 font-bold text-black w-2/12"
|
|
||||||
type="submit"
|
|
||||||
:class="isLoading ? '' : 'hover:bg-black hover:text-white'"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<template x-if="isLoading">
|
|
||||||
<div role="status">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6 animate-spin fill-black text-white"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Cargando...</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="!isLoading" class="lorem">
|
|
||||||
<span>Go!</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<p class="mt-2">
|
|
||||||
Please input <span class="font-bold">tt id</span> from IDMb
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
export function initCharts(tvShowParsed) {
|
|
||||||
new EpisodeChart(tvShowParsed, "episodes", 0);
|
|
||||||
new SeasonsChart(tvShowParsed, "seasons");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadSpecificSeason(tvShowParsed, seasonId) {
|
|
||||||
new EpisodeChart(tvShowParsed, "episodes", seasonId - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let chartInstance;
|
|
||||||
|
|
||||||
class SeasonsChart {
|
|
||||||
constructor(tvShowParsed, canvasId) {
|
|
||||||
this.tvShowParsed = tvShowParsed;
|
|
||||||
this.canvasId = canvasId;
|
|
||||||
this.createChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
createChart() {
|
|
||||||
const seasons = this.tvShowParsed.seasons;
|
|
||||||
const labels = seasons.map((season) => `Season ${season.number}`);
|
|
||||||
const averageRating = seasons.map((season) => season.avg_rating);
|
|
||||||
const medianRating = seasons.map((season) => season.median_rating);
|
|
||||||
const votes = seasons.map((season) => season.votes);
|
|
||||||
const title = "Seasons"
|
|
||||||
const ctx = document.getElementById(this.canvasId);
|
|
||||||
|
|
||||||
new Chart(ctx, {
|
|
||||||
type: "bar",
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Average rating",
|
|
||||||
data: averageRating,
|
|
||||||
backgroundColor: "rgba(75, 192, 192, 0.5)",
|
|
||||||
borderColor: "rgba(75, 192, 192, 1)",
|
|
||||||
borderWidth: 1,
|
|
||||||
Range: 10,
|
|
||||||
yAxisID: "y-axis-ratings",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Median rating",
|
|
||||||
data: medianRating,
|
|
||||||
backgroundColor: "rgba(255, 206, 86, 0.5)",
|
|
||||||
borderColor: "rgba(255, 206, 86, 1)",
|
|
||||||
borderWidth: 1,
|
|
||||||
Range: 10,
|
|
||||||
yAxisID: "y-axis-ratings",
|
|
||||||
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Votes",
|
|
||||||
data: votes,
|
|
||||||
type: "line",
|
|
||||||
tension: 0.4,
|
|
||||||
fill: false,
|
|
||||||
borderColor: "rgba(255, 99, 132, 1)",
|
|
||||||
borderWidth: 2,
|
|
||||||
yAxisID: "y-axis-votes",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
animation: {
|
|
||||||
duration: 0,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true,
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
"y-axis-ratings": {
|
|
||||||
min: 0,
|
|
||||||
max: 10,
|
|
||||||
type: "linear",
|
|
||||||
display: true,
|
|
||||||
position: "left",
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
"y-axis-votes": {
|
|
||||||
type: "linear",
|
|
||||||
display: true,
|
|
||||||
position: "right",
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: title,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EpisodeChart {
|
|
||||||
constructor(tvShowParsed, canvasId, seasonId) {
|
|
||||||
this.tvShowParsed = tvShowParsed;
|
|
||||||
this.canvasId = canvasId;
|
|
||||||
this.seasonId = seasonId;
|
|
||||||
this.createChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
createChart() {
|
|
||||||
if (chartInstance) {
|
|
||||||
chartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
const episodes = this.tvShowParsed.seasons[this.seasonId].episodes;
|
|
||||||
const labels = episodes.map((episode) => `Ep. ${episode.number}`);
|
|
||||||
const ratings = episodes.map((episode) => episode.avg_rating);
|
|
||||||
const votes = episodes.map((episode) => episode.votes);
|
|
||||||
const title = `Episodes of season ${this.seasonId + 1}`
|
|
||||||
const ctx = document.getElementById(this.canvasId);
|
|
||||||
|
|
||||||
chartInstance = new Chart(ctx, {
|
|
||||||
type: "bar",
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Average rating",
|
|
||||||
data: ratings,
|
|
||||||
backgroundColor: "rgba(75, 192, 192, 0.5)",
|
|
||||||
borderColor: "rgba(75, 192, 192, 1)",
|
|
||||||
borderWidth: 1,
|
|
||||||
Range: 10,
|
|
||||||
yAxisID: "y-axis-ratings",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Votes",
|
|
||||||
data: votes,
|
|
||||||
type: "line",
|
|
||||||
tension: 0.4,
|
|
||||||
fill: false,
|
|
||||||
borderColor: "rgba(255, 99, 132, 1)",
|
|
||||||
borderWidth: 2,
|
|
||||||
yAxisID: "y-axis-votes",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
animation: {
|
|
||||||
duration: 0,
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
"y-axis-ratings": {
|
|
||||||
min: 0,
|
|
||||||
max: 10,
|
|
||||||
type: "linear",
|
|
||||||
display: true,
|
|
||||||
position: "left",
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
"y-axis-votes": {
|
|
||||||
type: "linear",
|
|
||||||
display: true,
|
|
||||||
position: "right",
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
title: function (context) {
|
|
||||||
const index = context[0].dataIndex;
|
|
||||||
const episode = episodes[index];
|
|
||||||
return `${episode.title} (${episode.aired.split("T")[0]})`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export function search() {
|
|
||||||
return {
|
|
||||||
url: '/tv-show?id=',
|
|
||||||
ttID: '',
|
|
||||||
isLoading: false,
|
|
||||||
isError: false,
|
|
||||||
submit() {
|
|
||||||
this.isLoading = true;
|
|
||||||
fetch(this.url+this.ttID).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
window.location.href = this.url+this.ttID;
|
|
||||||
} else {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.isError = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import Alpine from "https://cdn.jsdelivr.net/npm/alpinejs@3.12.0/dist/module.esm.js";
|
|
||||||
import { search } from "./components/search.js";
|
|
||||||
|
|
||||||
window.Alpine = Alpine;
|
|
||||||
|
|
||||||
Alpine.data('search', search)
|
|
||||||
|
|
||||||
Alpine.start();
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link type="image/svg+xml" href="/vite.svg" rel="icon" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link href="/css/index.css" rel="stylesheet" />
|
|
||||||
<title>Rating Orama</title>
|
|
||||||
{{block "css" .}} {{end}}
|
|
||||||
</head>
|
|
||||||
<body class="min-h-screen bg-gray-100">
|
|
||||||
{{embed}}
|
|
||||||
|
|
||||||
<script type="module" src="/js/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<footer class="bg-gray-200" aria-label="Site Footer">
|
|
||||||
<ul class="mt-12 flex justify-center gap-6 p-12">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="text-gray-700 transition hover:text-gray-700/75"
|
|
||||||
href="https://www.instagram.com/zepyrshut/"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Instagram</span>
|
|
||||||
<svg
|
|
||||||
class="h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="text-gray-700 transition hover:text-gray-700/75"
|
|
||||||
href="https://github.com/zepyrshut"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span class="sr-only">GitHub</span>
|
|
||||||
<svg
|
|
||||||
class="h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="text-gray-700 transition hover:text-gray-700/75"
|
|
||||||
href="https://pedroperez.dev/"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Website</span>
|
|
||||||
<svg
|
|
||||||
class="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="text-gray-700 transition hover:text-gray-700/75"
|
|
||||||
href="https://github.com/zepyrshut/rating-orama"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Source</span>
|
|
||||||
<svg
|
|
||||||
class="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</footer>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<p class="mb-6 bg-yellow-400">
|
|
||||||
Work in progress - API 0.1.0 / Harvester 0.1.0 / Views 0.1.0 <a href="mailto:hola@pedroperez.dev" class="text-blue-800">Have you found any bugs or is it not working with a specific tv show?</a>
|
|
||||||
</p>
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
core:
|
||||||
|
container_name: rating-orama
|
||||||
|
image: rating-orama:latest
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
DRIVERNAME: pgx
|
||||||
|
DATASOURCE: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable
|
||||||
|
MIGRATE: true
|
||||||
|
TITLE_SELECTOR: ${TITLE_SELECTOR}
|
||||||
|
SEASON_SELECTOR: ${SEASON_SELECTOR}
|
||||||
|
EPISODE_CARD_SELECTOR: ${EPISODE_CARD_SELECTOR}
|
||||||
|
SEASON_EPISODE_AND_TITLE_SELECTOR: ${SEASON_EPISODE_AND_TITLE_SELECTOR}
|
||||||
|
RELEASED_DATE_SELECTOR: ${RELEASED_DATE_SELECTOR}
|
||||||
|
PLOT_SELECTOR: ${PLOT_SELECTOR}
|
||||||
|
STAR_RATING_SELECTOR: ${STAR_RATING_SELECTOR}
|
||||||
|
VOTE_COUNT_SELECTOR: ${VOTE_COUNT_SELECTOR}
|
||||||
|
IMDB_EPISODES_URL: ${IMDB_EPISODES_URL}
|
||||||
|
VISIT_URL: ${VISIT_URL}
|
||||||
|
ports:
|
||||||
|
- "8086:8080"
|
||||||
|
networks:
|
||||||
|
- ratingorama
|
||||||
|
db:
|
||||||
|
container_name: db-ratingorama
|
||||||
|
image: postgres:16.4-alpine3.20
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
volumes:
|
||||||
|
- ratingorama_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- ratingorama
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ratingorama:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ratingorama_data:
|
||||||
|
|
||||||
+18
-65
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -7,22 +7,18 @@
|
|||||||
<title>Inicio</title>
|
<title>Inicio</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="min-h-screen bg-gray-100">
|
<body class="min-h-screen bg-slate-50 p-2">
|
||||||
<p class="mb-6 bg-yellow-400">
|
|
||||||
Work in progress - API 0.1.0 / Harvester 0.1.0 / Views 0.1.0
|
|
||||||
<a class="text-blue-800" href="mailto:hola@pedroperez.dev"
|
|
||||||
>Have you found any bugs or is it not working with a specific tv
|
|
||||||
show?</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex w-full flex-col items-center">
|
|
||||||
<h1 class="mb-6 text-3xl font-bold">Breaking Bad</h1>
|
|
||||||
|
|
||||||
<div class="mb-6 rounded-lg bg-white p-6 shadow-md">
|
|
||||||
|
|
||||||
|
<div class="flex w-full flex-col items-center mt-6">
|
||||||
|
<h1 class="mb-6 text-4xl font-bold">Breaking Bad</h1>
|
||||||
|
|
||||||
|
<div class="mb-6 rounded-lg bg-white p-6 shadow-sm border border-gray-200">
|
||||||
<div class="mb-4 text-center">
|
<div class="mb-4 text-center">
|
||||||
<span class="text-gray-600">IMDb ID: </span>
|
<span class="text-gray-600">IMDb ID: </span>
|
||||||
<span class="font-semibold text-gray-800">0903747</span>
|
<span class="font-semibold text-gray-800">tt0903747</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-8">
|
<div class="grid grid-cols-2 gap-8">
|
||||||
@@ -115,23 +111,12 @@
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="text-gray-700 transition hover:text-gray-700/75"
|
class="text-gray-700 transition hover:text-gray-700/75"
|
||||||
href="https://www.instagram.com/zepyrshut/"
|
href="https://www.instagram.com/modelektor/"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="sr-only">Instagram</span>
|
<span class="sr-only">Instagram</span>
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-instagram"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"/></svg>
|
||||||
class="h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -143,18 +128,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="sr-only">GitHub</span>
|
<span class="sr-only">GitHub</span>
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
|
||||||
class="h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -166,19 +140,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="sr-only">Website</span>
|
<span class="sr-only">Website</span>
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
|
||||||
class="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -189,19 +151,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="sr-only">Source</span>
|
<span class="sr-only">Source</span>
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>
|
||||||
class="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -215,7 +165,10 @@
|
|||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
></script>
|
></script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { initCharts, loadSpecificSeason } from "./src/components/charts.js";
|
import {
|
||||||
|
initCharts,
|
||||||
|
loadSpecificSeason,
|
||||||
|
} from "./src/components/charts.js";
|
||||||
import { data } from "./src/data/breaking-bad.js";
|
import { data } from "./src/data/breaking-bad.js";
|
||||||
|
|
||||||
const tvShowParsed = data;
|
const tvShowParsed = data;
|
||||||
|
|||||||
+21
-58
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@@ -7,74 +8,36 @@
|
|||||||
<title>Inicio</title>
|
<title>Inicio</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="min-h-screen bg-gray-100">
|
<body class="min-h-screen bg-slate-50 p-2">
|
||||||
<p class="mb-6 bg-yellow-400">
|
|
||||||
Work in progress - API 0.1.0 / Harvester 0.1.0 / Views 0.1.0
|
|
||||||
<a class="text-blue-800" href="mailto:hola@pedroperez.dev"
|
|
||||||
>Have you found any bugs or is it not working with a specific tv
|
|
||||||
show?</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex min-h-screen flex-col items-center justify-center">
|
|
||||||
<h1 class="mb-6 text-3xl font-bold">Rating Orama</h1>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="w-full max-w-md">
|
|
||||||
<form x-data="search" @submit.prevent="submit" class="flex items-center gap-3" >
|
<div class="flex flex-col justify-center min-h-screen">
|
||||||
<input
|
<form class="max-w-md mx-auto w-full">
|
||||||
x-model="ttID"
|
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Buscar</label>
|
||||||
class="rounded-md border-2 border-black bg-white px-4 py-2 w-11/12"
|
<div class="relative">
|
||||||
id="search"
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||||
name="search"
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
type="text"
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
placeholder="tt0903747"
|
class="lucide lucide-search">
|
||||||
required
|
<circle cx="11" cy="11" r="8" />
|
||||||
/>
|
<path d="m21 21-4.3-4.3" />
|
||||||
<button
|
|
||||||
:disabled="isLoading"
|
|
||||||
class="rounded-md border-2 border-black bg-white px-4 py-2 font-bold text-black w-2/12"
|
|
||||||
type="submit"
|
|
||||||
:class="isLoading ? '' : 'hover:bg-black hover:text-white'"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<template x-if="isLoading">
|
|
||||||
<div role="status">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6 animate-spin fill-black text-white"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<span class="sr-only">Cargando...</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<input type="search" id="default-search"
|
||||||
<template x-if="!isLoading" class="lorem">
|
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
<span>Go!</span>
|
placeholder="IMDb ID" required />
|
||||||
</template>
|
<button type="submit"
|
||||||
|
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Search</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
<p class="mt-2">
|
|
||||||
Please input <span class="font-bold">tt id</span> from IDMb
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="./src/main.js" type="module"></script>
|
<script src="./src/main.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+11
-10
@@ -8,18 +8,16 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"prettier": "^3.4.2",
|
||||||
"autoprefixer": "^10.4.14",
|
"prettier-plugin-organize-attributes": "^1.0.0",
|
||||||
"postcss": "^8.4.21",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"prettier": "^2.8.7",
|
"standard": "^17.1.2",
|
||||||
"prettier-plugin-organize-attributes": "^0.0.5",
|
"tailwindcss": "^4.0.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.7",
|
"vite": "^6.0.11"
|
||||||
"standard": "^17.0.0",
|
|
||||||
"tailwindcss": "^3.3.1",
|
|
||||||
"vite": "^4.2.1"
|
|
||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
@@ -27,5 +25,8 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"*.config.js"
|
"*.config.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4433
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|||||||
@@ -5,7 +5,5 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {}
|
extend: {}
|
||||||
},
|
},
|
||||||
plugins: [require('@tailwindcss/forms')({
|
|
||||||
strategy: 'class'
|
|
||||||
})]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './'
|
base: './',
|
||||||
|
plugins: [
|
||||||
|
tailwindcss()
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user