save videos to save dir
This commit is contained in:
parent
f2c9c4e388
commit
a42bad7c48
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
/config.inc.sh
|
/config.inc.sh
|
||||||
/yt-dlp-telegram-bot
|
/yt-dlp-telegram-bot
|
||||||
|
/.vscode
|
||||||
|
/save
|
||||||
|
|||||||
@ -10,5 +10,7 @@ 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_dir
|
||||||
|
|
||||||
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_dir 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
|
||||||
|
|||||||
@ -5,3 +5,4 @@ ALLOWED_USERIDS=
|
|||||||
ADMIN_USERIDS=
|
ADMIN_USERIDS=
|
||||||
ALLOWED_GROUPIDS=
|
ALLOWED_GROUPIDS=
|
||||||
MAX_SIZE=
|
MAX_SIZE=
|
||||||
|
SAVE_DIR=/root/save_dir
|
||||||
|
|||||||
78
convert.go
78
convert.go
@ -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 {
|
||||||
@ -45,6 +47,8 @@ type Converter struct {
|
|||||||
VideoCodecs string
|
VideoCodecs string
|
||||||
VideoConvertNeeded bool
|
VideoConvertNeeded bool
|
||||||
SingleVideoStreamNeeded bool
|
SingleVideoStreamNeeded bool
|
||||||
|
VideoWidth int
|
||||||
|
VideoHeight int
|
||||||
|
|
||||||
AudioCodecs string
|
AudioCodecs string
|
||||||
AudioConvertNeeded bool
|
AudioConvertNeeded bool
|
||||||
@ -55,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)
|
||||||
}
|
}
|
||||||
@ -96,6 +95,12 @@ func (c *Converter) Probe(rr *ReReadCloser) error {
|
|||||||
}
|
}
|
||||||
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
|
||||||
@ -192,10 +197,7 @@ func (c *Converter) GetActionsNeeded() string {
|
|||||||
return strings.Join(convertNeeded, ", ")
|
return strings.Join(convertNeeded, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Converter) ConvertIfNeeded(ctx context.Context, rr *ReReadCloser) (reader io.ReadCloser, outputFormat string, err 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")
|
||||||
|
|
||||||
videoNeeded := true
|
videoNeeded := true
|
||||||
@ -205,7 +207,25 @@ func (c *Converter) ConvertIfNeeded(ctx context.Context, rr *ReReadCloser) (read
|
|||||||
outputFormat = "mp3"
|
outputFormat = "mp3"
|
||||||
}
|
}
|
||||||
|
|
||||||
args := ffmpeg_go.KwArgs{"format": outputFormat}
|
// Determine output path
|
||||||
|
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 {
|
if videoNeeded {
|
||||||
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"movflags": "frag_keyframe+empty_moov+faststart"}})
|
args = ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{args, {"movflags": "frag_keyframe+empty_moov+faststart"}})
|
||||||
@ -239,7 +259,7 @@ func (c *Converter) ConvertIfNeeded(ctx context.Context, rr *ReReadCloser) (read
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ff := ffmpeg_go.Input("pipe:0").Output("pipe:1", args)
|
ff := ffmpeg_go.Input(inputPath).Output(outputPath, args)
|
||||||
|
|
||||||
var progressSock net.Listener
|
var progressSock net.Listener
|
||||||
if c.UpdateProgressPercentCallback != nil {
|
if c.UpdateProgressPercentCallback != nil {
|
||||||
@ -254,26 +274,26 @@ func (c *Converter) ConvertIfNeeded(ctx context.Context, rr *ReReadCloser) (read
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, outputFormat, fmt.Errorf("error converting: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader, outputFormat, nil
|
if progressSock != nil {
|
||||||
|
progressSock.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputPath, outputFormat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Converter) NeedConvert() bool {
|
||||||
|
return c.VideoConvertNeeded || c.AudioConvertNeeded || c.SingleVideoStreamNeeded || c.SingleAudioStreamNeeded
|
||||||
}
|
}
|
||||||
|
|||||||
173
dl.go
173
dl.go
@ -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,49 +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, title string, 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()
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewReReadCloser(dlResult), result.Info.Title, nil
|
// 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, format string) (r io.ReadCloser, outputFormat, title string, err error) {
|
func (d *Downloader) DownloadAndConvertURL(ctx context.Context, url, format string) (*DownloadResult, error) {
|
||||||
rr, title, err := d.downloadURL(ctx, url)
|
return d.downloadURL(ctx, url)
|
||||||
if err != nil {
|
|
||||||
return nil, "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
conv := Converter{
|
|
||||||
Format: format,
|
|
||||||
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, outputFormat, err = conv.ConvertIfNeeded(ctx, rr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, outputFormat, title, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
docker build -t nonoo/yt-dlp-telegram-bot:latest --network=host .
|
|
||||||
17
docker-compose.yaml
Normal file
17
docker-compose.yaml
Normal file
@ -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
|
|
||||||
193
main.go
193
main.go
@ -7,9 +7,11 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/gotd/td/telegram"
|
"github.com/gotd/td/telegram"
|
||||||
"github.com/gotd/td/telegram/message"
|
"github.com/gotd/td/telegram/message"
|
||||||
"github.com/gotd/td/telegram/uploader"
|
"github.com/gotd/td/telegram/uploader"
|
||||||
@ -23,6 +25,9 @@ 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"
|
format := "video"
|
||||||
s := strings.Split(msg.Message, " ")
|
s := strings.Split(msg.Message, " ")
|
||||||
@ -55,6 +60,164 @@ func handleCmdDLPCancel(ctx context.Context, entities tg.Entities, u *tg.UpdateN
|
|||||||
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, saving 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
now := time.Now()
|
||||||
|
dateStr := now.Format("2006-01-02")
|
||||||
|
timestamp := now.Unix()
|
||||||
|
ext := filepath.Ext(fileName)
|
||||||
|
if ext == "" {
|
||||||
|
ext = ".mp4"
|
||||||
|
}
|
||||||
|
baseName := strings.TrimSuffix(fileName, ext)
|
||||||
|
safeFileName := fmt.Sprintf("%s-%d-%s%s", dateStr, timestamp, baseName, 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-%s-%d%s", dateStr, timestamp, baseName, i, ext)
|
||||||
|
filePath = filepath.Join(params.SaveDir, safeFileName)
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create progress message
|
||||||
|
reply := telegramSender.Reply(entities, u)
|
||||||
|
replyMsg, _ := reply.Text(ctx, "⬇️ Downloading video...")
|
||||||
|
replyUpdate := replyMsg.(*tg.UpdateShortSentMessage)
|
||||||
|
|
||||||
|
// Get file size
|
||||||
|
fileSize := videoFile.Size
|
||||||
|
|
||||||
|
// Download file using Telegram client
|
||||||
|
fileLoc := &tg.InputDocumentFileLocation{
|
||||||
|
ID: videoFile.ID,
|
||||||
|
AccessHash: videoFile.AccessHash,
|
||||||
|
FileReference: videoFile.FileReference,
|
||||||
|
}
|
||||||
|
|
||||||
|
documentID := videoFile.ID
|
||||||
|
documentAccessHash := videoFile.AccessHash
|
||||||
|
fmt.Printf(" downloading video: %s (size: %s)\n", fileName, humanize.Bytes(uint64(fileSize)))
|
||||||
|
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = telegramSender.Answer(entities, u).Edit(replyUpdate.ID).Text(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 <-ctx.Done():
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
_, _ = telegramSender.Answer(entities, u).Edit(replyUpdate.ID).Text(ctx, "❌ Canceled")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset+chunkSize > fileSize {
|
||||||
|
chunkSize = fileSize - offset
|
||||||
|
}
|
||||||
|
|
||||||
|
loc := &tg.InputDocumentFileLocation{
|
||||||
|
ID: documentID,
|
||||||
|
AccessHash: documentAccessHash,
|
||||||
|
FileReference: fileLoc.FileReference,
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk, err := telegramClient.API().UploadGetFile(ctx, &tg.UploadGetFileRequest{
|
||||||
|
Location: loc,
|
||||||
|
Offset: offset,
|
||||||
|
Limit: int(chunkSize),
|
||||||
|
Precise: true,
|
||||||
|
CDNSupported: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
_, _ = telegramSender.Answer(entities, u).Edit(replyUpdate.ID).Text(ctx, errorStr+": failed to download chunk: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkData, ok := chunk.(*tg.UploadFile)
|
||||||
|
if !ok {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
_, _ = telegramSender.Answer(entities, u).Edit(replyUpdate.ID).Text(ctx, errorStr+": unexpected response type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := file.Write(chunkData.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
_, _ = telegramSender.Answer(entities, u).Edit(replyUpdate.ID).Text(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)
|
||||||
|
_, _ = telegramSender.Answer(entities, u).Edit(replyUpdate.ID).Text(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)))
|
||||||
|
_, _ = telegramSender.Answer(entities, u).Edit(replyUpdate.ID).Text(ctx, savedMsg)
|
||||||
|
|
||||||
|
fmt.Printf(" video saved to: %s\n", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var client *telegram.Client
|
||||||
|
|
||||||
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 {
|
||||||
@ -85,6 +248,12 @@ 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] == '/' || msg.Message[0] == '!' {
|
if msg.Message[0] == '/' || msg.Message[0] == '!' {
|
||||||
cmd := strings.Split(msg.Message, " ")[0]
|
cmd := strings.Split(msg.Message, " ")[0]
|
||||||
@ -123,6 +292,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...")
|
||||||
|
|
||||||
@ -161,6 +353,7 @@ 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)
|
goutubedl.Path, err = exec.LookPath(goutubedl.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
12
params.go
12
params.go
@ -21,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
|
||||||
@ -138,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 != "" {
|
||||||
|
|||||||
114
queue.go
114
queue.go
@ -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"
|
||||||
)
|
)
|
||||||
@ -38,23 +41,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) {
|
||||||
@ -217,7 +207,8 @@ func (q *DownloadQueue) processQueueEntry(ctx context.Context, qEntry *DownloadQ
|
|||||||
UpdateProgressPercentFunc: q.HandleProgressPercentUpdate,
|
UpdateProgressPercentFunc: q.HandleProgressPercentUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
r, outputFormat, title, err := downloader.DownloadAndConvertURL(qEntry.Ctx, qEntry.OrigMsg.Message, qEntry.Format)
|
// 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()
|
||||||
@ -227,28 +218,105 @@ func (q *DownloadQueue) processQueueEntry(ctx context.Context, qEntry *DownloadQ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feeding the returned io.ReadCloser to the uploader.
|
fmt.Printf(" saved to %s (size: %s)\n", dlResult.FilePath, humanize.Bytes(uint64(dlResult.FileSize)))
|
||||||
fmt.Println(" processing...")
|
|
||||||
|
// Probe the downloaded file to check codec compatibility
|
||||||
|
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, outputFormat, title)
|
// 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)
|
||||||
|
|||||||
1
run.sh
1
run.sh
@ -14,5 +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 \
|
YTDLP_PATH=$YTDLP_PATH \
|
||||||
$bin
|
$bin
|
||||||
|
|||||||
94
upload.go
94
upload.go
@ -6,6 +6,7 @@ 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/flytam/filenamify"
|
||||||
@ -23,28 +24,34 @@ 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, format, title string) 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)
|
||||||
}
|
}
|
||||||
@ -55,7 +62,12 @@ func (p *Uploader) UploadFile(ctx context.Context, entities tg.Entities, u *tg.U
|
|||||||
if format == "mp3" {
|
if format == "mp3" {
|
||||||
document = message.UploadedDocument(upload).Filename(filename).Audio().Title(title)
|
document = message.UploadedDocument(upload).Filename(filename).Audio().Title(title)
|
||||||
} else {
|
} else {
|
||||||
document = message.UploadedDocument(upload).Filename(filename).Video()
|
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.
|
||||||
@ -65,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
|
||||||
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user