package main import ( "embed" "encoding/json" "fmt" "html/template" "log" "net/http" "os" "time" "github.com/PuerkitoBio/goquery" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" ) type Item struct { ID int64 `json:"id" db:"id"` URL string `json:"url" db:"url"` Title string `json:"title" db:"title"` Description string `json:"description" db:"description"` ImageURL string `json:"imageUrl" db:"image_url"` CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` } type ArticleMetadata struct { Title string Description string ImageURL string } func getArticleMetadata(url string) (*ArticleMetadata, error) { resp, err := http.Get(url) if err != nil { return nil, fmt.Errorf("failed to query article url: %v", err) } defer resp.Body.Close() doc, err := goquery.NewDocumentFromReader(resp.Body) if err != nil { return nil, fmt.Errorf("failed to parse page html: %v", err) } title := doc.Find(`head>meta[name="og:title"]`).AttrOr("content", "") if title == "" { title = doc.Find(`head>title`).Text() } description := doc.Find(`head>meta[name="og:description"]`).AttrOr("content", "") imageUrl := doc.Find(`head>meta[name="og:image"]`).AttrOr("content", "") if imageUrl == "" { doc.Find("img").Each(func(i int, s *goquery.Selection) { src := s.AttrOr("src", "") if src != "" { imageUrl = src return } }) } return &ArticleMetadata{ Title: title, Description: description, ImageURL: imageUrl, }, nil } var ( //go:embed views/* viewsFS embed.FS ) func main() { dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "./sqlite.db" } db, err := sqlx.Open("sqlite3", dbPath) if err != nil { log.Fatalf("failed to connect to db: %v\n", err) } defer db.Close() db.MustExec(` CREATE TABLE IF NOT EXISTS items ( id integer primary key not null, url varchar not null unique, title varchar not null, description varchar, image_url varchar not null, created_at datetime default current_timestamp, updated_at datetime default current_timestamp ); CREATE TRIGGER IF NOT EXISTS update_items_updated_at AFTER UPDATE ON items WHEN old.updated_at <> current_timestamp BEGIN UPDATE items SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id; END; `) tmpl := template.Must(template.ParseFS(viewsFS, "**/*.html")) mux := http.NewServeMux() mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { tmpl.ExecuteTemplate(w, "index.html", nil) }) mux.HandleFunc("POST /items", func(w http.ResponseWriter, r *http.Request) { pageUrl := r.FormValue("url") if pageUrl == "" { http.Error(w, "url field is required", 400) return } meta, err := getArticleMetadata(pageUrl) if err != nil { http.Error(w, fmt.Sprintf("failed to fetch page metadata: %v", err), 500) return } if _, err := db.NamedExec("INSERT INTO items (url, title, description, image_url) VALUES (:url, :title, :description, :image_url)", map[string]any{ "url": pageUrl, "title": meta.Title, "description": meta.Description, "image_url": meta.ImageURL, }); err != nil { http.Error(w, fmt.Sprintf("failed to add item to db: %v", err), 500) return } http.Redirect(w, r, "/", http.StatusFound) }) mux.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) { rows, err := db.Queryx("SELECT * FROM items") if err != nil { http.Error(w, fmt.Sprintf("failed to query db for items: %v", err), 500) return } items := []Item{} for rows.Next() { item := Item{} if err := rows.StructScan(&item); err != nil { continue } items = append(items, item) } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(items); err != nil { http.Error(w, err.Error(), 500) } }) log.Println("starting http server") if err := http.ListenAndServe(":5000", mux); err != nil { log.Fatalf("failed to start http server: %v\n", err) } }