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
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/charmbracelet/bubbles v0.20.0
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -37,14 +36,12 @@ require (
|
|||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // 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.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.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/oauth2 v0.24.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/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=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
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/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
|
|
|
@ -23,6 +23,7 @@ type Video struct {
|
|||
Views string
|
||||
Thumbnail string
|
||||
UploadDate string
|
||||
ParsedDate time.Time
|
||||
}
|
||||
|
||||
func formatViews(count uint64) string {
|
||||
|
@ -145,6 +146,13 @@ func FetchVideos(query string) ([]Video, error) {
|
|||
Thumbnail: item.Snippet.Thumbnails.Default.Url,
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
263
tui/tui.go
263
tui/tui.go
|
@ -3,14 +3,17 @@ package tui
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ytgo/player"
|
||||
"ytgo/scraper"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"ytgo/scraper"
|
||||
"ytgo/player"
|
||||
)
|
||||
|
||||
type videoItem struct {
|
||||
|
@ -18,19 +21,31 @@ 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) 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) FilterValue() string { return v.Video.Title }
|
||||
|
||||
// Add these constants for sort modes
|
||||
const (
|
||||
SortByDate = iota
|
||||
SortByViews
|
||||
SortByTitle
|
||||
SortByDuration
|
||||
)
|
||||
|
||||
type model struct {
|
||||
searchBar textinput.Model
|
||||
results list.Model
|
||||
videos []scraper.Video
|
||||
showSearch bool
|
||||
err error
|
||||
width int
|
||||
height int
|
||||
clock time.Time
|
||||
searchBar textinput.Model
|
||||
results list.Model
|
||||
videos []scraper.Video
|
||||
showSearch bool
|
||||
err error
|
||||
width int
|
||||
height int
|
||||
clock time.Time
|
||||
currentSort int
|
||||
sortAscending bool
|
||||
}
|
||||
|
||||
// Initial model setup
|
||||
|
@ -44,12 +59,13 @@ func initialModel() model {
|
|||
results.Title = "Search Results"
|
||||
|
||||
return model{
|
||||
searchBar: searchBar,
|
||||
results: results,
|
||||
showSearch: true,
|
||||
clock: time.Now(),
|
||||
width: 80, // Default width
|
||||
height: 24, // Default height
|
||||
searchBar: searchBar,
|
||||
results: results,
|
||||
showSearch: true,
|
||||
clock: time.Now(),
|
||||
width: 80, // Default width
|
||||
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) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
// Handle global keybindings first
|
||||
switch msg.String() {
|
||||
case "q", "ctrl+c":
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
|
||||
case "/":
|
||||
|
@ -80,9 +97,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.searchBar.Blur()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
case "enter":
|
||||
if m.showSearch {
|
||||
// Handle context-specific keybindings
|
||||
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()
|
||||
if query == "" {
|
||||
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.showSearch = false
|
||||
m.searchBar.Blur()
|
||||
} else {
|
||||
// Handle video selection
|
||||
return m, nil
|
||||
}
|
||||
} 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 {
|
||||
selectedVideo := m.videos[i]
|
||||
cmd := player.PlayVideo(selectedVideo.URL)
|
||||
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:
|
||||
|
@ -126,6 +171,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, tickCmd()
|
||||
}
|
||||
|
||||
// Handle updates for the active component
|
||||
var cmd tea.Cmd
|
||||
if m.showSearch {
|
||||
m.searchBar, cmd = m.searchBar.Update(msg)
|
||||
|
@ -155,6 +201,24 @@ func (m model) View() string {
|
|||
}
|
||||
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
|
||||
s.WriteString(m.results.View())
|
||||
|
||||
|
@ -177,3 +241,154 @@ func tickCmd() tea.Cmd {
|
|||
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