mirror of
https://github.com/fiatjaf/nak.git
synced 2026-07-30 19:36:15 +00:00
preliminary (half-broken) podcast support.
This commit is contained in:
@@ -52,6 +52,7 @@ var app = &cli.Command{
|
||||
nsite,
|
||||
git,
|
||||
group,
|
||||
podcast,
|
||||
nip,
|
||||
syncCmd,
|
||||
spell,
|
||||
|
||||
+626
@@ -0,0 +1,626 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip05"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
type podcastInfo struct {
|
||||
PubKey nostr.PubKey
|
||||
Title string
|
||||
Image string
|
||||
Relays []string
|
||||
Metadata nostr.Event
|
||||
}
|
||||
|
||||
var podcast = &cli.Command{
|
||||
Name: "podcast",
|
||||
Usage: "play podcasts from Nostr",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "play",
|
||||
Usage: "play latest episode from podcast or specific episode",
|
||||
ArgsUsage: "<pubkey|nip05|npub|nprofile|nevent>",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "player",
|
||||
Usage: "player command to use, defaults to auto-detect",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if target == "" {
|
||||
return fmt.Errorf("missing podcast or episode target")
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
episode, err := resolvePodcastEpisode(ctx, target, relays)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := playPodcastEpisode(c.String("player"), episode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "info",
|
||||
Usage: "display podcast metadata",
|
||||
ArgsUsage: "<pubkey|nip05|npub|nprofile>",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if target == "" {
|
||||
return fmt.Errorf("missing podcast target")
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podcasts, err := resolvePodcastsForPubkey(ctx, target, relays)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range podcasts {
|
||||
printPodcastInfo(p)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "list podcast episodes",
|
||||
ArgsUsage: "<pubkey|nip05|npub|nprofile>",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "limit",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "maximum number of episodes to fetch",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if target == "" {
|
||||
return fmt.Errorf("missing podcast target")
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podcasts, err := resolvePodcastsForPubkey(ctx, target, relays)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range podcasts {
|
||||
if err := printPodcastEpisodeList(ctx, p, int(c.Int("limit"))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func resolvePodcastEpisode(ctx context.Context, target string, relayHints []string) (*nostr.Event, error) {
|
||||
target = strings.TrimSpace(strings.TrimPrefix(target, "nostr:"))
|
||||
|
||||
if prefix, value, err := nip19.Decode(target); err == nil {
|
||||
switch prefix {
|
||||
case "nevent":
|
||||
pointer := value.(nostr.EventPointer)
|
||||
return fetchSpecificPodcastEpisode(ctx, pointer, relayHints)
|
||||
case "note":
|
||||
pointer := nostr.EventPointer{ID: value.(nostr.ID)}
|
||||
return fetchSpecificPodcastEpisode(ctx, pointer, relayHints)
|
||||
}
|
||||
}
|
||||
|
||||
if pk, extraRelays, err := resolvePodcastPubKeyTarget(ctx, target); err == nil {
|
||||
return fetchLatestEpisodeForPodcastSelection(ctx, pk, nostr.AppendUnique(relayHints, extraRelays...))
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid podcast target %q", target)
|
||||
}
|
||||
|
||||
func resolvePodcastPubKeyTarget(ctx context.Context, target string) (nostr.PubKey, []string, error) {
|
||||
if strings.TrimSpace(target) == "" {
|
||||
return nostr.ZeroPK, nil, fmt.Errorf("missing pubkey target")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(target, "nostr:") {
|
||||
target = strings.TrimPrefix(target, "nostr:")
|
||||
}
|
||||
|
||||
if prefix, value, err := nip19.Decode(target); err == nil {
|
||||
switch prefix {
|
||||
case "npub":
|
||||
return value.(nostr.PubKey), nil, nil
|
||||
case "nprofile":
|
||||
profile := value.(nostr.ProfilePointer)
|
||||
return profile.PublicKey, profile.Relays, nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(target, "@") {
|
||||
pp, err := nip05.QueryIdentifier(ctx, target)
|
||||
if err != nil {
|
||||
return nostr.ZeroPK, nil, err
|
||||
}
|
||||
return pp.PublicKey, pp.Relays, nil
|
||||
}
|
||||
|
||||
pk, err := nostr.PubKeyFromHex(target)
|
||||
if err != nil {
|
||||
return nostr.ZeroPK, nil, err
|
||||
}
|
||||
return pk, nil, nil
|
||||
}
|
||||
|
||||
func fetchSpecificPodcastEpisode(ctx context.Context, pointer nostr.EventPointer, relayHints []string) (*nostr.Event, error) {
|
||||
relays := slices.Clone(relayHints)
|
||||
relays = nostr.AppendUnique(relays, pointer.Relays...)
|
||||
if pointer.Author != nostr.ZeroPK {
|
||||
relays = nostr.AppendUnique(relays, sys.FetchOutboxRelays(ctx, pointer.Author, 3)...)
|
||||
}
|
||||
if len(relays) == 0 {
|
||||
return nil, fmt.Errorf("no relay hints found for episode")
|
||||
}
|
||||
|
||||
res := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{IDs: []nostr.ID{pointer.ID}}, nostr.SubscriptionOptions{Label: "nak-podcast"})
|
||||
if res == nil {
|
||||
return nil, fmt.Errorf("podcast episode not found")
|
||||
}
|
||||
if res.Event.Kind != 54 {
|
||||
return nil, fmt.Errorf("event is not podcast episode: expected kind 54, got %d", res.Event.Kind)
|
||||
}
|
||||
|
||||
return &res.Event, nil
|
||||
}
|
||||
|
||||
func fetchLatestEpisodeForPodcastSelection(ctx context.Context, pk nostr.PubKey, relayHints []string) (*nostr.Event, error) {
|
||||
choices, err := discoverPodcasts(ctx, pk, relayHints)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chosen := choices[0]
|
||||
if len(choices) > 1 {
|
||||
labels := make([]string, len(choices))
|
||||
for i, item := range choices {
|
||||
labels[i] = podcastLabel(item)
|
||||
}
|
||||
|
||||
var selected string
|
||||
if err := survey.AskOne(&survey.Select{
|
||||
Message: "choose podcast",
|
||||
Options: labels,
|
||||
PageSize: 12,
|
||||
}, &selected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, label := range labels {
|
||||
if label == selected {
|
||||
chosen = choices[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
episode, err := fetchLatestPodcastEpisode(ctx, chosen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return episode, nil
|
||||
}
|
||||
|
||||
func discoverPodcasts(ctx context.Context, pk nostr.PubKey, relayHints []string) ([]podcastInfo, error) {
|
||||
choices := make([]podcastInfo, 0, 4)
|
||||
seen := make(map[nostr.PubKey]struct{}, 4)
|
||||
|
||||
if direct, ok := fetchPodcastMetadata(ctx, pk, relayHints); ok {
|
||||
choices = append(choices, direct)
|
||||
seen[pk] = struct{}{}
|
||||
}
|
||||
|
||||
authoredRelays := nostr.AppendUnique(slices.Clone(relayHints), sys.FetchOutboxRelays(ctx, pk, 3)...)
|
||||
for _, candidatePubKey := range fetchAuthoredPodcastPubKeys(ctx, pk, authoredRelays) {
|
||||
if _, ok := seen[candidatePubKey]; ok {
|
||||
continue
|
||||
}
|
||||
meta, ok := fetchPodcastMetadata(ctx, candidatePubKey, relayHints)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !podcastClaimsAuthor(meta.Metadata, pk) {
|
||||
continue
|
||||
}
|
||||
choices = append(choices, meta)
|
||||
seen[candidatePubKey] = struct{}{}
|
||||
}
|
||||
|
||||
if len(choices) == 0 {
|
||||
return nil, fmt.Errorf("no podcasts found for %s", nip19.EncodeNpub(pk))
|
||||
}
|
||||
|
||||
return choices, nil
|
||||
}
|
||||
|
||||
func fetchPodcastMetadata(ctx context.Context, pk nostr.PubKey, relayHints []string) (podcastInfo, bool) {
|
||||
relays := nostr.AppendUnique(slices.Clone(relayHints), sys.FetchOutboxRelays(ctx, pk, 3)...)
|
||||
if len(relays) == 0 {
|
||||
return podcastInfo{}, false
|
||||
}
|
||||
|
||||
res := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{10154},
|
||||
Authors: []nostr.PubKey{pk},
|
||||
}, nostr.SubscriptionOptions{Label: "nak-podcast"})
|
||||
|
||||
evt, ok := res.Load(nostr.ReplaceableKey{PubKey: pk, D: ""})
|
||||
if !ok {
|
||||
return podcastInfo{}, false
|
||||
}
|
||||
|
||||
return podcastInfo{
|
||||
PubKey: pk,
|
||||
Title: podcastTagValue(evt, "title"),
|
||||
Image: podcastTagValue(evt, "image"),
|
||||
Relays: relays,
|
||||
Metadata: evt,
|
||||
}, true
|
||||
}
|
||||
|
||||
func fetchAuthoredPodcastPubKeys(ctx context.Context, pk nostr.PubKey, relays []string) []nostr.PubKey {
|
||||
if len(relays) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
found := make([]nostr.PubKey, 0, 4)
|
||||
seen := make(map[nostr.PubKey]struct{}, 4)
|
||||
for _, kind := range []nostr.Kind{10064, 10164} {
|
||||
res := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{kind},
|
||||
Authors: []nostr.PubKey{pk},
|
||||
}, nostr.SubscriptionOptions{Label: "nak-podcast"})
|
||||
|
||||
evt, ok := res.Load(nostr.ReplaceableKey{PubKey: pk, D: ""})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) < 2 || tag[0] != "p" {
|
||||
continue
|
||||
}
|
||||
podcastPK, err := nostr.PubKeyFromHex(tag[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[podcastPK]; ok {
|
||||
continue
|
||||
}
|
||||
seen[podcastPK] = struct{}{}
|
||||
found = append(found, podcastPK)
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func podcastClaimsAuthor(metadata nostr.Event, author nostr.PubKey) bool {
|
||||
for _, tag := range metadata.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "p" && tag[1] == author.Hex() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fetchLatestPodcastEpisode(ctx context.Context, podcast podcastInfo) (*nostr.Event, error) {
|
||||
relays := podcast.Relays
|
||||
if len(relays) == 0 {
|
||||
relays = sys.FetchOutboxRelays(ctx, podcast.PubKey, 3)
|
||||
}
|
||||
if len(relays) == 0 {
|
||||
return nil, fmt.Errorf("no relay hints found for podcast %s", nip19.EncodeNpub(podcast.PubKey))
|
||||
}
|
||||
|
||||
var latest *nostr.Event
|
||||
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{54},
|
||||
Authors: []nostr.PubKey{podcast.PubKey},
|
||||
Limit: 20,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-podcast"}) {
|
||||
if latest == nil || ie.Event.CreatedAt > latest.CreatedAt {
|
||||
event := ie.Event
|
||||
latest = &event
|
||||
}
|
||||
}
|
||||
|
||||
if latest == nil {
|
||||
return nil, fmt.Errorf("no episodes found for %s", podcastLabel(podcast))
|
||||
}
|
||||
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func playPodcastEpisode(player string, episode *nostr.Event) error {
|
||||
audioURL := firstPodcastAudioURL(*episode)
|
||||
if audioURL == "" {
|
||||
return fmt.Errorf("podcast episode has no audio tag")
|
||||
}
|
||||
|
||||
title := podcastTagValue(*episode, "title")
|
||||
if title == "" {
|
||||
title = episode.ID.Hex()
|
||||
}
|
||||
|
||||
stdout(colors.bold("playing:"), color.HiBlueString(title))
|
||||
stdout(colors.bold("audio:"), color.HiCyanString(audioURL))
|
||||
|
||||
cmd, err := buildPodcastPlayerCommand(player, audioURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = color.Output
|
||||
cmd.Stderr = color.Error
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func buildPodcastPlayerCommand(player string, audioURL string) (*exec.Cmd, error) {
|
||||
if strings.TrimSpace(player) != "" {
|
||||
parts := strings.Fields(player)
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("invalid player command %q", player)
|
||||
}
|
||||
return exec.Command(parts[0], append(parts[1:], audioURL)...), nil
|
||||
}
|
||||
|
||||
for _, candidate := range podcastPlayerCandidates() {
|
||||
if _, err := exec.LookPath(candidate.name); err == nil {
|
||||
return exec.Command(candidate.name, append(candidate.args, audioURL)...), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no media player found; install mpv, vlc, ffplay or pass --player")
|
||||
}
|
||||
|
||||
func podcastPlayerCandidates() []struct {
|
||||
name string
|
||||
args []string
|
||||
} {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"mpv", nil},
|
||||
{"vlc", nil},
|
||||
{"ffplay", []string{"-nodisp", "-autoexit"}},
|
||||
{"open", nil},
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
return []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"mpv.exe", nil},
|
||||
{"vlc.exe", nil},
|
||||
{"ffplay.exe", []string{"-nodisp", "-autoexit"}},
|
||||
{"cmd", []string{"/c", "start"}},
|
||||
}
|
||||
}
|
||||
|
||||
return []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"mpv", nil},
|
||||
{"vlc", nil},
|
||||
{"ffplay", []string{"-nodisp", "-autoexit"}},
|
||||
{"xdg-open", nil},
|
||||
}
|
||||
}
|
||||
|
||||
func firstPodcastAudioURL(evt nostr.Event) string {
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "audio" {
|
||||
return tag[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func podcastTagValue(evt nostr.Event, name string) string {
|
||||
if tag := evt.Tags.Find(name); len(tag) >= 2 {
|
||||
return tag[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func podcastLabel(info podcastInfo) string {
|
||||
title := strings.TrimSpace(info.Title)
|
||||
if title == "" {
|
||||
title = nip19.EncodeNpub(info.PubKey)
|
||||
} else {
|
||||
title += " (" + nip19.EncodeNpub(info.PubKey) + ")"
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func resolvePodcastsForPubkey(ctx context.Context, target string, relayHints []string) ([]podcastInfo, error) {
|
||||
pk, extraRelays, err := resolvePodcastPubKeyTarget(ctx, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allRelays := nostr.AppendUnique(relayHints, extraRelays...)
|
||||
choices, err := discoverPodcasts(ctx, pk, allRelays)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(choices) > 1 {
|
||||
labels := make([]string, len(choices))
|
||||
for i, item := range choices {
|
||||
labels[i] = podcastLabel(item)
|
||||
}
|
||||
|
||||
var selected string
|
||||
if err := survey.AskOne(&survey.Select{
|
||||
Message: "choose podcast",
|
||||
Options: labels,
|
||||
PageSize: 12,
|
||||
}, &selected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtered := make([]podcastInfo, 0, 1)
|
||||
for i, label := range labels {
|
||||
if label == selected {
|
||||
filtered = append(filtered, choices[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
return choices, nil
|
||||
}
|
||||
|
||||
func printPodcastInfo(p podcastInfo) {
|
||||
stdout(colors.bold("title:"), color.HiBlueString(p.Title))
|
||||
stdout(colors.bold("pubkey:"), color.HiCyanString(nip19.EncodeNpub(p.PubKey)))
|
||||
|
||||
if desc := podcastTagValue(p.Metadata, "description"); desc != "" {
|
||||
stdout(colors.bold("description:"), desc)
|
||||
}
|
||||
if image := podcastTagValue(p.Metadata, "image"); image != "" {
|
||||
stdout(colors.bold("image:"), color.HiCyanString(image))
|
||||
}
|
||||
if website := podcastTagValue(p.Metadata, "website"); website != "" {
|
||||
stdout(colors.bold("website:"), color.HiCyanString(website))
|
||||
}
|
||||
|
||||
authors := make([]string, 0, 2)
|
||||
for _, tag := range p.Metadata.Tags {
|
||||
if len(tag) >= 3 && tag[0] == "p" {
|
||||
role := ""
|
||||
if len(tag) >= 3 {
|
||||
role = tag[2]
|
||||
}
|
||||
pk, err := nostr.PubKeyFromHex(tag[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
authors = append(authors, nip19.EncodeNpub(pk)+" "+role)
|
||||
}
|
||||
}
|
||||
if len(authors) > 0 {
|
||||
stdout(colors.bold("authors:"), strings.Join(authors, ", "))
|
||||
}
|
||||
|
||||
stdout("")
|
||||
}
|
||||
|
||||
func printPodcastEpisodeList(ctx context.Context, p podcastInfo, limit int) error {
|
||||
episodes, err := fetchPodcastEpisodes(ctx, p, limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(episodes) == 0 {
|
||||
stdout(color.YellowString("no episodes found for %s"), p.Title)
|
||||
return nil
|
||||
}
|
||||
|
||||
stdout(color.CyanString("episodes for %s:"), p.Title)
|
||||
for _, ep := range episodes {
|
||||
title := podcastTagValue(ep, "title")
|
||||
if title == "" {
|
||||
title = ep.ID.Hex()[:16]
|
||||
}
|
||||
date := ep.CreatedAt.Time().Format("2006-01-02")
|
||||
desc := ep.Content
|
||||
if len(desc) > 100 {
|
||||
desc = desc[:100] + "..."
|
||||
}
|
||||
stdout(fmt.Sprintf("%s %s %s", date, title, desc))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchPodcastEpisodes(ctx context.Context, p podcastInfo, limit int) ([]nostr.Event, error) {
|
||||
relays := p.Relays
|
||||
if len(relays) == 0 {
|
||||
relays = sys.FetchOutboxRelays(ctx, p.PubKey, 3)
|
||||
}
|
||||
if len(relays) == 0 {
|
||||
return nil, fmt.Errorf("no relay hints found for podcast %s", nip19.EncodeNpub(p.PubKey))
|
||||
}
|
||||
|
||||
episodes := make([]nostr.Event, 0, limit)
|
||||
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{54},
|
||||
Authors: []nostr.PubKey{p.PubKey},
|
||||
Limit: limit,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-podcast-list"}) {
|
||||
episodes = append(episodes, ie.Event)
|
||||
}
|
||||
|
||||
return episodes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user