addet sorting
This commit is contained in:
parent
20ab5804c6
commit
27a221318e
4 changed files with 249 additions and 28 deletions
5
go.mod
5
go.mod
|
@ -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
1
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
263
tui/tui.go
263
tui/tui.go
|
@ -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 {
|
||||||
|
@ -18,19 +21,31 @@ 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
|
||||||
videos []scraper.Video
|
videos []scraper.Video
|
||||||
showSearch bool
|
showSearch bool
|
||||||
err error
|
err error
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
clock time.Time
|
clock time.Time
|
||||||
|
currentSort int
|
||||||
|
sortAscending bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial model setup
|
// Initial model setup
|
||||||
|
@ -44,12 +59,13 @@ func initialModel() model {
|
||||||
results.Title = "Search Results"
|
results.Title = "Search Results"
|
||||||
|
|
||||||
return model{
|
return model{
|
||||||
searchBar: searchBar,
|
searchBar: searchBar,
|
||||||
results: results,
|
results: results,
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
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()
|
||||||
} else {
|
return m, nil
|
||||||
// Handle video selection
|
}
|
||||||
|
} else {
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue