diff --git a/cmd/download/download_command.go b/cmd/download/download_command.go index 9ae598b..5e068e6 100644 --- a/cmd/download/download_command.go +++ b/cmd/download/download_command.go @@ -105,6 +105,27 @@ Available format options: `, Destination: &downloadParams.OutFormat, }, + &cli.BoolFlag{ + Name: "write-chat", + Value: false, + Category: "Streaming:", + Usage: "Save live chat into a json file.", + Destination: &downloadParams.WriteChat, + }, + &cli.BoolFlag{ + Name: "write-metadata-json", + Value: false, + Category: "Streaming:", + Usage: "Dump output stream MetaData into a json file.", + Destination: &downloadParams.WriteMetaDataJSON, + }, + &cli.BoolFlag{ + Name: "write-thumbnail", + Value: false, + Category: "Streaming:", + Usage: "Download thumbnail into a file.", + Destination: &downloadParams.WriteThumbnail, + }, &cli.IntFlag{ Name: "max-packet-loss", Value: 20, diff --git a/config.yaml b/config.yaml index 1a96652..ea8de99 100644 --- a/config.yaml +++ b/config.yaml @@ -51,6 +51,12 @@ defaultParams: outFormat: '{{ .ChannelID }} {{ .ChannelName }}/{{ .Date }} {{ .Title }}.{{ .Ext }}' ## Allow a maximum of packet loss before aborting stream download. (default: 20) packetLossMax: 20 + ## Save live chat into a json file. (default: false) + writeChat: false + ## Dump output MetaData into a json file. (default: false) + writeMetaDataJson: false + ## Download thumbnail into a file. (default: false) + writeThumbnail: false ## Wait until the broadcast goes live, then start recording. (default: true) waitForLive: true ## How many seconds between checks to see if broadcast is live. (default: 5s) diff --git a/withny/channel_watcher.go b/withny/channel_watcher.go index 2f16c3c..f9dbf8a 100644 --- a/withny/channel_watcher.go +++ b/withny/channel_watcher.go @@ -2,8 +2,11 @@ package withny import ( + "bytes" "context" + "encoding/json" "errors" + "io" "os" "strings" "time" @@ -149,6 +152,18 @@ func (w *ChannelWatcher) Process(ctx context.Context, meta api.MetaData) error { log.Err(err).Msg("notify failed") } + fnameInfo, err := PrepareFile(w.params.OutFormat, meta, w.params.Labels, "info.json") + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } + fnameThumb, err := PrepareFile(w.params.OutFormat, meta, w.params.Labels, "png") + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } fnameStream, err := PrepareFile(w.params.OutFormat, meta, w.params.Labels, "ts") if err != nil { span.RecordError(err) @@ -199,6 +214,45 @@ func (w *ChannelWatcher) Process(ctx context.Context, meta api.MetaData) error { ".combined.m4a", ) + if w.params.WriteMetaDataJSON { + w.log.Info().Str("fnameInfo", fnameInfo).Msg("writing info json") + func() { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(meta); err != nil { + w.log.Error().Err(err).Msg("failed to encode meta in info json") + return + } + if err := os.WriteFile(fnameInfo, buf.Bytes(), 0o755); err != nil { + w.log.Error().Err(err).Msg("failed to write meta in info json") + return + } + }() + } + + if w.params.WriteThumbnail { + w.log.Info().Str("fnameThumb", fnameThumb).Msg("writing thunnail") + func() { + url := meta.Stream.ThumbnailURL + resp, err := w.Get(url) + if err != nil { + w.log.Error().Err(err).Msg("failed to fetch thumbnail") + return + } + defer resp.Body.Close() + out, err := os.Create(fnameThumb) + if err != nil { + w.log.Error().Err(err).Msg("failed to open thumbnail file") + return + } + defer out.Close() + _, err = io.Copy(out, resp.Body) + if err != nil { + w.log.Error().Err(err).Msg("failed to download thumbnail file") + return + } + }() + } + span.AddEvent("downloading") state.DefaultState.SetChannelState( w.channelID, diff --git a/withny/params.go b/withny/params.go index cf12ea3..c11416b 100644 --- a/withny/params.go +++ b/withny/params.go @@ -12,6 +12,9 @@ type Params struct { QualityConstraint api.PlaylistConstraint `yaml:"quality,omitempty"` PacketLossMax int `yaml:"packetLossMax,omitempty"` OutFormat string `yaml:"outFormat,omitempty"` + WriteChat bool `yaml:"writeChat,omitempty"` + WriteMetaDataJSON bool `yaml:"writeMetaDataJson,omitempty"` + WriteThumbnail bool `yaml:"writeThumbnail,omitempty"` WaitForLive bool `yaml:"waitForLive,omitempty"` WaitPollInterval time.Duration `yaml:"waitPollInterval,omitempty"` Remux bool `yaml:"remux,omitempty"` @@ -35,6 +38,9 @@ type OptionalParams struct { QualityConstraint *api.PlaylistConstraint `yaml:"quality,omitempty"` PacketLossMax *int `yaml:"packetLossMax,omitempty"` OutFormat *string `yaml:"outFormat,omitempty"` + WriteChat *bool `yaml:"writeChat,omitempty"` + WriteMetaDataJSON *bool `yaml:"writeMetaDataJson,omitempty"` + WriteThumbnail *bool `yaml:"writeThumbnail,omitempty"` WaitForLive *bool `yaml:"waitForLive,omitempty"` WaitPollInterval *time.Duration `yaml:"waitPollInterval,omitempty"` Remux *bool `yaml:"remux,omitempty"` @@ -53,6 +59,9 @@ var DefaultParams = Params{ QualityConstraint: api.PlaylistConstraint{}, PacketLossMax: 20, OutFormat: "{{ .Date }} {{ .Title }} ({{ .ChannelName }}).{{ .Ext }}", + WriteChat: false, + WriteMetaDataJSON: false, + WriteThumbnail: false, WaitForLive: true, WaitPollInterval: 5 * time.Second, Remux: true, @@ -77,6 +86,15 @@ func (override *OptionalParams) Override(params *Params) { if override.OutFormat != nil { params.OutFormat = *override.OutFormat } + if override.WriteChat != nil { + params.WriteChat = *override.WriteChat + } + if override.WriteMetaDataJSON != nil { + params.WriteMetaDataJSON = *override.WriteMetaDataJSON + } + if override.WriteThumbnail != nil { + params.WriteThumbnail = *override.WriteThumbnail + } if override.WaitForLive != nil { params.WaitForLive = *override.WaitForLive } @@ -124,6 +142,9 @@ func (p *Params) Clone() *Params { QualityConstraint: p.QualityConstraint, PacketLossMax: p.PacketLossMax, OutFormat: p.OutFormat, + WriteChat: p.WriteChat, + WriteMetaDataJSON: p.WriteMetaDataJSON, + WriteThumbnail: p.WriteThumbnail, WaitForLive: p.WaitForLive, WaitPollInterval: p.WaitPollInterval, Remux: p.Remux,