working without api key
This commit is contained in:
parent
b97b89efb8
commit
999376e5f7
5 changed files with 371 additions and 166 deletions
107
tui/image.go
Normal file
107
tui/image.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package tui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/mattn/go-sixel"
|
||||
)
|
||||
|
||||
func FetchAndProcessThumbnail(url string) (string, error) {
|
||||
if url == "" {
|
||||
return "", fmt.Errorf("empty URL")
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch image: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to fetch image: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read image data: %w", err)
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
// Consistent thumbnail size
|
||||
width := 40
|
||||
height := 15
|
||||
|
||||
img = imaging.Resize(img, width*2, height*2, imaging.Lanczos)
|
||||
|
||||
// Try iTerm2 protocol first
|
||||
if isITerm() {
|
||||
return getITermImageSequence(img), nil
|
||||
}
|
||||
|
||||
// Try Sixel next
|
||||
var buf bytes.Buffer
|
||||
enc := sixel.NewEncoder(&buf)
|
||||
err = enc.Encode(img)
|
||||
if err == nil {
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Fall back to ASCII art
|
||||
return imageToASCII(img), nil
|
||||
}
|
||||
|
||||
func imageToASCII(img image.Image) string {
|
||||
bounds := img.Bounds()
|
||||
width := 40
|
||||
height := 15
|
||||
chars := []string{"█", "▓", "▒", "░", " "}
|
||||
|
||||
var ascii strings.Builder
|
||||
stepX := float64(bounds.Dx()) / float64(width)
|
||||
stepY := float64(bounds.Dy()) / float64(height)
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
px := int(float64(x) * stepX)
|
||||
py := int(float64(y) * stepY)
|
||||
c := color.GrayModel.Convert(img.At(px+bounds.Min.X, py+bounds.Min.Y)).(color.Gray)
|
||||
index := int(c.Y) * (len(chars) - 1) / 255
|
||||
ascii.WriteString(chars[index])
|
||||
}
|
||||
ascii.WriteString("\n")
|
||||
}
|
||||
|
||||
return ascii.String()
|
||||
}
|
||||
|
||||
func getITermImageSequence(img image.Image) string {
|
||||
var buf bytes.Buffer
|
||||
imaging.Encode(&buf, img, imaging.PNG)
|
||||
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
|
||||
return fmt.Sprintf("\033]1337;File=inline=1;width=40ch;height=15;preserveAspectRatio=1:%s\a", encoded)
|
||||
}
|
||||
|
||||
func isITerm() bool {
|
||||
return os.Getenv("TERM_PROGRAM") == "iTerm.app"
|
||||
}
|
91
tui/tui.go
91
tui/tui.go
|
@ -2,7 +2,7 @@ package tui
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
// "log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -19,7 +19,7 @@ type videoItem struct {
|
|||
|
||||
// Implement list.Item for videoItem
|
||||
func (v videoItem) Title() string { return v.Video.Title }
|
||||
func (v videoItem) Description() string { return fmt.Sprintf("%s | %s | %s | %s", v.Video.Channel, v.Video.Duration, v.Video.UploadDate, v.Video.Views) }
|
||||
func (v videoItem) Description() string { return fmt.Sprintf("%s | %s | %s", v.Video.Channel, v.Video.Duration, v.Video.UploadDate) }
|
||||
func (v videoItem) FilterValue() string { return v.Video.Title }
|
||||
|
||||
type model struct {
|
||||
|
@ -31,6 +31,13 @@ type model struct {
|
|||
width int
|
||||
height int
|
||||
clock time.Time
|
||||
isLoading bool
|
||||
spinner spinner
|
||||
}
|
||||
|
||||
type spinner struct {
|
||||
frames []string
|
||||
current int
|
||||
}
|
||||
|
||||
// Initial model setup
|
||||
|
@ -50,6 +57,10 @@ func initialModel() model {
|
|||
clock: time.Now(),
|
||||
width: 80, // Default width
|
||||
height: 24, // Default height
|
||||
spinner: spinner{
|
||||
frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
|
||||
current: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,33 +96,28 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if m.showSearch {
|
||||
query := m.searchBar.Value()
|
||||
if query == "" {
|
||||
return m, nil // Ignore empty queries
|
||||
}
|
||||
|
||||
// Fetch video results
|
||||
videos, err := scraper.FetchVideos(query)
|
||||
if err != nil {
|
||||
m.err = err
|
||||
log.Printf("Error fetching videos: %v", err)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Populate the results list
|
||||
items := make([]list.Item, len(videos))
|
||||
for i, v := range videos {
|
||||
items[i] = videoItem{Video: v}
|
||||
}
|
||||
m.videos = videos
|
||||
m.results.SetItems(items)
|
||||
m.showSearch = false
|
||||
m.searchBar.Blur()
|
||||
} else {
|
||||
// Handle video selection
|
||||
if i := m.results.Index(); i != -1 {
|
||||
selectedVideo := m.videos[i]
|
||||
cmd := player.PlayVideo(selectedVideo.URL)
|
||||
return m, tea.ExecProcess(cmd, nil)
|
||||
}
|
||||
m.isLoading = true
|
||||
// Return both the loading animation and the search command
|
||||
return m, tea.Batch(
|
||||
tickCmd(),
|
||||
func() tea.Msg {
|
||||
videos, err := scraper.FetchVideos(query)
|
||||
if err != nil {
|
||||
return searchErrorMsg{err: err}
|
||||
}
|
||||
return searchResultMsg{videos: videos}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Handle video selection
|
||||
if i := m.results.Index(); i != -1 {
|
||||
selectedVideo := m.videos[i]
|
||||
cmd := player.PlayVideo(selectedVideo.URL)
|
||||
return m, tea.ExecProcess(cmd, nil)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
@ -121,7 +127,27 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.height = msg.Height
|
||||
m.results.SetSize(msg.Width, msg.Height-3) // Leave space for search bar
|
||||
|
||||
case searchResultMsg:
|
||||
m.isLoading = false
|
||||
items := make([]list.Item, len(msg.videos))
|
||||
for i, v := range msg.videos {
|
||||
items[i] = videoItem{Video: v}
|
||||
}
|
||||
m.videos = msg.videos
|
||||
m.results.SetItems(items)
|
||||
m.showSearch = false
|
||||
m.searchBar.Blur()
|
||||
return m, nil
|
||||
|
||||
case searchErrorMsg:
|
||||
m.isLoading = false
|
||||
m.err = msg.err
|
||||
return m, nil
|
||||
|
||||
case time.Time:
|
||||
if m.isLoading {
|
||||
m.spinner.current = (m.spinner.current + 1) % len(m.spinner.frames)
|
||||
}
|
||||
m.clock = msg
|
||||
return m, tickCmd()
|
||||
}
|
||||
|
@ -148,11 +174,14 @@ func (m model) View() string {
|
|||
s.WriteString(clock + "\n\n")
|
||||
}
|
||||
|
||||
// Always show search bar at the top
|
||||
// Show search bar with loading animation if searching
|
||||
searchText := "Press '/' to search"
|
||||
if m.showSearch {
|
||||
searchText = m.searchBar.View()
|
||||
}
|
||||
if m.isLoading {
|
||||
searchText += " " + m.spinner.frames[m.spinner.current] + " Searching..."
|
||||
}
|
||||
s.WriteString(searchText + "\n\n")
|
||||
|
||||
// Show results
|
||||
|
@ -177,3 +206,11 @@ func tickCmd() tea.Cmd {
|
|||
return t
|
||||
})
|
||||
}
|
||||
|
||||
type searchResultMsg struct {
|
||||
videos []scraper.Video
|
||||
}
|
||||
|
||||
type searchErrorMsg struct {
|
||||
err error
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue