chore: 🚧 remove cinemagoer and fiber and add new techinque for scraping
This commit is contained in:
parent
6d88e96864
commit
cba9dd3ffc
40
core/Makefile
Normal file
40
core/Makefile
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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 .
|
||||||
@ -1,36 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Application struct {
|
|
||||||
*slog.Logger
|
|
||||||
Environment
|
|
||||||
}
|
|
||||||
|
|
||||||
type Environment struct {
|
|
||||||
Datasource string
|
|
||||||
HarvesterApi string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewApp(isProduction bool) *Application {
|
|
||||||
if isProduction {
|
|
||||||
return &Application{
|
|
||||||
newStructuredLogger(),
|
|
||||||
Environment{
|
|
||||||
Datasource: os.Getenv("DATASOURCE"),
|
|
||||||
HarvesterApi: os.Getenv("HARVESTER_API"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return &Application{
|
|
||||||
newStructuredLogger(),
|
|
||||||
Environment{
|
|
||||||
Datasource: "postgres://postgres:postgres@localhost:5432/postgres",
|
|
||||||
HarvesterApi: "http://localhost:5000/tv-show/%s",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newStructuredLogger() *slog.Logger {
|
|
||||||
logFile, _ := os.OpenFile(generateLogFileName(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
|
||||||
multiWriter := io.MultiWriter(os.Stdout, logFile)
|
|
||||||
|
|
||||||
return slog.New(slog.NewJSONHandler(multiWriter))
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateLogFileName() string {
|
|
||||||
currentTime := time.Now()
|
|
||||||
return "logs/" + currentTime.Format("2006-01-02") + ".log"
|
|
||||||
}
|
|
||||||
82
core/cmd/main.go
Normal file
82
core/cmd/main.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"gopher-toolbox/config"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopher-toolbox/db"
|
||||||
|
"gopher-toolbox/utils"
|
||||||
|
|
||||||
|
"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/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "0.2.0-beta.20241116"
|
||||||
|
|
||||||
|
var app *config.App
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(map[string]string{})
|
||||||
|
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("cannot load .env file", "error", err)
|
||||||
|
}
|
||||||
|
config.NewLogger(config.LogLevel(os.Getenv("LOG_LEVEL")))
|
||||||
|
slog.Info("starting server")
|
||||||
|
|
||||||
|
if os.Getenv("MIGRATE") == "true" {
|
||||||
|
migrateDB()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDB() {
|
||||||
|
slog.Info("migrating database")
|
||||||
|
m, err := migrate.New("file://database/migrations", os.Getenv("DATASOURCE"))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("cannot create migration", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.Up()
|
||||||
|
if err != nil && err != migrate.ErrNoChange {
|
||||||
|
slog.Error("cannot migrate", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err == migrate.ErrNoChange {
|
||||||
|
slog.Info("migration has no changes")
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("migration done")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
app = &config.App{
|
||||||
|
DataSource: os.Getenv("DATASOURCE"),
|
||||||
|
UseCache: utils.GetBool(os.Getenv("USE_CACHE")),
|
||||||
|
AppInfo: config.AppInfo{
|
||||||
|
GinMode: os.Getenv("GIN_MODE"),
|
||||||
|
Version: version,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPool := db.NewPostgresPool(app.DataSource)
|
||||||
|
defer dbPool.Close()
|
||||||
|
|
||||||
|
q := repository.NewPGXRepo(dbPool)
|
||||||
|
h := handlers.New(q, app)
|
||||||
|
r := Router(h, app)
|
||||||
|
|
||||||
|
slog.Info("server started", "port", "8080", "version", version)
|
||||||
|
err := http.ListenAndServe(":8080", r)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("cannot start server", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
28
core/cmd/routes.go
Normal file
28
core/cmd/routes.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/zepyrshut/rating-orama/internal/handlers"
|
||||||
|
|
||||||
|
"gopher-toolbox/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Router(h *handlers.Handlers, app *config.App) *gin.Engine {
|
||||||
|
gin.SetMode(app.AppInfo.GinMode)
|
||||||
|
r := gin.New()
|
||||||
|
|
||||||
|
// app.Use(recover.New())
|
||||||
|
|
||||||
|
// app.Static("/js", "./views/js")
|
||||||
|
// app.Static("/css", "./views/css")
|
||||||
|
|
||||||
|
// app.Get("/", handlers.Repo.Index)
|
||||||
|
// app.Get("/another", handlers.Repo.Another)
|
||||||
|
// app.Get("/tv-show", handlers.Repo.GetAllChapters)
|
||||||
|
|
||||||
|
// dev := app.Group("/dev")
|
||||||
|
// dev.Get("/ping", handlers.Repo.Ping)
|
||||||
|
// dev.Get("/panic", handlers.Repo.Panic)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
32
core/database/migrations/001_rating_schema.up.sql
Normal file
32
core/database/migrations/001_rating_schema.up.sql
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
create table if not exists tv_show (
|
||||||
|
id integer primary key,
|
||||||
|
"name" varchar not null,
|
||||||
|
tt_imdb varchar not null,
|
||||||
|
popularity int not null default 0,
|
||||||
|
|
||||||
|
created_at timestamp not null default (now()),
|
||||||
|
updated_at timestamp not null default (now())
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists idx_tv_show_title on "tv_show" ("title");
|
||||||
|
create index if not exists idx_tv_show_updated_at on "tv_show" ("updated_at");
|
||||||
|
|
||||||
|
create table if not exists episodes (
|
||||||
|
id integer primary key,
|
||||||
|
tv_show_id integer not null,
|
||||||
|
|
||||||
|
season integer not null,
|
||||||
|
episode integer not null,
|
||||||
|
released date,
|
||||||
|
"name" varchar not null,
|
||||||
|
plot text not null default '',
|
||||||
|
avg_rating numeric(1,1) not null default 0,
|
||||||
|
vote_count int not null default 0,
|
||||||
|
|
||||||
|
foreign key (tv_show_id) references tv_show (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
37
core/database/queries/tv_show.sql
Normal file
37
core/database/queries/tv_show.sql
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
-- name: CreateTVShow :one
|
||||||
|
insert into "tv_show" (name, tt_imdb)
|
||||||
|
values ($1, $2)
|
||||||
|
returning *;
|
||||||
|
|
||||||
|
-- name: CreateEpisodes :one
|
||||||
|
insert into "episodes" (tv_show_id, season, episode, released, name, plot, avg_rating, vote_count)
|
||||||
|
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
returning *;
|
||||||
|
|
||||||
|
-- name: CheckTVShowExists :one
|
||||||
|
select * from "tv_show"
|
||||||
|
where tt_imdb = $1;
|
||||||
|
|
||||||
|
-- name: GetEpisodes :many
|
||||||
|
select * from "episodes"
|
||||||
|
where tv_show_id = $1;
|
||||||
|
|
||||||
|
-- name: IncreasePopularity :exec
|
||||||
|
update "tv_show" set popularity = popularity + 1
|
||||||
|
where id = $1;
|
||||||
|
|
||||||
|
-- name: TvShowAverageRating :one
|
||||||
|
select avg(avg_rating) from "episodes"
|
||||||
|
where tv_show_id = $1;
|
||||||
|
|
||||||
|
-- name: TvShowMedianRating :one
|
||||||
|
select percentile_cont(0.5) within group (order by avg_rating) from "episodes"
|
||||||
|
where tv_show_id = $1;
|
||||||
|
|
||||||
|
-- name: SeasonAverageRating :one
|
||||||
|
select avg(avg_rating) from "episodes"
|
||||||
|
where tv_show_id = $1 and season = $2;
|
||||||
|
|
||||||
|
-- name: SeasonMedianRating :one
|
||||||
|
-- select percentile_cont(0.5) within group (order by avg_rating) from "episodes"
|
||||||
|
-- where tv_show_id = $1 and season = $2;
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
_ "github.com/jackc/pgconn"
|
|
||||||
_ "github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
_ "github.com/jackc/pgx/v5/stdlib"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewDBPool(dataSource string) *pgxpool.Pool {
|
|
||||||
dbPool, err := pgxpool.New(context.Background(), dataSource)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbPool
|
|
||||||
}
|
|
||||||
92
core/go.mod
92
core/go.mod
@ -1,39 +1,75 @@
|
|||||||
module github.com/zepyrshut/rating-orama
|
module github.com/zepyrshut/rating-orama
|
||||||
|
|
||||||
go 1.20
|
go 1.23.2
|
||||||
|
|
||||||
|
require github.com/jackc/pgx/v5 v5.7.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gofiber/fiber/v2 v2.43.0
|
github.com/PuerkitoBio/goquery v1.10.0 // indirect
|
||||||
github.com/gofiber/template v1.8.0
|
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||||
github.com/jackc/pgconn v1.14.0
|
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb // indirect
|
||||||
github.com/jackc/pgx/v5 v5.3.1
|
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
|
github.com/antchfx/htmlquery v1.3.3 // indirect
|
||||||
|
github.com/antchfx/xmlquery v1.4.2 // indirect
|
||||||
|
github.com/antchfx/xpath v1.3.2 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.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.3 // 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/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.8 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // 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/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/o1egl/paseto v1.0.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||||
|
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/net v0.30.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/gocolly/colly v1.2.0
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.18.1
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
github.com/jackc/pgio v1.0.0 // indirect
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.0 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/klauspost/compress v1.16.3 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
golang.org/x/crypto v0.28.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
golang.org/x/text v0.19.0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
gopher-toolbox v0.0.0-00010101000000-000000000000
|
||||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
|
||||||
github.com/tinylib/msgp v1.1.8 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/fasthttp v1.45.0 // indirect
|
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
||||||
golang.org/x/crypto v0.7.0 // indirect
|
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
|
||||||
golang.org/x/text v0.8.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace gopher-toolbox => ./../../gopher-toolbox
|
||||||
|
|||||||
1035
core/go.sum
1035
core/go.sum
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
"github.com/zepyrshut/rating-orama/app"
|
|
||||||
"github.com/zepyrshut/rating-orama/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Repository struct {
|
|
||||||
DB repository.DBRepo
|
|
||||||
App *app.Application
|
|
||||||
}
|
|
||||||
|
|
||||||
var Repo *Repository
|
|
||||||
|
|
||||||
func NewRepo(db *pgxpool.Pool, app *app.Application) *Repository {
|
|
||||||
return &Repository{
|
|
||||||
DB: repository.NewPostgresRepo(db),
|
|
||||||
App: app,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandlers(r *Repository) {
|
|
||||||
Repo = r
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (rp Repository) Index(c *fiber.Ctx) error {
|
|
||||||
return c.Render("index", fiber.Map{
|
|
||||||
"Title": "Template engine is working! We are in the index page!",
|
|
||||||
"SomeData": []string{"Some", "Data", "Here"},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rp Repository) Another(c *fiber.Ctx) error {
|
|
||||||
return c.Render("another", fiber.Map{
|
|
||||||
"Title": "Template engine is working! We are in the another page!",
|
|
||||||
"SomeData": []string{"Some", "Data", "Here"},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rp Repository) Ping(c *fiber.Ctx) error {
|
|
||||||
rp.App.Info("Ping!")
|
|
||||||
return c.SendString("Pong!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rp Repository) Panic(c *fiber.Ctx) error {
|
|
||||||
panic("Panic!")
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/zepyrshut/rating-orama/models"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (rp Repository) GetAllChapters(c *fiber.Ctx) error {
|
|
||||||
tvShow := models.TvShow{}
|
|
||||||
|
|
||||||
ttShowID := c.Query("id")
|
|
||||||
|
|
||||||
if ttShowID[0:2] == "tt" {
|
|
||||||
ttShowID = ttShowID[2:]
|
|
||||||
}
|
|
||||||
|
|
||||||
exist := rp.DB.CheckIfTvShowExists(ttShowID)
|
|
||||||
|
|
||||||
if !exist {
|
|
||||||
url := fmt.Sprintf(rp.App.Environment.HarvesterApi, ttShowID)
|
|
||||||
response, _ := http.Get(url)
|
|
||||||
body, _ := io.ReadAll(response.Body)
|
|
||||||
err := json.Unmarshal(body, &tvShow)
|
|
||||||
if err != nil {
|
|
||||||
rp.App.Error(err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).JSON(err)
|
|
||||||
}
|
|
||||||
err = rp.DB.InsertTvShow(tvShow)
|
|
||||||
if err != nil {
|
|
||||||
rp.App.Error(err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).JSON(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tvShow, err := rp.DB.FetchTvShow(ttShowID)
|
|
||||||
if err != nil {
|
|
||||||
rp.App.Error(err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).JSON(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tvShowJSON, err := json.Marshal(tvShow)
|
|
||||||
if err != nil {
|
|
||||||
rp.App.Error(err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).JSON(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Render("charts", fiber.Map{
|
|
||||||
"TvShow": tvShow,
|
|
||||||
"TvShowJSON": string(tvShowJSON),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
90
core/internal/handlers/handlers.go
Normal file
90
core/internal/handlers/handlers.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopher-toolbox/config"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"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 {
|
||||||
|
App *config.App
|
||||||
|
Queries repository.ExtendedQuerier
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(q repository.ExtendedQuerier, app *config.App) *Handlers {
|
||||||
|
return &Handlers{
|
||||||
|
Queries: q,
|
||||||
|
App: app,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
46
core/internal/handlers/tvshow.go
Normal file
46
core/internal/handlers/tvshow.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
//func (hq *Handlers) GetAllChapters(c *fiber.Ctx) error {
|
||||||
|
// tvShow := models.TvShow{}
|
||||||
|
|
||||||
|
// ttShowID := c.Query("id")
|
||||||
|
|
||||||
|
// if ttShowID[0:2] == "tt" {
|
||||||
|
// ttShowID = ttShowID[2:]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// exist := hq.DB.CheckIfTvShowExists(ttShowID)
|
||||||
|
|
||||||
|
// if !exist {
|
||||||
|
// url := fmt.Sprintf(hq.App.Environment.HarvesterApi, ttShowID)
|
||||||
|
// response, _ := http.Get(url)
|
||||||
|
// body, _ := io.ReadAll(response.Body)
|
||||||
|
// err := json.Unmarshal(body, &tvShow)
|
||||||
|
// if err != nil {
|
||||||
|
// hq.App.Error(err.Error())
|
||||||
|
// return c.Status(http.StatusInternalServerError).JSON(err)
|
||||||
|
// }
|
||||||
|
// err = hq.DB.InsertTvShow(tvShow)
|
||||||
|
// if err != nil {
|
||||||
|
// hq.App.Error(err.Error())
|
||||||
|
// return c.Status(http.StatusInternalServerError).JSON(err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// tvShow, err := hq.DB.FetchTvShow(ttShowID)
|
||||||
|
// if err != nil {
|
||||||
|
// hq.App.Error(err.Error())
|
||||||
|
// return c.Status(http.StatusInternalServerError).JSON(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// tvShowJSON, err := json.Marshal(tvShow)
|
||||||
|
// if err != nil {
|
||||||
|
// hq.App.Error(err.Error())
|
||||||
|
// return c.Status(http.StatusInternalServerError).JSON(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Render("charts", fiber.Map{
|
||||||
|
// "TvShow": tvShow,
|
||||||
|
// "TvShowJSON": string(tvShowJSON),
|
||||||
|
// })
|
||||||
|
//}
|
||||||
68
core/internal/models/tvshow.go
Normal file
68
core/internal/models/tvshow.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "strconv"
|
||||||
|
// "time"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// type Popularity struct {
|
||||||
|
// ShowID string `json:"show_id"`
|
||||||
|
// TimesViewed int `json:"times_viewed"`
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type TvShow struct {
|
||||||
|
// ShowID string `json:"show_id"`
|
||||||
|
// Title string `json:"title"`
|
||||||
|
// Runtime int `json:"runtime"`
|
||||||
|
// Votes int `json:"votes"`
|
||||||
|
// AvgRating float64 `json:"avg_rating"`
|
||||||
|
// MedianRating float64 `json:"median_rating"`
|
||||||
|
// Seasons []Season `json:"seasons"`
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type Season struct {
|
||||||
|
// Number int `json:"number"`
|
||||||
|
// AvgRating float64 `json:"avg_rating"`
|
||||||
|
// MedianRating float64 `json:"median_rating"`
|
||||||
|
// Votes int `json:"votes"`
|
||||||
|
// Episodes []Episode `json:"episodes"`
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type Episode struct {
|
||||||
|
// Number int `json:"number"`
|
||||||
|
// EpisodeID string `json:"episode_id"`
|
||||||
|
// Title string `json:"title"`
|
||||||
|
// Aired time.Time `json:"aired"`
|
||||||
|
// AvgRating float64 `json:"avg_rating"`
|
||||||
|
// Votes int `json:"votes"`
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (tvShow *TvShow) TvShowBuilder(tvShowDTO TvShowDTO) {
|
||||||
|
// tvShow.ShowID = tvShowDTO.ShowID
|
||||||
|
// tvShow.Title = tvShowDTO.Title
|
||||||
|
// tvShow.Runtime, _ = strconv.Atoi(tvShowDTO.Runtime)
|
||||||
|
|
||||||
|
// lastSeasonNumber := tvShowDTO.Episodes[len(tvShowDTO.Episodes)-1].SeasonID
|
||||||
|
// if lastSeasonNumber == -1 {
|
||||||
|
// lastSeasonNumber = tvShowDTO.Episodes[len(tvShowDTO.Episodes)-2].SeasonID
|
||||||
|
// }
|
||||||
|
// seasons := make([]Season, lastSeasonNumber)
|
||||||
|
|
||||||
|
// for currentSeason := 1; currentSeason <= lastSeasonNumber; currentSeason++ {
|
||||||
|
// for _, episode := range tvShowDTO.Episodes {
|
||||||
|
// if episode.SeasonID == currentSeason {
|
||||||
|
// seasons[currentSeason-1].Number = currentSeason
|
||||||
|
// seasons[currentSeason-1].Episodes = append(seasons[currentSeason-1].Episodes, Episode{
|
||||||
|
// Number: episode.Number,
|
||||||
|
// EpisodeID: episode.EpisodeID,
|
||||||
|
// Title: episode.Title,
|
||||||
|
// Aired: episode.Aired.Time,
|
||||||
|
// AvgRating: episode.AvgRating,
|
||||||
|
// Votes: episode.Votes,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// tvShow.Seasons = seasons
|
||||||
|
// }
|
||||||
52
core/internal/models/tvshowdto.go
Normal file
52
core/internal/models/tvshowdto.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// type TvShowDTO struct {
|
||||||
|
// ShowID string `json:"tt_show_id"`
|
||||||
|
// Title string `json:"title"`
|
||||||
|
// Runtime string `json:"runtime"`
|
||||||
|
// Episodes []EpisodeDTO `json:"episodes"`
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type EpisodeDTO struct {
|
||||||
|
// Number int `json:"number"`
|
||||||
|
// SeasonID int `json:"season_id"`
|
||||||
|
// EpisodeID string `json:"tt_episode_id"`
|
||||||
|
// Title string `json:"title"`
|
||||||
|
// Aired AiredTime `json:"aired"`
|
||||||
|
// AvgRating float64 `json:"avg_rating"`
|
||||||
|
// Votes int `json:"votes"`
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type AiredTime struct {
|
||||||
|
// time.Time
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (tvShow *TvShow) UnmarshalJSON(data []byte) error {
|
||||||
|
// var tvShowDTO TvShowDTO
|
||||||
|
// err := json.Unmarshal(data, &tvShowDTO)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// tvShow.TvShowBuilder(tvShowDTO)
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (aired *AiredTime) UnmarshalJSON(data []byte) error {
|
||||||
|
// if string(data) == "null" || string(data) == "" {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var s string
|
||||||
|
// if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// t, err := utils.TimeParser(s)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// aired.Time = t
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
40
core/internal/repository/pgxrepo.go
Normal file
40
core/internal/repository/pgxrepo.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/zepyrshut/rating-orama/internal/sqlc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pgxRepository struct {
|
||||||
|
*sqlc.Queries
|
||||||
|
db *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPGXRepo(db *pgxpool.Pool) ExtendedQuerier {
|
||||||
|
return &pgxRepository{
|
||||||
|
Queries: sqlc.New(db),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgxRepository) execTx(ctx context.Context, txFunc func(tx pgx.Tx) error) error {
|
||||||
|
slog.Info("starting transaction", "txFunc", txFunc)
|
||||||
|
tx, err := r.db.Begin(ctx)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("committing transaction", "txFunc", txFunc)
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
9
core/internal/repository/querier.go
Normal file
9
core/internal/repository/querier.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zepyrshut/rating-orama/internal/sqlc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExtendedQuerier interface {
|
||||||
|
sqlc.Querier
|
||||||
|
}
|
||||||
129
core/internal/scraper/tvshow.go
Normal file
129
core/internal/scraper/tvshow.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package scraper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gocolly/colly"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Episode struct {
|
||||||
|
Season int
|
||||||
|
Episode int
|
||||||
|
Released time.Time
|
||||||
|
Name string
|
||||||
|
Plot string
|
||||||
|
Rate float64
|
||||||
|
VoteCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Season []Episode
|
||||||
|
|
||||||
|
const seasonsSelector = "ul.ipc-tabs a[data-testid='tab-season-entry']"
|
||||||
|
const episodesSelector = "section.sc-1e7f96be-0.ZaQIL"
|
||||||
|
const nextSeasonButtonSelector = "#next-season-btn"
|
||||||
|
const imdbEpisodesURL = "https://www.imdb.com/title/%s/episodes?season=%d"
|
||||||
|
|
||||||
|
func scrapeSeasons(ttImdb string) {
|
||||||
|
c := colly.NewCollector(
|
||||||
|
colly.AllowedDomains("imdb.com", "www.imdb.com"),
|
||||||
|
)
|
||||||
|
|
||||||
|
var allEpisodes []Episode
|
||||||
|
var seasons []int
|
||||||
|
|
||||||
|
c.OnHTML("ul.ipc-tabs a[data-testid='tab-season-entry']", func(e *colly.HTMLElement) {
|
||||||
|
seasonText := strings.TrimSpace(e.Text)
|
||||||
|
seasonNum, err := strconv.Atoi(seasonText)
|
||||||
|
if err == nil {
|
||||||
|
seasons = append(seasons, seasonNum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.OnScraped(func(r *colly.Response) {
|
||||||
|
seasonMap := make(map[int]bool)
|
||||||
|
uniqueSeasons := []int{}
|
||||||
|
for _, seasonNum := range seasons {
|
||||||
|
if !seasonMap[seasonNum] {
|
||||||
|
seasonMap[seasonNum] = true
|
||||||
|
uniqueSeasons = append(uniqueSeasons, seasonNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Ints(uniqueSeasons)
|
||||||
|
|
||||||
|
episodeCollector := c.Clone()
|
||||||
|
|
||||||
|
episodeCollector.OnHTML(episodesSelector, func(e *colly.HTMLElement) {
|
||||||
|
seasonEpisodes := extractEpisodesFromSeason(e.Text)
|
||||||
|
allEpisodes = append(allEpisodes, seasonEpisodes...)
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, seasonNum := range uniqueSeasons {
|
||||||
|
seasonURL := fmt.Sprintf(imdbEpisodesURL, ttImdb, seasonNum)
|
||||||
|
slog.Info("visiting %s", seasonURL)
|
||||||
|
episodeCollector.Visit(seasonURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
episodeCollector.Wait()
|
||||||
|
|
||||||
|
// fmt.Println("Total de episodios:", len(allEpisodes))
|
||||||
|
// for _, episode := range allEpisodes {
|
||||||
|
// fmt.Printf("Temporada %d, Episodio %d: %s\n", episode.Season, episode.Episode, episode.Name)
|
||||||
|
// }
|
||||||
|
// TODO: Save to DB
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Visit("https://www.imdb.com/title/tt0903747/episodes")
|
||||||
|
c.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEpisodesFromSeason(data string) Season {
|
||||||
|
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))
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
var episode Episode
|
||||||
|
|
||||||
|
seasonEpisode := match[1]
|
||||||
|
|
||||||
|
name := strings.TrimSpace(match[2])
|
||||||
|
|
||||||
|
day := match[3]
|
||||||
|
dateRest := strings.TrimSpace(match[4])
|
||||||
|
year := match[5]
|
||||||
|
|
||||||
|
plot := strings.TrimSpace(match[6])
|
||||||
|
rate := match[7]
|
||||||
|
voteCount := match[8]
|
||||||
|
|
||||||
|
seasonNum := strings.TrimPrefix(strings.Split(seasonEpisode, ".")[0], "S")
|
||||||
|
episodeNum := strings.TrimPrefix(strings.Split(seasonEpisode, ".")[1], "E")
|
||||||
|
|
||||||
|
votes, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimSuffix(voteCount, "K"), "K"))
|
||||||
|
|
||||||
|
episode.Name = name
|
||||||
|
episode.Episode, _ = strconv.Atoi(episodeNum)
|
||||||
|
episode.Season, _ = strconv.Atoi(seasonNum)
|
||||||
|
episode.Released, _ = time.Parse("Mon, Jan 2, 2006", fmt.Sprintf("%s, %s, %s", day, dateRest, year))
|
||||||
|
episode.Plot = plot
|
||||||
|
episode.Rate, _ = strconv.ParseFloat(strings.TrimSuffix(rate, "/10"), 2)
|
||||||
|
episode.VoteCount = votes * 1000
|
||||||
|
|
||||||
|
episodes = append(episodes, episode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodes
|
||||||
|
}
|
||||||
32
core/internal/sqlc/db.go
Normal file
32
core/internal/sqlc/db.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||||
|
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||||
|
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
30
core/internal/sqlc/models.go
Normal file
30
core/internal/sqlc/models.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Episode struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
TvShowID int32 `json:"tv_show_id"`
|
||||||
|
Season int32 `json:"season"`
|
||||||
|
Episode int32 `json:"episode"`
|
||||||
|
Released pgtype.Date `json:"released"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Plot string `json:"plot"`
|
||||||
|
AvgRating pgtype.Numeric `json:"avg_rating"`
|
||||||
|
VoteCount int32 `json:"vote_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TvShow struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TtImdb string `json:"tt_imdb"`
|
||||||
|
Popularity int32 `json:"popularity"`
|
||||||
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
||||||
|
}
|
||||||
22
core/internal/sqlc/querier.go
Normal file
22
core/internal/sqlc/querier.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Querier interface {
|
||||||
|
CheckTVShowExists(ctx context.Context, ttImdb string) (TvShow, error)
|
||||||
|
CreateEpisodes(ctx context.Context, arg CreateEpisodesParams) (Episode, error)
|
||||||
|
CreateTVShow(ctx context.Context, arg CreateTVShowParams) (TvShow, error)
|
||||||
|
GetEpisodes(ctx context.Context, tvShowID int32) ([]Episode, error)
|
||||||
|
IncreasePopularity(ctx context.Context, id int32) error
|
||||||
|
SeasonAverageRating(ctx context.Context, arg SeasonAverageRatingParams) (float64, error)
|
||||||
|
TvShowAverageRating(ctx context.Context, tvShowID int32) (float64, error)
|
||||||
|
TvShowMedianRating(ctx context.Context, tvShowID int32) (float64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Querier = (*Queries)(nil)
|
||||||
185
core/internal/sqlc/tv_show.sql.go
Normal file
185
core/internal/sqlc/tv_show.sql.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
// source: tv_show.sql
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const checkTVShowExists = `-- name: CheckTVShowExists :one
|
||||||
|
select id, name, tt_imdb, popularity, created_at, updated_at from "tv_show"
|
||||||
|
where tt_imdb = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CheckTVShowExists(ctx context.Context, ttImdb string) (TvShow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, checkTVShowExists, ttImdb)
|
||||||
|
var i TvShow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.TtImdb,
|
||||||
|
&i.Popularity,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createEpisodes = `-- name: CreateEpisodes :one
|
||||||
|
insert into "episodes" (tv_show_id, season, episode, released, name, plot, avg_rating, vote_count)
|
||||||
|
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
returning id, tv_show_id, season, episode, released, name, plot, avg_rating, vote_count
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateEpisodesParams struct {
|
||||||
|
TvShowID int32 `json:"tv_show_id"`
|
||||||
|
Season int32 `json:"season"`
|
||||||
|
Episode int32 `json:"episode"`
|
||||||
|
Released pgtype.Date `json:"released"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Plot string `json:"plot"`
|
||||||
|
AvgRating pgtype.Numeric `json:"avg_rating"`
|
||||||
|
VoteCount int32 `json:"vote_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateEpisodes(ctx context.Context, arg CreateEpisodesParams) (Episode, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createEpisodes,
|
||||||
|
arg.TvShowID,
|
||||||
|
arg.Season,
|
||||||
|
arg.Episode,
|
||||||
|
arg.Released,
|
||||||
|
arg.Name,
|
||||||
|
arg.Plot,
|
||||||
|
arg.AvgRating,
|
||||||
|
arg.VoteCount,
|
||||||
|
)
|
||||||
|
var i Episode
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.TvShowID,
|
||||||
|
&i.Season,
|
||||||
|
&i.Episode,
|
||||||
|
&i.Released,
|
||||||
|
&i.Name,
|
||||||
|
&i.Plot,
|
||||||
|
&i.AvgRating,
|
||||||
|
&i.VoteCount,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTVShow = `-- name: CreateTVShow :one
|
||||||
|
insert into "tv_show" (name, tt_imdb)
|
||||||
|
values ($1, $2)
|
||||||
|
returning id, name, tt_imdb, popularity, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateTVShowParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
TtImdb string `json:"tt_imdb"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateTVShow(ctx context.Context, arg CreateTVShowParams) (TvShow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createTVShow, arg.Name, arg.TtImdb)
|
||||||
|
var i TvShow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.TtImdb,
|
||||||
|
&i.Popularity,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEpisodes = `-- name: GetEpisodes :many
|
||||||
|
select id, tv_show_id, season, episode, released, name, plot, avg_rating, vote_count from "episodes"
|
||||||
|
where tv_show_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetEpisodes(ctx context.Context, tvShowID int32) ([]Episode, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getEpisodes, tvShowID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []Episode{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i Episode
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.TvShowID,
|
||||||
|
&i.Season,
|
||||||
|
&i.Episode,
|
||||||
|
&i.Released,
|
||||||
|
&i.Name,
|
||||||
|
&i.Plot,
|
||||||
|
&i.AvgRating,
|
||||||
|
&i.VoteCount,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const increasePopularity = `-- name: IncreasePopularity :exec
|
||||||
|
update "tv_show" set popularity = popularity + 1
|
||||||
|
where id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) IncreasePopularity(ctx context.Context, id int32) error {
|
||||||
|
_, err := q.db.Exec(ctx, increasePopularity, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonAverageRating = `-- name: SeasonAverageRating :one
|
||||||
|
select avg(avg_rating) from "episodes"
|
||||||
|
where tv_show_id = $1 and season = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SeasonAverageRatingParams struct {
|
||||||
|
TvShowID int32 `json:"tv_show_id"`
|
||||||
|
Season int32 `json:"season"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SeasonAverageRating(ctx context.Context, arg SeasonAverageRatingParams) (float64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, seasonAverageRating, arg.TvShowID, arg.Season)
|
||||||
|
var avg float64
|
||||||
|
err := row.Scan(&avg)
|
||||||
|
return avg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const tvShowAverageRating = `-- name: TvShowAverageRating :one
|
||||||
|
select avg(avg_rating) from "episodes"
|
||||||
|
where tv_show_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) TvShowAverageRating(ctx context.Context, tvShowID int32) (float64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, tvShowAverageRating, tvShowID)
|
||||||
|
var avg float64
|
||||||
|
err := row.Scan(&avg)
|
||||||
|
return avg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const tvShowMedianRating = `-- name: TvShowMedianRating :one
|
||||||
|
select percentile_cont(0.5) within group (order by avg_rating) from "episodes"
|
||||||
|
where tv_show_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) TvShowMedianRating(ctx context.Context, tvShowID int32) (float64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, tvShowMedianRating, tvShowID)
|
||||||
|
var percentile_cont float64
|
||||||
|
err := row.Scan(&percentile_cont)
|
||||||
|
return percentile_cont, err
|
||||||
|
}
|
||||||
47
core/main.go
47
core/main.go
@ -1,47 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/template/html"
|
|
||||||
"github.com/zepyrshut/rating-orama/app"
|
|
||||||
"github.com/zepyrshut/rating-orama/db"
|
|
||||||
"github.com/zepyrshut/rating-orama/handlers"
|
|
||||||
"github.com/zepyrshut/rating-orama/router"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var application *app.Application
|
|
||||||
var isProduction = os.Getenv("IS_PRODUCTION") == "true"
|
|
||||||
|
|
||||||
const version = "0.1.0"
|
|
||||||
const appName = "Rating Orama Core"
|
|
||||||
const author = "Pedro Pérez"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
engine := html.New("./views", ".html")
|
|
||||||
|
|
||||||
fmt.Println(isProduction)
|
|
||||||
|
|
||||||
fiberApp := fiber.New(fiber.Config{
|
|
||||||
Views: engine,
|
|
||||||
ViewsLayout: "layouts/main",
|
|
||||||
})
|
|
||||||
application = app.NewApp(isProduction)
|
|
||||||
|
|
||||||
dbPool := db.NewDBPool(application.Datasource)
|
|
||||||
defer dbPool.Close()
|
|
||||||
|
|
||||||
repo := handlers.NewRepo(dbPool, application)
|
|
||||||
handlers.NewHandlers(repo)
|
|
||||||
router.Routes(fiberApp)
|
|
||||||
|
|
||||||
application.Logger.Info("API is running", "version", version, "app", appName, "author", author)
|
|
||||||
err := fiberApp.Listen("0.0.0.0:3000")
|
|
||||||
if err != nil {
|
|
||||||
application.Logger.Error(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Popularity struct {
|
|
||||||
ShowID string `json:"show_id"`
|
|
||||||
TimesViewed int `json:"times_viewed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TvShow struct {
|
|
||||||
ShowID string `json:"show_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Runtime int `json:"runtime"`
|
|
||||||
Votes int `json:"votes"`
|
|
||||||
AvgRating float64 `json:"avg_rating"`
|
|
||||||
MedianRating float64 `json:"median_rating"`
|
|
||||||
Seasons []Season `json:"seasons"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Season struct {
|
|
||||||
Number int `json:"number"`
|
|
||||||
AvgRating float64 `json:"avg_rating"`
|
|
||||||
MedianRating float64 `json:"median_rating"`
|
|
||||||
Votes int `json:"votes"`
|
|
||||||
Episodes []Episode `json:"episodes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Episode struct {
|
|
||||||
Number int `json:"number"`
|
|
||||||
EpisodeID string `json:"episode_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Aired time.Time `json:"aired"`
|
|
||||||
AvgRating float64 `json:"avg_rating"`
|
|
||||||
Votes int `json:"votes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tvShow *TvShow) TvShowBuilder(tvShowDTO TvShowDTO) {
|
|
||||||
tvShow.ShowID = tvShowDTO.ShowID
|
|
||||||
tvShow.Title = tvShowDTO.Title
|
|
||||||
tvShow.Runtime, _ = strconv.Atoi(tvShowDTO.Runtime)
|
|
||||||
|
|
||||||
lastSeasonNumber := tvShowDTO.Episodes[len(tvShowDTO.Episodes)-1].SeasonID
|
|
||||||
if lastSeasonNumber == -1 {
|
|
||||||
lastSeasonNumber = tvShowDTO.Episodes[len(tvShowDTO.Episodes)-2].SeasonID
|
|
||||||
}
|
|
||||||
seasons := make([]Season, lastSeasonNumber)
|
|
||||||
|
|
||||||
for currentSeason := 1; currentSeason <= lastSeasonNumber; currentSeason++ {
|
|
||||||
for _, episode := range tvShowDTO.Episodes {
|
|
||||||
if episode.SeasonID == currentSeason {
|
|
||||||
seasons[currentSeason-1].Number = currentSeason
|
|
||||||
seasons[currentSeason-1].Episodes = append(seasons[currentSeason-1].Episodes, Episode{
|
|
||||||
Number: episode.Number,
|
|
||||||
EpisodeID: episode.EpisodeID,
|
|
||||||
Title: episode.Title,
|
|
||||||
Aired: episode.Aired.Time,
|
|
||||||
AvgRating: episode.AvgRating,
|
|
||||||
Votes: episode.Votes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tvShow.Seasons = seasons
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/zepyrshut/rating-orama/utils"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TvShowDTO struct {
|
|
||||||
ShowID string `json:"tt_show_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Runtime string `json:"runtime"`
|
|
||||||
Episodes []EpisodeDTO `json:"episodes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EpisodeDTO struct {
|
|
||||||
Number int `json:"number"`
|
|
||||||
SeasonID int `json:"season_id"`
|
|
||||||
EpisodeID string `json:"tt_episode_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Aired AiredTime `json:"aired"`
|
|
||||||
AvgRating float64 `json:"avg_rating"`
|
|
||||||
Votes int `json:"votes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AiredTime struct {
|
|
||||||
time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tvShow *TvShow) UnmarshalJSON(data []byte) error {
|
|
||||||
var tvShowDTO TvShowDTO
|
|
||||||
err := json.Unmarshal(data, &tvShowDTO)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tvShow.TvShowBuilder(tvShowDTO)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (aired *AiredTime) UnmarshalJSON(data []byte) error {
|
|
||||||
if string(data) == "null" || string(data) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var s string
|
|
||||||
if err := json.Unmarshal(data, &s); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := utils.TimeParser(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
aired.Time = t
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
)
|
|
||||||
|
|
||||||
type postgresDBRepo struct {
|
|
||||||
DB *pgxpool.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPostgresRepo(conn *pgxpool.Pool) DBRepo {
|
|
||||||
return &postgresDBRepo{
|
|
||||||
DB: conn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import "github.com/zepyrshut/rating-orama/models"
|
|
||||||
|
|
||||||
type DBRepo interface {
|
|
||||||
CheckIfTvShowExists(showID string) bool
|
|
||||||
InsertTvShow(tvShow models.TvShow) error
|
|
||||||
InsertEpisodes(tvShow models.TvShow) error
|
|
||||||
FetchTvShow(showID string) (models.TvShow, error)
|
|
||||||
IncreasePopularity(showID string)
|
|
||||||
FetchEpisodes(showID string) ([]models.Season, error)
|
|
||||||
TvShowAverageRating(show *models.TvShow)
|
|
||||||
SeasonAverageRating(show *models.TvShow)
|
|
||||||
TvShowMedianRating(show *models.TvShow)
|
|
||||||
SeasonMedianRating(show *models.TvShow)
|
|
||||||
}
|
|
||||||
@ -1,299 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/zepyrshut/rating-orama/models"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) CheckIfTvShowExists(showID string) bool {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query := `SELECT show_id FROM tv_show WHERE show_id = $1`
|
|
||||||
|
|
||||||
var showIDFromDB string
|
|
||||||
err := pg.DB.QueryRow(ctx, query, showID).Scan(&showIDFromDB)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) InsertTvShow(tvShow models.TvShow) error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queryTvShow := `INSERT INTO tv_show (show_id, title, runtime) VALUES ($1, $2, $3)`
|
|
||||||
|
|
||||||
_, err := pg.DB.Exec(ctx, queryTvShow, tvShow.ShowID, tvShow.Title, tvShow.Runtime)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pg.InsertEpisodes(tvShow)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) InsertEpisodes(tvShow models.TvShow) error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query := `INSERT INTO episodes (episode_id, tv_show_id, season_number, title, number, aired, avg_rating, votes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
|
|
||||||
|
|
||||||
for k, season := range tvShow.Seasons {
|
|
||||||
for _, episode := range season.Episodes {
|
|
||||||
_, err := pg.DB.Exec(ctx, query, episode.EpisodeID, tvShow.ShowID, k+1, episode.Title, episode.Number, episode.Aired, episode.AvgRating, episode.Votes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) FetchTvShow(showID string) (models.TvShow, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query := `SELECT show_id, title, runtime FROM tv_show WHERE show_id = $1`
|
|
||||||
|
|
||||||
var tvShow models.TvShow
|
|
||||||
var tvShowID int
|
|
||||||
err := pg.DB.QueryRow(ctx, query, showID).Scan(&tvShowID, &tvShow.Title, &tvShow.Runtime)
|
|
||||||
if err != nil {
|
|
||||||
return tvShow, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tvShow.ShowID = fmt.Sprintf("%07d", tvShowID)
|
|
||||||
|
|
||||||
tvShow.Seasons, err = pg.FetchEpisodes(showID)
|
|
||||||
if err != nil {
|
|
||||||
return tvShow, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pg.TvShowAverageRating(&tvShow)
|
|
||||||
pg.SeasonAverageRating(&tvShow)
|
|
||||||
|
|
||||||
pg.TvShowMedianRating(&tvShow)
|
|
||||||
pg.SeasonMedianRating(&tvShow)
|
|
||||||
|
|
||||||
pg.IncreasePopularity(showID)
|
|
||||||
return tvShow, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) IncreasePopularity(showID string) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query := `UPDATE tv_show SET popularity = popularity + 1 WHERE show_id = $1`
|
|
||||||
|
|
||||||
_, err := pg.DB.Exec(ctx, query, showID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) FetchEpisodes(showID string) ([]models.Season, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query := `SELECT episode_id, season_number, title, number, aired, avg_rating, votes FROM episodes WHERE tv_show_id = $1 ORDER BY season_number, number`
|
|
||||||
|
|
||||||
rows, err := pg.DB.Query(ctx, query, showID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var seasons []models.Season
|
|
||||||
var episodeID int
|
|
||||||
for rows.Next() {
|
|
||||||
var episode models.Episode
|
|
||||||
var seasonNumber int
|
|
||||||
err = rows.Scan(&episodeID, &seasonNumber, &episode.Title, &episode.Number, &episode.Aired, &episode.AvgRating, &episode.Votes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
episode.EpisodeID = fmt.Sprintf("%07d", episodeID)
|
|
||||||
|
|
||||||
if len(seasons) < seasonNumber {
|
|
||||||
seasons = append(seasons, models.Season{})
|
|
||||||
}
|
|
||||||
|
|
||||||
seasons[seasonNumber-1].Number = seasonNumber
|
|
||||||
seasons[seasonNumber-1].Episodes = append(seasons[seasonNumber-1].Episodes, episode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return seasons, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) TvShowAverageRating(show *models.TvShow) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query :=
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
AVG(avg_rating), SUM(votes)
|
|
||||||
FROM
|
|
||||||
episodes
|
|
||||||
WHERE
|
|
||||||
tv_show_id = $1
|
|
||||||
AND
|
|
||||||
votes > 0 AND avg_rating > 0
|
|
||||||
`
|
|
||||||
|
|
||||||
var avgRating float64
|
|
||||||
var votes int
|
|
||||||
err := pg.DB.QueryRow(ctx, query, show.ShowID).Scan(&avgRating, &votes)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
show.AvgRating = avgRating
|
|
||||||
show.Votes = votes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) SeasonAverageRating(show *models.TvShow) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query :=
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
season_number, AVG(avg_rating), SUM(votes)
|
|
||||||
FROM
|
|
||||||
episodes
|
|
||||||
WHERE
|
|
||||||
tv_show_id = $1
|
|
||||||
AND
|
|
||||||
votes > 0 AND avg_rating > 0
|
|
||||||
GROUP BY
|
|
||||||
season_number
|
|
||||||
ORDER BY
|
|
||||||
season_number;
|
|
||||||
`
|
|
||||||
|
|
||||||
rows, err := pg.DB.Query(ctx, query, show.ShowID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var seasonNumber int
|
|
||||||
var avgRating float64
|
|
||||||
var votes int
|
|
||||||
err = rows.Scan(&seasonNumber, &avgRating, &votes)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
show.Seasons[seasonNumber-1].AvgRating = avgRating
|
|
||||||
show.Seasons[seasonNumber-1].Votes = votes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) TvShowMedianRating(show *models.TvShow) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query :=
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY avg_rating) AS median_rating
|
|
||||||
FROM
|
|
||||||
episodes
|
|
||||||
WHERE
|
|
||||||
tv_show_id = $1
|
|
||||||
AND
|
|
||||||
votes > 0 AND avg_rating > 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
var medianRating float64
|
|
||||||
err := pg.DB.QueryRow(ctx, query, show.ShowID).Scan(&medianRating)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
show.MedianRating = medianRating
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pg *postgresDBRepo) SeasonMedianRating(show *models.TvShow) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
query :=
|
|
||||||
`
|
|
||||||
WITH episodes_with_ranks AS (
|
|
||||||
SELECT
|
|
||||||
episode_id,
|
|
||||||
tv_show_id,
|
|
||||||
season_number,
|
|
||||||
avg_rating,
|
|
||||||
votes,
|
|
||||||
ROW_NUMBER() OVER (PARTITION BY season_number ORDER BY avg_rating) AS rank_asc,
|
|
||||||
ROW_NUMBER() OVER (PARTITION BY season_number ORDER BY avg_rating DESC) AS rank_desc,
|
|
||||||
COUNT(*) OVER (PARTITION BY season_number) AS season_episode_count
|
|
||||||
FROM
|
|
||||||
episodes
|
|
||||||
WHERE
|
|
||||||
tv_show_id = $1
|
|
||||||
AND
|
|
||||||
votes > 0 AND avg_rating > 0
|
|
||||||
),
|
|
||||||
episodes_filtered AS (
|
|
||||||
SELECT
|
|
||||||
episode_id,
|
|
||||||
tv_show_id,
|
|
||||||
season_number,
|
|
||||||
avg_rating,
|
|
||||||
votes
|
|
||||||
FROM
|
|
||||||
episodes_with_ranks
|
|
||||||
WHERE
|
|
||||||
rank_asc > season_episode_count * 0.01 AND rank_desc > season_episode_count * 0.01
|
|
||||||
),
|
|
||||||
seasons AS (
|
|
||||||
SELECT DISTINCT
|
|
||||||
season_number
|
|
||||||
FROM
|
|
||||||
episodes_filtered
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
seasons.season_number,
|
|
||||||
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY episodes_filtered.avg_rating) AS median_rating
|
|
||||||
FROM
|
|
||||||
seasons
|
|
||||||
JOIN
|
|
||||||
episodes_filtered ON seasons.season_number = episodes_filtered.season_number
|
|
||||||
GROUP BY
|
|
||||||
seasons.season_number
|
|
||||||
ORDER BY
|
|
||||||
seasons.season_number;
|
|
||||||
`
|
|
||||||
|
|
||||||
rows, err := pg.DB.Query(ctx, query, show.ShowID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var seasonNumber int
|
|
||||||
var medianRating float64
|
|
||||||
err = rows.Scan(&seasonNumber, &medianRating)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
show.Seasons[seasonNumber-1].MedianRating = medianRating
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
|
||||||
"github.com/zepyrshut/rating-orama/handlers"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Routes(app *fiber.App) {
|
|
||||||
|
|
||||||
app.Use(recover.New())
|
|
||||||
|
|
||||||
app.Static("/js", "./views/js")
|
|
||||||
app.Static("/css", "./views/css")
|
|
||||||
|
|
||||||
app.Get("/", handlers.Repo.Index)
|
|
||||||
app.Get("/another", handlers.Repo.Another)
|
|
||||||
app.Get("/tv-show", handlers.Repo.GetAllChapters)
|
|
||||||
|
|
||||||
dev := app.Group("/dev")
|
|
||||||
dev.Get("/ping", handlers.Repo.Ping)
|
|
||||||
dev.Get("/panic", handlers.Repo.Panic)
|
|
||||||
|
|
||||||
}
|
|
||||||
16
core/sqlc.yaml
Normal file
16
core/sqlc.yaml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
version: "2"
|
||||||
|
sql:
|
||||||
|
- engine: "postgresql"
|
||||||
|
schema: "./database/migrations/*"
|
||||||
|
queries: "./database/queries/*"
|
||||||
|
gen:
|
||||||
|
go:
|
||||||
|
package: "sqlc"
|
||||||
|
out: "./internal/sqlc"
|
||||||
|
sql_package: "pgx/v5"
|
||||||
|
emit_interface: true
|
||||||
|
emit_empty_slices: true
|
||||||
|
emit_json_tags: true
|
||||||
|
rename:
|
||||||
|
uuid: "UUID"
|
||||||
|
|
||||||
@ -2,6 +2,7 @@ package utils
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
// TODO: Move to toolbox
|
||||||
func TimeParser(timeString string) (time.Time, error) {
|
func TimeParser(timeString string) (time.Time, error) {
|
||||||
if len(timeString) == 1 {
|
if len(timeString) == 1 {
|
||||||
return time.Time{}, nil
|
return time.Time{}, nil
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
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:0.1.0
|
||||||
@ -14,7 +9,7 @@ services:
|
|||||||
HARVESTER_API: ${HARVESTER_API}
|
HARVESTER_API: ${HARVESTER_API}
|
||||||
IS_PRODUCTION: ${IS_PRODUCTION}
|
IS_PRODUCTION: ${IS_PRODUCTION}
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "8086:8080"
|
||||||
networks:
|
networks:
|
||||||
- ratingorama
|
- ratingorama
|
||||||
db:
|
db:
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS "tv_show" (
|
|
||||||
"show_id" integer PRIMARY KEY,
|
|
||||||
"title" varchar NOT NULL,
|
|
||||||
"runtime" integer NOT NULL,
|
|
||||||
"popularity" integer NOT NULL DEFAULT 0,
|
|
||||||
"created_at" timestamp NOT NULL DEFAULT (now()),
|
|
||||||
"updated_at" timestamp NOT NULL DEFAULT (now())
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "episodes" (
|
|
||||||
"episode_id" integer PRIMARY KEY,
|
|
||||||
"tv_show_id" integer NOT NULL,
|
|
||||||
"season_number" integer NOT NULL,
|
|
||||||
"title" varchar NOT NULL,
|
|
||||||
"number" int NOT NULL,
|
|
||||||
"aired" date NOT NULL,
|
|
||||||
"avg_rating" numeric NOT NULL,
|
|
||||||
"votes" int NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_tv_show_show_id ON "tv_show" ("show_id");
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_tv_show_title ON "tv_show" ("title");
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_tv_show_updated_at ON "tv_show" ("updated_at");
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_episodes_avg_rating ON "episodes" ("avg_rating");
|
|
||||||
|
|
||||||
ALTER TABLE "episodes" ADD FOREIGN KEY ("tv_show_id") REFERENCES "tv_show" ("show_id");
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
FROM python:3.11.3-alpine
|
|
||||||
LABEL authors="Pedro Pérez"
|
|
||||||
|
|
||||||
COPY requirements.txt requirements.txt
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
CMD ["python", "main.py"]
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import imdb
|
|
||||||
|
|
||||||
from models.tv_show import TVShow, Episode
|
|
||||||
|
|
||||||
ia = imdb.Cinemagoer()
|
|
||||||
|
|
||||||
|
|
||||||
def get_tv_show_episodes(tt_id):
|
|
||||||
tv_show_episodes = ia.get_movie(tt_id)
|
|
||||||
ia.update(tv_show_episodes, 'episodes')
|
|
||||||
|
|
||||||
runtime = tv_show_episodes['runtimes'][0] if 'runtimes' in tv_show_episodes else 0
|
|
||||||
tv_show = TVShow(tv_show_episodes.getID(), tv_show_episodes['original title'], runtime)
|
|
||||||
|
|
||||||
episodes = []
|
|
||||||
|
|
||||||
for season in tv_show_episodes['episodes']:
|
|
||||||
for episode in tv_show_episodes['episodes'][season]:
|
|
||||||
one_episode = Episode(
|
|
||||||
tv_show_episodes['episodes'][season][episode].getID(),
|
|
||||||
tv_show_episodes['episodes'][season][episode].get('title', "#{}.{}".format(season, episode)),
|
|
||||||
tv_show_episodes['episodes'][season][episode].get('episode', episode),
|
|
||||||
tv_show_episodes['episodes'][season][episode].get('original air date', 0),
|
|
||||||
tv_show_episodes['episodes'][season][episode].get('rating', 0),
|
|
||||||
tv_show_episodes['episodes'][season][episode].get('votes', 0),
|
|
||||||
tv_show_episodes['episodes'][season][episode].get('season', season))
|
|
||||||
|
|
||||||
episodes.append(one_episode.to_dict())
|
|
||||||
|
|
||||||
tv_show.add_episodes(episodes)
|
|
||||||
return tv_show
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
from flask import Flask
|
|
||||||
|
|
||||||
from routes.ping import ping
|
|
||||||
from routes.tv_show_routes import tv_show_bp
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
app.register_blueprint(tv_show_bp)
|
|
||||||
app.register_blueprint(ping)
|
|
||||||
|
|
||||||
version = '0.1.0'
|
|
||||||
appName = 'Rating Orama Harvester'
|
|
||||||
author = 'Pedro Pérez'
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=5000)
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
class Movie:
|
|
||||||
def __init__(self, tt_movie_id, title, year, avg_rating, votes, popularity, runtime):
|
|
||||||
self.tt_movie_id = tt_movie_id
|
|
||||||
self.title = title
|
|
||||||
self.year = year
|
|
||||||
self.avg_rating = avg_rating
|
|
||||||
self.votes = votes
|
|
||||||
self.popularity = popularity
|
|
||||||
self.runtime = runtime
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
class TVShow:
|
|
||||||
def __init__(self, tt_show_id, title, runtime):
|
|
||||||
self.tt_show_id = tt_show_id
|
|
||||||
self.title = title
|
|
||||||
self.runtime = runtime
|
|
||||||
self.episodes = []
|
|
||||||
|
|
||||||
def add_episodes(self, episodes):
|
|
||||||
self.episodes = episodes
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
'tt_show_id': self.tt_show_id,
|
|
||||||
'title': self.title,
|
|
||||||
'runtime': self.runtime,
|
|
||||||
'episodes': [episode.to_dict() for episode in self.episodes]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Episode:
|
|
||||||
def __init__(self, tt_episode_id, title, number, aired, avg_rating, votes, season_id):
|
|
||||||
self.tt_episode_id = tt_episode_id
|
|
||||||
self.title = title
|
|
||||||
self.number = number
|
|
||||||
self.aired = aired
|
|
||||||
self.avg_rating = avg_rating
|
|
||||||
self.votes = votes
|
|
||||||
self.season_id = season_id
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
'tt_episode_id': self.tt_episode_id,
|
|
||||||
'title': self.title,
|
|
||||||
'number': self.number,
|
|
||||||
'aired': self.aired,
|
|
||||||
'avg_rating': self.avg_rating,
|
|
||||||
'votes': self.votes,
|
|
||||||
'season_id': self.season_id
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
Flask==2.2.3
|
|
||||||
cinemagoer==2022.12.27
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
from flask import jsonify, Blueprint
|
|
||||||
|
|
||||||
ping = Blueprint('ping', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@ping.route('/ping', methods=['GET'])
|
|
||||||
def ping_pong():
|
|
||||||
return jsonify('pong!')
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
from flask import Blueprint, jsonify
|
|
||||||
|
|
||||||
from logic.utils import get_tv_show_episodes
|
|
||||||
|
|
||||||
tv_show_bp = Blueprint('tv_show_bp', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@tv_show_bp.route('/tv-show/<tt_id>', methods=['GET'])
|
|
||||||
def get_tv_show(tt_id):
|
|
||||||
try:
|
|
||||||
tv_show = get_tv_show_episodes(tt_id)
|
|
||||||
return jsonify(tv_show.__dict__)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'error': str(e)}), 500
|
|
||||||
Loading…
Reference in New Issue
Block a user