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 (
|
require (
|
||||||
github.com/gofiber/fiber/v2 v2.43.0
|
github.com/gofiber/fiber/v2 v2.43.0
|
||||||
github.com/gofiber/template v1.8.0
|
github.com/gofiber/template v1.8.0
|
||||||
|
github.com/samber/lo v1.38.1
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/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/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/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 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
||||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
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=
|
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-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-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-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-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/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=
|
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
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"go.fifitido.net/ytdl-web/ytdl"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Serve() error {
|
func Serve() error {
|
||||||
|
@ -13,10 +19,29 @@ func Serve() error {
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Get("/download", func(c *fiber.Ctx) 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{
|
return c.Render("views/download", fiber.Map{
|
||||||
"Url": url,
|
"Url": url, "Meta": meta, "Formats": formats,
|
||||||
}, "views/layouts/main")
|
}, "views/layouts/main")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
19
web/views.go
19
web/views.go
|
@ -2,8 +2,10 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/gofiber/template/html"
|
"github.com/gofiber/template/html"
|
||||||
)
|
)
|
||||||
|
@ -14,10 +16,23 @@ var viewsfs embed.FS
|
||||||
func ViewsEngine() *html.Engine {
|
func ViewsEngine() *html.Engine {
|
||||||
engine := html.NewFileSystem(http.FS(viewsfs), ".html")
|
engine := html.NewFileSystem(http.FS(viewsfs), ".html")
|
||||||
engine.AddFunc(
|
engine.AddFunc(
|
||||||
// add unescape function
|
"unsafe", func(s string) template.HTML {
|
||||||
"unescape", func(s string) template.HTML {
|
|
||||||
return template.HTML(s)
|
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
|
return engine
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,37 @@
|
||||||
<h1>Download</h1>
|
<div class="d-flex flex-column align-items-center">
|
||||||
<p>{{.Url}}</p>
|
<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">
|
<div class="mb-3">
|
||||||
<label for="url" class="form-label visually-hidden">Url</label>
|
<label for="url" class="form-label visually-hidden">Url</label>
|
||||||
<div class="input-group">
|
<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
|
<button
|
||||||
id="paste-button"
|
id="paste-button"
|
||||||
class="btn btn-outline-secondary"
|
class="btn btn-outline-secondary"
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body data-bs-theme="dark">
|
<body data-bs-theme="dark">
|
||||||
{{template "views/partials/navbar" .}}
|
{{template "views/partials/navbar" .}}
|
||||||
<main class="container mt-5">{{embed}}</main>
|
<main class="container my-5">{{embed}}</main>
|
||||||
<script>
|
<script>
|
||||||
const pasteButton = document.getElementById("paste-button");
|
const pasteButton = document.getElementById("paste-button");
|
||||||
const urlField = document.getElementById("url");
|
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