-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
copy link and refactor and rating page
- Loading branch information
Dima
committed
Jan 18, 2024
1 parent
aa63c0d
commit 432a21c
Showing
8 changed files
with
787 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,371 @@ | ||
package handlers | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
"time" | ||
"ytstalker/app/youtube" | ||
|
||
"zombiezen.com/go/sqlite" | ||
"zombiezen.com/go/sqlite/sqlitex" | ||
) | ||
|
||
type Message struct { | ||
Detail string `json:"detail"` | ||
} | ||
|
||
type VideoWithReactions struct { | ||
Video *Video `json:"video,omitempty"` | ||
Reactions Reactions `json:"reactions"` | ||
} | ||
|
||
type Video struct { | ||
ID string `json:"id"` | ||
UploadedAt int64 `json:"uploaded"` | ||
Title string `json:"title"` | ||
Views int64 `json:"views"` | ||
Vertical bool `json:"vertical"` | ||
Category int64 `json:"category"` | ||
} | ||
|
||
type Reactions struct { | ||
Cools int64 `json:"cools"` | ||
Trashes int64 `json:"trashes"` | ||
} | ||
|
||
type SearchCriteria struct { | ||
ViewsFrom string | ||
ViewsTo string | ||
YearsFrom string | ||
YearsTo string | ||
Category string | ||
Horizonly bool | ||
Musiconly bool | ||
} | ||
|
||
func ParseQueryParams(params url.Values) *SearchCriteria { | ||
|
||
sc := &SearchCriteria{} | ||
|
||
viewsValues := strings.Split(params.Get("views"), "-") | ||
if len(viewsValues) == 2 { | ||
sc.ViewsFrom = viewsValues[0] | ||
sc.ViewsTo = viewsValues[1] | ||
} | ||
|
||
yearsValues := strings.Split(params.Get("years"), "-") | ||
if len(viewsValues) == 2 { | ||
sc.YearsFrom = yearsValues[0] | ||
sc.YearsTo = yearsValues[1] | ||
} | ||
|
||
category := params.Get("category") | ||
_, err := strconv.Atoi(category) | ||
if err == nil { | ||
sc.Category = category | ||
} | ||
|
||
horizonly, err := strconv.ParseBool(params.Get("horizonly")) | ||
if err == nil { | ||
sc.Horizonly = horizonly | ||
} | ||
|
||
return sc | ||
} | ||
|
||
func (sc *SearchCriteria) MakeWhere() string { | ||
var conditions []string | ||
|
||
if _, err := strconv.Atoi(sc.ViewsFrom); err == nil { | ||
conditions = append(conditions, "views >= "+sc.ViewsFrom) | ||
} | ||
if _, err := strconv.Atoi(sc.ViewsTo); err == nil { | ||
conditions = append(conditions, "views <= "+sc.ViewsTo) | ||
} | ||
if year, err := strconv.Atoi(sc.YearsFrom); err == nil { | ||
timestamp := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() | ||
conditions = append(conditions, fmt.Sprintf("uploaded >= %d", timestamp)) | ||
} | ||
if year, err := strconv.Atoi(sc.YearsTo); err == nil { | ||
timestamp := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() | ||
conditions = append(conditions, fmt.Sprintf("uploaded <= %d", timestamp)) | ||
} | ||
if sc.Horizonly { | ||
conditions = append(conditions, "vertical = 0") | ||
} | ||
if sc.Category != "" { | ||
conditions = append(conditions, "category = "+sc.Category) | ||
} | ||
if len(conditions) > 0 { | ||
return "AND " + strings.Join(conditions, " AND ") | ||
} | ||
return "" | ||
} | ||
|
||
func (sc *SearchCriteria) CheckVideo(video *Video) bool { | ||
if viewsFrom, err := strconv.ParseInt(sc.ViewsFrom, 10, 64); err == nil && video.Views < viewsFrom { | ||
return false | ||
} | ||
if viewsTo, err := strconv.ParseInt(sc.ViewsTo, 10, 64); err == nil && video.Views > viewsTo { | ||
return false | ||
} | ||
if yearFrom, err := strconv.Atoi(sc.YearsFrom); err == nil { | ||
if video.UploadedAt < time.Date(yearFrom, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() { | ||
return false | ||
} | ||
} | ||
if yearTo, err := strconv.Atoi(sc.YearsTo); err == nil { | ||
if video.UploadedAt > time.Date(yearTo, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() { | ||
return false | ||
} | ||
} | ||
if sc.Horizonly && video.Vertical { | ||
return false | ||
} | ||
if category, err := strconv.ParseInt(sc.Category, 10, 64); err == nil && category != video.Category { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
func (s *Router) TakeFirstUnseen(conn *sqlite.Conn, sc *SearchCriteria, visitor string) (*Video, error) { | ||
|
||
video := &Video{} | ||
|
||
stmt, _, err := conn.PrepareTransient(fmt.Sprintf(` | ||
SELECT id, uploaded, title, views, vertical, category | ||
FROM videos | ||
WHERE id NOT IN ( | ||
SELECT videos_visitors.video_id | ||
FROM videos_visitors | ||
WHERE videos_visitors.visitor_id = %s | ||
) %s | ||
ORDER BY random() | ||
LIMIT 1`, | ||
visitor, | ||
sc.MakeWhere(), | ||
)) | ||
if err != nil { | ||
return nil, fmt.Errorf("error preparing query: %w", err) | ||
} | ||
rows, err := stmt.Step() | ||
if err != nil { | ||
return nil, err | ||
} | ||
if !rows { | ||
return nil, nil | ||
} | ||
|
||
video.ID = stmt.GetText("id") | ||
video.UploadedAt = stmt.GetInt64("uploaded") | ||
video.Title = stmt.GetText("title") | ||
video.Views = stmt.GetInt64("views") | ||
video.Vertical = stmt.GetBool("vertical") | ||
video.Category = stmt.GetInt64("category") | ||
|
||
err = stmt.Reset() | ||
if err != nil { | ||
return video, err | ||
} | ||
|
||
return video, nil | ||
} | ||
|
||
func (s *Router) StoreVideos(conn *sqlite.Conn, videos map[string]*Video) error { | ||
|
||
endFn, err := sqlitex.ImmediateTransaction(conn) | ||
if err != nil { | ||
return fmt.Errorf("error creating a transaction: %w", err) | ||
} | ||
defer endFn(&err) | ||
|
||
stmt := conn.Prep("INSERT INTO videos (id, uploaded, title, views, vertical, category) VALUES (?, ?, ?, ?, ?, ?);") | ||
for _, video := range videos { | ||
|
||
stmt.BindText(1, video.ID) | ||
stmt.BindInt64(2, video.UploadedAt) | ||
stmt.BindText(3, video.Title) | ||
stmt.BindInt64(4, int64(video.Views)) | ||
stmt.BindBool(5, video.Vertical) | ||
stmt.BindInt64(6, int64(video.Category)) | ||
|
||
if _, err := stmt.Step(); err != nil { | ||
return fmt.Errorf("stmt.Step: %w", err) | ||
} | ||
if err := stmt.Reset(); err != nil { | ||
return fmt.Errorf("stmt.Reset: %w", err) | ||
} | ||
if err := stmt.ClearBindings(); err != nil { | ||
return fmt.Errorf("stmt.ClearBindings: %w", err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (s *Router) RememberSeen(conn *sqlite.Conn, visitorId string, videoId string) error { | ||
|
||
endFn, err := sqlitex.ImmediateTransaction(conn) | ||
if err != nil { | ||
return fmt.Errorf("error creating a transaction: %w", err) | ||
} | ||
defer endFn(&err) | ||
|
||
stmt := conn.Prep(` | ||
INSERT INTO visitors (id, last_seen) | ||
VALUES(?, unixepoch()) | ||
ON CONFLICT (id) | ||
DO UPDATE SET last_seen=unixepoch() | ||
`) | ||
stmt.BindText(1, visitorId) | ||
if _, err = stmt.Step(); err != nil { | ||
return err | ||
} | ||
|
||
stmt, err = conn.Prepare(`INSERT INTO videos_visitors (visitor_id, video_id) VALUES (?, ?);`) | ||
if err != nil { | ||
return fmt.Errorf("error preparing query: %w", err) | ||
} | ||
stmt.BindText(1, visitorId) | ||
stmt.BindText(2, videoId) | ||
if _, err = stmt.Step(); err != nil { | ||
return err | ||
} | ||
err = stmt.Reset() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (s *Router) GetRandom(w http.ResponseWriter, r *http.Request) { | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
encoder := json.NewEncoder(w) | ||
|
||
visitor := r.Header.Get("visitor") | ||
params := r.URL.Query() | ||
|
||
conn := s.db.Get(r.Context()) | ||
defer s.db.Put(conn) | ||
|
||
searchCriteria := ParseQueryParams(params) | ||
video, err := s.TakeFirstUnseen(conn, searchCriteria, visitor) | ||
if err != nil { | ||
log.Println("take first unseen failed:", err.Error()) | ||
} | ||
|
||
response := &VideoWithReactions{} | ||
|
||
if video != nil { | ||
reactions, err := GetReaction(conn, video.ID) | ||
if err != nil { | ||
log.Println("couldn't save reaction:", err.Error()) | ||
} | ||
|
||
response.Video = video | ||
response.Reactions = reactions | ||
|
||
encoder.Encode(response) | ||
err = s.RememberSeen(conn, visitor, video.ID) | ||
if err != nil { | ||
log.Println("error remembering seen:", err.Error()) | ||
} | ||
return | ||
} | ||
log.Println("video in db not found, ask youtube api") | ||
|
||
// go ask youtube api for random video | ||
var found bool | ||
for !found { | ||
results := make(map[string]*Video) | ||
|
||
searchResult, err := s.ytr.Search("inurl:" + youtube.RandomYoutubeVideoId()) | ||
if err != nil { | ||
w.WriteHeader(http.StatusBadGateway) | ||
encoder.Encode(Message{"couldn't find video"}) | ||
return | ||
} | ||
if searchResult == nil { | ||
continue | ||
} | ||
nItems := len(searchResult.Items) | ||
if nItems == 0 { | ||
continue | ||
} | ||
log.Printf("%d videos found", nItems) | ||
|
||
for _, item := range searchResult.Items { | ||
video := Video{} | ||
video.ID = item.Id.VideoId | ||
video.Title = item.Snippet.Title | ||
parsed, err := time.Parse(time.RFC3339, item.Snippet.PublishedAt) | ||
if err == nil { | ||
video.UploadedAt = parsed.Unix() | ||
} | ||
results[item.Id.VideoId] = &video | ||
} | ||
|
||
ids := make([]string, len(results)) | ||
for _, video := range results { | ||
ids = append(ids, video.ID) | ||
} | ||
|
||
videoInfoResult, err := s.ytr.VideosInfo(ids) | ||
if err != nil { | ||
w.WriteHeader(http.StatusBadGateway) | ||
encoder.Encode(Message{"couldn't find video"}) | ||
return | ||
} | ||
for _, item := range videoInfoResult.Items { | ||
video := results[item.Id] | ||
video.Category, _ = strconv.ParseInt(item.Snippet.CategoryId, 10, 64) | ||
video.Views, _ = strconv.ParseInt(item.Statistics.ViewCount, 10, 64) | ||
} | ||
|
||
for _, video := range results { | ||
short, err := s.ytr.IsShort(video.ID, video.UploadedAt) | ||
if err != nil { | ||
log.Println("error defining short:", err.Error()) | ||
} | ||
video.Vertical = short | ||
} | ||
|
||
// clear out fucking gaming videos trash i hate it | ||
for videoId, video := range results { | ||
if video.Category == 20 { | ||
delete(results, videoId) | ||
} | ||
} | ||
|
||
for _, video := range results { | ||
if searchCriteria.CheckVideo(video) { | ||
response.Video = video | ||
encoder.Encode(response) | ||
found = true | ||
err = s.RememberSeen(conn, visitor, video.ID) | ||
if err != nil { | ||
log.Println("error remembering seen:", err.Error()) | ||
} | ||
break | ||
} | ||
} | ||
|
||
err = s.StoreVideos(conn, results) | ||
if err != nil { | ||
log.Println("couldn't store found videos:", err.Error()) | ||
} else { | ||
log.Println(len(results), "found videos stored") | ||
} | ||
} | ||
} | ||
|
||
func (v *Video) GetFormattedUploadDate(timestamp int64) string { | ||
t := time.Unix(v.UploadedAt, 0) | ||
return t.Format("02.01.06") | ||
} |
Oops, something went wrong.