Use custom ytdl wrapper
This commit is contained in:
		
							parent
							
								
									177d8d7e55
								
							
						
					
					
						commit
						05f23c53ec
					
				
							
								
								
									
										2
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										2
									
								
								go.mod
								
								
								
								
							| 
						 | 
				
			
			@ -5,7 +5,9 @@ go 1.20
 | 
			
		|||
require (
 | 
			
		||||
	github.com/gofiber/fiber/v2 v2.43.0
 | 
			
		||||
	github.com/gofiber/template v1.8.0
 | 
			
		||||
	github.com/samber/lo v1.38.1
 | 
			
		||||
	github.com/spf13/cobra v1.7.0
 | 
			
		||||
	golang.org/x/exp v0.0.0-20230321023759-10a507213a29
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										4
									
								
								go.sum
								
								
								
								
							| 
						 | 
				
			
			@ -331,6 +331,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 | 
			
		|||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
			
		||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 | 
			
		||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
 | 
			
		||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
 | 
			
		||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
 | 
			
		||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
 | 
			
		||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
 | 
			
		||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
 | 
			
		||||
| 
						 | 
				
			
			@ -413,6 +415,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 | 
			
		|||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 | 
			
		||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 | 
			
		||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 | 
			
		||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 | 
			
		||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 | 
			
		||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								web/serve.go
								
								
								
								
							
							
						
						
									
										29
									
								
								web/serve.go
								
								
								
								
							| 
						 | 
				
			
			@ -1,7 +1,13 @@
 | 
			
		|||
package web
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"sort"
 | 
			
		||||
 | 
			
		||||
	"github.com/gofiber/fiber/v2"
 | 
			
		||||
	"github.com/samber/lo"
 | 
			
		||||
	"go.fifitido.net/ytdl-web/ytdl"
 | 
			
		||||
	"golang.org/x/exp/slog"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Serve() error {
 | 
			
		||||
| 
						 | 
				
			
			@ -13,10 +19,29 @@ func Serve() error {
 | 
			
		|||
	})
 | 
			
		||||
 | 
			
		||||
	app.Get("/download", func(c *fiber.Ctx) error {
 | 
			
		||||
		url := c.Get("url")
 | 
			
		||||
		urlBytes, err := url.QueryUnescape(c.Query("url"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			slog.Error("Failed to decode url param", slog.String("error", err.Error()))
 | 
			
		||||
			return fiber.ErrBadRequest
 | 
			
		||||
		}
 | 
			
		||||
		url := string(urlBytes)
 | 
			
		||||
 | 
			
		||||
		meta, err := ytdl.GetMetadata(url)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			slog.Error("Failed to get metadata", slog.String("error", err.Error()))
 | 
			
		||||
			return fiber.ErrInternalServerError
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		formats := lo.Filter(meta.Formats, func(item ytdl.Format, _ int) bool {
 | 
			
		||||
			return item.ACodec != "none" && item.VCodec != "none"
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		sort.Slice(formats, func(i, j int) bool {
 | 
			
		||||
			return formats[i].Width > formats[j].Width
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		return c.Render("views/download", fiber.Map{
 | 
			
		||||
			"Url": url,
 | 
			
		||||
			"Url": url, "Meta": meta, "Formats": formats,
 | 
			
		||||
		}, "views/layouts/main")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										19
									
								
								web/views.go
								
								
								
								
							
							
						
						
									
										19
									
								
								web/views.go
								
								
								
								
							| 
						 | 
				
			
			@ -2,8 +2,10 @@ package web
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"github.com/gofiber/template/html"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -14,10 +16,23 @@ var viewsfs embed.FS
 | 
			
		|||
func ViewsEngine() *html.Engine {
 | 
			
		||||
	engine := html.NewFileSystem(http.FS(viewsfs), ".html")
 | 
			
		||||
	engine.AddFunc(
 | 
			
		||||
		// add unescape function
 | 
			
		||||
		"unescape", func(s string) template.HTML {
 | 
			
		||||
		"unsafe", func(s string) template.HTML {
 | 
			
		||||
			return template.HTML(s)
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	engine.AddFunc(
 | 
			
		||||
		"queryEscape", func(s string) string {
 | 
			
		||||
			return url.QueryEscape(s)
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	engine.AddFunc(
 | 
			
		||||
		"jsonMarshal", func(s any) (string, error) {
 | 
			
		||||
			j, err := json.MarshalIndent(s, "", "    ")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
			return string(j), nil
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	return engine
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,37 @@
 | 
			
		|||
<h1>Download</h1>
 | 
			
		||||
<p>{{.Url}}</p>
 | 
			
		||||
<div class="d-flex flex-column align-items-center">
 | 
			
		||||
  <h1>Download Video</h1>
 | 
			
		||||
  <h2 class="fs-4 text-muted">{{.Meta.Title}}</h2>
 | 
			
		||||
  <p style="font-size: 0.85rem">{{.Url}}</p>
 | 
			
		||||
  <img
 | 
			
		||||
    src="{{.Meta.Thumbnail}}"
 | 
			
		||||
    alt="{{.Meta.Title}}"
 | 
			
		||||
    style="max-height: 25rem; max-width: 100%"
 | 
			
		||||
  />
 | 
			
		||||
  <a
 | 
			
		||||
    href="/"
 | 
			
		||||
    class="btn btn-secondary btn-sm mt-3"
 | 
			
		||||
    style="width: 30rem; max-width: 100%"
 | 
			
		||||
    >Download Another Video</a
 | 
			
		||||
  >
 | 
			
		||||
</div>
 | 
			
		||||
{{$id := .Meta.ID}} {{$url := .Url}} {{range .Formats}}
 | 
			
		||||
<div class="d-flex gap-3 mt-5 align-items-center">
 | 
			
		||||
  <div style="width: 10rem">{{.Format}}</div>
 | 
			
		||||
  <div class="flex-grow-1 d-flex gap-3">
 | 
			
		||||
    <a
 | 
			
		||||
      class="btn btn-primary flex-grow-1"
 | 
			
		||||
      download="{{$id}}-{{.Resolution}}.{{.Ext}}"
 | 
			
		||||
      href="{{.Url}}"
 | 
			
		||||
    >
 | 
			
		||||
      Download (direct)
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      class="btn btn-primary flex-grow-1"
 | 
			
		||||
      download="{{$id}}-{{.Resolution}}.{{.Ext}}"
 | 
			
		||||
      href="/download/proxy?url={{queryEscape $url}}&format={{.FormatID}}"
 | 
			
		||||
    >
 | 
			
		||||
      Download (proxied)
 | 
			
		||||
    </a>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
  <div class="mb-3">
 | 
			
		||||
    <label for="url" class="form-label visually-hidden">Url</label>
 | 
			
		||||
    <div class="input-group">
 | 
			
		||||
      <input type="url" name="url" id="url" class="form-control" />
 | 
			
		||||
      <input type="url" name="url" id="url" class="form-control" required />
 | 
			
		||||
      <button
 | 
			
		||||
        id="paste-button"
 | 
			
		||||
        class="btn btn-outline-secondary"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,7 +46,7 @@
 | 
			
		|||
  </head>
 | 
			
		||||
  <body data-bs-theme="dark">
 | 
			
		||||
    {{template "views/partials/navbar" .}}
 | 
			
		||||
    <main class="container mt-5">{{embed}}</main>
 | 
			
		||||
    <main class="container my-5">{{embed}}</main>
 | 
			
		||||
    <script>
 | 
			
		||||
      const pasteButton = document.getElementById("paste-button");
 | 
			
		||||
      const urlField = document.getElementById("url");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,441 @@
 | 
			
		|||
package ytdl
 | 
			
		||||
 | 
			
		||||
type Metdata struct {
 | 
			
		||||
	// Video identifier.
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
 | 
			
		||||
	// Video title, unescaped. Set to an empty string if video has
 | 
			
		||||
	// no title as opposed to "None" which signifies that the
 | 
			
		||||
	// extractor failed to obtain a title
 | 
			
		||||
	Title *string `json:"title"`
 | 
			
		||||
 | 
			
		||||
	// A list of dictionaries for each format available, ordered
 | 
			
		||||
	// from worst to best quality.
 | 
			
		||||
	Formats []Format `json:"formats"`
 | 
			
		||||
 | 
			
		||||
	// Final video URL.
 | 
			
		||||
	Url string `json:"url"`
 | 
			
		||||
 | 
			
		||||
	// Video filename extension.
 | 
			
		||||
	Ext string `json:"ext"`
 | 
			
		||||
 | 
			
		||||
	// The video format, defaults to ext (used for --get-format)
 | 
			
		||||
	Format string `json:"format"`
 | 
			
		||||
 | 
			
		||||
	// True if a direct video file was given (must only be set by GenericIE)
 | 
			
		||||
	Direct *bool `json:"direct"`
 | 
			
		||||
 | 
			
		||||
	// A secondary title of the video.
 | 
			
		||||
	AltTitle *string `json:"alt_title"`
 | 
			
		||||
 | 
			
		||||
	// An alternative identifier for the video, not necessarily
 | 
			
		||||
	// unique, but available before title. Typically, id is
 | 
			
		||||
	// something like "4234987", title "Dancing naked mole rats",
 | 
			
		||||
	// and display_id "dancing-naked-mole-rats"
 | 
			
		||||
	DisplayID *string `json:"display_id"`
 | 
			
		||||
 | 
			
		||||
	Thumbnails []Thumbnail `json:"thumbnails"`
 | 
			
		||||
 | 
			
		||||
	// Full URL to a video thumbnail image.
 | 
			
		||||
	Thumbnail *string `json:"thumbnail"`
 | 
			
		||||
 | 
			
		||||
	// Full video description.
 | 
			
		||||
	Description *string `json:"description"`
 | 
			
		||||
 | 
			
		||||
	// Full name of the video uploader.
 | 
			
		||||
	Uploader *string `json:"uploader"`
 | 
			
		||||
 | 
			
		||||
	// License name the video is licensed under.
 | 
			
		||||
	License *string `json:"license"`
 | 
			
		||||
 | 
			
		||||
	// The creator of the video.
 | 
			
		||||
	Creator *string `json:"creator"`
 | 
			
		||||
 | 
			
		||||
	// UNIX timestamp of the moment the video was uploaded
 | 
			
		||||
	Timestamp *int64 `json:"timestamp"`
 | 
			
		||||
 | 
			
		||||
	// Video upload date in UTC (YYYYMMDD).
 | 
			
		||||
	// If not explicitly set, calculated from timestamp
 | 
			
		||||
	UploadDate *string `json:"upload_date"`
 | 
			
		||||
 | 
			
		||||
	// UNIX timestamp of the moment the video was released.
 | 
			
		||||
	// If it is not clear whether to use timestamp or this, use the former
 | 
			
		||||
	ReleaseTimestamp *int64 `json:"release_timestamp"`
 | 
			
		||||
 | 
			
		||||
	// The date (YYYYMMDD) when the video was released in UTC.
 | 
			
		||||
	// If not explicitly set, calculated from release_timestamp
 | 
			
		||||
	ReleaseDate *string `json:"release_date"`
 | 
			
		||||
 | 
			
		||||
	// UNIX timestamp of the moment the video was last modified.
 | 
			
		||||
	ModifiedTimestamp *int64 `json:"modified_timestamp"`
 | 
			
		||||
 | 
			
		||||
	// The date (YYYYMMDD) when the video was last modified in UTC.
 | 
			
		||||
	// If not explicitly set, calculated from modified_timestamp
 | 
			
		||||
	ModifiedDate *string `json:"modified_date"`
 | 
			
		||||
 | 
			
		||||
	// Nickname or id of the video uploader.
 | 
			
		||||
	UploaderId *string `json:"uploader_id"`
 | 
			
		||||
 | 
			
		||||
	// Full URL to a personal webpage of the video uploader.
 | 
			
		||||
	UploaderUrl *string `json:"uploader_url"`
 | 
			
		||||
 | 
			
		||||
	// Full name of the channel the video is uploaded on.
 | 
			
		||||
	// Note that channel fields may or may not repeat uploader
 | 
			
		||||
	// fields. This depends on a particular extractor.
 | 
			
		||||
	Channel *string `json:"channel"`
 | 
			
		||||
 | 
			
		||||
	// Id of the channel.
 | 
			
		||||
	ChannelId *string `json:"channel_id"`
 | 
			
		||||
 | 
			
		||||
	// Full URL to a channel webpage.
 | 
			
		||||
	ChannelUrl *string `json:"channel_url"`
 | 
			
		||||
 | 
			
		||||
	// Number of followers of the channel.
 | 
			
		||||
	ChannelFollowerCount *int `json:"channel_follower_count"`
 | 
			
		||||
 | 
			
		||||
	// Physical location where the video was filmed.
 | 
			
		||||
	Location *string `json:"location"`
 | 
			
		||||
 | 
			
		||||
	// The available subtitles as a dictionary in the format
 | 
			
		||||
	// {tag: subformats}. "tag" is usually a language code, and
 | 
			
		||||
	// "subformats" is a list sorted from lower to higher
 | 
			
		||||
	// preference
 | 
			
		||||
	Subtitles map[string][]Subtitle `json:"subtitles"`
 | 
			
		||||
 | 
			
		||||
	// Like 'subtitles'; contains automatically generated
 | 
			
		||||
	// captions instead of normal subtitles
 | 
			
		||||
	AutomaticCaptions map[string][]Subtitle `json:"automatic_captions"`
 | 
			
		||||
 | 
			
		||||
	// Length of the video in seconds, as an integer or float.
 | 
			
		||||
	Duration *float64 `json:"duration"`
 | 
			
		||||
 | 
			
		||||
	// How many users have watched the video on the platform.
 | 
			
		||||
	ViewCount *int64 `json:"view_count"`
 | 
			
		||||
 | 
			
		||||
	// How many users are currently watching the video on the platform.
 | 
			
		||||
	ConcurrentViewCount *int64 `json:"concurrent_view_count"`
 | 
			
		||||
 | 
			
		||||
	// Number of positive ratings of the video
 | 
			
		||||
	LikeCount *int64 `json:"like_count"`
 | 
			
		||||
 | 
			
		||||
	// Number of negative ratings of the video
 | 
			
		||||
	DislikeCount *int64 `json:"dislike_count"`
 | 
			
		||||
 | 
			
		||||
	// Number of reposts of the video
 | 
			
		||||
	RepostCount *int64 `json:"repost_count"`
 | 
			
		||||
 | 
			
		||||
	// Average rating give by users, the scale used depends on the webpage
 | 
			
		||||
	AverageRating *float64 `json:"average_rating"`
 | 
			
		||||
 | 
			
		||||
	// Number of comments on the video
 | 
			
		||||
	CommentCount *int64 `json:"comment_count"`
 | 
			
		||||
 | 
			
		||||
	// A list of comments
 | 
			
		||||
	Comments []Comment `json:"comments"`
 | 
			
		||||
 | 
			
		||||
	// Age restriction for the video, as an integer (years)
 | 
			
		||||
	AgeLimit *int `json:"age_limit"`
 | 
			
		||||
 | 
			
		||||
	// The URL to the video webpage, if given to yt-dlp it
 | 
			
		||||
	// should allow to get the same result again. (It will be set
 | 
			
		||||
	// by YoutubeDL if it's missing)
 | 
			
		||||
	WebpageUrl *string `json:"webpage_url"`
 | 
			
		||||
 | 
			
		||||
	// A list of categories that the video falls in
 | 
			
		||||
	Categories []string `json:"categories"`
 | 
			
		||||
 | 
			
		||||
	// A list of tags assigned to the video
 | 
			
		||||
	Tags []string `json:"tags"`
 | 
			
		||||
 | 
			
		||||
	// A list of the video cast
 | 
			
		||||
	Cast []string `json:"cast"`
 | 
			
		||||
 | 
			
		||||
	// Whether this video is a live stream that goes on instead of a fixed-length video.
 | 
			
		||||
	IsLive *bool `json:"is_live"`
 | 
			
		||||
 | 
			
		||||
	// Whether this video was originally a live stream.
 | 
			
		||||
	WasLive *bool `json:"was_live"`
 | 
			
		||||
 | 
			
		||||
	// None (=unknown), 'is_live', 'is_upcoming', 'was_live', 'not_live',
 | 
			
		||||
	// or 'post_live' (was live, but VOD is not yet processed)
 | 
			
		||||
	// If absent, automatically set from is_live, was_live
 | 
			
		||||
	LiveStatus *string `json:"live_status"`
 | 
			
		||||
 | 
			
		||||
	// Time in seconds where the reproduction should start, as
 | 
			
		||||
	// specified in the URL.
 | 
			
		||||
	StartTime *int64 `json:"start_time"`
 | 
			
		||||
 | 
			
		||||
	// Time in seconds where the reproduction should end, as
 | 
			
		||||
	// specified in the URL.
 | 
			
		||||
	EndTime *int64 `json:"end_time"`
 | 
			
		||||
 | 
			
		||||
	Chapters []Chapter `json:"chapters"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Format struct {
 | 
			
		||||
	// The mandatory URL representing the media:
 | 
			
		||||
	//   for plain file media - HTTP URL of this file,
 | 
			
		||||
	//   for RTMP - RTMP URL,
 | 
			
		||||
	//   for HLS - URL of the M3U8 media playlist,
 | 
			
		||||
	//   for HDS - URL of the F4M manifest,
 | 
			
		||||
	//   for DASH
 | 
			
		||||
	//     - HTTP URL to plain file media (in case of
 | 
			
		||||
	//   	unfragmented media)
 | 
			
		||||
	//     - URL of the MPD manifest or base URL
 | 
			
		||||
	// 	  representing the media if MPD manifest
 | 
			
		||||
	//   	is parsed from a string (in case of
 | 
			
		||||
	//   	fragmented media)
 | 
			
		||||
	//   for MSS - URL of the ISM manifest.
 | 
			
		||||
	Url string `json:"url"`
 | 
			
		||||
 | 
			
		||||
	// Will be calculated from URL if missing
 | 
			
		||||
	Ext string `json:"ext"`
 | 
			
		||||
 | 
			
		||||
	// A human-readable description of the format
 | 
			
		||||
	// ("mp4 container with h264/opus").
 | 
			
		||||
	// Calculated from the format_id, width, height.
 | 
			
		||||
	// and format_note fields if missing.
 | 
			
		||||
	Format string `json:"format"`
 | 
			
		||||
 | 
			
		||||
	// A short description of the format
 | 
			
		||||
	// ("mp4_h264_opus" or "19").
 | 
			
		||||
	// Technically optional, but strongly recommended.
 | 
			
		||||
	FormatID string `json:"format_id"`
 | 
			
		||||
 | 
			
		||||
	// Additional info about the format
 | 
			
		||||
	// ("3D" or "DASH video")
 | 
			
		||||
	FormatNote string `json:"format_note"`
 | 
			
		||||
 | 
			
		||||
	// Width of the video, if known
 | 
			
		||||
	Width int `json:"width"`
 | 
			
		||||
 | 
			
		||||
	// Height of the video, if known
 | 
			
		||||
	Height int `json:"height"`
 | 
			
		||||
 | 
			
		||||
	// Aspect ratio of the video, if known
 | 
			
		||||
	// Automatically calculated from width and height
 | 
			
		||||
	AspectRatio float64 `json:"aspect_ratio"`
 | 
			
		||||
 | 
			
		||||
	// Textual description of width and height
 | 
			
		||||
	// Automatically calculated from width and height
 | 
			
		||||
	Resolution string `json:"resolution"`
 | 
			
		||||
 | 
			
		||||
	// The dynamic range of the video. One of:
 | 
			
		||||
	// "SDR" (None), "HDR10", "HDR10+, "HDR12", "HLG, "DV"
 | 
			
		||||
	DynamicRange string `json:"dynamic_range"`
 | 
			
		||||
 | 
			
		||||
	// Average bitrate of audio and video in KBit/s
 | 
			
		||||
	Tbr float64 `json:"tbr"`
 | 
			
		||||
 | 
			
		||||
	// Average audio bitrate in KBit/s
 | 
			
		||||
	Abr float64 `json:"abr"`
 | 
			
		||||
 | 
			
		||||
	// Average video bitrate in KBit/s
 | 
			
		||||
	Vbr float64 `json:"vbr"`
 | 
			
		||||
 | 
			
		||||
	// Name of the audio codec in use
 | 
			
		||||
	ACodec string `json:"acodec"`
 | 
			
		||||
 | 
			
		||||
	// Name of the video codec in use
 | 
			
		||||
	VCodec string `json:"vcodec"`
 | 
			
		||||
 | 
			
		||||
	// Number of audio channels
 | 
			
		||||
	AudioChannels int `json:"audio_channels"`
 | 
			
		||||
 | 
			
		||||
	// Frame rate
 | 
			
		||||
	Fps float64 `json:"fps"`
 | 
			
		||||
 | 
			
		||||
	// Name of the container format
 | 
			
		||||
	Container string `json:"container"`
 | 
			
		||||
 | 
			
		||||
	// The number of bytes, if known in advance
 | 
			
		||||
	Filesize *int `json:"filesize"`
 | 
			
		||||
 | 
			
		||||
	// An estimate for the number of bytes
 | 
			
		||||
	FilesizeApprox int `json:"filesize_approx"`
 | 
			
		||||
 | 
			
		||||
	// The protocol that will be used for the actual
 | 
			
		||||
	// download, lower-case. One of "http", "https" or
 | 
			
		||||
	// one of the protocols defined in downloader.PROTOCOL_MAP
 | 
			
		||||
	Protocol string `json:"protocol"`
 | 
			
		||||
 | 
			
		||||
	// Base URL for fragments. Each fragment's path
 | 
			
		||||
	// value (if present) will be relative to
 | 
			
		||||
	// this URL.
 | 
			
		||||
	FragmentBaseUrl *string `json:"fragment_base_url"`
 | 
			
		||||
 | 
			
		||||
	// A list of fragments of a fragmented media.
 | 
			
		||||
	// Each fragment entry must contain either an url
 | 
			
		||||
	// or a path. If an url is present it should be
 | 
			
		||||
	// considered by a client. Otherwise both path and
 | 
			
		||||
	// fragment_base_url must be present.
 | 
			
		||||
	Fragments []Fragment `json:"fragments"`
 | 
			
		||||
 | 
			
		||||
	// Is a live format that can be downloaded from the start.
 | 
			
		||||
	IsFromStart bool `json:"is_from_start"`
 | 
			
		||||
 | 
			
		||||
	// Order number of this format. If this field is
 | 
			
		||||
	// present and not None, the formats get sorted
 | 
			
		||||
	// by this field, regardless of all other values.
 | 
			
		||||
	// -1 for default (order by other properties),
 | 
			
		||||
	// -2 or smaller for less than default.
 | 
			
		||||
	// < -1000 to hide the format (if there is
 | 
			
		||||
	//    another one which is strictly better)
 | 
			
		||||
	Preference *int `json:"preference"`
 | 
			
		||||
 | 
			
		||||
	// Language code, e.g. "de" or "en-US".
 | 
			
		||||
	Language string `json:"language"`
 | 
			
		||||
 | 
			
		||||
	// Is this in the language mentioned in
 | 
			
		||||
	// the URL?
 | 
			
		||||
	// 10 if it's what the URL is about,
 | 
			
		||||
	// -1 for default (don't know),
 | 
			
		||||
	// -10 otherwise, other values reserved for now.
 | 
			
		||||
	LanguagePreference int `json:"language_preference"`
 | 
			
		||||
 | 
			
		||||
	// Order number of the video quality of this
 | 
			
		||||
	// format, irrespective of the file format.
 | 
			
		||||
	// -1 for default (order by other properties),
 | 
			
		||||
	// -2 or smaller for less than default.
 | 
			
		||||
	Quality float64 `json:"quality"`
 | 
			
		||||
 | 
			
		||||
	// Order number for this video source
 | 
			
		||||
	// 	(quality takes higher priority)
 | 
			
		||||
	// 	-1 for default (order by other properties),
 | 
			
		||||
	// 	-2 or smaller for less than default.
 | 
			
		||||
	SourcePreference int `json:"source_preference"`
 | 
			
		||||
 | 
			
		||||
	// A dictionary of additional HTTP headers to add to the request.
 | 
			
		||||
	HttpHeaders map[string]string `json:"http_header"`
 | 
			
		||||
 | 
			
		||||
	// If given and not 1, indicates that the
 | 
			
		||||
	// video's pixels are not square.
 | 
			
		||||
	// width : height ratio as float.
 | 
			
		||||
	StretchedRatio *float64 `json:"stretched_ratio"`
 | 
			
		||||
 | 
			
		||||
	// The server does not support resuming the (HTTP or RTMP) download.
 | 
			
		||||
	NoResume bool `json:"no_resume"`
 | 
			
		||||
 | 
			
		||||
	// The format has DRM and cannot be downloaded.
 | 
			
		||||
	HasDrm bool `json:"has_drm"`
 | 
			
		||||
 | 
			
		||||
	// A query string to append to each
 | 
			
		||||
	// fragment's URL, or to update each existing query string
 | 
			
		||||
	// with. Only applied by the native HLS/DASH downloaders.
 | 
			
		||||
	ExtraParamToSegmentUrl string `json:"extra_param_to_segment_url"`
 | 
			
		||||
 | 
			
		||||
	// A dictionary of HLS AES-128 decryption information
 | 
			
		||||
	// used by the native HLS downloader to override the
 | 
			
		||||
	// values in the media playlist when an '#EXT-X-KEY' tag
 | 
			
		||||
	// is present in the playlist
 | 
			
		||||
	HlsAes *HlsAes `json:"hls_aes"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Fragment struct {
 | 
			
		||||
	// fragment's URL
 | 
			
		||||
	Url string `json:"url"`
 | 
			
		||||
 | 
			
		||||
	// fragment's path relative to fragment_base_url
 | 
			
		||||
	Path string `json:"path"`
 | 
			
		||||
 | 
			
		||||
	Duration *float64 `json:"duration"`
 | 
			
		||||
	Filesize *int     `json:"filesize"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HlsAes struct {
 | 
			
		||||
	// The URI from which the key will be downloaded
 | 
			
		||||
	Uri string `json:"uri"`
 | 
			
		||||
 | 
			
		||||
	// The key (as hex) used to decrypt fragments.
 | 
			
		||||
	// If `key` is given, any key URI will be ignored
 | 
			
		||||
	Key string `json:"key"`
 | 
			
		||||
 | 
			
		||||
	// The IV (as hex) used to decrypt fragments
 | 
			
		||||
	Iv string `json:"iv"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Thumbnail struct {
 | 
			
		||||
	// Thumbnail format ID
 | 
			
		||||
	ID *string `json:"id"`
 | 
			
		||||
 | 
			
		||||
	Url string `json:"url"`
 | 
			
		||||
 | 
			
		||||
	// Quality of the image
 | 
			
		||||
	Preference *int `json:"preference"`
 | 
			
		||||
 | 
			
		||||
	Width    *int `json:"width"`
 | 
			
		||||
	Height   *int `json:"height"`
 | 
			
		||||
	Filesize *int `json:"filesize"`
 | 
			
		||||
 | 
			
		||||
	// HTTP headers for the request
 | 
			
		||||
	HttpHeaders map[string]string `json:"http_headers"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Subtitle struct {
 | 
			
		||||
	// Will be calculated from URL if missing
 | 
			
		||||
	Ext string `json:"ext"`
 | 
			
		||||
 | 
			
		||||
	// The subtitles file contents
 | 
			
		||||
	Data string `json:"data"`
 | 
			
		||||
 | 
			
		||||
	// A URL pointing to the subtitles file
 | 
			
		||||
	Url string `json:"url"`
 | 
			
		||||
 | 
			
		||||
	// Name or description of the subtitles
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
 | 
			
		||||
	// A dictionary of additional HTTP headers to add to the request.
 | 
			
		||||
	HttpHeaders map[string]string `json:"http_headers"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Comment struct {
 | 
			
		||||
	// human-readable name of the comment author
 | 
			
		||||
	Author *string `json:"author"`
 | 
			
		||||
 | 
			
		||||
	// user ID of the comment author
 | 
			
		||||
	AuthorID *string `json:"author_id"`
 | 
			
		||||
 | 
			
		||||
	// The thumbnail of the comment author
 | 
			
		||||
	AuthorThumbnail *string `json:"author_thumbnail"`
 | 
			
		||||
 | 
			
		||||
	// Comment ID
 | 
			
		||||
	ID *string `json:"id"`
 | 
			
		||||
 | 
			
		||||
	// Comment as HTML
 | 
			
		||||
	HTML *string `json:"html"`
 | 
			
		||||
 | 
			
		||||
	// Plain text of the comment
 | 
			
		||||
	Text *string `json:"text"`
 | 
			
		||||
 | 
			
		||||
	// UNIX timestamp of comment
 | 
			
		||||
	Timestamp *int64 `json:"timestamp"`
 | 
			
		||||
 | 
			
		||||
	// ID of the comment this one is replying to.
 | 
			
		||||
	// Set to "root" to indicate that this is a
 | 
			
		||||
	// comment to the original video.
 | 
			
		||||
	Parent string `json:"parent"`
 | 
			
		||||
 | 
			
		||||
	// Number of positive ratings of the comment
 | 
			
		||||
	LikeCount *int64 `json:"like_count"`
 | 
			
		||||
 | 
			
		||||
	// Number of negative ratings of the comment
 | 
			
		||||
	DislikeCount *int64 `json:"dislike_count"`
 | 
			
		||||
 | 
			
		||||
	// Whether the comment is marked as
 | 
			
		||||
	// favorite by the video uploader
 | 
			
		||||
	IsFavorited bool `json:"is_favorited"`
 | 
			
		||||
 | 
			
		||||
	// Whether the comment is made by
 | 
			
		||||
	// the video uploader
 | 
			
		||||
	AuthorIsUploader bool `json:"author_is_uploader"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Chapter struct {
 | 
			
		||||
	// The start time of the chapter in seconds
 | 
			
		||||
	StartTime int64 `json:"start_time"`
 | 
			
		||||
 | 
			
		||||
	// The end time of the chapter in seconds
 | 
			
		||||
	EndTime int64 `json:"end_time"`
 | 
			
		||||
 | 
			
		||||
	Title *string `json:"title"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
package ytdl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetMetadata(url string) (Metdata, error) {
 | 
			
		||||
	cmd := exec.Command(
 | 
			
		||||
		"yt-dlp",
 | 
			
		||||
		"-J",
 | 
			
		||||
		url,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	var out bytes.Buffer
 | 
			
		||||
	cmd.Stdout = &out
 | 
			
		||||
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		return Metdata{}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var meta Metdata
 | 
			
		||||
	if err := json.Unmarshal(out.Bytes(), &meta); err != nil {
 | 
			
		||||
		return Metdata{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return meta, nil
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue