addet sorting

This commit is contained in:
pika 2025-01-02 15:19:55 +01:00
parent 20ab5804c6
commit 27a221318e
4 changed files with 249 additions and 28 deletions

5
go.mod
View file

@ -3,10 +3,9 @@ module ytgo
go 1.23.4 go 1.23.4
require ( require (
github.com/BurntSushi/toml v0.3.1
github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.2.4 github.com/charmbracelet/bubbletea v1.2.4
github.com/disintegration/imaging v1.6.2
github.com/mattn/go-sixel v0.0.5
google.golang.org/api v0.210.0 google.golang.org/api v0.210.0
) )
@ -37,14 +36,12 @@ require (
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/soniakeys/quant v1.0.0 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.29.0 // indirect golang.org/x/crypto v0.29.0 // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/net v0.31.0 // indirect golang.org/x/net v0.31.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.9.0 // indirect golang.org/x/sync v0.9.0 // indirect

1
go.sum
View file

@ -5,6 +5,7 @@ cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyT
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=

View file

@ -23,6 +23,7 @@ type Video struct {
Views string Views string
Thumbnail string Thumbnail string
UploadDate string UploadDate string
ParsedDate time.Time
} }
func formatViews(count uint64) string { func formatViews(count uint64) string {
@ -145,6 +146,13 @@ func FetchVideos(query string) ([]Video, error) {
Thumbnail: item.Snippet.Thumbnails.Default.Url, Thumbnail: item.Snippet.Thumbnails.Default.Url,
UploadDate: item.Snippet.PublishedAt, UploadDate: item.Snippet.PublishedAt,
} }
// Parse the upload date and store it
parsedDate, err := time.Parse(time.RFC3339, video.UploadDate)
if err == nil {
video.ParsedDate = parsedDate
}
videos = append(videos, video) videos = append(videos, video)
} }

View file

