first commit
This commit is contained in:
commit
58cfd95600
35 files changed
+5357
No files matched your search
+260
@@ -0,0 +1,260 @@
|
||||
// x360-bt: non-interactive BitTorrent helper based on the anacrolix/torrent
|
||||
// library, also used by the user's minerva-pick tool. It emits machine-readable
|
||||
// status and never starts downloading payload data while inspecting a magnet.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Connections int `json:"connections"`
|
||||
KnownPeers int `json:"known_peers"`
|
||||
Downloaded int64 `json:"downloaded,omitempty"`
|
||||
Total int64 `json:"total,omitempty"`
|
||||
Speed int64 `json:"speed,omitempty"`
|
||||
Elapsed int64 `json:"elapsed,omitempty"`
|
||||
}
|
||||
|
||||
func writeState(path string, s state) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return
|
||||
}
|
||||
f, err := os.CreateTemp(filepath.Dir(path), ".bt-state-*")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
if _, err = f.Write(b); err != nil {
|
||||
f.Close()
|
||||
return
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.Rename(f.Name(), path)
|
||||
}
|
||||
|
||||
func newClient(dataDir string, port int) (*torrent.Client, error) {
|
||||
cfg := torrent.NewDefaultClientConfig()
|
||||
cfg.DisableIPv6 = true
|
||||
cfg.DataDir = dataDir
|
||||
cfg.Seed = false
|
||||
cfg.NoDefaultPortForwarding = true
|
||||
cfg.ListenPort = port
|
||||
return torrent.NewClient(cfg)
|
||||
}
|
||||
|
||||
func inspect(ctx context.Context, magnet, output, statusPath string, port int, timeout time.Duration) error {
|
||||
if !strings.HasPrefix(strings.ToLower(magnet), "magnet:?") {
|
||||
return errors.New("nieprawidlowy magnet")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(output), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := newClient(filepath.Dir(output), port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("klient BT: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
t, err := client.AddMagnet(magnet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("magnet: %w", err)
|
||||
}
|
||||
started := time.Now()
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
snapshot := state{Status: "loading", Connections: len(t.PeerConns()), KnownPeers: len(t.KnownSwarm()), Elapsed: int64(time.Since(started).Seconds())}
|
||||
writeState(statusPath, snapshot)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("brak metadanych po %s (polaczenia=%d, znani peerzy=%d)", timeout, snapshot.Connections, snapshot.KnownPeers)
|
||||
case <-t.GotInfo():
|
||||
mi := t.Metainfo()
|
||||
f, err := os.CreateTemp(filepath.Dir(output), ".bt-meta-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temp := f.Name()
|
||||
defer os.Remove(temp)
|
||||
if err = mi.Write(f); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if info, err := os.Stat(temp); err != nil || info.Size() > 32*1024*1024 {
|
||||
return errors.New("metadane przekraczaja 32 MiB")
|
||||
}
|
||||
if err = os.Rename(temp, output); err != nil {
|
||||
return err
|
||||
}
|
||||
writeState(statusPath, state{Status: "ready", Connections: snapshot.Connections, KnownPeers: snapshot.KnownPeers, Elapsed: snapshot.Elapsed})
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseSelected(selection string, files []*torrent.File) ([]*torrent.File, error) {
|
||||
if selection == "" {
|
||||
return nil, errors.New("brak wybranych plikow")
|
||||
}
|
||||
used := make(map[int]bool)
|
||||
out := make([]*torrent.File, 0)
|
||||
for _, s := range strings.Split(selection, ",") {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(s))
|
||||
if err != nil || n < 1 || n > len(files) || used[n] {
|
||||
return nil, fmt.Errorf("niepoprawny indeks pliku %q", s)
|
||||
}
|
||||
used[n] = true
|
||||
out = append(out, files[n-1])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func progress(files []*torrent.File) (done, total int64) {
|
||||
for _, f := range files {
|
||||
total += f.Length()
|
||||
n := f.BytesCompleted()
|
||||
if n > f.Length() {
|
||||
n = f.Length()
|
||||
}
|
||||
done += n
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fetch(ctx context.Context, input, selection, outDir, statusPath string, port int) error {
|
||||
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := newClient(outDir, port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("klient BT: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
t, err := client.AddTorrentFromFile(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("plik torrent: %w", err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-t.GotInfo():
|
||||
}
|
||||
selected, err := parseSelected(selection, t.Files())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range selected {
|
||||
f.Download()
|
||||
}
|
||||
started := time.Now()
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
lastDone := int64(0)
|
||||
lastAt := time.Now()
|
||||
for {
|
||||
done, total := progress(selected)
|
||||
now := time.Now()
|
||||
seconds := now.Sub(lastAt).Seconds()
|
||||
var speed int64
|
||||
if seconds > 0 {
|
||||
speed = int64(float64(done-lastDone) / seconds)
|
||||
if speed < 0 {
|
||||
speed = 0
|
||||
}
|
||||
}
|
||||
lastDone = done
|
||||
lastAt = now
|
||||
if total == 0 || done >= total {
|
||||
writeState(statusPath, state{Status: "done", Total: total, Downloaded: done, Connections: len(t.PeerConns()), KnownPeers: len(t.KnownSwarm())})
|
||||
return nil
|
||||
}
|
||||
writeState(statusPath, state{Status: "downloading", Total: total, Downloaded: done, Speed: speed, Connections: len(t.PeerConns()), KnownPeers: len(t.KnownSwarm()), Elapsed: int64(time.Since(started).Seconds())})
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) (string, error) {
|
||||
if len(os.Args) < 2 {
|
||||
return "", errors.New("tryb inspect albo fetch")
|
||||
}
|
||||
command := os.Args[1]
|
||||
flags := flag.NewFlagSet(command, flag.ContinueOnError)
|
||||
magnet := flags.String("magnet", "", "magnet link")
|
||||
input := flags.String("torrent", "", "plik .torrent")
|
||||
selection := flags.String("select", "", "indeksy 1-based oddzielone przecinkiem")
|
||||
out := flags.String("out", "", "sciezka wyjsciowa")
|
||||
statusPath := flags.String("status", "", "sciezka JSON statusu")
|
||||
port := flags.Int("port", 0, "port TCP/UDP (0 = losowy)")
|
||||
timeout := flags.Duration("timeout", 5*time.Minute, "timeout metadanych")
|
||||
if err := flags.Parse(os.Args[2:]); err != nil {
|
||||
return *statusPath, err
|
||||
}
|
||||
if *port < 0 || *port > 65535 {
|
||||
return *statusPath, errors.New("niepoprawny port")
|
||||
}
|
||||
var err error
|
||||
switch command {
|
||||
case "inspect":
|
||||
if *out == "" || *magnet == "" {
|
||||
err = errors.New("podaj -magnet i -out")
|
||||
} else {
|
||||
err = inspect(ctx, *magnet, *out, *statusPath, *port, *timeout)
|
||||
}
|
||||
case "fetch":
|
||||
if *input == "" || *out == "" {
|
||||
err = errors.New("podaj -torrent i -out")
|
||||
} else {
|
||||
err = fetch(ctx, *input, *selection, *out, *statusPath, *port)
|
||||
}
|
||||
default:
|
||||
err = errors.New("nieznany tryb: " + command)
|
||||
}
|
||||
return *statusPath, err
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
path, err := run(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "x360-bt:", err)
|
||||
writeState(path, state{Status: "error", Error: err.Error()})
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user