summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Hurst <julian.hurst92@gmail.com>2020-10-14 18:19:00 +0200
committerJulian Hurst <julian.hurst92@gmail.com>2020-10-14 18:19:00 +0200
commit8891eea532102b58a23a71a491be2c3f151720b5 (patch)
treede9e577df2b2ff9c0072fda07b90d1884088b7c9
downloadgrimtube-8891eea532102b58a23a71a491be2c3f151720b5.tar.gz
Initial commit
-rw-r--r--go.mod3
-rw-r--r--grimtube.go80
-rw-r--r--static/style.css36
-rw-r--r--templates/base.html11
-rw-r--r--templates/index.html6
-rw-r--r--templates/search.html23
-rw-r--r--ytparser/ytparser.go163
7 files changed, 322 insertions, 0 deletions
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..dab7b92
--- /dev/null
+++ b/go.mod
@@ -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)
+}