Skip to content

Commit 448de8c

Browse files
committed
add clip cmd
1 parent 22f9736 commit 448de8c

5 files changed

Lines changed: 801 additions & 30 deletions

File tree

cmd/clip.go

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hardhacker/podwise-cli/internal/api"
8+
"github.com/hardhacker/podwise-cli/internal/config"
9+
"github.com/hardhacker/podwise-cli/internal/episode"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// podwise clip <subcommand>
14+
var clipCmd = &cobra.Command{
15+
Use: "clip <subcommand>",
16+
Short: "List or export your episode clips",
17+
Long: "Manage clips you saved in Podwise for podcast episodes: list clips by episode, or export them when available.",
18+
Example: ` podwise clip list https://podwise.ai/dashboard/episodes/7360326 --json
19+
podwise clip export markdown https://podwise.ai/dashboard/episodes/7360326
20+
podwise clip export obsidian https://podwise.ai/dashboard/episodes/7360326
21+
podwise clip export readwise https://podwise.ai/dashboard/episodes/7360326
22+
podwise clip export notion https://podwise.ai/dashboard/episodes/7360326`,
23+
}
24+
25+
var clipListJSONOutput bool
26+
27+
// podwise clip list <episode-url>
28+
var clipListCmd = &cobra.Command{
29+
Use: "list <episode-url>",
30+
Short: "List your clips for an episode",
31+
Long: `List all clips the authenticated user saved for a Podwise episode.`,
32+
Example: ` podwise clip list https://podwise.ai/dashboard/episodes/7360326
33+
podwise clip list https://podwise.ai/dashboard/episodes/7360326 --json`,
34+
Args: cobra.ExactArgs(1),
35+
RunE: runClipList,
36+
}
37+
38+
var clipExportMarkdownOutput string
39+
var clipExportObsidianFolder string
40+
41+
// podwise clip export
42+
var clipExportCmd = &cobra.Command{
43+
Use: "export <subcommand>",
44+
Short: "Export episode clips to files or services (Readwise, Notion)",
45+
Long: "Export clips for a Podwise episode — for example to a Markdown file generated by the Podwise API.",
46+
Example: ` podwise clip export markdown https://podwise.ai/dashboard/episodes/7360326
47+
podwise clip export obsidian https://podwise.ai/dashboard/episodes/7360326
48+
podwise clip export readwise https://podwise.ai/dashboard/episodes/7360326
49+
podwise clip export notion https://podwise.ai/dashboard/episodes/7360326`,
50+
}
51+
52+
// podwise clip export markdown <episode-url>
53+
var clipExportMarkdownCmd = &cobra.Command{
54+
Use: "markdown <episode-url>",
55+
Short: "Export ready clips to a local Markdown file",
56+
Long: "Download a Markdown document that includes every ready clip for this episode.",
57+
Example: ` podwise clip export markdown https://podwise.ai/dashboard/episodes/7360326
58+
podwise clip export markdown https://podwise.ai/dashboard/episodes/7360326 --output ~/notes/podcasts`,
59+
Args: cobra.ExactArgs(1),
60+
RunE: runClipExportMarkdown,
61+
}
62+
63+
// podwise clip export obsidian <episode-url>
64+
var clipExportObsidianCmd = &cobra.Command{
65+
Use: "obsidian <episode-url>",
66+
Short: "Save clips Markdown into your Obsidian vault",
67+
Long: `Download a Markdown file of ready clips, then save it into your Obsidian vault.
68+
69+
The vault is located automatically from Obsidian's configuration file (obsidian.json).
70+
71+
If no vault can be found the .md file is written to the current directory with instructions for manual import.`,
72+
Example: ` podwise clip export obsidian https://podwise.ai/dashboard/episodes/7360326
73+
podwise clip export obsidian https://podwise.ai/dashboard/episodes/7360326 --folder Podcasts/2026`,
74+
Args: cobra.ExactArgs(1),
75+
RunE: runClipExportObsidian,
76+
}
77+
78+
// podwise clip export readwise <episode-url>
79+
var clipExportReadwiseCmd = &cobra.Command{
80+
Use: "readwise <episode-url>",
81+
Short: "Send ready clips to Readwise Highlights",
82+
Long: "Send every ready clip for this episode to Readwise Highlights",
83+
Example: ` podwise clip export readwise https://podwise.ai/dashboard/episodes/7360326`,
84+
Args: cobra.ExactArgs(1),
85+
RunE: runClipExportReadwise,
86+
}
87+
88+
// podwise clip export notion <episode-url>
89+
var clipExportNotionCmd = &cobra.Command{
90+
Use: "notion <episode-url>",
91+
Short: "Send ready clips to Notion",
92+
Long: "Send every ready clip for this episode to your configured Notion clip database.",
93+
Example: ` podwise clip export notion https://podwise.ai/dashboard/episodes/7360326`,
94+
Args: cobra.ExactArgs(1),
95+
RunE: runClipExportNotion,
96+
}
97+
98+
func init() {
99+
clipListCmd.Flags().BoolVar(&clipListJSONOutput, "json", false, "output results as formatted JSON instead of markdown")
100+
clipExportMarkdownCmd.Flags().StringVar(&clipExportMarkdownOutput, "output", "", "directory to write the .md file into (default: current directory)")
101+
clipExportObsidianCmd.Flags().StringVar(&clipExportObsidianFolder, "folder", "", "vault-relative folder to place the note in (e.g. Podcasts/2026); defaults to vault root")
102+
103+
clipExportCmd.AddCommand(clipExportMarkdownCmd)
104+
clipExportCmd.AddCommand(clipExportObsidianCmd)
105+
clipExportCmd.AddCommand(clipExportReadwiseCmd)
106+
clipExportCmd.AddCommand(clipExportNotionCmd)
107+
clipCmd.AddCommand(clipListCmd)
108+
clipCmd.AddCommand(clipExportCmd)
109+
}
110+
111+
func runClipList(cmd *cobra.Command, args []string) error {
112+
seq, err := episode.ParseSeq(args[0])
113+
if err != nil {
114+
return err
115+
}
116+
117+
cfg, err := config.Load()
118+
if err != nil {
119+
return err
120+
}
121+
if err := config.Validate(cfg); err != nil {
122+
return err
123+
}
124+
125+
client := api.New(cfg.APIBaseURL, cfg.APIKey)
126+
result, err := episode.FetchEpisodeClips(context.Background(), client, seq)
127+
if err != nil {
128+
return err
129+
}
130+
131+
if clipListJSONOutput {
132+
data, err := result.FormatJSON()
133+
if err != nil {
134+
return err
135+
}
136+
fmt.Fprintln(cmd.OutOrStdout(), string(data))
137+
return nil
138+
}
139+
140+
printMarkdown(cmd, result.FormatText())
141+
return nil
142+
}
143+
144+
func runClipExportMarkdown(cmd *cobra.Command, args []string) error {
145+
seq, err := episode.ParseSeq(args[0])
146+
if err != nil {
147+
return fmt.Errorf("invalid episode: %w", err)
148+
}
149+
150+
cfg, err := config.Load()
151+
if err != nil {
152+
return err
153+
}
154+
if err := config.Validate(cfg); err != nil {
155+
return err
156+
}
157+
158+
opts := episode.ClipsMarkdownExportOptions{
159+
OutputDir: clipExportMarkdownOutput,
160+
}
161+
162+
client := api.New(cfg.APIBaseURL, cfg.APIKey)
163+
ctx := context.Background()
164+
165+
fmt.Printf("Exporting clips for %s...\n", episode.BuildEpisodeURL(seq))
166+
167+
result, err := episode.ExportClipsToMarkdown(ctx, client, seq, opts)
168+
if err != nil {
169+
return err
170+
}
171+
172+
fmt.Printf("\n✓ Clips Markdown file saved\n")
173+
fmt.Printf(" File: %s\n", result.FilePath)
174+
fmt.Printf(" Ready clips included: %d\n", result.SuccessCount)
175+
if result.UnexportableClipCount > 0 {
176+
fmt.Printf(" Not ready / skipped: %d\n", result.UnexportableClipCount)
177+
}
178+
179+
return nil
180+
}
181+
182+
func runClipExportObsidian(cmd *cobra.Command, args []string) error {
183+
seq, err := episode.ParseSeq(args[0])
184+
if err != nil {
185+
return fmt.Errorf("invalid episode: %w", err)
186+
}
187+
188+
cfg, err := config.Load()
189+
if err != nil {
190+
return err
191+
}
192+
if err := config.Validate(cfg); err != nil {
193+
return err
194+
}
195+
196+
client := api.New(cfg.APIBaseURL, cfg.APIKey)
197+
ctx := context.Background()
198+
199+
fmt.Printf("Fetching clips Markdown for %s for Obsidian export...\n", episode.BuildEpisodeURL(seq))
200+
201+
result, err := episode.ExportClipsToObsidian(ctx, client, seq, clipExportObsidianFolder)
202+
if err != nil {
203+
return err
204+
}
205+
206+
if result.WrittenToVault {
207+
fmt.Printf("\n✓ Saved to Obsidian vault\n")
208+
fmt.Printf(" Path: %s\n", result.FilePath)
209+
} else {
210+
fmt.Printf("\n✓ Markdown file saved (Obsidian vault not found)\n")
211+
fmt.Printf(" File: %s\n", result.FilePath)
212+
fmt.Printf("\n To import manually:\n")
213+
fmt.Printf(" • Drag and drop %s into the Obsidian File Explorer, or\n", result.FilePath)
214+
fmt.Printf(" • Copy the file directly into your Obsidian vault folder\n")
215+
}
216+
fmt.Printf(" Ready clips included: %d\n", result.SuccessCount)
217+
if result.UnexportableClipCount > 0 {
218+
fmt.Printf(" Not ready / skipped: %d\n", result.UnexportableClipCount)
219+
}
220+
221+
return nil
222+
}
223+
224+
func runClipExportReadwise(cmd *cobra.Command, args []string) error {
225+
seq, err := episode.ParseSeq(args[0])
226+
if err != nil {
227+
return fmt.Errorf("invalid episode: %w", err)
228+
}
229+
230+
cfg, err := config.Load()
231+
if err != nil {
232+
return err
233+
}
234+
if err := config.Validate(cfg); err != nil {
235+
return err
236+
}
237+
238+
client := api.New(cfg.APIBaseURL, cfg.APIKey)
239+
ctx := context.Background()
240+
241+
fmt.Printf("Sending clips to Readwise for %s...\n", episode.BuildEpisodeURL(seq))
242+
243+
result, err := episode.ExportClipsToReadwise(ctx, client, seq)
244+
if err != nil {
245+
return err
246+
}
247+
248+
fmt.Printf("\n✓ Clips sent to Readwise Highlights\n")
249+
fmt.Printf(" URL: %s\n", result.URL)
250+
fmt.Printf(" Ready clips sent: %d\n", result.SuccessCount)
251+
if result.UnexportableClipCount > 0 {
252+
fmt.Printf(" Not ready / skipped: %d\n", result.UnexportableClipCount)
253+
}
254+
255+
return nil
256+
}
257+
258+
func runClipExportNotion(cmd *cobra.Command, args []string) error {
259+
seq, err := episode.ParseSeq(args[0])
260+
if err != nil {
261+
return fmt.Errorf("invalid episode: %w", err)
262+
}
263+
264+
cfg, err := config.Load()
265+
if err != nil {
266+
return err
267+
}
268+
if err := config.Validate(cfg); err != nil {
269+
return err
270+
}
271+
272+
client := api.New(cfg.APIBaseURL, cfg.APIKey)
273+
ctx := context.Background()
274+
275+
fmt.Printf("Sending clips to Notion for %s...\n", episode.BuildEpisodeURL(seq))
276+
277+
result, err := episode.ExportClipsToNotion(ctx, client, seq)
278+
if err != nil {
279+
return err
280+
}
281+
282+
fmt.Printf("\n✓ Clips sent to Notion\n")
283+
fmt.Printf(" Page URL: %s\n", result.URL)
284+
fmt.Printf(" Ready clips sent: %d\n", result.SuccessCount)
285+
if result.UnexportableClipCount > 0 {
286+
fmt.Printf(" Not ready / skipped: %d\n", result.UnexportableClipCount)
287+
}
288+
289+
return nil
290+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,5 @@ func init() {
105105
rootCmd.AddCommand(exportCmd)
106106
rootCmd.AddCommand(historyCmd)
107107
rootCmd.AddCommand(translateCmd)
108+
rootCmd.AddCommand(clipCmd)
108109
}

0 commit comments

Comments
 (0)