bt2qbt/bt2qbt.go

449 lines
14 KiB
Go
Raw Normal View History

2017-12-26 16:25:10 +01:00
package main
import (
"bufio"
"bytes"
"fmt"
2018-04-02 12:17:50 +02:00
"github.com/fatih/color"
2018-04-12 16:10:22 +02:00
"github.com/go-ini/ini"
goflags "github.com/jessevdk/go-flags"
"github.com/rumanzo/bt2qbt/libtorrent"
"github.com/rumanzo/bt2qbt/replace"
"github.com/zeebo/bencode"
2017-12-26 16:25:10 +01:00
"io"
"io/ioutil"
"log"
"os"
2019-08-14 21:30:48 +02:00
"os/user"
"regexp"
2018-09-19 23:27:48 +02:00
"runtime"
2018-05-11 14:37:55 +02:00
"runtime/debug"
2017-12-27 21:43:12 +01:00
"strconv"
2018-04-02 12:17:50 +02:00
"strings"
"sync"
2018-05-11 13:35:20 +02:00
"time"
2017-12-26 16:25:10 +01:00
)
2020-02-16 11:36:24 +01:00
type Flags struct {
BitDir string `short:"s" long:"source" description:"Source directory that contains resume.dat and torrents files"`
QBitDir string `short:"d" long:"destination" description:"Destination directory BT_backup (as default)"`
2021-01-10 14:48:50 +01:00
Config string `short:"c" long:"config" description:"qBittorrent config file (for write tags)"`
WithoutLabels bool `long:"without-labels" description:"Do not export/import labels"`
WithoutTags bool `long:"without-tags" description:"Do not export/import tags"`
SearchPaths []string `short:"t" long:"search" description:"Additional search path for torrents files\n Example: --search='/mnt/olddisk/savedtorrents' --search='/mnt/olddisk/workstorrents'"`
2020-04-04 16:15:16 +02:00
Replaces []string `short:"r" long:"replace" description:"Replace paths.\n Delimiter for from/to is comma - ,\n Example: -r \"D:\\films,/home/user/films\" -r \"D:\\music,/home/user/music\"\n"`
PathSeparator string `long:"sep" description:"Default path separator that will use in all paths. You may need use this flag if you migrating from windows to linux in some cases"`
2020-02-16 11:36:24 +01:00
}
type Channels struct {
comChannel chan string
errChannel chan string
boundedChannel chan bool
}
func encodetorrentfile(path string, newstructure *libtorrent.NewTorrentStructure) error {
2020-02-16 11:36:24 +01:00
if _, err := os.Stat(path); os.IsNotExist(err) {
os.Create(path)
}
file, err := os.OpenFile(path, os.O_WRONLY, 0666)
if err != nil {
return err
}
defer file.Close()
bufferedWriter := bufio.NewWriter(file)
enc := bencode.NewEncoder(bufferedWriter)
if err := enc.Encode(newstructure); err != nil {
return err
}
bufferedWriter.Flush()
return nil
}
2018-05-13 23:40:36 +02:00
func ASCIIconvert(s string) string {
var buffer bytes.Buffer
2018-04-12 00:46:33 +02:00
for _, c := range s {
if c > 127 {
2018-05-13 23:40:36 +02:00
buffer.WriteString(`\x` + strconv.FormatUint(uint64(c), 16))
2018-04-12 00:46:33 +02:00
} else {
2018-05-13 23:40:36 +02:00
buffer.WriteString(string(c))
2018-04-12 00:46:33 +02:00
}
}
2018-05-13 23:40:36 +02:00
return buffer.String()
2018-04-12 00:46:33 +02:00
}
2018-04-13 17:16:38 +02:00
func checknotexists(s string, tags []string) (bool, string) {
2018-04-12 00:46:33 +02:00
for _, value := range tags {
if value == s {
2018-04-13 17:16:38 +02:00
return false, s
2018-04-12 00:46:33 +02:00
}
}
2018-04-13 17:16:38 +02:00
return true, s
2018-04-12 00:46:33 +02:00
}
2018-04-02 12:17:50 +02:00
func decodetorrentfile(path string) (map[string]interface{}, error) {
2017-12-26 16:25:10 +01:00
dat, err := ioutil.ReadFile(path)
if err != nil {
2018-04-02 12:17:50 +02:00
return nil, err
2017-12-26 16:25:10 +01:00
}
var torrent map[string]interface{}
2020-02-16 11:36:24 +01:00
if err := bencode.DecodeBytes(dat, &torrent); err != nil {
2018-04-02 12:17:50 +02:00
return nil, err
2017-12-26 16:25:10 +01:00
}
2018-04-02 12:17:50 +02:00
return torrent, nil
2017-12-26 16:25:10 +01:00
}
func copyfile(src string, dst string) error {
2017-12-28 22:43:07 +01:00
originalFile, err := os.Open(src)
if err != nil {
return err
}
defer originalFile.Close()
newFile, err := os.Create(dst)
if err != nil {
return err
}
defer newFile.Close()
if _, err := io.Copy(newFile, originalFile); err != nil {
return err
}
2018-04-06 15:18:06 +02:00
if err := newFile.Sync(); err != nil {
2017-12-28 22:43:07 +01:00
return err
}
return nil
}
2020-02-16 11:36:24 +01:00
func logic(key string, value map[string]interface{}, flags *Flags, chans *Channels, position int, wg *sync.WaitGroup) error {
defer wg.Done()
2018-09-19 23:27:48 +02:00
defer func() {
2020-02-16 11:36:24 +01:00
<-chans.boundedChannel
2018-09-19 23:27:48 +02:00
}()
defer func() {
if r := recover(); r != nil {
2020-02-16 11:36:24 +01:00
chans.errChannel <- fmt.Sprintf(
2018-05-11 16:29:06 +02:00
"Panic while processing torrent %v:\n======\nReason: %v.\nText panic:\n%v\n======",
key, r, string(debug.Stack()))
}
}()
2018-04-18 13:29:45 +02:00
var err error
2020-04-15 22:59:03 +02:00
newstructure := libtorrent.NewTorrentStructure{
ActiveTime: 0,
AddedTime: 0,
Allocation: "sparse",
AutoManaged: 0,
CompletedTime: 0,
DownloadRateLimit: -1,
FileFormat: "libtorrent resume file",
FileVersion: 1,
FinishedTime: 0,
LastDownload: 0,
LastSeenComplete: 0,
LastUpload: 0,
LibTorrentVersion: "1.2.5.0",
MaxConnections: 100,
MaxUploads: 100,
NumDownloaded: 0,
NumIncomplete: 0,
QbtQueuePosition: 1,
QbtRatioLimit: -2000,
QbtSeedStatus: 1,
QbtSeedingTimeLimit: -2,
QbttempPathDisabled: 0,
SeedMode: 0,
SeedingTime: 0,
SequentialDownload: 0,
SuperSeeding: 0,
StopWhenReady: 0,
TotalDownloaded: 0,
TotalUploaded: 0,
UploadRateLimit: 0,
QbtName: "",
WithoutLabels: flags.WithoutLabels,
WithoutTags: flags.WithoutTags,
Separator: flags.PathSeparator,
}
2021-01-10 14:49:16 +01:00
if isAbs, _ := regexp.MatchString(`^([A-Za-z]:)?\\`, key); isAbs == true {
if runtime.GOOS == "windows" {
newstructure.TorrentFilePath = key
} else { // for unix system find in search paths
pathparts := strings.Split(key, "\\")
newstructure.TorrentFilePath = pathparts[len(pathparts)-1]
}
2018-05-10 22:07:59 +02:00
} else {
newstructure.TorrentFilePath = flags.BitDir + key // additional search required
2018-05-10 22:07:59 +02:00
}
if _, err = os.Stat(newstructure.TorrentFilePath); os.IsNotExist(err) {
for _, searchPath := range flags.SearchPaths {
if _, err = os.Stat(searchPath + newstructure.TorrentFilePath); err == nil {
newstructure.TorrentFilePath = searchPath + newstructure.TorrentFilePath
goto CONTINUE
}
}
chans.errChannel <- fmt.Sprintf("Can't find torrent file %v for %v", newstructure.TorrentFilePath, key)
2018-04-18 01:33:00 +02:00
return err
CONTINUE:
2018-04-18 01:33:00 +02:00
}
newstructure.TorrentFile, err = decodetorrentfile(newstructure.TorrentFilePath)
2018-04-18 01:33:00 +02:00
if err != nil {
chans.errChannel <- fmt.Sprintf("Can't decode torrent file %v for %v", newstructure.TorrentFilePath, key)
2018-04-18 01:33:00 +02:00
return err
}
2020-02-16 12:58:26 +01:00
for _, str := range flags.Replaces {
patterns := strings.Split(str, ",")
newstructure.Replace = append(newstructure.Replace, replace.Replace{
From: patterns[0],
To: patterns[1],
})
2020-02-16 12:58:26 +01:00
}
if _, ok := newstructure.TorrentFile["info"].(map[string]interface{})["files"]; ok {
newstructure.HasFiles = true
2018-04-18 01:33:00 +02:00
} else {
newstructure.HasFiles = false
2018-04-18 01:33:00 +02:00
}
if value["path"].(string)[len(value["path"].(string))-1] == os.PathSeparator {
newstructure.Path = value["path"].(string)[:len(value["path"].(string))-1]
2018-04-18 14:03:13 +02:00
} else {
newstructure.Path = value["path"].(string)
2018-04-18 01:33:00 +02:00
}
// if torrent name was renamed, add modified name
if value["caption"] != nil {
newstructure.QbtName = value["caption"].(string)
}
2020-02-16 11:36:24 +01:00
newstructure.ActiveTime = value["runtime"].(int64)
newstructure.AddedTime = value["added_on"].(int64)
newstructure.CompletedTime = value["completed_on"].(int64)
newstructure.InfoHash = value["info"].(string)
2020-02-16 11:36:24 +01:00
newstructure.SeedingTime = value["runtime"].(int64)
newstructure.QbtQueuePosition = position
newstructure.Started(value["started"].(int64))
2020-02-16 11:36:24 +01:00
newstructure.FinishedTime = int64(time.Since(time.Unix(value["completed_on"].(int64), 0)).Minutes())
newstructure.TotalDownloaded = value["downloaded"].(int64)
newstructure.TotalUploaded = value["uploaded"].(int64)
newstructure.UploadRateLimit = value["upspeed"].(int64)
newstructure.IfTags(value["labels"])
2018-04-18 13:29:45 +02:00
if value["label"] != nil {
newstructure.IfLabel(value["label"].(string))
2018-04-18 13:29:45 +02:00
} else {
newstructure.IfLabel("")
2018-04-18 13:29:45 +02:00
}
newstructure.GetTrackers(value["trackers"])
newstructure.PrioConvert(value["prio"].(string))
2018-05-11 16:29:06 +02:00
// https://libtorrent.org/manual-ref.html#fast-resume
newstructure.PieceLenght = newstructure.TorrentFile["info"].(map[string]interface{})["piece length"].(int64)
2018-05-11 16:29:06 +02:00
2018-05-06 17:35:32 +02:00
/*
2018-05-08 18:10:02 +02:00
pieces maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20,
each of which is the SHA1 hash of the piece at the corresponding index.
http://www.bittorrent.org/beps/bep_0003.html
2018-05-06 17:35:32 +02:00
*/
newstructure.NumPieces = int64(len(newstructure.TorrentFile["info"].(map[string]interface{})["pieces"].(string))) / 20
newstructure.FillMissing()
newbasename := newstructure.GetHash()
2018-04-18 13:29:45 +02:00
if err = encodetorrentfile(flags.QBitDir+newbasename+".fastresume", &newstructure); err != nil {
chans.errChannel <- fmt.Sprintf("Can't create qBittorrent fastresume file %v", flags.QBitDir+newbasename+".fastresume")
2018-04-02 12:17:50 +02:00
return err
2017-12-28 22:43:07 +01:00
}
if err = copyfile(newstructure.TorrentFilePath, flags.QBitDir+newbasename+".torrent"); err != nil {
chans.errChannel <- fmt.Sprintf("Can't create qBittorrent torrent file %v", flags.QBitDir+newbasename+".torrent")
2018-04-02 12:17:50 +02:00
return err
2017-12-28 22:43:07 +01:00
}
2020-02-16 11:36:24 +01:00
chans.comChannel <- fmt.Sprintf("Sucessfully imported %v", key)
2018-04-02 12:17:50 +02:00
return nil
2017-12-26 16:25:10 +01:00
}
func main() {
flags := Flags{PathSeparator: string(os.PathSeparator)}
2019-08-14 21:30:48 +02:00
sep := string(os.PathSeparator)
switch OS := runtime.GOOS; OS {
case "windows":
flags.BitDir = os.Getenv("APPDATA") + sep + "uTorrent" + sep
flags.Config = os.Getenv("APPDATA") + sep + "qBittorrent" + sep + "qBittorrent.ini"
flags.QBitDir = os.Getenv("LOCALAPPDATA") + sep + "qBittorrent" + sep + "BT_backup" + sep
2020-02-16 13:07:16 +01:00
case "linux":
usr, err := user.Current()
if err != nil {
panic(err)
}
flags.BitDir = "/mnt/uTorrent/"
flags.Config = usr.HomeDir + sep + ".config" + sep + "qBittorrent" + sep + "qBittorrent.conf"
flags.QBitDir = usr.HomeDir + sep + ".local" + sep + "share" + sep + "data" + sep + "qBittorrent" + sep + "BT_backup" + sep
2019-08-14 21:30:48 +02:00
case "darwin":
usr, err := user.Current()
if err != nil {
panic(err)
}
flags.BitDir = usr.HomeDir + sep + "Library" + sep + "Application Support" + sep + "uTorrent" + sep
flags.Config = usr.HomeDir + sep + ".config" + sep + "qBittorrent" + sep + "qbittorrent.ini"
flags.QBitDir = usr.HomeDir + sep + "Library" + sep + "Application Support" + sep + "QBittorrent" + sep + "BT_backup" + sep
2019-08-14 21:30:48 +02:00
}
if _, err := goflags.Parse(&flags); err != nil { // https://godoc.org/github.com/jessevdk/go-flags#ErrorType
if flagsErr, ok := err.(*goflags.Error); ok && flagsErr.Type == goflags.ErrHelp {
os.Exit(0)
} else {
log.Println(err)
time.Sleep(30 * time.Second)
os.Exit(1)
}
}
2018-04-09 12:09:59 +02:00
if len(flags.Replaces) != 0 {
for _, str := range flags.Replaces {
2020-02-16 12:58:26 +01:00
patterns := strings.Split(str, ",")
if len(patterns) < 2 {
log.Println("Bad replace pattern")
time.Sleep(30 * time.Second)
os.Exit(1)
}
}
}
if flags.BitDir[len(flags.BitDir)-1] != os.PathSeparator {
flags.BitDir += string(os.PathSeparator)
2018-04-09 12:09:59 +02:00
}
if flags.QBitDir[len(flags.QBitDir)-1] != os.PathSeparator {
flags.QBitDir += string(os.PathSeparator)
}
for index, searchPath := range flags.SearchPaths {
if searchPath[len(searchPath)-1] != os.PathSeparator {
flags.SearchPaths[index] += string(os.PathSeparator)
}
2018-04-09 12:09:59 +02:00
}
if _, err := os.Stat(flags.BitDir); os.IsNotExist(err) {
2018-04-02 12:17:50 +02:00
log.Println("Can't find uTorrent\\Bittorrent folder")
time.Sleep(30 * time.Second)
os.Exit(1)
}
2020-10-26 21:33:08 +01:00
flags.SearchPaths = append(flags.SearchPaths, flags.BitDir)
if _, err := os.Stat(flags.QBitDir); os.IsNotExist(err) {
2018-04-02 12:17:50 +02:00
log.Println("Can't find qBittorrent folder")
time.Sleep(30 * time.Second)
os.Exit(1)
}
resumefilepath := flags.BitDir + "resume.dat"
2018-04-02 12:17:50 +02:00
if _, err := os.Stat(resumefilepath); os.IsNotExist(err) {
log.Println("Can't find uTorrent\\Bittorrent resume file")
time.Sleep(30 * time.Second)
os.Exit(1)
}
resumefile, err := decodetorrentfile(resumefilepath)
if err != nil {
log.Println("Can't decode uTorrent\\Bittorrent resume file")
time.Sleep(30 * time.Second)
os.Exit(1)
}
if flags.WithoutTags == false {
if _, err := os.Stat(flags.Config); os.IsNotExist(err) {
2018-05-11 16:29:06 +02:00
fmt.Println("Can not read qBittorrent config file. Try run and close qBittorrent if you have not done" +
" so already, or specify the path explicitly or do not import tags")
2018-04-12 16:10:22 +02:00
time.Sleep(30 * time.Second)
os.Exit(1)
}
2018-04-12 00:46:33 +02:00
}
totaljobs := len(resumefile)
chans := Channels{comChannel: make(chan string, totaljobs),
errChannel: make(chan string, totaljobs),
boundedChannel: make(chan bool, runtime.GOMAXPROCS(0)*2)}
color.Green("It will be performed processing from directory %v to directory %v\n", flags.BitDir, flags.QBitDir)
2018-05-11 16:29:06 +02:00
color.HiRed("Check that the qBittorrent is turned off and the directory %v and config %v is backed up.\n\n",
flags.QBitDir, flags.Config)
2018-04-02 12:17:50 +02:00
fmt.Println("Press Enter to start")
2018-04-18 14:26:29 +02:00
fmt.Scanln()
2018-05-10 22:09:40 +02:00
log.Println("Started")
transfertorrents(chans, flags, resumefile, totaljobs)
fmt.Println("\nPress Enter to exit")
fmt.Scanln()
}
func transfertorrents(chans Channels, flags Flags, resumefile map[string]interface{}, totaljobs int) {
2018-04-11 19:49:24 +02:00
numjob := 1
2018-04-12 16:10:22 +02:00
var oldtags string
var newtags []string
var wg sync.WaitGroup
2018-05-11 13:44:46 +02:00
positionnum := 0
2018-04-02 12:17:50 +02:00
for key, value := range resumefile {
2017-12-27 22:00:50 +01:00
if key != ".fileguard" && key != "rec" {
2018-05-11 13:44:46 +02:00
positionnum++
if flags.WithoutTags == false {
2018-05-29 09:47:45 +02:00
if labels, ok := value.(map[string]interface{})["labels"]; ok {
for _, label := range labels.([]interface{}) {
if len(label.(string)) > 0 {
if ok, tag := checknotexists(ASCIIconvert(label.(string)), newtags); ok {
newtags = append(newtags, tag)
}
2018-04-12 16:10:22 +02:00
}
}
}
}
wg.Add(1)
2020-02-16 11:36:24 +01:00
chans.boundedChannel <- true
go logic(key, value.(map[string]interface{}), &flags, &chans, positionnum, &wg)
} else {
totaljobs--
2017-12-26 16:25:10 +01:00
}
}
2018-05-11 13:35:20 +02:00
go func() {
wg.Wait()
2020-02-16 11:36:24 +01:00
close(chans.comChannel)
close(chans.errChannel)
}()
2020-02-16 11:36:24 +01:00
for message := range chans.comChannel {
2018-04-02 12:17:50 +02:00
fmt.Printf("%v/%v %v \n", numjob, totaljobs, message)
numjob++
}
2018-07-31 17:44:25 +02:00
var waserrors bool
2020-02-16 11:36:24 +01:00
for message := range chans.errChannel {
fmt.Printf("%v/%v %v \n", numjob, totaljobs, message)
2018-07-31 17:44:25 +02:00
waserrors = true
2018-05-11 20:43:48 +02:00
numjob++
2018-04-02 12:17:50 +02:00
}
if flags.WithoutTags == false {
cfg, err := ini.Load(flags.Config)
2018-04-12 16:10:22 +02:00
ini.PrettyFormat = false
ini.PrettySection = false
if err != nil {
fmt.Println("Can not read qBittorrent config file. Try to specify the path explicitly or do not import tags")
time.Sleep(30 * time.Second)
os.Exit(1)
}
if _, err := cfg.GetSection("BitTorrent"); err != nil {
cfg.NewSection("BitTorrent")
2018-04-12 00:46:33 +02:00
2018-04-12 16:10:22 +02:00
//Dirty hack for section order. Sorry
kv := cfg.Section("Network").KeysHash()
cfg.DeleteSection("Network")
cfg.NewSection("Network")
for key, value := range kv {
cfg.Section("Network").NewKey(key, value)
}
//End of dirty hack
}
if cfg.Section("BitTorrent").HasKey("Session\\Tags") {
oldtags = cfg.Section("BitTorrent").Key("Session\\Tags").String()
for _, tag := range strings.Split(oldtags, ", ") {
2018-04-13 17:16:38 +02:00
if ok, t := checknotexists(tag, newtags); ok {
newtags = append(newtags, t)
2018-04-12 16:10:22 +02:00
}
}
cfg.Section("BitTorrent").Key("Session\\Tags").SetValue(strings.Join(newtags, ", "))
} else {
cfg.Section("BitTorrent").NewKey("Session\\Tags", strings.Join(newtags, ", "))
2018-04-12 00:46:33 +02:00
}
cfg.SaveTo(flags.Config)
2018-04-12 00:46:33 +02:00
}
fmt.Println()
2018-05-10 22:09:40 +02:00
log.Println("Ended")
2018-07-31 17:44:25 +02:00
if waserrors {
log.Println("Not all torrents was processed")
}
2020-02-11 23:10:35 +01:00
}