addet some fixes and styling
This commit is contained in:
parent
859c6d328e
commit
cc92bc3e26
3 changed files with 127 additions and 4 deletions
4
go.mod
4
go.mod
|
@ -5,6 +5,8 @@ go 1.23.4
|
||||||
require (
|
require (
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,12 +37,14 @@ 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
|
||||||
|
|
|
@ -3,7 +3,10 @@ package scraper
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
// "log"
|
// "log"
|
||||||
|
"html"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
"google.golang.org/api/youtube/v3"
|
"google.golang.org/api/youtube/v3"
|
||||||
|
@ -22,6 +25,93 @@ type Video struct {
|
||||||
// Replace with your actual API key
|
// Replace with your actual API key
|
||||||
const API_KEY = "AIzaSyAzsihRkp8mYTOXLOkVN09yTqld9TJ4Nts"
|
const API_KEY = "AIzaSyAzsihRkp8mYTOXLOkVN09yTqld9TJ4Nts"
|
||||||
|
|
||||||
|
func formatViews(count uint64) string {
|
||||||
|
switch {
|
||||||
|
case count >= 1000000000:
|
||||||
|
return fmt.Sprintf("%.1fB views", float64(count)/1000000000)
|
||||||
|
case count >= 1000000:
|
||||||
|
return fmt.Sprintf("%.1fM views", float64(count)/1000000)
|
||||||
|
case count >= 1000:
|
||||||
|
return fmt.Sprintf("%.1fK views", float64(count)/1000)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d views", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDuration(duration string) string {
|
||||||
|
// Remove PT from the start
|
||||||
|
duration = strings.TrimPrefix(duration, "PT")
|
||||||
|
|
||||||
|
var result strings.Builder
|
||||||
|
|
||||||
|
// Handle hours
|
||||||
|
if i := strings.Index(duration, "H"); i != -1 {
|
||||||
|
result.WriteString(duration[:i])
|
||||||
|
result.WriteString(":")
|
||||||
|
duration = duration[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle minutes
|
||||||
|
if i := strings.Index(duration, "M"); i != -1 {
|
||||||
|
minutes := duration[:i]
|
||||||
|
if len(minutes) == 1 {
|
||||||
|
result.WriteString("0")
|
||||||
|
}
|
||||||
|
result.WriteString(minutes)
|
||||||
|
result.WriteString(":")
|
||||||
|
duration = duration[i+1:]
|
||||||
|
} else if result.Len() > 0 {
|
||||||
|
result.WriteString("00:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle seconds
|
||||||
|
if i := strings.Index(duration, "S"); i != -1 {
|
||||||
|
seconds := duration[:i]
|
||||||
|
if len(seconds) == 1 {
|
||||||
|
result.WriteString("0")
|
||||||
|
}
|
||||||
|
result.WriteString(seconds)
|
||||||
|
} else {
|
||||||
|
result.WriteString("00")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatUploadDate(uploadDate string) string {
|
||||||
|
t, err := time.Parse(time.RFC3339, uploadDate)
|
||||||
|
if err != nil {
|
||||||
|
return uploadDate
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
diff := now.Sub(t)
|
||||||
|
days := int(diff.Hours() / 24)
|
||||||
|
|
||||||
|
formattedDate := t.Format("02-01-2006")
|
||||||
|
|
||||||
|
// If video is less than 30 days old, add "X days ago"
|
||||||
|
if days < 30 {
|
||||||
|
var timeAgo string
|
||||||
|
switch {
|
||||||
|
case days == 0:
|
||||||
|
hours := int(diff.Hours())
|
||||||
|
if hours == 0 {
|
||||||
|
timeAgo = "just now"
|
||||||
|
} else {
|
||||||
|
timeAgo = fmt.Sprintf("%dh ago", hours)
|
||||||
|
}
|
||||||
|
case days == 1:
|
||||||
|
timeAgo = "1 day ago"
|
||||||
|
default:
|
||||||
|
timeAgo = fmt.Sprintf("%d days ago", days)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (%s)", formattedDate, timeAgo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedDate
|
||||||
|
}
|
||||||
|
|
||||||
func FetchVideos(query string) ([]Video, error) {
|
func FetchVideos(query string) ([]Video, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
youtubeService, err := youtube.NewService(ctx, option.WithAPIKey(API_KEY))
|
youtubeService, err := youtube.NewService(ctx, option.WithAPIKey(API_KEY))
|
||||||
|
@ -70,8 +160,10 @@ func FetchVideos(query string) ([]Video, error) {
|
||||||
// Update videos with additional information
|
// Update videos with additional information
|
||||||
for i, stat := range statsResponse.Items {
|
for i, stat := range statsResponse.Items {
|
||||||
if i < len(videos) {
|
if i < len(videos) {
|
||||||
videos[i].Duration = stat.ContentDetails.Duration
|
videos[i].Duration = formatDuration(stat.ContentDetails.Duration)
|
||||||
videos[i].Views = fmt.Sprintf("%s views", stat.Statistics.ViewCount)
|
videos[i].Views = formatViews(stat.Statistics.ViewCount)
|
||||||
|
videos[i].Title = html.UnescapeString(videos[i].Title)
|
||||||
|
videos[i].UploadDate = formatUploadDate(videos[i].UploadDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
tui/tui.go
31
tui/tui.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
@ -29,6 +30,7 @@ type model struct {
|
||||||
err error
|
err error
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
clock time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial model setup
|
// Initial model setup
|
||||||
|
@ -44,13 +46,19 @@ func initialModel() model {
|
||||||
return model{
|
return model{
|
||||||
searchBar: searchBar,
|
searchBar: searchBar,
|
||||||
results: results,
|
results: results,
|
||||||
showSearch: true, // Start in search mode
|
showSearch: true,
|
||||||
|
clock: time.Now(),
|
||||||
|
width: 80, // Default width
|
||||||
|
height: 24, // Default height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the Bubble Tea program
|
// Init initializes the Bubble Tea program
|
||||||
func (m model) Init() tea.Cmd {
|
func (m model) Init() tea.Cmd {
|
||||||
return textinput.Blink
|
return tea.Batch(
|
||||||
|
textinput.Blink,
|
||||||
|
tickCmd(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update handles updates to the model based on user input
|
// Update handles updates to the model based on user input
|
||||||
|
@ -112,6 +120,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
m.results.SetSize(msg.Width, msg.Height-3) // Leave space for search bar
|
m.results.SetSize(msg.Width, msg.Height-3) // Leave space for search bar
|
||||||
|
|
||||||
|
case time.Time:
|
||||||
|
m.clock = msg
|
||||||
|
return m, tickCmd()
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
|
@ -127,6 +139,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
func (m model) View() string {
|
func (m model) View() string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
|
// Add clock to top-right corner with safety check for window width
|
||||||
|
clock := m.clock.Format("15:04:05 02-01-2006")
|
||||||
|
if m.width > len(clock) {
|
||||||
|
padding := strings.Repeat(" ", m.width-len(clock))
|
||||||
|
s.WriteString(padding + clock + "\n\n")
|
||||||
|
} else {
|
||||||
|
s.WriteString(clock + "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
// Always show search bar at the top
|
// Always show search bar at the top
|
||||||
searchText := "Press '/' to search"
|
searchText := "Press '/' to search"
|
||||||
if m.showSearch {
|
if m.showSearch {
|
||||||
|
@ -150,3 +171,9 @@ func Run() error {
|
||||||
p := tea.NewProgram(initialModel())
|
p := tea.NewProgram(initialModel())
|
||||||
return p.Start()
|
return p.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tickCmd() tea.Cmd {
|
||||||
|
return tea.Every(time.Second, func(t time.Time) tea.Msg {
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue