Init
This commit is contained in:
parent
895e23624e
commit
392e09bc20
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/run.sh
|
||||
/yt-dlp-telegram-bot
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"go.lintTool": "golangci-lint"
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Norbert Varga
|
||||
Copyright (c) 2023 Norbert "Nonoo" Varga <nonoo@nonoo.hu>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
96
README.md
Normal file
96
README.md
Normal file
@ -0,0 +1,96 @@
|
||||
# yt-dlp-telegram-bot
|
||||
|
||||
This bot downloads videos from various supported sources
|
||||
(see [yt-dlp](https://github.com/yt-dlp/yt-dlp) and then re-uploads them
|
||||
to Telegram, so they can be viewed with Telegram's built-in video player.
|
||||
|
||||
<p align="center"><img src="screenshot.png?raw=true"/></p>
|
||||
|
||||
The bot displays the progress and further information during processing by
|
||||
responding to the message with the URL. Requests are queued, only one gets
|
||||
processed at a time.
|
||||
|
||||
The bot uses the [Telegram MTProto API](https://github.com/gotd/td), which
|
||||
supports larger video uploads than the default 50MB with the standard
|
||||
Telegram bot API. Videos are not saved on disk, they are simultaneously
|
||||
uploaded from the source to Telegram. Incompatible video and audio streams
|
||||
are automatically converted to match those which are supported by Telegram's
|
||||
built-in video player.
|
||||
|
||||
The only dependencies are [yt-dlp](https://github.com/yt-dlp/yt-dlp) and
|
||||
[ffmpeg](https://github.com/FFmpeg/FFmpeg). Tested on Linux, but should be
|
||||
able to run on other operating systems.
|
||||
|
||||
## Compiling
|
||||
|
||||
You'll need Go installed on your computer. Install a recent package of `golang`.
|
||||
Then:
|
||||
|
||||
```
|
||||
go get github.com/nonoo/yt-dlp-telegram-bot
|
||||
go install github.com/nonoo/yt-dlp-telegram-bot
|
||||
```
|
||||
|
||||
This will typically install `yt-dlp-telegram-bot` into `$HOME/go/bin`.
|
||||
|
||||
Or just enter `go build` in the cloned Git source repo directory.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Create a Telegram bot using [BotFather](https://t.me/BotFather) and get the
|
||||
bot's `token`.
|
||||
2. [Get your Telegram API Keys](https://my.telegram.org/apps)
|
||||
(`api_id` and `api_hash`). You'll need to create an app if you haven't
|
||||
created one already. Description is optional, set the category to "other".
|
||||
If an error dialog pops up, then try creating the app using your phone's
|
||||
browser.
|
||||
3. Make sure `yt-dlp`, `ffprobe` and `ffmpeg` commands are available on your
|
||||
system.
|
||||
|
||||
## Running
|
||||
|
||||
You can get the available command line arguments with `-h`.
|
||||
Mandatory arguments are:
|
||||
|
||||
- `-api-id`: set this to your Telegram app `api_id`
|
||||
- `-api-hash`: set this to your Telegram app `api_hash`
|
||||
- `-bot-token`: set this to your Telegram bot's `token`
|
||||
|
||||
Set your Telegram user ID as an admin with the `-admin-user-ids` argument.
|
||||
Admins will get a message when the bot starts and when a newer version of
|
||||
`yt-dlp` is available (checked every 24 hours).
|
||||
|
||||
Other user/group IDs can be set with the `-allowed-user-ids` and
|
||||
`-allowed-group-ids` arguments. IDs should be separated by commas.
|
||||
|
||||
You can get Telegram user IDs by writing a message to the bot and checking
|
||||
the app's log, as it logs all incoming messages.
|
||||
|
||||
All command line arguments can be set through OS environment variables.
|
||||
Note that using a command line argument overwrites a setting by the environment
|
||||
variable. Available OS environment variables are:
|
||||
|
||||
- `APP_ID`
|
||||
- `API_HASH`
|
||||
- `BOT_TOKEN`
|
||||
- `YTDLP_PATH`
|
||||
- `ALLOWED_USERIDS`
|
||||
- `ADMIN_USERIDS`
|
||||
- `ALLOWED_GROUPIDS`
|
||||
|
||||
## Supported commands
|
||||
|
||||
- `/dlp` - Download
|
||||
- `/dlpcancel` - Cancel ongoing download
|
||||
|
||||
You don't need to enter the `/dlp` command if you send an URL to the bot using
|
||||
a private chat.
|
||||
|
||||
## Contributors
|
||||
|
||||
- Norbert Varga [nonoo@nonoo.hu](mailto:nonoo@nonoo.hu)
|
||||
- Akos Marton
|
||||
|
||||
## Donations
|
||||
|
||||
If you find this bot useful then [buy me a beer](https://paypal.me/ha2non). :)
|
||||
52
cmd.go
Normal file
52
cmd.go
Normal file
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// https://stackoverflow.com/questions/71714228/go-exec-commandcontext-is-not-being-terminated-after-context-timeout
|
||||
|
||||
type Cmd struct {
|
||||
ctx context.Context
|
||||
*exec.Cmd
|
||||
}
|
||||
|
||||
// NewCommand is like exec.CommandContext but ensures that subprocesses
|
||||
// are killed when the context times out, not just the top level process.
|
||||
func NewCommand(ctx context.Context, command string, args ...string) *Cmd {
|
||||
return &Cmd{ctx, exec.Command(command, args...)}
|
||||
}
|
||||
|
||||
func (c *Cmd) Start() error {
|
||||
// Force-enable setpgid bit so that we can kill child processes when the
|
||||
// context times out or is canceled.
|
||||
if c.Cmd.SysProcAttr == nil {
|
||||
c.Cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
}
|
||||
c.Cmd.SysProcAttr.Setpgid = true
|
||||
err := c.Cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
<-c.ctx.Done()
|
||||
p := c.Cmd.Process
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
// Kill by negative PID to kill the process group, which includes
|
||||
// the top-level process we spawned as well as any subprocesses
|
||||
// it spawned.
|
||||
_ = syscall.Kill(-p.Pid, syscall.SIGKILL)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cmd) Run() error {
|
||||
if err := c.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Wait()
|
||||
}
|
||||
246
convert.go
Normal file
246
convert.go
Normal file
@ -0,0 +1,246 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ffmpeg_go "github.com/u2takey/ffmpeg-go"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const probeTimeout = 10 * time.Second
|
||||
const maxFFmpegProbeBytes = 20 * 1024 * 1024
|
||||
|
||||
var compatibleVideoCodecs = []string{"h264", "vp9"}
|
||||
var compatibleAudioCodecs = []string{"aac", "opus", "mp3"}
|
||||
|
||||
type ffmpegProbeDataStreamsStream struct {
|
||||
CodecName string `json:"codec_name"`
|
||||
CodecType string `json:"codec_type"`
|
||||
}
|
||||
|
||||
type ffmpegProbeDataFormat struct {
|
||||
FormatName string `json:"format_name"`
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
|
||||
type ffmpegProbeData struct {
|
||||
Streams []ffmpegProbeDataStreamsStream `json:"streams"`
|
||||
Format ffmpegProbeDataFormat `json:"format"`
|
||||
}
|
||||
|
||||
type Converter struct {
|
||||
VideoCodecs string
|
||||
VideoConvertNeeded bool
|
||||
SingleVideoStreamNeeded bool
|
||||
|
||||
AudioCodecs string
|
||||
AudioConvertNeeded bool
|
||||
SingleAudioStreamNeeded bool
|
||||
|
||||
Duration float64
|
||||
|
||||
UpdateProgressPercentCallback UpdateProgressPercentCallbackFunc
|
||||
}
|
||||
|
||||
func (c *Converter) Probe(rr *ReReadCloser) error {
|
||||
defer func() {
|
||||
// Restart and replay buffer data used when probing
|
||||
rr.Restarted = true
|
||||
}()
|
||||
|
||||
fmt.Println(" probing file...")
|
||||
i, err := ffmpeg_go.ProbeReaderWithTimeout(io.LimitReader(rr, maxFFmpegProbeBytes), probeTimeout, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error probing file: %w", err)
|
||||
}
|
||||
|
||||
pd := ffmpegProbeData{}
|
||||
err = json.Unmarshal([]byte(i), &pd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding probe result: %w", err)
|
||||
}
|
||||
|
||||
c.Duration, err = strconv.ParseFloat(pd.Format.Duration, 64)
|
||||
if err != nil {
|
||||
fmt.Println(" error parsing duration:", err)
|
||||
}
|
||||
|
||||
gotVideoStream := false
|
||||
gotAudioStream := false
|
||||
for _, stream := range pd.Streams {
|
||||
if stream.CodecType == "video" {
|
||||
if c.VideoCodecs != "" {
|
||||
c.VideoCodecs += ", "
|
||||
}
|
||||
c.VideoCodecs += stream.CodecName
|
||||
|
||||
if gotVideoStream {
|
||||
fmt.Println(" got additional video stream")
|
||||
c.SingleVideoStreamNeeded = true
|
||||
} else if !c.VideoConvertNeeded {
|
||||
if !slices.Contains(compatibleVideoCodecs, stream.CodecName) {
|
||||
fmt.Println(" found incompatible video codec:", stream.CodecName)
|
||||
c.VideoConvertNeeded = true
|
||||
} else {
|
||||
fmt.Println(" found video codec:", stream.CodecName)
|
||||
}
|
||||
gotVideoStream = true
|
||||
}
|
||||
} else if stream.CodecType == "audio" {
|
||||
if c.AudioCodecs != "" {
|
||||
c.AudioCodecs += ", "
|
||||
}
|
||||
c.AudioCodecs += stream.CodecName
|
||||
|
||||
if gotAudioStream {
|
||||
fmt.Println(" got additional audio stream")
|
||||
c.SingleAudioStreamNeeded = true
|
||||
} else if !c.AudioConvertNeeded {
|
||||
if !slices.Contains(compatibleAudioCodecs, stream.CodecName) {
|
||||
fmt.Println(" found not compatible audio codec:", stream.CodecName)
|
||||
c.AudioConvertNeeded = true
|
||||
} else {
|
||||
fmt.Println(" found audio codec:", stream.CodecName)
|
||||
}
|
||||
gotAudioStream = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !gotVideoStream {
|
||||
return fmt.Errorf("no video stream found in file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Converter) ffmpegProgressSock() (sockFilename string, sock net.Listener, err error) {
|
||||
sockFilename = path.Join(os.TempDir(), fmt.Sprintf("yt-dlp-telegram-bot-%d.sock", rand.Int()))
|
||||
sock, err = net.Listen("unix", sockFilename)
|
||||
if err != nil {
|
||||
fmt.Println(" ffmpeg progress socket create error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
re := regexp.MustCompile(`out_time_ms=(\d+)\n`)
|
||||
|
||||
fd, err := sock.Accept()
|
||||
if err != nil {
|
||||
fmt.Println(" ffmpeg progress socket accept error:", err)
|
||||
return
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
buf := make([]byte, 64)
|
||||
data := ""
|
||||
|
||||
for {
|
||||
_, err := fd.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data += string(buf)
|
||||
a := re.FindAllStringSubmatch(data, -1)
|
||||
|
||||
if len(a) > 0 && len(a[len(a)-1]) > 0 {
|
||||
data = ""
|
||||
l, _ := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1])
|
||||
c.UpdateProgressPercentCallback(int(100 * float64(l) / c.Duration / 1000000))
|
||||
}
|
||||
|
||||
if strings.Contains(data, "progress=end") {
|
||||
c.UpdateProgressPercentCallback(100)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Converter) GetActionsNeeded() string {
|
||||
var convertNeeded []string
|
||||
if c.VideoConvertNeeded || c.SingleVideoStreamNeeded {
|
||||
convertNeeded = append(convertNeeded, "video")
|
||||
}
|
||||
if c.AudioConvertNeeded || c.SingleAudioStreamNeeded {
|
||||
convertNeeded = append(convertNeeded, "audio")
|
||||
}
|
||||
return strings.Join(convertNeeded, ", ")
|
||||
}
|
||||
|
||||
func (c *Converter) ConvertIfNeeded(ctx context.Context, rr *ReReadCloser) (io.ReadCloser, error) {
|
||||
reader, writer := io.Pipe()
|
||||
var cmd *Cmd
|
||||
|
||||
fmt.Print(" converting ", c.GetActionsNeeded(), "...\n")
|
||||
|
||||
args := ffmpeg_go.KwArgs{"format": "mp4", "movflags": "frag_keyframe+empty_moov+faststart"}
|
||||
|
||||
if c.VideoConvertNeeded {
|
||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:v": "libx264", "crf": 30, "preset": "veryfast"}})
|
||||
} else {
|
||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:v": "copy"}})
|
||||
}
|
||||
|
||||
if c.AudioConvertNeeded {
|
||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:a": "mp3", "q:a": 0}})
|
||||
} else {
|
||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:a": "copy"}})
|
||||
}
|
||||
|
||||
if c.SingleVideoStreamNeeded || c.SingleAudioStreamNeeded {
|
||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"map": "0:v:0,0:a:0"}})
|
||||
}
|
||||
|
||||
ff := ffmpeg_go.Input("pipe:0").Output("pipe:1", args)
|
||||
|
||||
var err error
|
||||
var progressSock net.Listener
|
||||
if c.UpdateProgressPercentCallback != nil {
|
||||
if c.Duration > 0 {
|
||||
var progressSockFilename string
|
||||
progressSockFilename, progressSock, err = c.ffmpegProgressSock()
|
||||
if err == nil {
|
||||
ff = ff.GlobalArgs("-progress", "unix:"+progressSockFilename)
|
||||
}
|
||||
} else {
|
||||
c.UpdateProgressPercentCallback(-1)
|
||||
}
|
||||
}
|
||||
|
||||
ffCmd := ff.WithInput(rr).WithOutput(writer).Compile()
|
||||
|
||||
// Creating a new cmd with a timeout context, which will kill the cmd if it takes too long.
|
||||
cmd = NewCommand(ctx, ffCmd.Args[0], ffCmd.Args[1:]...)
|
||||
cmd.Stdin = ffCmd.Stdin
|
||||
cmd.Stdout = ffCmd.Stdout
|
||||
|
||||
// This goroutine handles copying from the input (either rr or cmd.Stdout) to writer.
|
||||
go func() {
|
||||
err = cmd.Run()
|
||||
writer.Close()
|
||||
if progressSock != nil {
|
||||
progressSock.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
writer.Close()
|
||||
return nil, fmt.Errorf("error converting: %w", err)
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
78
dl.go
Normal file
78
dl.go
Normal file
@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/wader/goutubedl"
|
||||
)
|
||||
|
||||
const downloadAndConvertTimeout = 5 * time.Minute
|
||||
|
||||
type ProbeStartCallbackFunc func(ctx context.Context)
|
||||
type ConvertStartCallbackFunc func(ctx context.Context, videoCodecs, audioCodecs, convertActionsNeeded string)
|
||||
type UpdateProgressPercentCallbackFunc func(progressPercent int)
|
||||
|
||||
type Downloader struct {
|
||||
ProbeStartFunc ProbeStartCallbackFunc
|
||||
ConvertStartFunc ConvertStartCallbackFunc
|
||||
UpdateProgressPercentFunc UpdateProgressPercentCallbackFunc
|
||||
}
|
||||
|
||||
type goYouTubeDLLogger struct{}
|
||||
|
||||
func (l goYouTubeDLLogger) Print(v ...interface{}) {
|
||||
fmt.Print(" yt-dlp dbg:")
|
||||
fmt.Println(v...)
|
||||
}
|
||||
|
||||
func (d *Downloader) downloadURL(dlCtx context.Context, url string) (rr *ReReadCloser, err error) {
|
||||
result, err := goutubedl.New(dlCtx, url, goutubedl.Options{
|
||||
Type: goutubedl.TypeSingle,
|
||||
DebugLog: goYouTubeDLLogger{},
|
||||
MergeOutputFormat: "mkv", // This handles VP9 properly. yt-dlp uses mp4 by default, which doesn't.
|
||||
SortingFormat: "res:720", // Prefer videos no larger than 720p to keep their size small.
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("preparing download %q: %w", url, err)
|
||||
}
|
||||
|
||||
dlResult, err := result.Download(dlCtx, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("downloading %q: %w", url, err)
|
||||
}
|
||||
|
||||
return NewReReadCloser(dlResult), nil
|
||||
}
|
||||
|
||||
func (d *Downloader) DownloadAndConvertURL(ctx context.Context, url string) (r io.ReadCloser, err error) {
|
||||
rr, err := d.downloadURL(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conv := Converter{
|
||||
UpdateProgressPercentCallback: d.UpdateProgressPercentFunc,
|
||||
}
|
||||
|
||||
if d.ProbeStartFunc != nil {
|
||||
d.ProbeStartFunc(ctx)
|
||||
}
|
||||
|
||||
if err := conv.Probe(rr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.ConvertStartFunc != nil {
|
||||
d.ConvertStartFunc(ctx, conv.VideoCodecs, conv.AudioCodecs, conv.GetActionsNeeded())
|
||||
}
|
||||
|
||||
r, err = conv.ConvertIfNeeded(ctx, rr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
45
go.mod
Normal file
45
go.mod
Normal file
@ -0,0 +1,45 @@
|
||||
module github.com/nonoo/yt-dlp-telegram-bot
|
||||
|
||||
go 1.20
|
||||
|
||||
replace github.com/wader/goutubedl => github.com/nonoo/goutubedl v0.0.0-20230814114826-c1dcced79138
|
||||
|
||||
require (
|
||||
github.com/google/go-github/v53 v53.2.0
|
||||
github.com/gotd/td v0.84.0
|
||||
github.com/u2takey/ffmpeg-go v0.5.0
|
||||
github.com/wader/goutubedl v0.0.0-00010101000000-000000000000
|
||||
golang.org/x/exp v0.0.0-20230116083435-1de6713980de
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
||||
github.com/aws/aws-sdk-go v1.38.20 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/go-faster/errors v0.6.1 // indirect
|
||||
github.com/go-faster/jx v1.0.1 // indirect
|
||||
github.com/go-faster/xor v1.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/gotd/ige v0.2.2 // indirect
|
||||
github.com/gotd/neo v0.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/u2takey/go-utils v0.3.1 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.25.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/oauth2 v0.8.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
184
go.sum
Normal file
184
go.sum
Normal file
@ -0,0 +1,184 @@
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/aws/aws-sdk-go v1.38.20 h1:QbzNx/tdfATbdKfubBpkt84OM6oBkxQZRw6+bW2GyeA=
|
||||
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
||||
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
||||
github.com/go-faster/jx v1.0.1 h1:NhSJEZtqj6KmXf63On7Hg7/sjUX+gotSc/eM6bZCZ00=
|
||||
github.com/go-faster/jx v1.0.1/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
|
||||
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
||||
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
|
||||
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI=
|
||||
github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
|
||||
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
||||
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
||||
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||
github.com/gotd/td v0.84.0 h1:oWMp5HczCAFSgKWgWFCuYjELBgcRVcRpGLdQ1bP2kpg=
|
||||
github.com/gotd/td v0.84.0/go.mod h1:3dQsGL9rxMcS1Z9Na3S7U8e/pLMzbLIT2jM3E5IuUk0=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/nonoo/goutubedl v0.0.0-20230814114826-c1dcced79138 h1:zS49Q/Vxi7m1iPm10Or61K4tXUWFlb/D2jh3lk0sbfw=
|
||||
github.com/nonoo/goutubedl v0.0.0-20230814114826-c1dcced79138/go.mod h1:5KXd5tImdbmz4JoVhePtbIokCwAfEhUVVx3WLHmjYuw=
|
||||
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU=
|
||||
github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=
|
||||
github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=
|
||||
github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071 h1:QkrG4Zr5OVFuC9aaMPmFI0ibfhBZlAgtzDYWfu7tqQk=
|
||||
github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071/go.mod h1:XD6emOFPHVzb0+qQpiNOdPL2XZ0SRUM0N5JHuq6OmXo=
|
||||
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
|
||||
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
|
||||
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/exp v0.0.0-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0=
|
||||
golang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
66
helper.go
Normal file
66
helper.go
Normal file
@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
)
|
||||
|
||||
// Helper function to pretty-print any Telegram API object to find out which it needs to be cast to.
|
||||
// https://github.com/gotd/td/blob/main/examples/pretty-print/main.go
|
||||
|
||||
// func formatObject(input interface{}) string {
|
||||
// o, ok := input.(tdp.Object)
|
||||
// if !ok {
|
||||
// // Handle tg.*Box values.
|
||||
// rv := reflect.Indirect(reflect.ValueOf(input))
|
||||
// for i := 0; i < rv.NumField(); i++ {
|
||||
// if v, ok := rv.Field(i).Interface().(tdp.Object); ok {
|
||||
// return formatObject(v)
|
||||
// }
|
||||
// }
|
||||
|
||||
// return fmt.Sprintf("%T (not object)", input)
|
||||
// }
|
||||
// return tdp.Format(o)
|
||||
// }
|
||||
|
||||
func getProgressbar(progressPercent, progressBarLen int) (progressBar string) {
|
||||
i := 0
|
||||
for ; i < progressPercent/(100/progressBarLen); i++ {
|
||||
progressBar += "▰"
|
||||
}
|
||||
for ; i < progressBarLen; i++ {
|
||||
progressBar += "▱"
|
||||
}
|
||||
progressBar += " " + fmt.Sprint(progressPercent) + "%"
|
||||
return
|
||||
}
|
||||
|
||||
func resolveMsgSrc(msg *tg.Message) (fromUser *tg.PeerUser, fromGroup *tg.PeerChat) {
|
||||
fromGroup, isGroupMsg := msg.PeerID.(*tg.PeerChat)
|
||||
if isGroupMsg {
|
||||
fromUser = msg.FromID.(*tg.PeerUser)
|
||||
} else {
|
||||
fromUser = msg.PeerID.(*tg.PeerUser)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getFromUsername(entities tg.Entities, fromUID int64) string {
|
||||
if fromUser, ok := entities.Users[fromUID]; ok {
|
||||
if un, ok := fromUser.GetUsername(); ok {
|
||||
return un
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func sendTextToAdmins(ctx context.Context, msg string) {
|
||||
for _, id := range params.AdminUserIDs {
|
||||
_, _ = telegramSender.To(&tg.InputPeerUser{
|
||||
UserID: id,
|
||||
}).Text(ctx, msg)
|
||||
}
|
||||
}
|
||||
184
main.go
Normal file
184
main.go
Normal file
@ -0,0 +1,184 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/telegram"
|
||||
"github.com/gotd/td/telegram/message"
|
||||
"github.com/gotd/td/telegram/uploader"
|
||||
"github.com/gotd/td/tg"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var dlQueue DownloadQueue
|
||||
|
||||
var telegramUploader *uploader.Uploader
|
||||
var telegramSender *message.Sender
|
||||
|
||||
func uploadFile(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, f io.ReadCloser) error {
|
||||
upload, err := telegramUploader.FromReader(ctx, "yt-dlp", f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("uploading %w", err)
|
||||
}
|
||||
|
||||
// Now we have uploaded file handle, sending it as styled message. First, preparing message.
|
||||
document := message.UploadedDocument(upload).Video()
|
||||
|
||||
// Sending message with media.
|
||||
if _, err := telegramSender.Answer(entities, u).Media(ctx, document); err != nil {
|
||||
return fmt.Errorf("send: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleCmdDLP(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, msg *tg.Message) {
|
||||
// Check if message is an URL.
|
||||
validURI := true
|
||||
uri, err := url.ParseRequestURI(msg.Message)
|
||||
if err != nil || (uri.Scheme != "http" && uri.Scheme != "https") {
|
||||
validURI = false
|
||||
} else {
|
||||
_, err = net.LookupHost(uri.Host)
|
||||
if err != nil {
|
||||
validURI = false
|
||||
}
|
||||
}
|
||||
if !validURI {
|
||||
fmt.Println(" (not an url)")
|
||||
_, _ = telegramSender.Reply(entities, u).Text(ctx, errorStr+": please enter an URL to download")
|
||||
return
|
||||
}
|
||||
|
||||
dlQueue.Add(ctx, entities, u, msg.Message)
|
||||
}
|
||||
|
||||
func handleCmdDLPCancel(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, msg *tg.Message) {
|
||||
dlQueue.CancelCurrentEntry(ctx, entities, u, msg.Message)
|
||||
}
|
||||
|
||||
func handleMsg(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage) error {
|
||||
msg, ok := u.Message.(*tg.Message)
|
||||
if !ok || msg.Out {
|
||||
// Outgoing message, not interesting.
|
||||
return nil
|
||||
}
|
||||
|
||||
fromUser, fromGroup := resolveMsgSrc(msg)
|
||||
fromUsername := getFromUsername(entities, fromUser.UserID)
|
||||
|
||||
fmt.Print("got message")
|
||||
if fromUsername != "" {
|
||||
fmt.Print(" from ", fromUsername, "#", fromUser.UserID)
|
||||
}
|
||||
fmt.Println(":", msg.Message)
|
||||
|
||||
if fromGroup != nil {
|
||||
fmt.Print(" msg from group #", -fromGroup.ChatID)
|
||||
if !slices.Contains(params.AllowedGroupIDs, -fromGroup.ChatID) {
|
||||
fmt.Println(", group not allowed, ignoring")
|
||||
return nil
|
||||
}
|
||||
fmt.Println()
|
||||
} else {
|
||||
if !slices.Contains(params.AllowedUserIDs, fromUser.UserID) {
|
||||
fmt.Println(" user not allowed, ignoring")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check if message is a command.
|
||||
if msg.Message[0] == '/' {
|
||||
cmd := strings.Split(msg.Message, " ")[0]
|
||||
if strings.Contains(cmd, "@") {
|
||||
cmd = strings.Split(cmd, "@")[0]
|
||||
}
|
||||
msg.Message = strings.TrimPrefix(msg.Message, cmd+" ")
|
||||
switch cmd {
|
||||
case "/dlp":
|
||||
handleCmdDLP(ctx, entities, u, msg)
|
||||
return nil
|
||||
case "/dlpcancel":
|
||||
handleCmdDLPCancel(ctx, entities, u, msg)
|
||||
return nil
|
||||
default:
|
||||
fmt.Println(" (invalid cmd)")
|
||||
_, _ = telegramSender.Reply(entities, u).Text(ctx, errorStr+": invalid command")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
handleCmdDLP(ctx, entities, u, msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("yt-dlp-telegram-bot starting...")
|
||||
|
||||
if err := params.Init(); err != nil {
|
||||
fmt.Println("error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Dispatcher handles incoming updates.
|
||||
dispatcher := tg.NewUpdateDispatcher()
|
||||
opts := telegram.Options{
|
||||
UpdateHandler: dispatcher,
|
||||
}
|
||||
var err error
|
||||
opts, err = telegram.OptionsFromEnvironment(opts)
|
||||
if err != nil {
|
||||
panic(fmt.Sprint("options from env err: ", err))
|
||||
}
|
||||
|
||||
client := telegram.NewClient(params.ApiID, params.ApiHash, opts)
|
||||
|
||||
if err := client.Run(context.Background(), func(ctx context.Context) error {
|
||||
status, err := client.Auth().Status(ctx)
|
||||
if err != nil {
|
||||
panic(fmt.Sprint("auth status err: ", err))
|
||||
}
|
||||
|
||||
if !status.Authorized { // Not logged in?
|
||||
fmt.Println("logging in...")
|
||||
if _, err := client.Auth().Bot(ctx, params.BotToken); err != nil {
|
||||
panic(fmt.Sprint("login err: ", err))
|
||||
}
|
||||
}
|
||||
|
||||
api := client.API()
|
||||
|
||||
telegramUploader = uploader.NewUploader(api)
|
||||
telegramSender = message.NewSender(api).WithUploader(telegramUploader)
|
||||
|
||||
dlQueue.Init(ctx)
|
||||
|
||||
dispatcher.OnNewMessage(handleMsg)
|
||||
|
||||
fmt.Println("telegram connection up")
|
||||
|
||||
ytdlpVersionCheckStr, _ := ytdlpVersionCheckGetStr(ctx)
|
||||
sendTextToAdmins(ctx, "🤖 Bot started, "+ytdlpVersionCheckStr)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(24 * time.Hour)
|
||||
if s, updateNeededOrError := ytdlpVersionCheckGetStr(ctx); updateNeededOrError {
|
||||
sendTextToAdmins(ctx, s)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
131
params.go
Normal file
131
params.go
Normal file
@ -0,0 +1,131 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/wader/goutubedl"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type paramsType struct {
|
||||
ApiID int
|
||||
ApiHash string
|
||||
BotToken string
|
||||
|
||||
AllowedUserIDs []int64
|
||||
AdminUserIDs []int64
|
||||
AllowedGroupIDs []int64
|
||||
}
|
||||
|
||||
var params paramsType
|
||||
|
||||
func (p *paramsType) Init() error {
|
||||
// Further available environment variables:
|
||||
// SESSION_FILE: path to session file
|
||||
// SESSION_DIR: path to session directory, if SESSION_FILE is not set
|
||||
|
||||
var apiID string
|
||||
flag.StringVar(&apiID, "api-id", "", "telegram api_id")
|
||||
flag.StringVar(&p.ApiHash, "api-hash", "", "telegram api_hash")
|
||||
flag.StringVar(&p.BotToken, "bot-token", "", "telegram bot token")
|
||||
flag.StringVar(&goutubedl.Path, "yt-dlp-path", "", "yt-dlp path")
|
||||
var allowedUserIDs string
|
||||
flag.StringVar(&allowedUserIDs, "allowed-user-ids", "", "allowed telegram user ids")
|
||||
var adminUserIDs string
|
||||
flag.StringVar(&adminUserIDs, "admin-user-ids", "", "admin telegram user ids")
|
||||
var allowedGroupIDs string
|
||||
flag.StringVar(&allowedGroupIDs, "allowed-group-ids", "", "allowed telegram group ids")
|
||||
flag.Parse()
|
||||
|
||||
var err error
|
||||
if apiID == "" {
|
||||
apiID = os.Getenv("API_ID")
|
||||
}
|
||||
if apiID == "" {
|
||||
return fmt.Errorf("api id not set")
|
||||
}
|
||||
p.ApiID, err = strconv.Atoi(apiID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid env var API_ID")
|
||||
}
|
||||
|
||||
if p.ApiHash == "" {
|
||||
p.ApiHash = os.Getenv("API_HASH")
|
||||
}
|
||||
if p.ApiHash == "" {
|
||||
return fmt.Errorf("api hash not set")
|
||||
}
|
||||
|
||||
if p.BotToken == "" {
|
||||
p.BotToken = os.Getenv("BOT_TOKEN")
|
||||
}
|
||||
if p.BotToken == "" {
|
||||
return fmt.Errorf("bot token not set")
|
||||
}
|
||||
|
||||
if goutubedl.Path == "" {
|
||||
goutubedl.Path = os.Getenv("YTDLP_PATH")
|
||||
}
|
||||
if goutubedl.Path == "" {
|
||||
goutubedl.Path = "yt-dlp"
|
||||
}
|
||||
goutubedl.Path, err = exec.LookPath(goutubedl.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("yt-dlp not found")
|
||||
}
|
||||
|
||||
if allowedUserIDs == "" {
|
||||
allowedUserIDs = os.Getenv("ALLOWED_USERIDS")
|
||||
}
|
||||
sa := strings.Split(allowedUserIDs, ",")
|
||||
for _, idStr := range sa {
|
||||
if idStr == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("env var ALLOWED_USERIDS contains invalid user ID: " + idStr)
|
||||
}
|
||||
p.AllowedUserIDs = append(p.AllowedUserIDs, id)
|
||||
}
|
||||
|
||||
if adminUserIDs == "" {
|
||||
adminUserIDs = os.Getenv("ADMIN_USERIDS")
|
||||
}
|
||||
sa = strings.Split(adminUserIDs, ",")
|
||||
for _, idStr := range sa {
|
||||
if idStr == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("env var ADMIN_USERIDS contains invalid user ID: " + idStr)
|
||||
}
|
||||
p.AdminUserIDs = append(p.AdminUserIDs, id)
|
||||
if !slices.Contains(p.AllowedUserIDs, id) {
|
||||
p.AllowedUserIDs = append(p.AllowedUserIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
if allowedGroupIDs == "" {
|
||||
allowedGroupIDs = os.Getenv("ALLOWED_GROUPIDS")
|
||||
}
|
||||
sa = strings.Split(allowedGroupIDs, ",")
|
||||
for _, idStr := range sa {
|
||||
if idStr == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("env var ALLOWED_GROUPIDS contains invalid group ID: " + idStr)
|
||||
}
|
||||
p.AllowedGroupIDs = append(p.AllowedUserIDs, id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
262
queue.go
Normal file
262
queue.go
Normal file
@ -0,0 +1,262 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/telegram/message"
|
||||
"github.com/gotd/td/tg"
|
||||
)
|
||||
|
||||
const processStartStr = "🔍 Getting information..."
|
||||
const processStr = "🔨 Processing"
|
||||
const processDoneStr = "🏁 Processing"
|
||||
const errorStr = "❌ Error"
|
||||
const canceledStr = "❌ Canceled"
|
||||
|
||||
const maxProgressPercentUpdateInterval = time.Second
|
||||
const progressBarLength = 10
|
||||
|
||||
type DownloadQueueEntry struct {
|
||||
URL string
|
||||
|
||||
OrigEntities tg.Entities
|
||||
OrigMsgUpdate *tg.UpdateNewMessage
|
||||
OrigMsg *tg.Message
|
||||
FromUser *tg.PeerUser
|
||||
FromGroup *tg.PeerChat
|
||||
|
||||
Reply *message.Builder
|
||||
ReplyMsg *tg.UpdateShortSentMessage
|
||||
|
||||
Ctx context.Context
|
||||
CtxCancel context.CancelFunc
|
||||
Canceled bool
|
||||
}
|
||||
|
||||
// func (e *DownloadQueueEntry) getTypingActionDst() tg.InputPeerClass {
|
||||
// if e.FromGroup != nil {
|
||||
// return &tg.InputPeerChat{
|
||||
// ChatID: e.FromGroup.ChatID,
|
||||
// }
|
||||
// }
|
||||
// return &tg.InputPeerUser{
|
||||
// UserID: e.FromUser.UserID,
|
||||
// }
|
||||
// }
|
||||
|
||||
func (e *DownloadQueueEntry) sendTypingAction(ctx context.Context) {
|
||||
// _ = telegramSender.To(e.getTypingActionDst()).TypingAction().Typing(ctx)
|
||||
}
|
||||
|
||||
func (e *DownloadQueueEntry) sendTypingCancelAction(ctx context.Context) {
|
||||
// _ = telegramSender.To(e.getTypingActionDst()).TypingAction().Cancel(ctx)
|
||||
}
|
||||
|
||||
func (e *DownloadQueueEntry) editReply(ctx context.Context, s string) {
|
||||
_, _ = e.Reply.Edit(e.ReplyMsg.ID).Text(ctx, s)
|
||||
e.sendTypingAction(ctx)
|
||||
}
|
||||
|
||||
type DownloadQueue struct {
|
||||
mutex sync.Mutex
|
||||
entries []DownloadQueueEntry
|
||||
processReqChan chan bool
|
||||
}
|
||||
|
||||
func (e *DownloadQueue) getQueuePositionString(pos int) string {
|
||||
return "👨👦👦 Request queued at position #" + fmt.Sprint(pos)
|
||||
}
|
||||
|
||||
func (q *DownloadQueue) Add(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, url string) {
|
||||
q.mutex.Lock()
|
||||
|
||||
var replyStr string
|
||||
if len(q.entries) == 0 {
|
||||
replyStr = processStartStr
|
||||
} else {
|
||||
fmt.Println(" queueing request at position #", len(q.entries))
|
||||
replyStr = q.getQueuePositionString(len(q.entries))
|
||||
}
|
||||
|
||||
newEntry := DownloadQueueEntry{
|
||||
URL: url,
|
||||
OrigEntities: entities,
|
||||
OrigMsgUpdate: u,
|
||||
OrigMsg: u.Message.(*tg.Message),
|
||||
}
|
||||
|
||||
newEntry.Reply = telegramSender.Reply(entities, u)
|
||||
replyText, _ := newEntry.Reply.Text(ctx, replyStr)
|
||||
newEntry.ReplyMsg = replyText.(*tg.UpdateShortSentMessage)
|
||||
|
||||
newEntry.FromUser, newEntry.FromGroup = resolveMsgSrc(newEntry.OrigMsg)
|
||||
|
||||
q.entries = append(q.entries, newEntry)
|
||||
q.mutex.Unlock()
|
||||
|
||||
select {
|
||||
case q.processReqChan <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (q *DownloadQueue) CancelCurrentEntry(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, url string) {
|
||||
q.mutex.Lock()
|
||||
if len(q.entries) > 0 {
|
||||
q.entries[0].Canceled = true
|
||||
q.entries[0].CtxCancel()
|
||||
} else {
|
||||
fmt.Println(" no active request to cancel")
|
||||
_, _ = telegramSender.Reply(entities, u).Text(ctx, errorStr+": no active request to cancel")
|
||||
}
|
||||
q.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (q *DownloadQueue) updateProgress(ctx context.Context, qEntry *DownloadQueueEntry, progressPercent int, sourceCodecInfo string) {
|
||||
if progressPercent < 0 {
|
||||
qEntry.editReply(ctx, processStr+"... (no progress available)\n"+sourceCodecInfo)
|
||||
return
|
||||
}
|
||||
if progressPercent == 0 {
|
||||
qEntry.editReply(ctx, processStr+"...\n"+sourceCodecInfo)
|
||||
return
|
||||
}
|
||||
fmt.Print(" progress: ", progressPercent, "%\n")
|
||||
qEntry.editReply(ctx, processStr+": "+getProgressbar(progressPercent, progressBarLength)+"\n"+sourceCodecInfo)
|
||||
}
|
||||
|
||||
func (q *DownloadQueue) processQueueEntry(ctx context.Context, qEntry *DownloadQueueEntry) {
|
||||
fromUsername := getFromUsername(qEntry.OrigEntities, qEntry.FromUser.UserID)
|
||||
fmt.Print("processing request by")
|
||||
if fromUsername != "" {
|
||||
fmt.Print(" from ", fromUsername, "#", qEntry.FromUser.UserID)
|
||||
}
|
||||
fmt.Println(":", qEntry.URL)
|
||||
|
||||
qEntry.editReply(ctx, processStartStr)
|
||||
|
||||
var disableProgressPercentUpdate bool
|
||||
var lastProgressPercentUpdateAt time.Time
|
||||
var lastProgressPercent int
|
||||
var progressUpdateTimer *time.Timer
|
||||
var sourceCodecInfo string
|
||||
downloader := Downloader{
|
||||
ProbeStartFunc: func(ctx context.Context) {
|
||||
qEntry.editReply(ctx, "🎬 Getting video format...")
|
||||
},
|
||||
ConvertStartFunc: func(ctx context.Context, videoCodecs, audioCodecs, convertActionsNeeded string) {
|
||||
sourceCodecInfo = "🎬 Source: " + videoCodecs
|
||||
if audioCodecs == "" {
|
||||
sourceCodecInfo += ", no audio"
|
||||
} else {
|
||||
sourceCodecInfo += " / " + audioCodecs
|
||||
}
|
||||
if convertActionsNeeded == "" {
|
||||
sourceCodecInfo += " (no conversion needed)"
|
||||
} else {
|
||||
sourceCodecInfo += " (converting: " + convertActionsNeeded + ")"
|
||||
}
|
||||
qEntry.editReply(ctx, "🎬 Preparing download...\n"+sourceCodecInfo)
|
||||
},
|
||||
UpdateProgressPercentFunc: func(progressPercent int) {
|
||||
if disableProgressPercentUpdate || lastProgressPercent == progressPercent {
|
||||
return
|
||||
}
|
||||
lastProgressPercent = progressPercent
|
||||
if progressPercent < 0 {
|
||||
disableProgressPercentUpdate = true
|
||||
q.updateProgress(ctx, qEntry, progressPercent, sourceCodecInfo)
|
||||
return
|
||||
}
|
||||
|
||||
if progressUpdateTimer != nil {
|
||||
progressUpdateTimer.Stop()
|
||||
select {
|
||||
case <-progressUpdateTimer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
timeElapsedSinceLastUpdate := time.Since(lastProgressPercentUpdateAt)
|
||||
if timeElapsedSinceLastUpdate < maxProgressPercentUpdateInterval {
|
||||
progressUpdateTimer = time.AfterFunc(maxProgressPercentUpdateInterval-timeElapsedSinceLastUpdate, func() {
|
||||
q.updateProgress(ctx, qEntry, progressPercent, sourceCodecInfo)
|
||||
lastProgressPercentUpdateAt = time.Now()
|
||||
})
|
||||
return
|
||||
}
|
||||
q.updateProgress(ctx, qEntry, progressPercent, sourceCodecInfo)
|
||||
lastProgressPercentUpdateAt = time.Now()
|
||||
},
|
||||
}
|
||||
|
||||
r, err := downloader.DownloadAndConvertURL(qEntry.Ctx, qEntry.OrigMsg.Message)
|
||||
if err != nil {
|
||||
fmt.Println(" error downloading:", err)
|
||||
qEntry.editReply(ctx, fmt.Sprint(errorStr+": ", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Feeding the returned io.ReadCloser to the uploader.
|
||||
fmt.Println(" processing...")
|
||||
q.updateProgress(ctx, qEntry, lastProgressPercent, sourceCodecInfo)
|
||||
err = uploadFile(ctx, qEntry.OrigEntities, qEntry.OrigMsgUpdate, r)
|
||||
if err != nil {
|
||||
fmt.Println(" error processing:", err)
|
||||
disableProgressPercentUpdate = true
|
||||
r.Close()
|
||||
qEntry.editReply(ctx, fmt.Sprint(errorStr+": ", err))
|
||||
return
|
||||
}
|
||||
disableProgressPercentUpdate = true
|
||||
r.Close()
|
||||
|
||||
if qEntry.Canceled {
|
||||
fmt.Print(" canceled\n")
|
||||
qEntry.editReply(ctx, canceledStr+": "+getProgressbar(lastProgressPercent, progressBarLength)+"\n"+sourceCodecInfo)
|
||||
} else if lastProgressPercent < 100 {
|
||||
fmt.Print(" progress: 100%\n")
|
||||
qEntry.editReply(ctx, processDoneStr+": "+getProgressbar(100, progressBarLength)+"\n"+sourceCodecInfo)
|
||||
}
|
||||
qEntry.sendTypingCancelAction(ctx)
|
||||
}
|
||||
|
||||
func (q *DownloadQueue) processor(ctx context.Context) {
|
||||
for {
|
||||
q.mutex.Lock()
|
||||
if (len(q.entries)) == 0 {
|
||||
q.mutex.Unlock()
|
||||
<-q.processReqChan
|
||||
continue
|
||||
}
|
||||
|
||||
// Updating queue positions for all waiting entries.
|
||||
for i := 1; i < len(q.entries); i++ {
|
||||
q.entries[i].editReply(ctx, q.getQueuePositionString(i))
|
||||
q.entries[i].sendTypingCancelAction(ctx)
|
||||
}
|
||||
|
||||
q.entries[0].Ctx, q.entries[0].CtxCancel = context.WithTimeout(ctx, downloadAndConvertTimeout)
|
||||
|
||||
qEntry := &q.entries[0]
|
||||
q.mutex.Unlock()
|
||||
|
||||
q.processQueueEntry(ctx, qEntry)
|
||||
|
||||
q.mutex.Lock()
|
||||
q.entries[0].CtxCancel()
|
||||
q.entries = q.entries[1:]
|
||||
if len(q.entries) == 0 {
|
||||
fmt.Print("finished queue processing\n")
|
||||
}
|
||||
q.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (q *DownloadQueue) Init(ctx context.Context) {
|
||||
q.processReqChan = make(chan bool)
|
||||
go q.processor(ctx)
|
||||
}
|
||||
78
rereader.go
Normal file
78
rereader.go
Normal file
@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
// Copyright (c) 2016 Mattias Wadman
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type restartBuffer struct {
|
||||
Buffer bytes.Buffer
|
||||
Restarted bool
|
||||
}
|
||||
|
||||
func (rb *restartBuffer) Read(r io.Reader, p []byte) (n int, err error) {
|
||||
if rb.Restarted {
|
||||
if rb.Buffer.Len() > 0 {
|
||||
return rb.Buffer.Read(p)
|
||||
}
|
||||
n, err = r.Read(p)
|
||||
return n, err
|
||||
}
|
||||
|
||||
n, err = r.Read(p)
|
||||
rb.Buffer.Write(p[:n])
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ReReader transparently buffers all reads from a reader until Restarted
|
||||
// is set to true. When restarted buffered data will be replayed on read and
|
||||
// after that normal reading from the reader continues.
|
||||
type ReReader struct {
|
||||
io.Reader
|
||||
restartBuffer
|
||||
}
|
||||
|
||||
// NewReReader return a initialized ReReader
|
||||
func NewReReader(r io.Reader) *ReReader {
|
||||
return &ReReader{Reader: r}
|
||||
}
|
||||
|
||||
func (rr *ReReader) Read(p []byte) (n int, err error) {
|
||||
return rr.restartBuffer.Read(rr.Reader, p)
|
||||
}
|
||||
|
||||
// ReReadCloser is same as ReReader but also forwards Close calls
|
||||
type ReReadCloser struct {
|
||||
io.ReadCloser
|
||||
restartBuffer
|
||||
}
|
||||
|
||||
// NewReReadCloser return a initialized ReReadCloser
|
||||
func NewReReadCloser(rc io.ReadCloser) *ReReadCloser {
|
||||
return &ReReadCloser{ReadCloser: rc}
|
||||
}
|
||||
|
||||
func (rc *ReReadCloser) Read(p []byte) (n int, err error) {
|
||||
return rc.restartBuffer.Read(rc.ReadCloser, p)
|
||||
}
|
||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
52
vercheck.go
Normal file
52
vercheck.go
Normal file
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v53/github"
|
||||
"github.com/wader/goutubedl"
|
||||
)
|
||||
|
||||
const ytdlpVersionCheckTimeout = time.Second * 10
|
||||
|
||||
func ytdlpVersionCheck(ctx context.Context) (latestVersion, currentVersion string, err error) {
|
||||
client := github.NewClient(nil)
|
||||
|
||||
release, _, err := client.Repositories.GetLatestRelease(ctx, "yt-dlp", "yt-dlp")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("getting latest yt-dlp version: %w", err)
|
||||
}
|
||||
latestVersion = release.GetTagName()
|
||||
|
||||
out, err := exec.Command(goutubedl.Path, "--version").Output()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("getting current yt-dlp version: %w", err)
|
||||
}
|
||||
|
||||
currentVersion = strings.TrimSpace(string(out))
|
||||
return
|
||||
}
|
||||
|
||||
func ytdlpVersionCheckGetStr(ctx context.Context) (res string, updateNeededOrError bool) {
|
||||
verCheckCtx, verCheckCtxCancel := context.WithTimeout(ctx, ytdlpVersionCheckTimeout)
|
||||
defer verCheckCtxCancel()
|
||||
|
||||
var latestVersion, currentVersion string
|
||||
var err error
|
||||
if latestVersion, currentVersion, err = ytdlpVersionCheck(verCheckCtx); err != nil {
|
||||
return errorStr + ": " + err.Error(), true
|
||||
}
|
||||
|
||||
updateNeededOrError = currentVersion != latestVersion
|
||||
res = "yt-dlp version: " + currentVersion
|
||||
if updateNeededOrError {
|
||||
res = "📢 " + res + " 📢 Update needed! Latest version is " + latestVersion + " 📢"
|
||||
} else {
|
||||
res += " (up to date)"
|
||||
}
|
||||
return
|
||||
}
|
||||
7
yt-dlp-telegram-bot.code-workspace
Normal file
7
yt-dlp-telegram-bot.code-workspace
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user