@ -3,14 +3,17 @@ package tui
import ( import (
"fmt" "fmt"
"log" "log"
"sort"
"strconv"
"strings" "strings"
"time" "time"
"ytgo/player"
"ytgo/scraper"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"ytgo/scraper"
"ytgo/player"
) )
type videoItem struct { type videoItem struct {
@ -19,9 +22,19 @@ type videoItem struct {
// Implement list.Item for videoItem // Implement list.Item for videoItem
func (v videoItem) Title() string { return v.Video.Title } 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 | %s", v.Video.Channel, v.Video.Duration, v.Video.UploadDate, v.Video.Views)
}
func (v videoItem) FilterValue() string { return v.Video.Title } func (v videoItem) FilterValue() string { return v.Video.Title }
// Add these constants for sort modes
const (
SortByDate = iota
SortByViews
SortByTitle
SortByDuration
)
type model struct { type model struct {
searchBar textinput.Model searchBar textinput.Model
results list.Model results list.Model
@ -31,6 +44,8 @@ type model struct {
width int width int
height int height int
clock time.Time clock time.Time
currentSort int
sortAscending bool
} }
// Initial model setup // Initial model setup
@ -50,6 +65,7 @@ func initialModel() model {
clock: time.Now(), clock: time.Now(),
width: 80, // Default width width: 80, // Default width
height: 24, // Default height height: 24, // Default height
sortAscending: false,
} }
} }
@ -65,8 +81,9 @@ func (m model) Init() tea.Cmd {
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
// Handle global keybindings first
switch msg.String() { switch msg.String() {
case "q", "ctrl+c": case "ctrl+c":
return m, tea.Quit return m, tea.Quit
case "/": case "/":
@ -80,9 +97,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.searchBar.Blur() m.searchBar.Blur()
} }
return m, nil return m, nil
}
case "enter": // Handle context-specific keybindings
if m.showSearch { if m.showSearch {
// When in search mode, only handle enter and let other keys go to the search input
switch msg.String() {
case "enter":
query := m.searchBar.Value() query := m.searchBar.Value()
if query == "" { if query == "" {
return m, nil // Ignore empty queries return m, nil // Ignore empty queries
@ -105,15 +126,39 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.results.SetItems(items) m.results.SetItems(items)
m.showSearch = false m.showSearch = false
m.searchBar.Blur() m.searchBar.Blur()
return m, nil
}
} else { } else {
// Handle video selection // When not in search mode, handle navigation and sort keybindings
switch msg.String() {
case "q":
return m, tea.Quit
case "enter":
if i := m.results.Index(); i != -1 { if i := m.results.Index(); i != -1 {
selectedVideo := m.videos[i] selectedVideo := m.videos[i]
cmd := player.PlayVideo(selectedVideo.URL) cmd := player.PlayVideo(selectedVideo.URL)
return m, tea.ExecProcess(cmd, nil) return m, tea.ExecProcess(cmd, nil)
} }
}
case "s":
m.currentSort = (m.currentSort + 1) % 4
m.sortVideos()
return m, nil return m, nil
case "S":
m.currentSort = (m.currentSort - 1)
if m.currentSort < 0 {
m.currentSort = 3
}
m.sortVideos()
return m, nil
case "r":
m.sortAscending = !m.sortAscending
m.sortVideos()
return m, nil
}
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
@ -126,6 +171,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tickCmd() return m, tickCmd()
} }
// Handle updates for the active component
var cmd tea.Cmd var cmd tea.Cmd
if m.showSearch { if m.showSearch {
m.searchBar, cmd = m.searchBar.Update(msg) m.searchBar, cmd = m.searchBar.Update(msg)
@ -155,6 +201,24 @@ func (m model) View() string {
} }
s.WriteString(searchText + "\n\n") s.WriteString(searchText + "\n\n")
// Add sort mode indicator with better visual feedback
sortModes := []string{"Date", "Views", "Title", "Duration"}
if len(m.results.Items()) > 0 {
s.WriteString("Sort ('s'/'S' to change, 'r' to reverse): ")
for i, mode := range sortModes {
arrow := "↓"
if m.sortAscending {
arrow = "↑"
}
if i == m.currentSort {
s.WriteString(fmt.Sprintf("[%s %s]", mode, arrow))
} else {
s.WriteString(fmt.Sprintf(" %s ", mode))
}
}
s.WriteString("\n\n")
}
// Show results // Show results
s.WriteString(m.results.View()) s.WriteString(m.results.View())
@ -177,3 +241,154 @@ func tickCmd() tea.Cmd {
return t return t
}) })
} }
// Add this new method to the model
func (m *model) sortVideos() {
items := m.results.Items()
switch m.currentSort {
case SortByDate:
sort.Slice(items, func(i, j int) bool {
vi := items[i].(videoItem).Video
vj := items[j].(videoItem).Video
// Use ParsedDate for sorting
if m.sortAscending {
return vi.ParsedDate.Before(vj.ParsedDate)
}
return vi.ParsedDate.After(vj.ParsedDate)
})
case SortByViews:
sort.Slice(items, func(i, j int) bool {
vi := items[i].(videoItem).Video
vj := items[j].(videoItem).Video
viewsI := parseViews(vi.Views)
viewsJ := parseViews(vj.Views)
if m.sortAscending {
return viewsI < viewsJ
}
return viewsI > viewsJ
})
case SortByTitle:
sort.Slice(items, func(i, j int) bool {
if m.sortAscending {
return items[i].(videoItem).Video.Title < items[j].(videoItem).Video.Title
}
return items[i].(videoItem).Video.Title > items[j].(videoItem).Video.Title
})
case SortByDuration:
sort.Slice(items, func(i, j int) bool {
vi := items[i].(videoItem).Video
vj := items[j].(videoItem).Video
di := parseDuration(vi.Duration)
dj := parseDuration(vj.Duration)
if m.sortAscending {
return di < dj
}
return di > dj
})
}
m.results.SetItems(items)
}
// Add this helper function
func parseDuration(dur string) int {
// Simple duration parser for formats like "5:30" or "1:23:45"
parts := strings.Split(dur, ":")
total := 0
if len(parts) == 2 {
min, _ := strconv.Atoi(parts[0])
sec, _ := strconv.Atoi(parts[1])
total = min*60 + sec
} else if len(parts) == 3 {
hour, _ := strconv.Atoi(parts[0])
min, _ := strconv.Atoi(parts[1])
sec, _ := strconv.Atoi(parts[2])
total = hour*3600 + min*60 + sec
}
return total
}
func parseDate(date string) time.Time {
// Try parsing absolute date formats first
formats := []string{
"2006-01-02",
"Jan 2, 2006",
"2 Jan 2006",
// Add more formats as needed
}
for _, format := range formats {
if t, err := time.Parse(format, date); err == nil {
return t
}
}
// If absolute date parsing fails, handle relative dates
if strings.Contains(date, "ago") {
return parseRelativeDate(date)
}
return time.Time{} // Return zero time if parsing fails
}
func parseRelativeDate(date string) time.Time {
now := time.Now()
parts := strings.Fields(date)
if len(parts) < 2 {
return now
}
num, err := strconv.Atoi(parts[0])
if err != nil {
return now
}
// Calculate time in the past based on the unit
switch {
case strings.HasPrefix(parts[1], "second"):
return now.Add(-time.Duration(num) * time.Second)
case strings.HasPrefix(parts[1], "minute"):
return now.Add(-time.Duration(num) * time.Minute)
case strings.HasPrefix(parts[1], "hour"):
return now.Add(-time.Duration(num) * time.Hour)
case strings.HasPrefix(parts[1], "day"):
return now.AddDate(0, 0, -num)
case strings.HasPrefix(parts[1], "week"):
return now.AddDate(0, 0, -num*7)
case strings.HasPrefix(parts[1], "month"):
return now.AddDate(0, -num, 0)
case strings.HasPrefix(parts[1], "year"):
return now.AddDate(-num, 0, 0)
default:
return now
}
}
func parseViews(views string) int64 {
// Remove "views" and any commas
views = strings.ToLower(views)
views = strings.ReplaceAll(views, "views", "")
views = strings.ReplaceAll(views, ",", "")
views = strings.TrimSpace(views)
// Handle K, M, B suffixes
multiplier := int64(1)
if strings.HasSuffix(views, "k") {
multiplier = 1000
views = views[:len(views)-1]
} else if strings.HasSuffix(views, "m") {
multiplier = 1000000
views = views[:len(views)-1]
} else if strings.HasSuffix(views, "b") {
multiplier = 1000000000
views = views[:len(views)-1]
}
// Convert to float to handle decimal values
if num, err := strconv.ParseFloat(views, 64); err == nil {
return int64(num * float64(multiplier))
}
return 0
}