diff --git a/main.go b/main.go index 0fdcf8b..46e2d4d 100644 --- a/main.go +++ b/main.go @@ -111,6 +111,8 @@ func handleSearch(w http.ResponseWriter, r *http.Request) { handleTextSearch(w, query, safe, lang) case "image": handleImageSearch(w, query, safe, lang, page) + case "video": + videoSearchEndpointHandler(w, r) default: http.ServeFile(w, r, "static/search.html") } diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..469dfac --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go run main.go text.go images.go imageproxy.go video.go \ No newline at end of file diff --git a/templates/videos.html b/templates/videos.html new file mode 100644 index 0000000..47a4c94 --- /dev/null +++ b/templates/videos.html @@ -0,0 +1,29 @@ + + + + + + Video Results + + + +
+

Video Results

+ {{ if .Results }} + {{ range .Results }} +
+

{{ .Title }}

+ + {{ .Title }} + +

{{ .Views }} views | {{ .Duration }}

+

By {{ .Creator }}

+
+ + {{ end }} + {{ else }} +

No results found. Try adjusting your search term.

+ {{ end }} +
+ + diff --git a/video.go b/video.go new file mode 100644 index 0000000..9719ddd --- /dev/null +++ b/video.go @@ -0,0 +1,143 @@ +package main + +import ( + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "net/url" + "strconv" + "time" +) + +const PIPED_INSTANCE = "pipedapi.kavin.rocks" + +// VideoResult reflects the structured data for a video result +type VideoResult struct { + Href string + Title string + Date string + Views string + Creator string + Publisher string + Image string + Duration string +} + +// VideoAPIResponse matches the structure of the JSON response from the Piped API +type VideoAPIResponse struct { + Items []struct { + URL string `json:"url"` + Title string `json:"title"` + UploaderName string `json:"uploaderName"` + Views int `json:"views"` + Thumbnail string `json:"thumbnail"` + Duration int `json:"duration"` + UploadedDate string `json:"uploadedDate"` + Type string `json:"type"` + } `json:"items"` +} + +// Function to format views similarly to the Python code +func formatViews(views int) string { + switch { + case views >= 1_000_000_000: + return fmt.Sprintf("%.1fB views", float64(views)/1_000_000_000) + case views >= 1_000_000: + return fmt.Sprintf("%dM views", views/1_000_000) + case views >= 10_000: + return fmt.Sprintf("%.1fK views", float64(views)/1_000) + default: + return fmt.Sprintf("%d views", views) + } +} + +// formatDuration formats video duration as done in the Python code +func formatDuration(seconds int) string { + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + seconds = seconds % 60 + if hours > 0 { + return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) + } + return fmt.Sprintf("%02d:%02d", minutes, seconds) +} + +// makeHTMLRequest fetches search results from the Piped API, similarly to the Python `makeHTMLRequest` +func makeHTMLRequest(query string) (*VideoAPIResponse, error) { + resp, err := http.Get(fmt.Sprintf("https://%s/search?q=%s&filter=all", PIPED_INSTANCE, url.QueryEscape(query))) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var apiResp VideoAPIResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + return nil, fmt.Errorf("error decoding response: %w", err) + } + + return &apiResp, nil +} + +func videoSearchEndpointHandler(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + safe := r.URL.Query().Get("safe") // Assuming "safe" is a query parameter + lang := r.URL.Query().Get("lang") // Assuming "lang" is a query parameter + pageStr := r.URL.Query().Get("page") + page, err := strconv.Atoi(pageStr) + if err != nil { + // Handle error, perhaps set page to a default value if it's not critical + page = 1 // Default page + } + + handleVideoSearch(w, query, safe, lang, page) +} + +// handleVideoSearch adapted from the Python `videoResults`, handles video search requests +func handleVideoSearch(w http.ResponseWriter, query, safe, lang string, page int) { + start := time.Now() + + // Modify `makeHTMLRequest` to also accept `safe`, `lang`, and `page` parameters if necessary + apiResp, err := makeHTMLRequest(query) + if err != nil { + log.Printf("Error fetching video results: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + var results []VideoResult + for _, item := range apiResp.Items { + if item.Type == "channel" || item.Type == "playlist" { + continue + } + results = append(results, VideoResult{ + Href: fmt.Sprintf("https://%s%s", PIPED_INSTANCE, item.URL), + Title: item.Title, + Date: item.UploadedDate, + Views: formatViews(item.Views), + Creator: item.UploaderName, + Publisher: "Piped", + Image: fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)), + Duration: formatDuration(item.Duration), + }) + } + + elapsed := time.Since(start) + tmpl, err := template.ParseFiles("templates/videos.html") + if err != nil { + log.Printf("Error parsing template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + tmpl.Execute(w, map[string]interface{}{ + "Results": results, + "Query": query, + "Fetched": fmt.Sprintf("%.2f seconds", elapsed.Seconds()), + }) +}