download

package
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2024 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package download provide a command for downloading a live withny stream.

Index

Constants

This section is empty.

Variables

View Source
var Command = &cli.Command{
	Name:      "download",
	Usage:     "Download a withny live stream.",
	ArgsUsage: "channelID",
	Flags: []cli.Flag{
		&cli.Int64Flag{
			Name:        "quality.min-height",
			Category:    "Streaming:",
			Usage:       `Minimum inclusive height of the stream.`,
			Destination: &downloadParams.QualityConstraint.MinHeight,
		},
		&cli.Int64Flag{
			Name:        "quality.max-height",
			Category:    "Streaming:",
			Usage:       `Maximum inclusive height of the stream.`,
			Destination: &downloadParams.QualityConstraint.MaxHeight,
		},
		&cli.Int64Flag{
			Name:        "quality.min-width",
			Category:    "Streaming:",
			Usage:       `Minimum inclusive width of the stream.`,
			Destination: &downloadParams.QualityConstraint.MinWidth,
		},
		&cli.Int64Flag{
			Name:        "quality.max-width",
			Category:    "Streaming:",
			Usage:       `Maximum inclusive width of the stream.`,
			Destination: &downloadParams.QualityConstraint.MaxWidth,
		},
		&cli.Float64Flag{
			Name:        "quality.min-framerate",
			Category:    "Streaming:",
			Usage:       `Minimum inclusive framerate of the stream.`,
			Destination: &downloadParams.QualityConstraint.MinFrameRate,
		},
		&cli.Float64Flag{
			Name:        "quality.max-framerate",
			Category:    "Streaming:",
			Usage:       `Maximum inclusive framerate of the stream.`,
			Destination: &downloadParams.QualityConstraint.MaxFrameRate,
		},
		&cli.Int64Flag{
			Name:        "quality.min-bandwidth",
			Category:    "Streaming:",
			Usage:       `Minimum inclusive bandwidth of the stream.`,
			Destination: &downloadParams.QualityConstraint.MinBandwidth,
		},
		&cli.Int64Flag{
			Name:        "quality.max-bandwidth",
			Category:    "Streaming:",
			Usage:       `Maximum inclusive bandwidth of the stream.`,
			Destination: &downloadParams.QualityConstraint.MaxBandwidth,
		},
		&cli.BoolFlag{
			Name:        "quality.audio-only",
			Category:    "Streaming:",
			Usage:       "Only download audio streams.",
			Destination: &downloadParams.QualityConstraint.AudioOnly,
		},
		&cli.StringFlag{
			Name:     "format",
			Value:    "{{ .Date }} {{ .Title }} ({{ .ChannelName }}).{{ .Ext }}",
			Category: "Post-Processing:",
			Usage: `Golang templating format. Available fields: ChannelID, ChannelName, Date, Time, Title, Ext, Labels.Key.
Available format options:
  ChannelID: ID of the broadcast
  ChannelName: broadcaster's profile name
  Date: local date YYYY-MM-DD
  Time: local time HHMMSS
  Ext: file extension
  Title: title of the live broadcast
  Labels.Key: custom labels
`,
			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,
			Category:    "Post-Processing:",
			Usage:       "Allow a maximum of packet loss before aborting stream download.",
			Destination: &downloadParams.PacketLossMax,
		},
		&cli.BoolFlag{
			Name:       "no-remux",
			Value:      false,
			HasBeenSet: true,
			Category:   "Post-Processing:",
			Usage:      "Do not remux recordings into mp4/m4a after it is finished.",
			Action: func(_ *cli.Context, b bool) error {
				downloadParams.Remux = !b
				return nil
			},
		},
		&cli.StringFlag{
			Name:        "remux-format",
			Value:       "mp4",
			Category:    "Post-Processing:",
			Usage:       "Remux format of the video.",
			Destination: &downloadParams.RemuxFormat,
		},
		&cli.BoolFlag{
			Name:        "concat",
			Value:       false,
			Category:    "Post-Processing:",
			Usage:       "Concatenate and remux with previous recordings after it is finished. ",
			Destination: &downloadParams.Concat,
		},
		&cli.BoolFlag{
			Name:        "keep-intermediates",
			Value:       false,
			Category:    "Post-Processing:",
			Usage:       "Keep the raw .ts recordings after it has been remuxed.",
			Aliases:     []string{"k"},
			Destination: &downloadParams.KeepIntermediates,
		},
		&cli.StringFlag{
			Name:        "scan-directory",
			Value:       "",
			Category:    "Cleaning Routine:",
			Usage:       "Directory to be scanned for .ts files to be deleted after concatenation.",
			Destination: &downloadParams.ScanDirectory,
		},
		&cli.DurationFlag{
			Name:        "eligible-for-cleaning-age",
			Value:       48 * time.Hour,
			Category:    "Cleaning Routine:",
			Usage:       "Minimum age of .combined files to be eligible for cleaning.",
			Aliases:     []string{"cleaning-age"},
			Destination: &downloadParams.EligibleForCleaningAge,
		},
		&cli.BoolFlag{
			Name:       "no-delete-corrupted",
			Value:      false,
			HasBeenSet: true,
			Category:   "Post-Processing:",
			Usage:      "Delete corrupted .ts recordings.",
			Action: func(_ *cli.Context, b bool) error {
				downloadParams.DeleteCorrupted = !b
				return nil
			},
		},
		&cli.BoolFlag{
			Name:        "extract-audio",
			Value:       false,
			Category:    "Post-Processing:",
			Usage:       "Generate an audio-only copy of the stream.",
			Aliases:     []string{"x"},
			Destination: &downloadParams.ExtractAudio,
		},
		&cli.PathFlag{
			Name:        "credentials-file",
			Usage:       "Path to a credentials file. Format is YAML and must contain 'username' and 'password' or 'access-token' and 'refresh-token'.",
			Category:    "Streaming:",
			Destination: &credentialFile,
		},
		&cli.StringFlag{
			Name:        "credentials.username",
			Usage:       "Username/email for withny login",
			Category:    "Streaming:",
			Aliases:     []string{"credentials.email"},
			Destination: &credentialsStatic.Username,
		},
		&cli.StringFlag{
			Name:        "credentials.password",
			Usage:       "Password for withny login",
			Category:    "Streaming:",
			Destination: &credentialsStatic.Password,
		},
		&cli.StringFlag{
			Name:        "credentials.access-token",
			Usage:       "Access token for withny login. You should also provide a refresh token.",
			Category:    "Streaming:",
			Destination: &credentialsStatic.Token,
		},
		&cli.StringFlag{
			Name:        "credentials.refresh-token",
			Usage:       "Refresh token for withny login.",
			Category:    "Streaming:",
			Destination: &credentialsStatic.RefreshToken,
		},
		&cli.BoolFlag{
			Name:       "no-wait",
			Value:      false,
			HasBeenSet: true,
			Category:   "Polling:",
			Usage:      "Don't wait until the broadcast goes live, then start recording.",
			Action: func(_ *cli.Context, b bool) error {
				downloadParams.WaitForLive = !b
				return nil
			},
		},
		&cli.DurationFlag{
			Name:        "poll-interval",
			Value:       5 * time.Second,
			Category:    "Polling:",
			Usage:       "How many seconds between checks to see if broadcast is live.",
			Destination: &downloadParams.WaitPollInterval,
		},
		&cli.IntFlag{
			Name:        "max-tries",
			Value:       10,
			Category:    "Polling:",
			Usage:       "On failure, keep retrying (cancellation and end of stream will still force abort).",
			Destination: &maxTries,
		},
		&cli.BoolFlag{
			Name:        "loop",
			Value:       false,
			Category:    "Polling:",
			Usage:       "Continue to download streams indefinitely.",
			Destination: &loop,
		},
	},
	Action: func(cCtx *cli.Context) error {
		ctx, cancel := context.WithCancel(cCtx.Context)

		cleanChan := make(chan os.Signal, 1)
		signal.Notify(cleanChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
		go func() {
			<-cleanChan
			cancel()
		}()

		channelID := cCtx.Args().Get(0)
		if channelID == "" {
			log.Error().Msg("channel ID is empty")
			return errors.New("missing channel")
		}

		jar, err := cookiejar.New(&cookiejar.Options{})
		if err != nil {
			log.Panic().Err(err).Msg("failed to create cookie jar")
		}
		hclient := &http.Client{Jar: jar, Timeout: time.Minute}

		var reader api.CredentialsReader
		if credentialsStatic.Username != "" || credentialsStatic.Token != "" {
			reader = &credentialsStatic
		}
		if credentialFile != "" {
			reader = secret.NewReader(credentialFile)
		}
		client := api.NewClient(hclient, reader)

		if err := client.Login(ctx); err != nil {
			log.Err(err).
				Msg("failed to login to withny")
			return err
		}

		downloader := withny.NewChannelWatcher(client, &downloadParams, channelID)
		log.Info().Any("params", downloadParams).Msg("running")

		if loop {
			for {
				_, err := downloader.Watch(ctx)
				if errors.Is(err, context.Canceled) {
					log.Info().Str("channelID", channelID).Msg("abort watching channel")
					break
				}
				if err != nil {
					log.Err(err).Msg("failed to download")
				}
				time.Sleep(time.Second)
			}
			return nil
		}

		return try.DoExponentialBackoff(maxTries, time.Second, 2, time.Minute, func() error {
			_, err := downloader.Watch(ctx)
			if err == io.EOF || errors.Is(err, context.Canceled) {
				return nil
			}
			return err
		})
	},
}

Command is the command for downloading a live withny stream.

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL