chore: 🚧 remove cinemagoer and fiber and add new techinque for scraping

This commit is contained in:
Pedro Pérez 2024-11-06 01:59:13 +01:00
parent 6d88e96864
commit cba9dd3ffc
46 changed files with 1179 additions and 1764 deletions

40
core/Makefile Normal file
View 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 .

View File

@ -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",
},
}
}
}

View File

@ -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
View 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
View 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
}

View 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)
);

View 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;

View File

@ -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
}

View File

@ -1,39 +1,75 @@
module github.com/zepyrshut/rating-orama
go 1.20
go 1.23.2
require github.com/jackc/pgx/v5 v5.7.1
require (
github.com/gofiber/fiber/v2 v2.43.0
github.com/gofiber/template v1.8.0
github.com/jackc/pgconn v1.14.0
github.com/jackc/pgx/v5 v5.3.1
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
github.com/PuerkitoBio/goquery v1.10.0 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb // indirect
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
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 (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gin-gonic/gin v1.10.0
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/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.16.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
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
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopher-toolbox v0.0.0-00010101000000-000000000000
)
replace gopher-toolbox => ./../../gopher-toolbox

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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!")
}

View File

@ -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),
})
}

View 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
}

View 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),
// })
//}

View 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
// }

View 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
// }

View 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)
}

View File

@ -0,0 +1,9 @@
package repository
import (
"github.com/zepyrshut/rating-orama/internal/sqlc"
)
type ExtendedQuerier interface {
sqlc.Querier
}

View 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
View 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,
}
}

View 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"`
}

View 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)

View 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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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
View 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"

View File

@ -2,6 +2,7 @@ package utils
import "time"
// TODO: Move to toolbox
func TimeParser(timeString string) (time.Time, error) {
if len(timeString) == 1 {
return time.Time{}, nil

View File

@ -1,11 +1,6 @@
version: '3'
services:
harvester:
container_name: harvester-ratingorama
image: harvester:0.1.0
networks:
- ratingorama
core:
container_name: core-ratingorama
image: core:0.1.0
@ -14,7 +9,7 @@ services:
HARVESTER_API: ${HARVESTER_API}
IS_PRODUCTION: ${IS_PRODUCTION}
ports:
- "3000:3000"
- "8086:8080"
networks:
- ratingorama
db:

View File

@ -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");

View File

@ -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"]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -1,2 +0,0 @@
Flask==2.2.3
cinemagoer==2022.12.27

View File

@ -1,8 +0,0 @@
from flask import jsonify, Blueprint
ping = Blueprint('ping', __name__)
@ping.route('/ping', methods=['GET'])
def ping_pong():
return jsonify('pong!')

View File

@ -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