Compare commits
12
Commits
5184d43d78
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e21f26de0a | ||
|
|
a42bad7c48 | ||
|
|
f2c9c4e388 | ||
|
|
f9d5615320 | ||
|
|
a1c66784d2 | ||
|
|
6b67239a90 | ||
|
|
2b3658ceed | ||
|
|
548e7effb7 | ||
|
|
1d75df37cf | ||
|
|
f1724719ec | ||
|
|
540ddc2bad | ||
|
|
d5b81a3b5a |
@@ -1,2 +1,4 @@
|
|||||||
/config.inc.sh
|
/config.inc.sh
|
||||||
/yt-dlp-telegram-bot
|
/yt-dlp-telegram-bot
|
||||||
|
/.vscode
|
||||||
|
/save
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.20 as builder
|
FROM golang:1.22 AS builder
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
COPY go.mod go.sum /app/
|
COPY go.mod go.sum /app/
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
@@ -6,11 +6,11 @@ COPY . .
|
|||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v
|
||||||
|
|
||||||
FROM python:alpine
|
FROM python:alpine
|
||||||
RUN wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp && \
|
|
||||||
chmod 755 /usr/local/bin/yt-dlp
|
|
||||||
RUN apk update && apk upgrade && apk add --no-cache ffmpeg
|
RUN apk update && apk upgrade && apk add --no-cache ffmpeg
|
||||||
COPY --from=builder /app/yt-dlp-telegram-bot /app/yt-dlp-telegram-bot
|
COPY --from=builder /app/yt-dlp-telegram-bot /app/yt-dlp-telegram-bot
|
||||||
COPY --from=builder /app/yt-dlp.conf /root/yt-dlp.conf
|
COPY --from=builder /app/yt-dlp.conf /root/yt-dlp.conf
|
||||||
|
|
||||||
|
RUN mkdir -p /root/save
|
||||||
|
|
||||||
ENTRYPOINT ["/app/yt-dlp-telegram-bot"]
|
ENTRYPOINT ["/app/yt-dlp-telegram-bot"]
|
||||||
ENV API_ID= API_HASH= BOT_TOKEN= ALLOWED_USERIDS= ADMIN_USERIDS= ALLOWED_GROUPIDS= YTDLP_COOKIES=
|
ENV API_ID= API_HASH= BOT_TOKEN= ALLOWED_USERIDS= ADMIN_USERIDS= ALLOWED_GROUPIDS= SAVE_DIR=/root/save YTDLP_COOKIES=
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ processed at a time.
|
|||||||
|
|
||||||
The bot uses the [Telegram MTProto API](https://github.com/gotd/td), which
|
The bot uses the [Telegram MTProto API](https://github.com/gotd/td), which
|
||||||
supports larger video uploads than the default 50MB with the standard
|
supports larger video uploads than the default 50MB with the standard
|
||||||
Telegram bot API. Videos are not saved on disk. Incompatible video and audio
|
Telegram bot API. Videos are saved to disk in the configured `SAVE_DIR`
|
||||||
streams are automatically converted to match those which are supported by
|
(default: `/root/save_dir`). Incompatible video and audio streams are
|
||||||
Telegram's built-in video player.
|
automatically converted to match those which are supported by Telegram's
|
||||||
|
built-in video player. Videos larger than 512MB are saved but not uploaded
|
||||||
|
to Telegram.
|
||||||
|
|
||||||
The only dependencies are [yt-dlp](https://github.com/yt-dlp/yt-dlp) and
|
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
|
[ffmpeg](https://github.com/FFmpeg/FFmpeg). Tested on Linux, but should be
|
||||||
@@ -80,6 +82,7 @@ variable. Available OS environment variables are:
|
|||||||
- `ADMIN_USERIDS`
|
- `ADMIN_USERIDS`
|
||||||
- `ALLOWED_GROUPIDS`
|
- `ALLOWED_GROUPIDS`
|
||||||
- `MAX_SIZE`
|
- `MAX_SIZE`
|
||||||
|
- `SAVE_DIR` - Directory where downloaded videos are saved (default: `/root/save_dir`)
|
||||||
- `YTDLP_COOKIES`
|
- `YTDLP_COOKIES`
|
||||||
|
|
||||||
The contents of the `YTDLP_COOKIES` environment variable will be written to the
|
The contents of the `YTDLP_COOKIES` environment variable will be written to the
|
||||||
@@ -89,7 +92,8 @@ cookie file.
|
|||||||
|
|
||||||
## Supported commands
|
## Supported commands
|
||||||
|
|
||||||
- `/dlp` - Download
|
- `/dlp` - Download given URL. If the first attribute is "mp3" then only the
|
||||||
|
audio stream will be downloaded and converted (if needed) to 320k MP3
|
||||||
- `/dlpcancel` - Cancel ongoing download
|
- `/dlpcancel` - Cancel ongoing download
|
||||||
|
|
||||||
You don't need to enter the `/dlp` command if you send an URL to the bot using
|
You don't need to enter the `/dlp` command if you send an URL to the bot using
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ ALLOWED_USERIDS=
|
|||||||
ADMIN_USERIDS=
|
ADMIN_USERIDS=
|
||||||
ALLOWED_GROUPIDS=
|
ALLOWED_GROUPIDS=
|
||||||
MAX_SIZE=
|
MAX_SIZE=
|
||||||
|
SAVE_DIR=/root/save_dir
|
||||||
|
|||||||
+92
-39
@@ -4,11 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -27,6 +27,8 @@ var compatibleAudioCodecs = []string{"aac", "opus", "mp3"}
|
|||||||
type ffmpegProbeDataStreamsStream struct {
|
type ffmpegProbeDataStreamsStream struct {
|
||||||
CodecName string `json:"codec_name"`
|
CodecName string `json:"codec_name"`
|
||||||
CodecType string `json:"codec_type"`
|
CodecType string `json:"codec_type"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ffmpegProbeDataFormat struct {
|
type ffmpegProbeDataFormat struct {
|
||||||
@@ -40,9 +42,13 @@ type ffmpegProbeData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Converter struct {
|
type Converter struct {
|
||||||
|
Format string
|
||||||
|
|
||||||
VideoCodecs string
|
VideoCodecs string
|
||||||
VideoConvertNeeded bool
|
VideoConvertNeeded bool
|
||||||
SingleVideoStreamNeeded bool
|
SingleVideoStreamNeeded bool
|
||||||
|
VideoWidth int
|
||||||
|
VideoHeight int
|
||||||
|
|
||||||
AudioCodecs string
|
AudioCodecs string
|
||||||
AudioConvertNeeded bool
|
AudioConvertNeeded bool
|
||||||
@@ -53,14 +59,9 @@ type Converter struct {
|
|||||||
UpdateProgressPercentCallback UpdateProgressPercentCallbackFunc
|
UpdateProgressPercentCallback UpdateProgressPercentCallbackFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Converter) Probe(rr *ReReadCloser) error {
|
func (c *Converter) ProbeFile(filePath string) error {
|
||||||
defer func() {
|
|
||||||
// Restart and replay buffer data used when probing
|
|
||||||
rr.Restarted = true
|
|
||||||
}()
|
|
||||||
|
|
||||||
fmt.Println(" probing file...")
|
fmt.Println(" probing file...")
|
||||||
i, err := ffmpeg_go.ProbeReaderWithTimeout(io.LimitReader(rr, maxFFmpegProbeBytes), probeTimeout, nil)
|
i, err := ffmpeg_go.ProbeWithTimeout(filePath, probeTimeout, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error probing file: %w", err)
|
return fmt.Errorf("error probing file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -76,15 +77,30 @@ func (c *Converter) Probe(rr *ReReadCloser) error {
|
|||||||
fmt.Println(" error parsing duration:", err)
|
fmt.Println(" error parsing duration:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compatibleVideoCodecsCopy := compatibleVideoCodecs
|
||||||
|
if c.Format == "mp3" {
|
||||||
|
compatibleVideoCodecsCopy = []string{}
|
||||||
|
}
|
||||||
|
compatibleAudioCodecsCopy := compatibleAudioCodecs
|
||||||
|
if c.Format == "mp3" {
|
||||||
|
compatibleAudioCodecsCopy = []string{"mp3"}
|
||||||
|
}
|
||||||
|
|
||||||
gotVideoStream := false
|
gotVideoStream := false
|
||||||
gotAudioStream := false
|
gotAudioStream := false
|
||||||
for _, stream := range pd.Streams {
|
for _, stream := range pd.Streams {
|
||||||
if stream.CodecType == "video" {
|
if stream.CodecType == "video" && len(compatibleVideoCodecsCopy) > 0 {
|
||||||
if c.VideoCodecs != "" {
|
if c.VideoCodecs != "" {
|
||||||
c.VideoCodecs += ", "
|
c.VideoCodecs += ", "
|
||||||
}
|
}
|
||||||
c.VideoCodecs += stream.CodecName
|
c.VideoCodecs += stream.CodecName
|
||||||
|
|
||||||
|
// Store video dimensions for aspect ratio preservation
|
||||||
|
if stream.Width > 0 && stream.Height > 0 {
|
||||||
|
c.VideoWidth = stream.Width
|
||||||
|
c.VideoHeight = stream.Height
|
||||||
|
}
|
||||||
|
|
||||||
if gotVideoStream {
|
if gotVideoStream {
|
||||||
fmt.Println(" got additional video stream")
|
fmt.Println(" got additional video stream")
|
||||||
c.SingleVideoStreamNeeded = true
|
c.SingleVideoStreamNeeded = true
|
||||||
@@ -107,7 +123,7 @@ func (c *Converter) Probe(rr *ReReadCloser) error {
|
|||||||
fmt.Println(" got additional audio stream")
|
fmt.Println(" got additional audio stream")
|
||||||
c.SingleAudioStreamNeeded = true
|
c.SingleAudioStreamNeeded = true
|
||||||
} else if !c.AudioConvertNeeded {
|
} else if !c.AudioConvertNeeded {
|
||||||
if !slices.Contains(compatibleAudioCodecs, stream.CodecName) {
|
if !slices.Contains(compatibleAudioCodecsCopy, stream.CodecName) {
|
||||||
fmt.Println(" found not compatible audio codec:", stream.CodecName)
|
fmt.Println(" found not compatible audio codec:", stream.CodecName)
|
||||||
c.AudioConvertNeeded = true
|
c.AudioConvertNeeded = true
|
||||||
} else {
|
} else {
|
||||||
@@ -118,7 +134,7 @@ func (c *Converter) Probe(rr *ReReadCloser) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gotVideoStream {
|
if len(compatibleVideoCodecsCopy) > 0 && !gotVideoStream {
|
||||||
return fmt.Errorf("no video stream found in file")
|
return fmt.Errorf("no video stream found in file")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,33 +197,70 @@ func (c *Converter) GetActionsNeeded() string {
|
|||||||
return strings.Join(convertNeeded, ", ")
|
return strings.Join(convertNeeded, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Converter) ConvertIfNeeded(ctx context.Context, rr *ReReadCloser) (io.ReadCloser, error) {
|
func (c *Converter) ConvertIfNeeded(ctx context.Context, inputPath, outputDir string) (outputPath string, outputFormat string, err error) {
|
||||||
reader, writer := io.Pipe()
|
|
||||||
var cmd *Cmd
|
|
||||||
|
|
||||||
fmt.Print(" converting ", c.GetActionsNeeded(), "...\n")
|
fmt.Print(" converting ", c.GetActionsNeeded(), "...\n")
|
||||||
|
|
||||||
args := ffmpeg_go.KwArgs{"format": "mp4", "movflags": "frag_keyframe+empty_moov+faststart"}
|
videoNeeded := true
|
||||||
|
outputFormat = "mp4"
|
||||||
|
if c.Format == "mp3" {
|
||||||
|
videoNeeded = false
|
||||||
|
outputFormat = "mp3"
|
||||||
|
}
|
||||||
|
|
||||||
if c.VideoConvertNeeded {
|
// Determine output path
|
||||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:v": "libx264", "crf": 30, "preset": "veryfast"}})
|
ext := filepath.Ext(inputPath)
|
||||||
|
base := strings.TrimSuffix(filepath.Base(inputPath), ext)
|
||||||
|
outputPath = filepath.Join(outputDir, base+"_converted."+outputFormat)
|
||||||
|
|
||||||
|
// Check if conversion is needed
|
||||||
|
if !c.VideoConvertNeeded && !c.AudioConvertNeeded && !c.SingleVideoStreamNeeded && !c.SingleAudioStreamNeeded {
|
||||||
|
if outputFormat == "mp4" && ext == ".mkv" {
|
||||||
|
// Just remux from mkv to mp4, no encoding needed
|
||||||
|
fmt.Println(" remuxing mkv to mp4...")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" no conversion needed, using original file")
|
||||||
|
return inputPath, outputFormat, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := ffmpeg_go.KwArgs{
|
||||||
|
"format": outputFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
if videoNeeded {
|
||||||
|
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"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"}})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:v": "copy"}})
|
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"vn": ""}})
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.AudioConvertNeeded {
|
if c.AudioConvertNeeded {
|
||||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:a": "mp3", "q:a": 0}})
|
if c.Format == "mp3" {
|
||||||
|
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:a": "mp3", "b:a": "320k"}})
|
||||||
|
} else {
|
||||||
|
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:a": "mp3", "q:a": 0}})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:a": "copy"}})
|
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"c:a": "copy"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SingleVideoStreamNeeded || c.SingleAudioStreamNeeded {
|
if videoNeeded {
|
||||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"map": "0:v:0,0:a:0"}})
|
if c.SingleVideoStreamNeeded || c.SingleAudioStreamNeeded {
|
||||||
|
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"map": "0:v:0,0:a:0"}})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if c.SingleAudioStreamNeeded {
|
||||||
|
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"map": "0:a:0"}})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ff := ffmpeg_go.Input("pipe:0").Output("pipe:1", args)
|
ff := ffmpeg_go.Input(inputPath).Output(outputPath, args)
|
||||||
|
|
||||||
var err error
|
|
||||||
var progressSock net.Listener
|
var progressSock net.Listener
|
||||||
if c.UpdateProgressPercentCallback != nil {
|
if c.UpdateProgressPercentCallback != nil {
|
||||||
if c.Duration > 0 {
|
if c.Duration > 0 {
|
||||||
@@ -221,26 +274,26 @@ func (c *Converter) ConvertIfNeeded(ctx context.Context, rr *ReReadCloser) (io.R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ffCmd := ff.WithInput(rr).WithOutput(writer).Compile()
|
// Run ffmpeg
|
||||||
|
cmd := ff.Compile()
|
||||||
|
|
||||||
// Creating a new cmd with a timeout context, which will kill the cmd if it takes too long.
|
// Creating a new cmd with a timeout context
|
||||||
cmd = NewCommand(ctx, ffCmd.Args[0], ffCmd.Args[1:]...)
|
cmdCtx := NewCommand(ctx, cmd.Args[0], cmd.Args[1:]...)
|
||||||
cmd.Stdin = ffCmd.Stdin
|
|
||||||
cmd.Stdout = ffCmd.Stdout
|
|
||||||
|
|
||||||
// This goroutine handles copying from the input (either rr or cmd.Stdout) to writer.
|
if err := cmdCtx.Run(); err != nil {
|
||||||
go func() {
|
|
||||||
err = cmd.Run()
|
|
||||||
writer.Close()
|
|
||||||
if progressSock != nil {
|
if progressSock != nil {
|
||||||
progressSock.Close()
|
progressSock.Close()
|
||||||
}
|
}
|
||||||
}()
|
return "", "", fmt.Errorf("error converting: %w", err)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
writer.Close()
|
|
||||||
return nil, fmt.Errorf("error converting: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader, nil
|
if progressSock != nil {
|
||||||
|
progressSock.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputPath, outputFormat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Converter) NeedConvert() bool {
|
||||||
|
return c.VideoConvertNeeded || c.AudioConvertNeeded || c.SingleVideoStreamNeeded || c.SingleAudioStreamNeeded
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/wader/goutubedl"
|
"github.com/wader/goutubedl"
|
||||||
)
|
)
|
||||||
|
|
||||||
const downloadAndConvertTimeout = 5 * time.Minute
|
const downloadAndConvertTimeout = 30 * time.Minute
|
||||||
|
const telegramUploadThreshold = 512 * 1024 * 1024 // 512MB
|
||||||
|
const downloadProgressUpdateInterval = time.Second
|
||||||
|
|
||||||
type ConvertStartCallbackFunc func(ctx context.Context, videoCodecs, audioCodecs, convertActionsNeeded string)
|
type ConvertStartCallbackFunc func(ctx context.Context, videoCodecs, audioCodecs, convertActionsNeeded string)
|
||||||
type UpdateProgressPercentCallbackFunc func(progressStr string, progressPercent int)
|
type UpdateProgressPercentCallbackFunc func(progressStr string, progressPercent int)
|
||||||
@@ -26,48 +32,146 @@ func (l goYouTubeDLLogger) Print(v ...interface{}) {
|
|||||||
fmt.Println(v...)
|
fmt.Println(v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) downloadURL(dlCtx context.Context, url string) (rr *ReReadCloser, err error) {
|
type DownloadResult struct {
|
||||||
|
Title string
|
||||||
|
FilePath string
|
||||||
|
FileSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// progressWriter wraps an io.Writer and tracks bytes written
|
||||||
|
type progressWriter struct {
|
||||||
|
writer io.Writer
|
||||||
|
written int64
|
||||||
|
total int64 // estimated total size, 0 if unknown
|
||||||
|
callback UpdateProgressPercentCallbackFunc
|
||||||
|
lastUpdate time.Time
|
||||||
|
updateInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProgressWriter(w io.Writer, total int64, callback UpdateProgressPercentCallbackFunc) *progressWriter {
|
||||||
|
return &progressWriter{
|
||||||
|
writer: w,
|
||||||
|
total: total,
|
||||||
|
callback: callback,
|
||||||
|
lastUpdate: time.Now(),
|
||||||
|
updateInterval: downloadProgressUpdateInterval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *progressWriter) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = pw.writer.Write(p)
|
||||||
|
if n > 0 {
|
||||||
|
atomic.AddInt64(&pw.written, int64(n))
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(pw.lastUpdate) >= pw.updateInterval {
|
||||||
|
pw.lastUpdate = now
|
||||||
|
written := atomic.LoadInt64(&pw.written)
|
||||||
|
|
||||||
|
if pw.total > 0 && pw.callback != nil {
|
||||||
|
percent := int(float64(written) * 100 / float64(pw.total))
|
||||||
|
if percent > 100 {
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
pw.callback("⬇️ Downloading", percent)
|
||||||
|
} else if pw.callback != nil {
|
||||||
|
// Unknown total size, just show bytes downloaded
|
||||||
|
pw.callback(fmt.Sprintf("⬇️ Downloaded %s", humanize.Bytes(uint64(written))), -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *progressWriter) Written() int64 {
|
||||||
|
return atomic.LoadInt64(&pw.written)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) downloadURL(dlCtx context.Context, url string) (*DownloadResult, error) {
|
||||||
|
// Use 4K quality for saving, but fall back to best available
|
||||||
result, err := goutubedl.New(dlCtx, url, goutubedl.Options{
|
result, err := goutubedl.New(dlCtx, url, goutubedl.Options{
|
||||||
Type: goutubedl.TypeSingle,
|
Type: goutubedl.TypeSingle,
|
||||||
DebugLog: goYouTubeDLLogger{},
|
DebugLog: goYouTubeDLLogger{},
|
||||||
// StderrFn: func(cmd *exec.Cmd) io.Writer { return io.Writer(os.Stdout) },
|
MergeOutputFormat: "mkv", // This handles VP9 properly. yt-dlp uses mp4 by default, which doesn't.
|
||||||
MergeOutputFormat: "mkv", // This handles VP9 properly. yt-dlp uses mp4 by default, which doesn't.
|
SortingFormat: "res:2160", // Prefer videos up to 4K (2160p)
|
||||||
SortingFormat: "res:720", // Prefer videos no larger than 720p to keep their size small.
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("preparing download %q: %w", url, err)
|
return nil, fmt.Errorf("preparing download %q: %w", url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create filename with date and timestamp format: 2025-11-22-{timestamp}.mkv
|
||||||
|
now := time.Now()
|
||||||
|
dateStr := now.Format("2006-01-02")
|
||||||
|
timestamp := now.Unix()
|
||||||
|
fileName := fmt.Sprintf("%s-%d.mkv", dateStr, timestamp)
|
||||||
|
filePath := filepath.Join(params.SaveDir, fileName)
|
||||||
|
|
||||||
|
// Check if file already exists, if so add a suffix
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
for i := 1; i < 1000; i++ {
|
||||||
|
fileName = fmt.Sprintf("%s-%d-%d.mkv", dateStr, timestamp, i)
|
||||||
|
filePath = filepath.Join(params.SaveDir, fileName)
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dlResult, err := result.Download(dlCtx, "")
|
dlResult, err := result.Download(dlCtx, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("downloading %q: %w", url, err)
|
return nil, fmt.Errorf("downloading %q: %w", url, err)
|
||||||
}
|
}
|
||||||
|
defer dlResult.Close()
|
||||||
|
|
||||||
return NewReReadCloser(dlResult), nil
|
// Create file
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating file %q: %w", filePath, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Get estimated file size from format info
|
||||||
|
var estimatedSize int64
|
||||||
|
if len(result.Info.Formats) > 0 {
|
||||||
|
// Try to get filesize from the selected format
|
||||||
|
for _, f := range result.Info.Formats {
|
||||||
|
if f.Filesize > 0 {
|
||||||
|
fs := int64(f.Filesize)
|
||||||
|
if fs > estimatedSize {
|
||||||
|
estimatedSize = fs
|
||||||
|
}
|
||||||
|
} else if f.FilesizeApprox > 0 && estimatedSize == 0 {
|
||||||
|
estimatedSize = int64(f.FilesizeApprox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if estimatedSize == 0 && result.Info.Filesize > 0 {
|
||||||
|
estimatedSize = int64(result.Info.Filesize)
|
||||||
|
}
|
||||||
|
if estimatedSize == 0 && result.Info.FilesizeApprox > 0 {
|
||||||
|
estimatedSize = int64(result.Info.FilesizeApprox)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create progress writer
|
||||||
|
pw := newProgressWriter(file, estimatedSize, d.UpdateProgressPercentFunc)
|
||||||
|
|
||||||
|
// Copy data to file with progress tracking
|
||||||
|
_, err = io.Copy(pw, dlResult)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(filePath)
|
||||||
|
return nil, fmt.Errorf("writing to file %q: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
written := pw.Written()
|
||||||
|
fmt.Printf(" saved to %s (%s)\n", filePath, humanize.Bytes(uint64(written)))
|
||||||
|
|
||||||
|
return &DownloadResult{
|
||||||
|
Title: result.Info.Title,
|
||||||
|
FilePath: filePath,
|
||||||
|
FileSize: written,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) DownloadAndConvertURL(ctx context.Context, url string) (r io.ReadCloser, err error) {
|
func (d *Downloader) DownloadAndConvertURL(ctx context.Context, url, format string) (*DownloadResult, error) {
|
||||||
rr, err := d.downloadURL(ctx, url)
|
return d.downloadURL(ctx, url)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conv := Converter{
|
|
||||||
UpdateProgressPercentCallback: d.UpdateProgressPercentFunc,
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
docker build -t nonoo/yt-dlp-telegram-bot:latest --network=host .
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
downloader:
|
||||||
|
build: .
|
||||||
|
image: yt-dlp-telegram-bot:latest
|
||||||
|
container_name: downloader
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./yt-dlp.conf:/root/yt-dlp.conf
|
||||||
|
- /var/apps/docker-chromium/shares/chromium:/root/chromium
|
||||||
|
- ./save:/root/save
|
||||||
|
environment:
|
||||||
|
- API_ID=32195099
|
||||||
|
- API_HASH=16bd171827e9e8ee21d9e1a3192ac30b
|
||||||
|
- BOT_TOKEN=8681926392:AAEszGJxIQaslfXuWQw5eMqcuGxSL_-3xQU
|
||||||
|
- ALLOWED_USERIDS=1143940780,6073512239
|
||||||
|
- ADMIN_USERIDS=1143940780
|
||||||
|
- SAVE_DIR=/root/save
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
docker push nonoo/yt-dlp-telegram-bot:latest
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
module github.com/nonoo/yt-dlp-telegram-bot
|
module github.com/nonoo/yt-dlp-telegram-bot
|
||||||
|
|
||||||
go 1.20
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
|
github.com/flytam/filenamify v1.2.0
|
||||||
github.com/google/go-github/v53 v53.2.0
|
github.com/google/go-github/v53 v53.2.0
|
||||||
github.com/gotd/td v0.84.0
|
github.com/gotd/td v0.84.0
|
||||||
github.com/u2takey/ffmpeg-go v0.5.0
|
github.com/u2takey/ffmpeg-go v0.5.0
|
||||||
github.com/wader/goutubedl v0.0.0-20230815212531-02ec4fe77de3
|
github.com/wader/goutubedl v0.0.0-20240626070646-8cef76d0c092
|
||||||
golang.org/x/exp v0.0.0-20230116083435-1de6713980de
|
golang.org/x/exp v0.0.0-20230116083435-1de6713980de
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0g
|
|||||||
github.com/aws/aws-sdk-go v1.38.20 h1:QbzNx/tdfATbdKfubBpkt84OM6oBkxQZRw6+bW2GyeA=
|
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/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||||
|
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
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 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
@@ -15,6 +16,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/flytam/filenamify v1.2.0 h1:7RiSqXYR4cJftDQ5NuvljKMfd/ubKnW/j9C6iekChgI=
|
||||||
|
github.com/flytam/filenamify v1.2.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
|
||||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
@@ -54,6 +57,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.2/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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI=
|
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-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 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
@@ -76,6 +80,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
|
|||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
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.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
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/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.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
@@ -85,6 +90,7 @@ 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/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.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -104,6 +110,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU=
|
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/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 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=
|
||||||
@@ -112,8 +119,8 @@ 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 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 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/wader/goutubedl v0.0.0-20230815212531-02ec4fe77de3 h1:JS151H65F6dF4KYtSSo2PRj7s4Gb+YhOz40e24zO/e8=
|
github.com/wader/goutubedl v0.0.0-20240626070646-8cef76d0c092 h1:BQ+eGEAUeSzrXx3ruK+pLM50FczmxhhtdA1UNYWRioQ=
|
||||||
github.com/wader/goutubedl v0.0.0-20230815212531-02ec4fe77de3/go.mod h1:5KXd5tImdbmz4JoVhePtbIokCwAfEhUVVx3WLHmjYuw=
|
github.com/wader/goutubedl v0.0.0-20240626070646-8cef76d0c092/go.mod h1:5KXd5tImdbmz4JoVhePtbIokCwAfEhUVVx3WLHmjYuw=
|
||||||
github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071 h1:QkrG4Zr5OVFuC9aaMPmFI0ibfhBZlAgtzDYWfu7tqQk=
|
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=
|
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 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||||
@@ -123,6 +130,7 @@ go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLk
|
|||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
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/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
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/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 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
|
||||||
@@ -162,6 +170,7 @@ 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.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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
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/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -178,7 +187,9 @@ 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.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.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
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 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message"
|
"github.com/gotd/td/telegram/message"
|
||||||
"github.com/gotd/td/telegram/uploader"
|
"github.com/gotd/td/telegram/uploader"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/wader/goutubedl"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,7 +23,17 @@ var dlQueue DownloadQueue
|
|||||||
var telegramUploader *uploader.Uploader
|
var telegramUploader *uploader.Uploader
|
||||||
var telegramSender *message.Sender
|
var telegramSender *message.Sender
|
||||||
|
|
||||||
|
// telegramClient is the global client reference for video download
|
||||||
|
var telegramClient *telegram.Client
|
||||||
|
|
||||||
func handleCmdDLP(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, msg *tg.Message) {
|
func handleCmdDLP(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, msg *tg.Message) {
|
||||||
|
format := "video"
|
||||||
|
s := strings.Split(msg.Message, " ")
|
||||||
|
if len(s) >= 2 && s[0] == "mp3" {
|
||||||
|
msg.Message = strings.Join(s[1:], " ")
|
||||||
|
format = "mp3"
|
||||||
|
}
|
||||||
|
|
||||||
// Check if message is an URL.
|
// Check if message is an URL.
|
||||||
validURI := true
|
validURI := true
|
||||||
uri, err := url.ParseRequestURI(msg.Message)
|
uri, err := url.ParseRequestURI(msg.Message)
|
||||||
@@ -39,13 +51,34 @@ func handleCmdDLP(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMess
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dlQueue.Add(ctx, entities, u, msg.Message)
|
dlQueue.Add(ctx, entities, u, msg.Message, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCmdDLPCancel(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, msg *tg.Message) {
|
func handleCmdDLPCancel(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, msg *tg.Message) {
|
||||||
dlQueue.CancelCurrentEntry(ctx, entities, u, msg.Message)
|
dlQueue.CancelCurrentEntry(ctx, entities, u, msg.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleVideoMessage(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, msg *tg.Message) {
|
||||||
|
fmt.Println(" (video message, queueing to save_dir)")
|
||||||
|
|
||||||
|
// Get video info from media
|
||||||
|
var videoFile *tg.Document
|
||||||
|
switch media := msg.Media.(type) {
|
||||||
|
case *tg.MessageMediaDocument:
|
||||||
|
if doc, ok := media.Document.(*tg.Document); ok {
|
||||||
|
videoFile = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if videoFile == nil {
|
||||||
|
_, _ = telegramSender.Reply(entities, u).Text(ctx, errorStr+": could not get video info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to queue for processing
|
||||||
|
dlQueue.AddVideo(ctx, entities, u, videoFile)
|
||||||
|
}
|
||||||
|
|
||||||
func handleMsg(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage) error {
|
func handleMsg(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage) error {
|
||||||
msg, ok := u.Message.(*tg.Message)
|
msg, ok := u.Message.(*tg.Message)
|
||||||
if !ok || msg.Out {
|
if !ok || msg.Out {
|
||||||
@@ -76,21 +109,28 @@ func handleMsg(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if message contains a video
|
||||||
|
if isVideoMessage(msg) {
|
||||||
|
handleVideoMessage(ctx, entities, u, msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Check if message is a command.
|
// Check if message is a command.
|
||||||
if msg.Message[0] == '/' {
|
if msg.Message[0] == '/' || msg.Message[0] == '!' {
|
||||||
cmd := strings.Split(msg.Message, " ")[0]
|
cmd := strings.Split(msg.Message, " ")[0]
|
||||||
|
msg.Message = strings.TrimPrefix(msg.Message, cmd+" ")
|
||||||
if strings.Contains(cmd, "@") {
|
if strings.Contains(cmd, "@") {
|
||||||
cmd = strings.Split(cmd, "@")[0]
|
cmd = strings.Split(cmd, "@")[0]
|
||||||
}
|
}
|
||||||
msg.Message = strings.TrimPrefix(msg.Message, cmd+" ")
|
cmd = cmd[1:] // Cutting the command character.
|
||||||
switch cmd {
|
switch cmd {
|
||||||
case "/dlp":
|
case "dlp":
|
||||||
handleCmdDLP(ctx, entities, u, msg)
|
handleCmdDLP(ctx, entities, u, msg)
|
||||||
return nil
|
return nil
|
||||||
case "/dlpcancel":
|
case "dlpcancel":
|
||||||
handleCmdDLPCancel(ctx, entities, u, msg)
|
handleCmdDLPCancel(ctx, entities, u, msg)
|
||||||
return nil
|
return nil
|
||||||
case "/start":
|
case "start":
|
||||||
fmt.Println(" (start cmd)")
|
fmt.Println(" (start cmd)")
|
||||||
if fromGroup == nil {
|
if fromGroup == nil {
|
||||||
_, _ = telegramSender.Reply(entities, u).Text(ctx, "🤖 Welcome! This bot downloads videos from various "+
|
_, _ = telegramSender.Reply(entities, u).Text(ctx, "🤖 Welcome! This bot downloads videos from various "+
|
||||||
@@ -113,6 +153,29 @@ func handleMsg(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isVideoMessage(msg *tg.Message) bool {
|
||||||
|
if msg.Media == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch media := msg.Media.(type) {
|
||||||
|
case *tg.MessageMediaDocument:
|
||||||
|
if doc, ok := media.Document.(*tg.Document); ok {
|
||||||
|
// Check if it's a video mime type
|
||||||
|
mimeType := doc.MimeType
|
||||||
|
if strings.HasPrefix(mimeType, "video/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Also check attributes for video
|
||||||
|
for _, attr := range doc.Attributes {
|
||||||
|
if _, ok := attr.(*tg.DocumentAttributeVideo); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("yt-dlp-telegram-bot starting...")
|
fmt.Println("yt-dlp-telegram-bot starting...")
|
||||||
|
|
||||||
@@ -151,6 +214,15 @@ func main() {
|
|||||||
|
|
||||||
telegramUploader = uploader.NewUploader(api).WithProgress(dlUploader)
|
telegramUploader = uploader.NewUploader(api).WithProgress(dlUploader)
|
||||||
telegramSender = message.NewSender(api).WithUploader(telegramUploader)
|
telegramSender = message.NewSender(api).WithUploader(telegramUploader)
|
||||||
|
telegramClient = client
|
||||||
|
|
||||||
|
goutubedl.Path, err = exec.LookPath(goutubedl.Path)
|
||||||
|
if err != nil {
|
||||||
|
goutubedl.Path, err = ytdlpDownloadLatest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprint("error: ", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dlQueue.Init(ctx)
|
dlQueue.Init(ctx)
|
||||||
|
|
||||||
@@ -158,14 +230,29 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("telegram connection up")
|
fmt.Println("telegram connection up")
|
||||||
|
|
||||||
ytdlpVersionCheckStr, _ := ytdlpVersionCheckGetStr(ctx)
|
ytdlpVersionCheckStr, updateNeeded, _ := ytdlpVersionCheckGetStr(ctx)
|
||||||
|
if updateNeeded {
|
||||||
|
goutubedl.Path, err = ytdlpDownloadLatest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprint("error: ", err))
|
||||||
|
}
|
||||||
|
ytdlpVersionCheckStr, _, _ = ytdlpVersionCheckGetStr(ctx)
|
||||||
|
}
|
||||||
sendTextToAdmins(ctx, "🤖 Bot started, "+ytdlpVersionCheckStr)
|
sendTextToAdmins(ctx, "🤖 Bot started, "+ytdlpVersionCheckStr)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(24 * time.Hour)
|
time.Sleep(24 * time.Hour)
|
||||||
if s, updateNeededOrError := ytdlpVersionCheckGetStr(ctx); updateNeededOrError {
|
s, updateNeeded, gotError := ytdlpVersionCheckGetStr(ctx)
|
||||||
|
if gotError {
|
||||||
sendTextToAdmins(ctx, s)
|
sendTextToAdmins(ctx, s)
|
||||||
|
} else if updateNeeded {
|
||||||
|
goutubedl.Path, err = ytdlpDownloadLatest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprint("error: ", err))
|
||||||
|
}
|
||||||
|
ytdlpVersionCheckStr, _, _ = ytdlpVersionCheckGetStr(ctx)
|
||||||
|
sendTextToAdmins(ctx, "🤖 Bot updated, "+ytdlpVersionCheckStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -22,7 +21,8 @@ type paramsType struct {
|
|||||||
AdminUserIDs []int64
|
AdminUserIDs []int64
|
||||||
AllowedGroupIDs []int64
|
AllowedGroupIDs []int64
|
||||||
|
|
||||||
MaxSize int64
|
MaxSize int64
|
||||||
|
SaveDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
var params paramsType
|
var params paramsType
|
||||||
@@ -79,10 +79,6 @@ func (p *paramsType) Init() error {
|
|||||||
if goutubedl.Path == "" {
|
if goutubedl.Path == "" {
|
||||||
goutubedl.Path = "yt-dlp"
|
goutubedl.Path = "yt-dlp"
|
||||||
}
|
}
|
||||||
goutubedl.Path, err = exec.LookPath(goutubedl.Path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("yt-dlp not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if allowedUserIDs == "" {
|
if allowedUserIDs == "" {
|
||||||
allowedUserIDs = os.Getenv("ALLOWED_USERIDS")
|
allowedUserIDs = os.Getenv("ALLOWED_USERIDS")
|
||||||
@@ -143,6 +139,15 @@ func (p *paramsType) Init() error {
|
|||||||
p.MaxSize = b.Int64()
|
p.MaxSize = b.Int64()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.SaveDir = os.Getenv("SAVE_DIR")
|
||||||
|
if p.SaveDir == "" {
|
||||||
|
p.SaveDir = "/root/save_dir"
|
||||||
|
}
|
||||||
|
// Create save directory if it doesn't exist
|
||||||
|
if err := os.MkdirAll(p.SaveDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("couldn't create save directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Writing env. var YTDLP_COOKIES contents to a file.
|
// Writing env. var YTDLP_COOKIES contents to a file.
|
||||||
// In case a docker container is used, the yt-dlp.conf points yt-dlp to this cookie file.
|
// In case a docker container is used, the yt-dlp.conf points yt-dlp to this cookie file.
|
||||||
if cookies := os.Getenv("YTDLP_COOKIES"); cookies != "" {
|
if cookies := os.Getenv("YTDLP_COOKIES"); cookies != "" {
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/gotd/td/telegram/message"
|
"github.com/gotd/td/telegram/message"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
)
|
)
|
||||||
@@ -21,7 +24,13 @@ const maxProgressPercentUpdateInterval = time.Second
|
|||||||
const progressBarLength = 10
|
const progressBarLength = 10
|
||||||
|
|
||||||
type DownloadQueueEntry struct {
|
type DownloadQueueEntry struct {
|
||||||
URL string
|
URL string
|
||||||
|
Format string
|
||||||
|
|
||||||
|
// IsVideoMessage is true if this entry is for a video message download
|
||||||
|
IsVideoMessage bool
|
||||||
|
// VideoDocument stores the video document info for video messages
|
||||||
|
VideoDocument *tg.Document
|
||||||
|
|
||||||
OrigEntities tg.Entities
|
OrigEntities tg.Entities
|
||||||
OrigMsgUpdate *tg.UpdateNewMessage
|
OrigMsgUpdate *tg.UpdateNewMessage
|
||||||
@@ -37,23 +46,10 @@ type DownloadQueueEntry struct {
|
|||||||
Canceled bool
|
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) {
|
func (e *DownloadQueueEntry) sendTypingAction(ctx context.Context) {
|
||||||
// _ = telegramSender.To(e.getTypingActionDst()).TypingAction().Typing(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *DownloadQueueEntry) sendTypingCancelAction(ctx context.Context) {
|
func (e *DownloadQueueEntry) sendTypingCancelAction(ctx context.Context) {
|
||||||
// _ = telegramSender.To(e.getTypingActionDst()).TypingAction().Cancel(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *DownloadQueueEntry) editReply(ctx context.Context, s string) {
|
func (e *DownloadQueueEntry) editReply(ctx context.Context, s string) {
|
||||||
@@ -87,22 +83,38 @@ func (e *DownloadQueue) getQueuePositionString(pos int) string {
|
|||||||
return "👨👦👦 Request queued at position #" + fmt.Sprint(pos)
|
return "👨👦👦 Request queued at position #" + fmt.Sprint(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *DownloadQueue) Add(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, url string) {
|
func (q *DownloadQueue) Add(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, url, format string) {
|
||||||
|
q.addEntry(ctx, entities, u, url, format, false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddVideo adds a video message download to the queue
|
||||||
|
func (q *DownloadQueue) AddVideo(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, videoDocument *tg.Document) {
|
||||||
|
q.addEntry(ctx, entities, u, "", "", true, videoDocument)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *DownloadQueue) addEntry(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, url, format string, isVideo bool, videoDocument *tg.Document) {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
|
|
||||||
var replyStr string
|
var replyStr string
|
||||||
if len(q.entries) == 0 {
|
if len(q.entries) == 0 {
|
||||||
replyStr = processStartStr
|
if isVideo {
|
||||||
|
replyStr = "⬇️ Downloading video..."
|
||||||
|
} else {
|
||||||
|
replyStr = processStartStr
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" queueing request at position #", len(q.entries))
|
fmt.Println(" queueing request at position #", len(q.entries))
|
||||||
replyStr = q.getQueuePositionString(len(q.entries))
|
replyStr = q.getQueuePositionString(len(q.entries))
|
||||||
}
|
}
|
||||||
|
|
||||||
newEntry := DownloadQueueEntry{
|
newEntry := DownloadQueueEntry{
|
||||||
URL: url,
|
URL: url,
|
||||||
OrigEntities: entities,
|
Format: format,
|
||||||
OrigMsgUpdate: u,
|
IsVideoMessage: isVideo,
|
||||||
OrigMsg: u.Message.(*tg.Message),
|
VideoDocument: videoDocument,
|
||||||
|
OrigEntities: entities,
|
||||||
|
OrigMsgUpdate: u,
|
||||||
|
OrigMsg: u.Message.(*tg.Message),
|
||||||
}
|
}
|
||||||
|
|
||||||
newEntry.Reply = telegramSender.Reply(entities, u)
|
newEntry.Reply = telegramSender.Reply(entities, u)
|
||||||
@@ -190,8 +202,15 @@ func (q *DownloadQueue) processQueueEntry(ctx context.Context, qEntry *DownloadQ
|
|||||||
if fromUsername != "" {
|
if fromUsername != "" {
|
||||||
fmt.Print(" from ", fromUsername, "#", qEntry.FromUser.UserID)
|
fmt.Print(" from ", fromUsername, "#", qEntry.FromUser.UserID)
|
||||||
}
|
}
|
||||||
fmt.Println(":", qEntry.URL)
|
|
||||||
|
|
||||||
|
// Handle video message downloads differently
|
||||||
|
if qEntry.IsVideoMessage {
|
||||||
|
fmt.Println(": [video message]")
|
||||||
|
q.processVideoMessageEntry(ctx, qEntry)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(":", qEntry.URL)
|
||||||
qEntry.editReply(ctx, processStartStr)
|
qEntry.editReply(ctx, processStartStr)
|
||||||
|
|
||||||
downloader := Downloader{
|
downloader := Downloader{
|
||||||
@@ -200,7 +219,10 @@ func (q *DownloadQueue) processQueueEntry(ctx context.Context, qEntry *DownloadQ
|
|||||||
if audioCodecs == "" {
|
if audioCodecs == "" {
|
||||||
q.currentlyDownloadedEntry.sourceCodecInfo += ", no audio"
|
q.currentlyDownloadedEntry.sourceCodecInfo += ", no audio"
|
||||||
} else {
|
} else {
|
||||||
q.currentlyDownloadedEntry.sourceCodecInfo += " / " + audioCodecs
|
if videoCodecs != "" {
|
||||||
|
q.currentlyDownloadedEntry.sourceCodecInfo += " / "
|
||||||
|
}
|
||||||
|
q.currentlyDownloadedEntry.sourceCodecInfo += audioCodecs
|
||||||
}
|
}
|
||||||
if convertActionsNeeded == "" {
|
if convertActionsNeeded == "" {
|
||||||
q.currentlyDownloadedEntry.sourceCodecInfo += " (no conversion needed)"
|
q.currentlyDownloadedEntry.sourceCodecInfo += " (no conversion needed)"
|
||||||
@@ -212,7 +234,8 @@ func (q *DownloadQueue) processQueueEntry(ctx context.Context, qEntry *DownloadQ
|
|||||||
UpdateProgressPercentFunc: q.HandleProgressPercentUpdate,
|
UpdateProgressPercentFunc: q.HandleProgressPercentUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := downloader.DownloadAndConvertURL(qEntry.Ctx, qEntry.OrigMsg.Message)
|
// Download the file to SAVE_DIR
|
||||||
|
dlResult, err := downloader.DownloadAndConvertURL(qEntry.Ctx, qEntry.OrigMsg.Message, qEntry.Format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(" error downloading:", err)
|
fmt.Println(" error downloading:", err)
|
||||||
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
||||||
@@ -222,28 +245,103 @@ func (q *DownloadQueue) processQueueEntry(ctx context.Context, qEntry *DownloadQ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feeding the returned io.ReadCloser to the uploader.
|
// Probe the downloaded file to check codec compatibility
|
||||||
fmt.Println(" processing...")
|
conv := Converter{
|
||||||
|
Format: qEntry.Format,
|
||||||
|
UpdateProgressPercentCallback: q.HandleProgressPercentUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conv.ProbeFile(dlResult.FilePath); err != nil {
|
||||||
|
fmt.Println(" error probing file:", err)
|
||||||
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
||||||
|
q.currentlyDownloadedEntry.disableProgressPercentUpdate = true
|
||||||
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Unlock()
|
||||||
|
qEntry.editReply(ctx, fmt.Sprint(errorStr+": ", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update codec info in UI
|
||||||
|
if downloader.ConvertStartFunc != nil {
|
||||||
|
downloader.ConvertStartFunc(ctx, conv.VideoCodecs, conv.AudioCodecs, conv.GetActionsNeeded())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file is small enough to upload to Telegram (<512MB)
|
||||||
|
if dlResult.FileSize >= telegramUploadThreshold {
|
||||||
|
// File too large, only save to disk
|
||||||
|
fmt.Printf(" file too large (%s >= 512MB), skipping Telegram upload\n", humanize.Bytes(uint64(dlResult.FileSize)))
|
||||||
|
qEntry.editReply(ctx, fmt.Sprintf("✅ Saved to server\n📁 %s\n💾 Size: %s\n⚠️ File too large for Telegram upload (>512MB)",
|
||||||
|
dlResult.FilePath, humanize.Bytes(uint64(dlResult.FileSize))))
|
||||||
|
qEntry.sendTypingCancelAction(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// File is small enough, process for upload
|
||||||
|
fmt.Println(" processing for upload...")
|
||||||
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
||||||
q.updateProgress(ctx, qEntry, processStr, q.currentlyDownloadedEntry.lastProgressPercent)
|
q.updateProgress(ctx, qEntry, processStr, q.currentlyDownloadedEntry.lastProgressPercent)
|
||||||
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Unlock()
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Unlock()
|
||||||
|
|
||||||
err = dlUploader.UploadFile(qEntry.Ctx, qEntry.OrigEntities, qEntry.OrigMsgUpdate, r)
|
// Convert if needed, then upload
|
||||||
|
uploadPath := dlResult.FilePath
|
||||||
|
uploadFormat := "mkv"
|
||||||
|
if qEntry.Format == "mp3" {
|
||||||
|
uploadFormat = "mp3"
|
||||||
|
}
|
||||||
|
|
||||||
|
// For video format, determine the actual format from filename
|
||||||
|
if qEntry.Format != "mp3" {
|
||||||
|
ext := filepath.Ext(uploadPath)
|
||||||
|
if ext != "" {
|
||||||
|
uploadFormat = ext[1:] // Remove the leading dot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conv.NeedConvert() {
|
||||||
|
// Need conversion
|
||||||
|
outputPath, outputFormat, err := conv.ConvertIfNeeded(qEntry.Ctx, dlResult.FilePath, params.SaveDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(" error converting:", err)
|
||||||
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
||||||
|
q.currentlyDownloadedEntry.disableProgressPercentUpdate = true
|
||||||
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Unlock()
|
||||||
|
qEntry.editReply(ctx, fmt.Sprint(errorStr+": ", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadPath = outputPath
|
||||||
|
uploadFormat = outputFormat
|
||||||
|
// Keep both original and converted files
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file for upload
|
||||||
|
file, err := os.Open(uploadPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(" error processing:", err)
|
fmt.Println(" error opening file:", err)
|
||||||
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
||||||
q.currentlyDownloadedEntry.disableProgressPercentUpdate = true
|
q.currentlyDownloadedEntry.disableProgressPercentUpdate = true
|
||||||
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Unlock()
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Unlock()
|
||||||
r.Close()
|
|
||||||
qEntry.editReply(ctx, fmt.Sprint(errorStr+": ", err))
|
qEntry.editReply(ctx, fmt.Sprint(errorStr+": ", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
defer file.Close()
|
||||||
q.currentlyDownloadedEntry.disableProgressPercentUpdate = true
|
|
||||||
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Unlock()
|
// Upload to Telegram with video dimensions
|
||||||
r.Close()
|
err = dlUploader.UploadFile(qEntry.Ctx, qEntry.OrigEntities, qEntry.OrigMsgUpdate, file, uploadFormat, dlResult.Title, conv.VideoWidth, conv.VideoHeight)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(" error uploading:", err)
|
||||||
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
||||||
|
q.currentlyDownloadedEntry.disableProgressPercentUpdate = true
|
||||||
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Unlock()
|
||||||
|
file.Close()
|
||||||
|
qEntry.editReply(ctx, fmt.Sprint(errorStr+": ", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
// Remove the uploaded file (since it's saved in SAVE_DIR, we keep it only if needed)
|
||||||
|
// Actually, we keep the file in SAVE_DIR as requested
|
||||||
|
|
||||||
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
q.currentlyDownloadedEntry.progressPercentUpdateMutex.Lock()
|
||||||
|
q.currentlyDownloadedEntry.disableProgressPercentUpdate = true
|
||||||
if qEntry.Canceled {
|
if qEntry.Canceled {
|
||||||
fmt.Print(" canceled\n")
|
fmt.Print(" canceled\n")
|
||||||
q.updateProgress(ctx, qEntry, canceledStr, q.currentlyDownloadedEntry.lastProgressPercent)
|
q.updateProgress(ctx, qEntry, canceledStr, q.currentlyDownloadedEntry.lastProgressPercent)
|
||||||
@@ -255,6 +353,157 @@ func (q *DownloadQueue) processQueueEntry(ctx context.Context, qEntry *DownloadQ
|
|||||||
qEntry.sendTypingCancelAction(ctx)
|
qEntry.sendTypingCancelAction(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *DownloadQueue) processVideoMessageEntry(ctx context.Context, qEntry *DownloadQueueEntry) {
|
||||||
|
videoFile := qEntry.VideoDocument
|
||||||
|
if videoFile == nil {
|
||||||
|
qEntry.editReply(ctx, errorStr+": could not get video info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file name
|
||||||
|
var fileName string
|
||||||
|
for _, attr := range videoFile.Attributes {
|
||||||
|
if docAttr, ok := attr.(*tg.DocumentAttributeFilename); ok {
|
||||||
|
fileName = docAttr.FileName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileName == "" {
|
||||||
|
fileName = fmt.Sprintf("video_%d.mp4", time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create safe filename with date prefix: YYYY-MM-DD-{timestamp}-{filename}.{ext}
|
||||||
|
now := time.Now()
|
||||||
|
dateStr := now.Format("2006-01-02")
|
||||||
|
timestamp := now.Unix()
|
||||||
|
ext := filepath.Ext(fileName)
|
||||||
|
if ext == "" {
|
||||||
|
ext = ".mp4"
|
||||||
|
}
|
||||||
|
safeFileName := fmt.Sprintf("%s-%d%s", dateStr, timestamp, ext)
|
||||||
|
filePath := filepath.Join(params.SaveDir, safeFileName)
|
||||||
|
|
||||||
|
// Check if file already exists
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
for i := 1; i < 1000; i++ {
|
||||||
|
safeFileName = fmt.Sprintf("%s-%d-%d%s", dateStr, timestamp, i, ext)
|
||||||
|
filePath = filepath.Join(params.SaveDir, safeFileName)
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress message
|
||||||
|
qEntry.editReply(ctx, "⬇️ Downloading video...")
|
||||||
|
|
||||||
|
// Get file size
|
||||||
|
fileSize := videoFile.Size
|
||||||
|
|
||||||
|
// Download file using Telegram client
|
||||||
|
fileLoc := &tg.InputDocumentFileLocation{
|
||||||
|
ID: videoFile.ID,
|
||||||
|
AccessHash: videoFile.AccessHash,
|
||||||
|
FileReference: videoFile.FileReference,
|
||||||
|
}
|
||||||
|
fmt.Printf(" downloading video: %s (size: %s)\n", fileName, humanize.Bytes(uint64(fileSize)))
|
||||||
|
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
qEntry.editReply(ctx, errorStr+": could not create file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download with progress
|
||||||
|
offset := int64(0)
|
||||||
|
chunkSize := int64(1024 * 1024) // 1MB chunks
|
||||||
|
lastPercent := 0
|
||||||
|
written := int64(0)
|
||||||
|
|
||||||
|
for offset < fileSize {
|
||||||
|
select {
|
||||||
|
case <-qEntry.Ctx.Done():
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
qEntry.editReply(ctx, "❌ Canceled")
|
||||||
|
qEntry.Canceled = true
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset+chunkSize > fileSize {
|
||||||
|
chunkSize = fileSize - offset
|
||||||
|
}
|
||||||
|
// Telegram API requires limit to be divisible by 1KB
|
||||||
|
if chunkSize%1024 != 0 {
|
||||||
|
// Round down to nearest 1KB boundary
|
||||||
|
chunkSize = (chunkSize / 1024) * 1024
|
||||||
|
if chunkSize == 0 {
|
||||||
|
// If remaining data is less than 1KB, round up to 1KB
|
||||||
|
// The API will return the actual remaining bytes
|
||||||
|
chunkSize = 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loc := &tg.InputDocumentFileLocation{
|
||||||
|
ID: videoFile.ID,
|
||||||
|
AccessHash: videoFile.AccessHash,
|
||||||
|
FileReference: fileLoc.FileReference,
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk, err := telegramClient.API().UploadGetFile(qEntry.Ctx, &tg.UploadGetFileRequest{
|
||||||
|
Location: loc,
|
||||||
|
Offset: offset,
|
||||||
|
Limit: int(chunkSize),
|
||||||
|
Precise: true,
|
||||||
|
CDNSupported: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
qEntry.editReply(ctx, errorStr+": failed to download chunk: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkData, ok := chunk.(*tg.UploadFile)
|
||||||
|
if !ok {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
qEntry.editReply(ctx, errorStr+": unexpected response type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := file.Write(chunkData.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
qEntry.editReply(ctx, errorStr+": failed to write to file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += int64(n)
|
||||||
|
written += int64(n)
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
if fileSize > 0 {
|
||||||
|
percent := int(float64(written) * 100 / float64(fileSize))
|
||||||
|
if percent != lastPercent && percent%10 == 0 {
|
||||||
|
lastPercent = percent
|
||||||
|
progressBar := getProgressbar(percent, progressBarLength)
|
||||||
|
qEntry.editReply(ctx, "⬇️ Downloading video...\n"+progressBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
// Send success message
|
||||||
|
savedMsg := fmt.Sprintf("✅ Video saved\n📁 %s\n💾 Size: %s", safeFileName, humanize.Bytes(uint64(written)))
|
||||||
|
qEntry.editReply(ctx, savedMsg)
|
||||||
|
|
||||||
|
fmt.Printf(" video saved to: %s\n", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *DownloadQueue) processor() {
|
func (q *DownloadQueue) processor() {
|
||||||
for {
|
for {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ ALLOWED_USERIDS=$ALLOWED_USERIDS \
|
|||||||
ADMIN_USERIDS=$ADMIN_USERIDS \
|
ADMIN_USERIDS=$ADMIN_USERIDS \
|
||||||
ALLOWED_GROUPIDS=$ALLOWED_GROUPIDS \
|
ALLOWED_GROUPIDS=$ALLOWED_GROUPIDS \
|
||||||
MAX_SIZE=$MAX_SIZE \
|
MAX_SIZE=$MAX_SIZE \
|
||||||
|
SAVE_DIR=$SAVE_DIR \
|
||||||
|
YTDLP_PATH=$YTDLP_PATH \
|
||||||
$bin
|
$bin
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/flytam/filenamify"
|
||||||
"github.com/gotd/td/telegram/message"
|
"github.com/gotd/td/telegram/message"
|
||||||
"github.com/gotd/td/telegram/uploader"
|
"github.com/gotd/td/telegram/uploader"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
@@ -22,34 +24,51 @@ func (p Uploader) Chunk(ctx context.Context, state uploader.ProgressState) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Uploader) UploadFile(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, f io.ReadCloser) error {
|
func (p *Uploader) UploadFile(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, f io.ReadCloser, format, title string, width, height int) error {
|
||||||
// Reading to a buffer first, because we don't know the file size.
|
// Get file size by seeking if it's a file
|
||||||
var buf bytes.Buffer
|
var fileSize int64
|
||||||
for {
|
if file, ok := f.(*os.File); ok {
|
||||||
b := make([]byte, 1024)
|
stat, err := file.Stat()
|
||||||
n, err := f.Read(b)
|
if err != nil {
|
||||||
if err != nil && err != io.EOF {
|
return fmt.Errorf("getting file stat error: %w", err)
|
||||||
return fmt.Errorf("reading to buffer error: %w", err)
|
|
||||||
}
|
}
|
||||||
if n == 0 {
|
fileSize = stat.Size()
|
||||||
break
|
|
||||||
}
|
if params.MaxSize > 0 && fileSize > params.MaxSize {
|
||||||
if params.MaxSize > 0 && buf.Len() > int(params.MaxSize) {
|
|
||||||
return fmt.Errorf("file is too big, max. allowed size is %s", humanize.BigBytes(big.NewInt(int64(params.MaxSize))))
|
return fmt.Errorf("file is too big, max. allowed size is %s", humanize.BigBytes(big.NewInt(int64(params.MaxSize))))
|
||||||
}
|
}
|
||||||
buf.Write(b[:n])
|
} else {
|
||||||
|
// Fallback: read to buffer for non-file readers
|
||||||
|
return p.uploadFromBuffer(ctx, entities, u, f, format, title, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(" got", buf.Len(), "bytes, uploading...")
|
fmt.Println(" got", fileSize, "bytes, uploading...")
|
||||||
dlQueue.currentlyDownloadedEntry.progressInfo = fmt.Sprint(" (", humanize.BigBytes(big.NewInt(int64(buf.Len()))), ")")
|
dlQueue.currentlyDownloadedEntry.progressInfo = fmt.Sprint(" (", humanize.BigBytes(big.NewInt(fileSize)), ")")
|
||||||
|
|
||||||
upload, err := telegramUploader.FromBytes(ctx, "yt-dlp", buf.Bytes())
|
// Reset file pointer to beginning
|
||||||
|
if _, err := f.(*os.File).Seek(0, 0); err != nil {
|
||||||
|
return fmt.Errorf("seeking file error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use uploader.NewUpload with progress callback
|
||||||
|
upload, err := telegramUploader.Upload(ctx, uploader.NewUpload("yt-dlp", f, fileSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("uploading %w", err)
|
return fmt.Errorf("uploading %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we have uploaded file handle, sending it as styled message. First, preparing message.
|
// Now we have uploaded file handle, sending it as styled message. First, preparing message.
|
||||||
document := message.UploadedDocument(upload).Video()
|
var document message.MediaOption
|
||||||
|
filename, _ := filenamify.Filenamify(title+"."+format, filenamify.Options{Replacement: " "})
|
||||||
|
if format == "mp3" {
|
||||||
|
document = message.UploadedDocument(upload).Filename(filename).Audio().Title(title)
|
||||||
|
} else {
|
||||||
|
doc := message.UploadedDocument(upload).Filename(filename).Video()
|
||||||
|
// Set resolution to help Telegram display correct aspect ratio
|
||||||
|
if width > 0 && height > 0 {
|
||||||
|
doc = doc.Resolution(width, height)
|
||||||
|
}
|
||||||
|
document = doc
|
||||||
|
}
|
||||||
|
|
||||||
// Sending message with media.
|
// Sending message with media.
|
||||||
if _, err := telegramSender.Answer(entities, u).Media(ctx, document); err != nil {
|
if _, err := telegramSender.Answer(entities, u).Media(ctx, document); err != nil {
|
||||||
@@ -58,3 +77,51 @@ func (p *Uploader) UploadFile(ctx context.Context, entities tg.Entities, u *tg.U
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Uploader) uploadFromBuffer(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage, f io.ReadCloser, format, title string, width, height int) error {
|
||||||
|
// Fallback for non-file io.ReadCloser - read all to buffer
|
||||||
|
buf := make([]byte, 0)
|
||||||
|
tempBuf := make([]byte, 8192)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := f.Read(tempBuf)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return fmt.Errorf("reading to buffer error: %w", err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf = append(buf, tempBuf[:n]...)
|
||||||
|
if params.MaxSize > 0 && len(buf) > int(params.MaxSize) {
|
||||||
|
return fmt.Errorf("file is too big, max. allowed size is %s", humanize.BigBytes(big.NewInt(int64(params.MaxSize))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(" got", len(buf), "bytes, uploading...")
|
||||||
|
dlQueue.currentlyDownloadedEntry.progressInfo = fmt.Sprint(" (", humanize.BigBytes(big.NewInt(int64(len(buf)))), ")")
|
||||||
|
|
||||||
|
// Use Upload with progress for buffer too
|
||||||
|
upload, err := telegramUploader.Upload(ctx, uploader.NewUpload("yt-dlp", bytes.NewReader(buf), int64(len(buf))))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("uploading %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var document message.MediaOption
|
||||||
|
filename, _ := filenamify.Filenamify(title+"."+format, filenamify.Options{Replacement: " "})
|
||||||
|
if format == "mp3" {
|
||||||
|
document = message.UploadedDocument(upload).Filename(filename).Audio().Title(title)
|
||||||
|
} else {
|
||||||
|
doc := message.UploadedDocument(upload).Filename(filename).Video()
|
||||||
|
// Set resolution to help Telegram display correct aspect ratio
|
||||||
|
if width > 0 && height > 0 {
|
||||||
|
doc = doc.Resolution(width, height)
|
||||||
|
}
|
||||||
|
document = doc
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := telegramSender.Answer(entities, u).Media(ctx, document); err != nil {
|
||||||
|
return fmt.Errorf("send: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+100
-7
@@ -2,7 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-github/v53/github"
|
"github.com/google/go-github/v53/github"
|
||||||
@@ -11,12 +16,100 @@ import (
|
|||||||
|
|
||||||
const ytdlpVersionCheckTimeout = time.Second * 10
|
const ytdlpVersionCheckTimeout = time.Second * 10
|
||||||
|
|
||||||
func ytdlpVersionCheck(ctx context.Context) (latestVersion, currentVersion string, err error) {
|
func ytdlpGetLatestRelease(ctx context.Context) (release *github.RepositoryRelease, err error) {
|
||||||
client := github.NewClient(nil)
|
client := github.NewClient(nil)
|
||||||
|
|
||||||
release, _, err := client.Repositories.GetLatestRelease(ctx, "yt-dlp", "yt-dlp")
|
release, _, err = client.Repositories.GetLatestRelease(ctx, "yt-dlp", "yt-dlp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("getting latest yt-dlp version: %w", err)
|
return nil, fmt.Errorf("getting latest yt-dlp version: %w", err)
|
||||||
|
}
|
||||||
|
return release, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ytdlpGithubReleaseAsset struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"browser_download_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ytdlpGetLatestReleaseURL(ctx context.Context) (url string, err error) {
|
||||||
|
release, err := ytdlpGetLatestRelease(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
assetsURL := release.GetAssetsURL()
|
||||||
|
if assetsURL == "" {
|
||||||
|
return "", fmt.Errorf("downloading latest yt-dlp: no assets url")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(assetsURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("downloading latest yt-dlp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("downloading latest yt-dlp: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var assets []ytdlpGithubReleaseAsset
|
||||||
|
err = json.Unmarshal(body, &assets)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("downloading latest yt-dlp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(assets) == 0 {
|
||||||
|
return "", fmt.Errorf("downloading latest yt-dlp: no release assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, asset := range assets {
|
||||||
|
if asset.Name == "yt-dlp" {
|
||||||
|
url = asset.URL
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url == "" {
|
||||||
|
return "", fmt.Errorf("downloading latest yt-dlp: no release asset url")
|
||||||
|
}
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ytdlpDownloadLatest(ctx context.Context) (path string, err error) {
|
||||||
|
url, err := ytdlpGetLatestReleaseURL(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("downloading latest yt-dlp: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
file, err := os.Create(filepath.Join(os.TempDir(), "yt-dlp"))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("downloading latest yt-dlp: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Chmod(file.Name(), 0755)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ytdlpVersionCheck(ctx context.Context) (latestVersion, currentVersion string, err error) {
|
||||||
|
release, err := ytdlpGetLatestRelease(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
}
|
}
|
||||||
latestVersion = release.GetTagName()
|
latestVersion = release.GetTagName()
|
||||||
|
|
||||||
@@ -27,19 +120,19 @@ func ytdlpVersionCheck(ctx context.Context) (latestVersion, currentVersion strin
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func ytdlpVersionCheckGetStr(ctx context.Context) (res string, updateNeededOrError bool) {
|
func ytdlpVersionCheckGetStr(ctx context.Context) (res string, updateNeeded, gotError bool) {
|
||||||
verCheckCtx, verCheckCtxCancel := context.WithTimeout(ctx, ytdlpVersionCheckTimeout)
|
verCheckCtx, verCheckCtxCancel := context.WithTimeout(ctx, ytdlpVersionCheckTimeout)
|
||||||
defer verCheckCtxCancel()
|
defer verCheckCtxCancel()
|
||||||
|
|
||||||
var latestVersion, currentVersion string
|
var latestVersion, currentVersion string
|
||||||
var err error
|
var err error
|
||||||
if latestVersion, currentVersion, err = ytdlpVersionCheck(verCheckCtx); err != nil {
|
if latestVersion, currentVersion, err = ytdlpVersionCheck(verCheckCtx); err != nil {
|
||||||
return errorStr + ": " + err.Error(), true
|
return errorStr + ": " + err.Error(), false, true
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNeededOrError = currentVersion != latestVersion
|
updateNeeded = currentVersion != latestVersion
|
||||||
res = "yt-dlp version: " + currentVersion
|
res = "yt-dlp version: " + currentVersion
|
||||||
if updateNeededOrError {
|
if updateNeeded {
|
||||||
res = "📢 " + res + " 📢 Update needed! Latest version is " + latestVersion + " 📢"
|
res = "📢 " + res + " 📢 Update needed! Latest version is " + latestVersion + " 📢"
|
||||||
} else {
|
} else {
|
||||||
res += " (up to date)"
|
res += " (up to date)"
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
--cookies=/tmp/ytdlp-cookies.txt
|
--cookies-from-browser "chrome:/root/chromium/config/.config/chromium"
|
||||||
|
--user-agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
||||||
|
|||||||
Reference in New Issue
Block a user