diff options
| -rw-r--r-- | go.mod | 3 | ||||
| -rw-r--r-- | grimtube.go | 80 | ||||
| -rw-r--r-- | static/style.css | 36 | ||||
| -rw-r--r-- | templates/base.html | 11 | ||||
| -rw-r--r-- | templates/index.html | 6 | ||||
| -rw-r--r-- | templates/search.html | 23 | ||||
| -rw-r--r-- | ytparser/ytparser.go | 163 |
7 files changed, 322 insertions, 0 deletions
@@ -0,0 +1,3 @@ +module grimtube + +go 1.15 diff --git a/grimtube.go b/grimtube.go new file mode 100644 index 0000000..6751543 --- /dev/null +++ b/grimtube.go @@ -0,0 +1,80 @@ +package main + +import ( + "text/template" + "net/http" + //"io" + "log" + "strconv" + "path" + + "grimtube/ytparser" +) + +func serve(w http.ResponseWriter, templatePath string, data interface{}) { + funcMap := template.FuncMap { + "inc": func(i int) int { + return i + 1 + }, + "dec": func(i int) int { + return i - 1 + }, + } + base := path.Base(templatePath) + log.Println(base) + t, err := template.New("base.html").Funcs(funcMap).ParseFiles("templates/base.html", templatePath) + if err != nil { + panic(err) + } else { + if err := t.Execute(w, data); err != nil { + log.Fatal(err) + } + } + //log.Println(err) +} + +func index(w http.ResponseWriter, r *http.Request) { + serve(w, "templates/index.html", nil) +} + +func search(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + query := r.URL.Query() + term := query.Get("term") + sPage := query.Get("page") + var page int + if sPage == "" { + page = 0 + } else { + p, err := strconv.Atoi(sPage) + if err != nil { + page = 0 + } else { + page = p + } + } + items := ytparser.Search(term, page) + data := struct { + Items []ytparser.Item + Term string + Page int + }{ + items, + term, + page, + } + serve(w, "templates/search.html", data) + default: + } +} + +func main() { + fs := http.FileServer(http.Dir("static")) + http.Handle("/static/", http.StripPrefix("/static/", fs)) + http.HandleFunc("/", index) + http.HandleFunc("/search", search) + + + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..0f92c0b --- /dev/null +++ b/static/style.css @@ -0,0 +1,36 @@ +a { + color: black; +} + +li { + list-style-type: none; +} + +body { + margin: 0 auto; + margin-top: 10px; + max-width: 80ex; +} + +table { + border-collapse: collapse; + width: 100%; +} + +tr { + border-bottom: 1px solid black; + border-top: 1px solid black; +} + +td { + padding-top: 10px; + padding-bottom: 10px; +} + +form { + margin-bottom: 10px; +} + +input { + width: 100%; +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..7336dda --- /dev/null +++ b/templates/base.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> + <head> + <link rel="stylesheet" href="/static/style.css"> + <title>{{block "title" .}}{{end}} - grimtube</title> + </head> + <body> + {{block "content" .}} + {{end}} + </body> +</html> diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..7628712 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,6 @@ +{{define "title"}}Index{{end}} +{{define "content"}} + <form action="/search" method="get"> + <input id="term" name="term" type="text" placeholder="search" required> + </form> +{{end}} diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..b921eb6 --- /dev/null +++ b/templates/search.html @@ -0,0 +1,23 @@ +{{define "title"}}Search{{end}} +{{define "content"}} + <form action="/search" method="get"> + <input id="term" name="term" type="text" placeholder="search" required> + </form> + <table> + {{range .Items}} + <tr> + <td> + <a href="{{.Url}}"><img width=120 src="{{.Thumb}}"></a> + </td> + <td> + <a href="{{.Url}}"><span>{{.Title}}</span></a> + </td> + <td> + <a href="{{.ChannelUrl}}"><span>{{.ChannelTitle}}</span></a> + </td> + </tr> + {{end}} + </table> + <a href="/search?term={{.Term}}&page={{dec .Page}}">Prev Page</a> | + <a href="/search?term={{.Term}}&page={{inc .Page}}">Next Page</a> +{{end}} diff --git a/ytparser/ytparser.go b/ytparser/ytparser.go new file mode 100644 index 0000000..ec89406 --- /dev/null +++ b/ytparser/ytparser.go @@ -0,0 +1,163 @@ +package ytparser + +import ( + "text/template" + "encoding/json" + "fmt" + "net/http" + "bytes" + "io" + "io/ioutil" + "strings" + "net/url" +) + +const initDataString string = "window[\"ytInitialData\"] = " +const baseUrl string = "https://youtube.com" + +type Item struct { + Id string + Title string + Url string + Thumb string + ChannelTitle string + ChannelUrl string +} + +func (item Item) Format(t *template.Template) string { + var b strings.Builder + err := t.Execute(&b, item) + if err != nil { + panic(err) + } + return b.String() +} + +func (item Item) String() string { + return fmt.Sprintf("id: %s, title: %s, url: %s, thumb: %s", item.Id, item.Title, item.Url, item.Thumb) +} + +func parsejson(data string) []Item { + //fmt.Println(data) + dec := json.NewDecoder(strings.NewReader(data)) + + + depth := 0 + isArray := false + isValue := false + var items []Item + var item Item + var names []string + nbItems := 0 + for { + tok, err := dec.Token() + if err == io.EOF { + break + } else if err != nil { + panic(err) + } + + switch t := tok.(type) { + case json.Delim: + if t == '{' { + depth++ + } else if t == '}' { + depth-- + names = names[:depth] + } + isArray = t == '[' + isValue = false + case string: + if !isArray { + if !isValue { + if t == "videoRenderer" { + if nbItems > 0 { + items = append(items, item) + } + item = Item{} + nbItems++ + } + if depth > len(names) { + names = append(names, t) + } else { + names[depth - 1] = t + } + //fmt.Println(t, depth, len(names), names[depth - 1]) + isValue = true + } else { + //fmt.Println(names[len(names) - 1]) + if names[depth-1] == "videoId" { + item.Id = t + item.Url = fmt.Sprintf("https://youtube.com/watch?v=%s", t) + } + if depth >= 3 && names[depth-3] == "title" && + names[depth-2] == "runs" && + names[depth-1] == "text" { + item.Title = t + } + if depth >= 3 && names[depth-3] == "ownerText" && + names[depth-2] == "runs" && + names[depth-1] == "text" { + item.ChannelTitle = t + } + if depth >= 6 && + names[depth-6] == "ownerText" && + names[depth-5] == "runs" && + names[depth-4] == "navigationEndpoint" && + names[depth-3] == "commandMetadata" && + names[depth-2] == "webCommandMetadata" && + names[depth-1] == "url" { + item.ChannelUrl = baseUrl + t + } + if depth >= 4 && + names[depth-4] == "videoRenderer" && + names[depth-3] == "thumbnail" && + names[depth-2] == "thumbnails" && + names[depth-1] == "url" { + item.Thumb = t + } + isValue = false + } + } + default: + } + } + return items + //fmt.Println(names) +} + +func PrintItems(items []Item, format string) { + t := template.Must(template.New("items").Parse(format)) + for _, i := range items { + fmt.Println(i.Format(t)) + //fmt.Println(i) + } +} + +func request(query string, page int) (string, error) { + q := url.QueryEscape(query) + url := fmt.Sprintf("https://www.youtube.com/results?search_query=%s&page=%d", q, page) + res, err := http.DefaultClient.Get(url) + if err != nil { + return "", err + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", err + } + idx := bytes.Index(body, []byte(initDataString)) + idx += len(initDataString) + startData := body[idx:] + idx = bytes.Index(startData, []byte(";\n")) + startData = startData[:idx] + //fmt.Println(string(startData)) + return string(startData), nil +} + +func Search(query string, page int) []Item { + data, err := request(query, page) + if err != nil { + panic(err) + } + return parsejson(data) +} |
