From c72dfab37fd6f0d739ea70d42779b2a8c85e9915 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Wed, 26 Nov 2025 00:58:34 -0500 Subject: Add Youtube RSS parser --- cmd/web/handlers/blog.go | 1 + cmd/web/handlers/routes.go | 8 ++ cmd/web/handlers/rss.go | 218 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 cmd/web/handlers/rss.go (limited to 'cmd/web') diff --git a/cmd/web/handlers/blog.go b/cmd/web/handlers/blog.go index 14a0abe..e9512c0 100644 --- a/cmd/web/handlers/blog.go +++ b/cmd/web/handlers/blog.go @@ -37,6 +37,7 @@ func (ctx *blogContext) viewPost(w http.ResponseWriter, r *http.Request) { ctx.err.Printf("Could not prepare statement for DB: %v\n", err) return } + defer stmt.Close() row := stmt.QueryRow(postId) if err != nil { diff --git a/cmd/web/handlers/routes.go b/cmd/web/handlers/routes.go index 6a938fb..be48ae3 100644 --- a/cmd/web/handlers/routes.go +++ b/cmd/web/handlers/routes.go @@ -23,6 +23,10 @@ func RegisterEndpoints(app types.Application, db *sql.DB) *http.ServeMux { err: app.Err, db: db, } + feeds := rssContext{ + err: app.Err, + db: db, + } audio := fsContext{ err: app.Err, path: app.AudioDir, @@ -41,6 +45,10 @@ func RegisterEndpoints(app types.Application, db *sql.DB) *http.ServeMux { blogRouter.HandleFunc("/login", login.handle) blogRouter.HandleFunc("/logout", auth.Resolve(login.logout)) + blogRouter.HandleFunc("/feeds", auth.CheckAndInvalidate(feeds.index)) + blogRouter.HandleFunc("/feeds/new", auth.Resolve(feeds.new)) + blogRouter.HandleFunc("/feed", feeds.feed) + blogRouter.HandleFunc("/audio", audio.readdir) blogRouter.HandleFunc("/audio/get", audio.get) diff --git a/cmd/web/handlers/rss.go b/cmd/web/handlers/rss.go new file mode 100644 index 0000000..3e3019d --- /dev/null +++ b/cmd/web/handlers/rss.go @@ -0,0 +1,218 @@ +package handlers + +import ( + "database/sql" + "html/template" + "log" + "net/http" + "strings" + "time" + + "github.com/mmcdole/gofeed" + "paterissa.net/mblog/internal/models" +) + +type video struct { + Title string + Description string + Published string + Link string +} + +type rssContext struct { + err *log.Logger + db *sql.DB + + Feed models.Feed + Videos []video + Rows []models.Feed + IsAuth bool +} + +func (ctx *rssContext) new(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Method Not Allowed")) + return + } + + cookie, err := r.Cookie("paterissa_session_token") + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized")) + return + } + + err = r.ParseForm() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Server Error")) + return + } + + name := r.Form.Get("name") + url := r.Form.Get("url") + + stmt, err := ctx.db.Prepare("SELECT * FROM cookies WHERE content = $1;") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load cookies from DB: %v\n", err) + return + } + defer stmt.Close() + + var id uint64 + var content string + var userId uint64 + var expiration time.Time + + row := stmt.QueryRow(cookie.Value) + err = row.Scan(&id, &content, &userId, &expiration) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + return + } + + insertStmt, err := ctx.db.Prepare("INSERT INTO feeds (name, user_id, url) VALUES ($1, $2, $3);") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not prepare insert statement to DB: %v\n", err) + return + } + defer insertStmt.Close() + + _, err = insertStmt.Exec(name, userId, url) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not execute feed into DB: %v\n", err) + return + } + + http.Redirect(w, r, "/feeds", http.StatusFound) + return +} + +func (ctx *rssContext) feed(w http.ResponseWriter, r *http.Request) { + ctx.Videos = []video{} + ctx.Feed = models.Feed{} + + id := r.URL.Query().Get("id") + + stmt, err := ctx.db.Prepare("SELECT * FROM feeds WHERE id = $1;") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load feeds from DB: %v\n", err) + return + } + defer stmt.Close() + + var f models.Feed + + row := stmt.QueryRow(id) + err = row.Scan(&f.Id, &f.Name, &f.User, &f.Url) + + ctx.Feed = f + + parser := gofeed.NewParser() + feed, err := parser.ParseURL(f.Url) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not parse RSS feed: %v\n", err) + return + } + + for _, raw := range feed.Items { + var v video + + v.Title = raw.Title + v.Description = raw.Extensions["media"]["group"][0].Children["description"][0].Value + v.Published = raw.Published + v.Link = strings.Replace(raw.Link, "watch?v=", "embed/", 1) + + ctx.Videos = append(ctx.Videos, v) + } + + files := []string{ + "ui/html/base.tmpl.html", + "ui/html/music_player.tmpl.html", + "ui/html/pages/feed.tmpl.html", + } + + compiled, err := template.ParseFiles(files...) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not parse template: %v\n", err) + return + } + + err = compiled.ExecuteTemplate(w, "base", ctx) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not execute template: %v\n", err) + return + } + + return +} + +func (ctx *rssContext) index(w http.ResponseWriter, r *http.Request) { + ctx.Rows = []models.Feed{} + ctx.IsAuth = false + + _, err := r.Cookie("paterissa_session_token") + if err == nil { + ctx.IsAuth = true + } + + rows, err := ctx.db.Query("SELECT f.id, f.name, u.name, f.url FROM feeds f INNER JOIN logins u ON f.user_id = u.id ORDER BY f.name DESC;") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load posts from DB: %v\n", err) + return + } + defer rows.Close() + + for rows.Next() { + var f models.Feed + + if err = rows.Scan(&f.Id, &f.Name, &f.User, &f.Url); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load posts from DB: %v\n", err) + return + } + + ctx.Rows = append(ctx.Rows, f) + } + + files := []string{ + "ui/html/base.tmpl.html", + "ui/html/music_player.tmpl.html", + "ui/html/pages/feed_index.tmpl.html", + } + + compiled, err := template.ParseFiles(files...) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not parse template: %v\n", err) + return + } + + err = compiled.ExecuteTemplate(w, "base", ctx) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not execute template: %v\n", err) + return + } +} -- cgit v1.2.3