Skip to content

Commit 4cd68f5

Browse files
committed
feat(commands): add play slash command
1 parent be85380 commit 4cd68f5

File tree

11 files changed

+148
-111
lines changed

11 files changed

+148
-111
lines changed

internal/app/update.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,37 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
445445
m.ErrMsg = ""
446446
return m, cmd
447447

448+
case types.StartPlayURLMsg:
449+
m.State = types.StateLoading
450+
m.LoadingType = "fetch_info"
451+
m.Player.URL = msg.URL
452+
cmd = utils.FetchVideoInfo(m.FormatsManager, msg.URL)
453+
return m, cmd
454+
455+
case types.PlayURLResultMsg:
456+
if msg.Err != "" {
457+
m.State = types.StateSearchInput
458+
if msg.Err != "Canceled" {
459+
m.ErrMsg = msg.Err
460+
}
461+
m.Player = models.PlayerModel{}
462+
return m, nil
463+
}
464+
465+
m.Player.Video = msg.SelectedVideo
466+
if m.Player.URL == "" {
467+
m.Player.URL = utils.BuildVideoURL(msg.SelectedVideo.ID)
468+
}
469+
470+
playFormat := config.GetDefault().GetDefaultFormat()
471+
if cfg, err := config.Load(); err == nil {
472+
playFormat = cfg.GetDefaultFormat()
473+
}
474+
475+
m.State = types.StateVideoPlaying
476+
cmd = m.PlayerManager.PlayURL(m.Player.URL, playFormat, msg.SelectedVideo, m.Program)
477+
return m, cmd
478+
448479
case types.StartPlaylistURLMsg:
449480
m.State = types.StateLoading
450481
m.LoadingType = "playlist"
@@ -638,7 +669,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
638669
switch msg.String() {
639670
case "c", "esc":
640671
switch m.LoadingType {
641-
case "format":
672+
case "format", "fetch_info":
642673
cmd = utils.CancelFormats(m.FormatsManager)
643674
default:
644675
cmd = utils.CancelSearch(m.SearchManager)

internal/app/view.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ func getStatusBarText(m *Model, cfg StatusBarConfig) string {
4646
)
4747
}
4848

49-
return models.FormatKeysForStatusBar(cfg.Keys)
49+
return models.FormatKeysForStatusBar(models.StatusKeys{
50+
Quit: cfg.Keys.Quit,
51+
StarOnGithub: cfg.Keys.StarOnGithub,
52+
})
5053
case types.StateLoading:
5154
return models.FormatKeysForStatusBar(models.LoadingStatusKeys(cfg.Keys))
5255
case types.StateVideoList:

internal/models/download.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ func (m DownloadModel) View() string {
282282
s.WriteRune('\n')
283283
s.WriteString(styles.MutedStyle.Render(fmt.Sprintf("📺 %s", m.SelectedVideo.Channel)))
284284
s.WriteRune('\n')
285+
s.WriteString(lipgloss.NewStyle().Foreground(styles.PinkColor).Render(fmt.Sprintf("🔗 %s", utils.BuildVideoURL(m.SelectedVideo.ID))))
286+
s.WriteRune('\n')
285287
}
286288

287289
statusText := "⇣ Downloading"

internal/models/formatList.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"log"
66
"strings"
77

8+
"github.com/charmbracelet/lipgloss"
89
"github.com/xdagiz/xytz/internal/styles"
910
"github.com/xdagiz/xytz/internal/types"
1011
"github.com/xdagiz/xytz/internal/utils"
@@ -110,6 +111,8 @@ func (m FormatListModel) View() string {
110111
s.WriteRune('\n')
111112
s.WriteString(styles.MutedStyle.Render(fmt.Sprintf("📺 %s", m.SelectedVideo.Channel)))
112113
s.WriteRune('\n')
114+
s.WriteString(lipgloss.NewStyle().Foreground(styles.PinkColor).Render(fmt.Sprintf("🔗 %s", utils.BuildVideoURL(m.SelectedVideo.ID))))
115+
s.WriteRune('\n')
113116
}
114117

115118
s.WriteString(styles.SectionHeaderStyle.Foreground(styles.MauveColor).Padding(1, 0).Render("Select a Format"))

internal/models/help.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func NewHelpModel() HelpModel {
5050
Title: "commands",
5151
Content: ` /channel <username> Search videos from a channel
5252
/playlist <url or id> Search video for a playlist
53+
/play <url> Play a video from a url
5354
/resume Resume unfinished downloads
5455
/help Show this help message`,
5556
},

internal/models/search.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func (m SearchModel) View() string {
143143

144144
if m.ErrMsg != "" {
145145
s.WriteString("\n")
146-
s.WriteString(styles.ErrorMessageStyle.PaddingLeft(1).Render(m.ErrMsg))
146+
s.WriteString(styles.ErrorMessageStyle.PaddingLeft(1).Render("⚠ " + m.ErrMsg))
147147
}
148148

149149
if m.Autocomplete.Visible {
@@ -432,11 +432,14 @@ func (m SearchModel) handleEnterKey() (SearchModel, tea.Cmd) {
432432

433433
func (m *SearchModel) executeSlashCommand(slashCmd, query, args string) tea.Cmd {
434434
var cmd tea.Cmd
435+
435436
switch slashCmd {
436437
case "channel":
437438
if args == "" {
438439
m.Input.SetValue("/channel ")
439440
m.Input.CursorEnd()
441+
} else if len(strings.SplitAfter(args, " ")) > 1 {
442+
m.ErrMsg = "Channel username cannot contain spaces"
440443
} else {
441444
m.History.Add(query)
442445
channelName := utils.ExtractChannelUsername(args)
@@ -449,13 +452,28 @@ func (m *SearchModel) executeSlashCommand(slashCmd, query, args string) tea.Cmd
449452
if args == "" {
450453
m.Input.SetValue("/playlist ")
451454
m.Input.CursorEnd()
455+
} else if len(strings.SplitAfter(args, " ")) > 1 {
456+
m.ErrMsg = "Playlist id/url cannot contain spaces"
452457
} else {
453458
m.History.Add(query)
454459
cmd = func() tea.Msg {
455460
return types.StartPlaylistURLMsg{Query: args}
456461
}
457462
}
458463

464+
case "play":
465+
if args == "" {
466+
m.Input.SetValue("/play ")
467+
m.Input.CursorEnd()
468+
} else if len(strings.SplitAfter(args, " ")) > 1 {
469+
m.ErrMsg = "Url cannot contain spaces"
470+
} else {
471+
m.History.Add(query)
472+
cmd = func() tea.Msg {
473+
return types.StartPlayURLMsg{URL: args}
474+
}
475+
}
476+
459477
case "resume":
460478
m.ResumeList.Show()
461479
m.Input.SetValue("")

internal/models/statusKeys.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type StatusKeys struct {
2727
SelectVideos key.Binding
2828
SelectAll key.Binding
2929
CopyURL key.Binding
30+
StarOnGithub key.Binding
3031
}
3132

3233
func newQuitKey() key.Binding {
@@ -127,6 +128,13 @@ func newCopyURLKey() key.Binding {
127128
)
128129
}
129130

131+
func newStarOnGithubKey() key.Binding {
132+
return key.NewBinding(
133+
key.WithKeys("ctrl+o"),
134+
key.WithHelp("Ctrl+o", "★ star on github"),
135+
)
136+
}
137+
130138
func GetStatusKeys(state types.State, resumeVisible bool) StatusKeys {
131139
keys := StatusKeys{
132140
Quit: newQuitKey(),
@@ -135,6 +143,7 @@ func GetStatusKeys(state types.State, resumeVisible bool) StatusKeys {
135143
switch state {
136144
case types.StateSearchInput:
137145
keys.Quit = newQuitCtrlCKey()
146+
keys.StarOnGithub = newStarOnGithubKey()
138147
if resumeVisible {
139148
keys.Cancel = newCancelEscKey()
140149
keys.Delete = newDeleteKey()
@@ -229,6 +238,7 @@ func orderedStatusFields(keys StatusKeys) []statusKeyField {
229238
{name: "SelectVideos", binding: keys.SelectVideos},
230239
{name: "SelectAll", binding: keys.SelectAll},
231240
{name: "CopyURL", binding: keys.CopyURL},
241+
{name: "StarOnGithub", binding: keys.StarOnGithub},
232242
}
233243
}
234244

internal/slash/commands.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ var AllCommands = []Command{
2626
Usage: "/playlist <id>",
2727
HasArg: true,
2828
},
29+
{
30+
Name: "play",
31+
Description: "Play a video with url",
32+
Usage: "/play <url>",
33+
HasArg: true,
34+
},
2935
{
3036
Name: "resume",
3137
Description: "Resume unfinished download",

internal/types/types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,13 @@ type ShowToastMsg struct {
176176
}
177177

178178
type ClearToastMsg struct{}
179+
180+
type StartPlayURLMsg struct {
181+
URL string
182+
}
183+
184+
type PlayURLResultMsg struct {
185+
URL string
186+
SelectedVideo VideoItem
187+
Err string
188+
}

internal/utils/formats.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,64 @@ func CancelFormats(fm *FormatsManager) tea.Cmd {
459459
return types.CancelFormatsMsg{}
460460
})
461461
}
462+
463+
func FetchVideoInfo(fm *FormatsManager, url string) tea.Cmd {
464+
return tea.Cmd(func() tea.Msg {
465+
cfg, err := config.Load()
466+
if err != nil {
467+
log.Printf("Warning: Failed to load config, using defaults: %v", err)
468+
cfg = config.GetDefault()
469+
}
470+
471+
ytDlpPath := cfg.YTDLPPath
472+
if ytDlpPath == "" {
473+
ytDlpPath = "yt-dlp"
474+
}
475+
476+
cmd := exec.Command(ytDlpPath, "-J", url)
477+
478+
fm.SetCmd(cmd)
479+
480+
stdout, err := cmd.StdoutPipe()
481+
if err != nil {
482+
return types.PlayURLResultMsg{URL: url, Err: fmt.Sprintf("Failed to get video info: %v", err)}
483+
}
484+
485+
if err := cmd.Start(); err != nil {
486+
fm.Clear()
487+
return types.PlayURLResultMsg{URL: url, Err: fmt.Sprintf("Failed to start yt-dlp: %v", err)}
488+
}
489+
490+
out, err := io.ReadAll(stdout)
491+
if closeErr := stdout.Close(); closeErr != nil {
492+
log.Printf("failed to close video info stdout: %v", closeErr)
493+
}
494+
495+
if fm.ClearAndCheckCanceled() {
496+
return types.PlayURLResultMsg{URL: url, Err: "Canceled"}
497+
}
498+
499+
if err != nil {
500+
return types.PlayURLResultMsg{URL: url, Err: fmt.Sprintf("Failed to read video info: %v", err)}
501+
}
502+
503+
if len(out) == 0 {
504+
return types.PlayURLResultMsg{URL: url, Err: "No video info found"}
505+
}
506+
507+
var data map[string]any
508+
if err := json.Unmarshal(out, &data); err != nil {
509+
return types.PlayURLResultMsg{URL: url, Err: fmt.Sprintf("Failed to parse video info: %v", err)}
510+
}
511+
512+
videoInfo := extractVideoInfo(data)
513+
if videoInfo.ID == "" {
514+
return types.PlayURLResultMsg{URL: url, Err: "Could not extract video ID from URL"}
515+
}
516+
517+
return types.PlayURLResultMsg{
518+
URL: url,
519+
SelectedVideo: videoInfo,
520+
}
521+
})
522+
}

0 commit comments

Comments
 (0)