mirror of
https://github.com/rumanzo/bt2qbt.git
synced 2024-11-13 22:24:16 +01:00
Modern qBittorrent doesn't save tags to config file. It uses categories.json instead.
Rewritted handleLabels function. Tests
This commit is contained in:
parent
46103167ef
commit
e166194b56
@ -52,8 +52,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
color.Green("It will be performed processing from directory %v to directory %v\n", opts.BitDir, opts.QBitDir)
|
color.Green("It will be performed processing from directory %v to directory %v\n", opts.BitDir, opts.QBitDir)
|
||||||
color.HiRed("Check that the qBittorrent is turned off and the directory %v and config %v is backed up.\n",
|
color.HiRed("Check that the qBittorrent is turned off and the directory %v and %v is backed up.\n",
|
||||||
opts.QBitDir, opts.Config)
|
opts.QBitDir, opts.Categories)
|
||||||
color.HiRed("Check that you previously disable option \"Append .!ut/.!bt to incomplete files\" in preferences of uTorrent/Bittorrent \n\n")
|
color.HiRed("Check that you previously disable option \"Append .!ut/.!bt to incomplete files\" in preferences of uTorrent/Bittorrent \n\n")
|
||||||
fmt.Println("Press Enter to start")
|
fmt.Println("Press Enter to start")
|
||||||
fmt.Scanln()
|
fmt.Scanln()
|
||||||
|
@ -15,7 +15,7 @@ import (
|
|||||||
type Opts struct {
|
type Opts struct {
|
||||||
BitDir string `short:"s" long:"source" description:"Source directory that contains resume.dat and torrents files"`
|
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)"`
|
QBitDir string `short:"d" long:"destination" description:"Destination directory BT_backup (as default)"`
|
||||||
Config string `short:"c" long:"config" description:"qBittorrent config file (for write tags)"`
|
Categories string `short:"c" long:"categories" description:"path to qBittorrent categories.json file (for write tags)"`
|
||||||
WithoutLabels bool `long:"without-labels" description:"Do not export/import labels"`
|
WithoutLabels bool `long:"without-labels" description:"Do not export/import labels"`
|
||||||
WithoutTags bool `long:"without-tags" description:"Do not export/import tags"`
|
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'"`
|
SearchPaths []string `short:"t" long:"search" description:"Additional search path for torrents files\n Example: --search='/mnt/olddisk/savedtorrents' --search='/mnt/olddisk/workstorrents'"`
|
||||||
@ -29,7 +29,7 @@ func PrepareOpts() *Opts {
|
|||||||
switch OS := runtime.GOOS; OS {
|
switch OS := runtime.GOOS; OS {
|
||||||
case "windows":
|
case "windows":
|
||||||
opts.BitDir = filepath.Join(os.Getenv("APPDATA"), "uTorrent")
|
opts.BitDir = filepath.Join(os.Getenv("APPDATA"), "uTorrent")
|
||||||
opts.Config = filepath.Join(os.Getenv("APPDATA"), "qBittorrent", "qBittorrent.ini")
|
opts.Categories = filepath.Join(os.Getenv("APPDATA"), "qBittorrent", "categories.json")
|
||||||
opts.QBitDir = filepath.Join(os.Getenv("LOCALAPPDATA"), "qBittorrent", "BT_backup")
|
opts.QBitDir = filepath.Join(os.Getenv("LOCALAPPDATA"), "qBittorrent", "BT_backup")
|
||||||
case "linux":
|
case "linux":
|
||||||
usr, err := user.Current()
|
usr, err := user.Current()
|
||||||
@ -37,7 +37,7 @@ func PrepareOpts() *Opts {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
opts.BitDir = "/mnt/uTorrent/"
|
opts.BitDir = "/mnt/uTorrent/"
|
||||||
opts.Config = filepath.Join(usr.HomeDir, ".config", "qBittorrent", "qBittorrent.conf")
|
opts.Categories = filepath.Join(usr.HomeDir, ".config", "qBittorrent", "categories.json")
|
||||||
opts.QBitDir = filepath.Join(usr.HomeDir, ".local", "share", "data", "qBittorrent", "BT_backup")
|
opts.QBitDir = filepath.Join(usr.HomeDir, ".local", "share", "data", "qBittorrent", "BT_backup")
|
||||||
case "darwin":
|
case "darwin":
|
||||||
usr, err := user.Current()
|
usr, err := user.Current()
|
||||||
@ -45,7 +45,7 @@ func PrepareOpts() *Opts {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
opts.BitDir = filepath.Join(usr.HomeDir, "Library", "Application Support", "uTorrent")
|
opts.BitDir = filepath.Join(usr.HomeDir, "Library", "Application Support", "uTorrent")
|
||||||
opts.Config = filepath.Join(usr.HomeDir, ".config", "qBittorrent", "qbittorrent.ini")
|
opts.Categories = filepath.Join(usr.HomeDir, ".config", "qBittorrent", "categories.json")
|
||||||
opts.QBitDir = filepath.Join(usr.HomeDir, "Library", "Application Support", "QBittorrent", "BT_backup")
|
opts.QBitDir = filepath.Join(usr.HomeDir, "Library", "Application Support", "QBittorrent", "BT_backup")
|
||||||
}
|
}
|
||||||
return opts
|
return opts
|
||||||
@ -86,12 +86,6 @@ func OptsCheck(opts *Opts) error {
|
|||||||
return fmt.Errorf("can't find qBittorrent folder")
|
return fmt.Errorf("can't find qBittorrent folder")
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.WithoutTags == false {
|
|
||||||
if _, err := os.Stat(opts.Config); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
if opts.SearchPaths == nil {
|
if opts.SearchPaths == nil {
|
||||||
return fmt.Errorf("on linux systems you must define search path for torrents")
|
return fmt.Errorf("on linux systems you must define search path for torrents")
|
||||||
|
@ -33,7 +33,7 @@ func TestOptionsArgs(t *testing.T) {
|
|||||||
args: []string{
|
args: []string{
|
||||||
"-s", "/dir",
|
"-s", "/dir",
|
||||||
"-d", "/dir",
|
"-d", "/dir",
|
||||||
"-c", "/dir/q.conf",
|
"-c", "/dir/q.json",
|
||||||
"-r", "dir1,dir2", "-r", "dir3,dir4",
|
"-r", "dir1,dir2", "-r", "dir3,dir4",
|
||||||
"--sep", "/",
|
"--sep", "/",
|
||||||
"-t", "/dir5", "-t", "/dir6/",
|
"-t", "/dir5", "-t", "/dir6/",
|
||||||
@ -42,7 +42,7 @@ func TestOptionsArgs(t *testing.T) {
|
|||||||
expected: &Opts{
|
expected: &Opts{
|
||||||
BitDir: "/dir",
|
BitDir: "/dir",
|
||||||
QBitDir: "/dir",
|
QBitDir: "/dir",
|
||||||
Config: "/dir/q.conf",
|
Categories: "/dir/q.json",
|
||||||
Replaces: []string{"dir1,dir2", "dir3,dir4"},
|
Replaces: []string{"dir1,dir2", "dir3,dir4"},
|
||||||
PathSeparator: "/",
|
PathSeparator: "/",
|
||||||
SearchPaths: []string{"/dir5", "/dir6/"},
|
SearchPaths: []string{"/dir5", "/dir6/"},
|
||||||
@ -54,7 +54,7 @@ func TestOptionsArgs(t *testing.T) {
|
|||||||
args: []string{
|
args: []string{
|
||||||
"--source", "/dir",
|
"--source", "/dir",
|
||||||
"--destination", "/dir",
|
"--destination", "/dir",
|
||||||
"--config", "/dir/q.conf",
|
"--categories", "/dir/q.json",
|
||||||
"--replace", "dir1,dir2", "-r", "dir3,dir4",
|
"--replace", "dir1,dir2", "-r", "dir3,dir4",
|
||||||
"--sep", "/",
|
"--sep", "/",
|
||||||
"--search", "/dir5", "-t", "/dir6/",
|
"--search", "/dir5", "-t", "/dir6/",
|
||||||
@ -63,7 +63,7 @@ func TestOptionsArgs(t *testing.T) {
|
|||||||
expected: &Opts{
|
expected: &Opts{
|
||||||
BitDir: "/dir",
|
BitDir: "/dir",
|
||||||
QBitDir: "/dir",
|
QBitDir: "/dir",
|
||||||
Config: "/dir/q.conf",
|
Categories: "/dir/q.json",
|
||||||
Replaces: []string{"dir1,dir2", "dir3,dir4"},
|
Replaces: []string{"dir1,dir2", "dir3,dir4"},
|
||||||
PathSeparator: "/",
|
PathSeparator: "/",
|
||||||
SearchPaths: []string{"/dir5", "/dir6/"},
|
SearchPaths: []string{"/dir5", "/dir6/"},
|
||||||
@ -96,7 +96,7 @@ func TestOptionsHandle(t *testing.T) {
|
|||||||
opts: &Opts{
|
opts: &Opts{
|
||||||
BitDir: "/dir",
|
BitDir: "/dir",
|
||||||
QBitDir: "/dir",
|
QBitDir: "/dir",
|
||||||
Config: "/dir/q.conf",
|
Categories: "/dir/q.json",
|
||||||
Replaces: []string{"dir1,dir2", "dir3,dir4"},
|
Replaces: []string{"dir1,dir2", "dir3,dir4"},
|
||||||
PathSeparator: "/",
|
PathSeparator: "/",
|
||||||
SearchPaths: []string{"/dir5", "/dir6/"},
|
SearchPaths: []string{"/dir5", "/dir6/"},
|
||||||
@ -142,7 +142,7 @@ func TestOptionsChecks(t *testing.T) {
|
|||||||
opts: &Opts{
|
opts: &Opts{
|
||||||
BitDir: "/dir",
|
BitDir: "/dir",
|
||||||
QBitDir: "/dir",
|
QBitDir: "/dir",
|
||||||
Config: "/dir/q.conf",
|
Categories: "/dir/q.json",
|
||||||
Replaces: []string{"dir1,dir2", "dir3,dir4"},
|
Replaces: []string{"dir1,dir2", "dir3,dir4"},
|
||||||
PathSeparator: "/",
|
PathSeparator: "/",
|
||||||
SearchPaths: []string{"/dir5", "/dir6/"},
|
SearchPaths: []string{"/dir5", "/dir6/"},
|
||||||
@ -155,7 +155,6 @@ func TestOptionsChecks(t *testing.T) {
|
|||||||
opts: &Opts{
|
opts: &Opts{
|
||||||
BitDir: "../../test/data",
|
BitDir: "../../test/data",
|
||||||
QBitDir: "../../test/data",
|
QBitDir: "../../test/data",
|
||||||
Config: "../../test/data/testfileset.torrent",
|
|
||||||
SearchPaths: []string{},
|
SearchPaths: []string{},
|
||||||
},
|
},
|
||||||
mustFail: false,
|
mustFail: false,
|
||||||
@ -165,23 +164,14 @@ func TestOptionsChecks(t *testing.T) {
|
|||||||
opts: &Opts{
|
opts: &Opts{
|
||||||
BitDir: "/dir",
|
BitDir: "/dir",
|
||||||
QBitDir: "/dir",
|
QBitDir: "/dir",
|
||||||
Config: "/dir/q.conf",
|
Categories: "/dir/q.json",
|
||||||
Replaces: []string{"dir1,dir2,dir4", "dir4"},
|
Replaces: []string{"dir1,dir2,dir4", "dir4"},
|
||||||
SearchPaths: []string{"/dir5", "/dir6/"},
|
SearchPaths: []string{"/dir5", "/dir6/"},
|
||||||
},
|
},
|
||||||
mustFail: true,
|
mustFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "004 Must fail do not exists config test",
|
name: "004 Must fail do not exists qbitdir test",
|
||||||
opts: &Opts{
|
|
||||||
BitDir: "../../test/data",
|
|
||||||
QBitDir: "../../test/data",
|
|
||||||
Config: "/dir/q.conf",
|
|
||||||
},
|
|
||||||
mustFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "005 Must fail do not exists qbitdir test",
|
|
||||||
opts: &Opts{
|
opts: &Opts{
|
||||||
BitDir: "../../test/data",
|
BitDir: "../../test/data",
|
||||||
QBitDir: "/dir",
|
QBitDir: "/dir",
|
||||||
|
@ -1,50 +1,65 @@
|
|||||||
package transfer
|
package transfer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-ini/ini"
|
|
||||||
"github.com/rumanzo/bt2qbt/internal/options"
|
"github.com/rumanzo/bt2qbt/internal/options"
|
||||||
"github.com/rumanzo/bt2qbt/pkg/helpers"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProcessLabels(opts *options.Opts, newtags []string) {
|
func ProcessLabels(opts *options.Opts, newtags []string) error {
|
||||||
var oldtags string
|
categories := map[string]map[string]string{}
|
||||||
cfg, err := ini.Load(opts.Config)
|
|
||||||
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")
|
|
||||||
|
|
||||||
//Dirty hack for section order. Sorry
|
// check if categories is new file. If it exists it must be unmarshaled. Default categories file contains only {}
|
||||||
kv := cfg.Section("Network").KeysHash()
|
var categoriesIsNew bool
|
||||||
cfg.DeleteSection("Network")
|
file, err := os.OpenFile(opts.Categories, os.O_RDWR, 0644)
|
||||||
cfg.NewSection("Network")
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
for key, value := range kv {
|
categoriesIsNew = true
|
||||||
cfg.Section("Network").NewKey(key, value)
|
} else if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Unexpected error while open categories.json. Error:\n%v\n", err))
|
||||||
}
|
}
|
||||||
//End of dirty hack
|
|
||||||
}
|
if !categoriesIsNew {
|
||||||
if cfg.Section("BitTorrent").HasKey("Session\\Tags") {
|
dataRaw, err := ioutil.ReadAll(file)
|
||||||
oldtags = cfg.Section("BitTorrent").Key("Session\\Tags").String()
|
|
||||||
for _, tag := range strings.Split(oldtags, ", ") {
|
|
||||||
if exists, t := helpers.CheckExists(tag, newtags); !exists {
|
|
||||||
newtags = append(newtags, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg.Section("BitTorrent").Key("Session\\Tags").SetValue(strings.Join(newtags, ", "))
|
|
||||||
} else {
|
|
||||||
cfg.Section("BitTorrent").NewKey("Session\\Tags", strings.Join(newtags, ", "))
|
|
||||||
}
|
|
||||||
err = cfg.SaveTo(opts.Config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Unexpected error while save qBittorrent config.ini. Error:\n%v\n", err)
|
return errors.New(fmt.Sprintf("Unexpected error while read categories.json. Error:\n%v\n", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Can't close categories.json. Error:\n%v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(dataRaw, &categories)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Unexpected error while unmarshaling categories.json. Error:\n%v\n", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range newtags {
|
||||||
|
if _, ok := categories[tag]; !ok { // append only if key doesn't already exist
|
||||||
|
categories[tag] = map[string]string{"save_path": ""}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !categoriesIsNew {
|
||||||
|
err = os.Rename(opts.Categories, opts.Categories+".bak")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Can't move categories.json to categories.bak. Error:\n%v\n", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newCategories, err := json.Marshal(categories)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Can't marshal categories. Error:\n%v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(opts.Categories, newCategories, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Can't write categories.json. Error:\n%v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
39
internal/transfer/labels_test.go
Normal file
39
internal/transfer/labels_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rumanzo/bt2qbt/internal/options"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProcessLabelsExisting(t *testing.T) {
|
||||||
|
err := ioutil.WriteFile("../../test/categories_existing.json", []byte("{}"), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Can't write empty categories test file. Err: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &options.Opts{Categories: "../../test/categories_existing.json"}
|
||||||
|
err = ProcessLabels(opts, []string{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpecter error with handle categories. Err: %v", err.Error())
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err = os.Remove(opts.Categories + ".bak")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("It must exists bak file. Err: %v", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessLabelsNotExisting(t *testing.T) {
|
||||||
|
opts := &options.Opts{Categories: "../../test/categories_not_existing.json"}
|
||||||
|
os.Remove(opts.Categories)
|
||||||
|
err := ProcessLabels(opts, []string{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Remove(opts.Categories)
|
||||||
|
})
|
||||||
|
}
|
@ -95,7 +95,7 @@ func HandleResumeItems(opts *options.Opts, resumeItems map[string]*utorrentStruc
|
|||||||
if opts.WithoutTags == false {
|
if opts.WithoutTags == false {
|
||||||
if resumeItem.Labels != nil {
|
if resumeItem.Labels != nil {
|
||||||
for _, label := range resumeItem.Labels {
|
for _, label := range resumeItem.Labels {
|
||||||
if exists, tag := helpers.CheckExists(helpers.ASCIIConvert(label), newTags); !exists {
|
if exists, tag := helpers.CheckExists(label, newTags); !exists {
|
||||||
newTags = append(newTags, tag)
|
newTags = append(newTags, tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,7 +125,10 @@ func HandleResumeItems(opts *options.Opts, resumeItems map[string]*utorrentStruc
|
|||||||
numJob++
|
numJob++
|
||||||
}
|
}
|
||||||
if opts.WithoutTags == false {
|
if opts.WithoutTags == false {
|
||||||
ProcessLabels(opts, newTags)
|
err := ProcessLabels(opts, newTags)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Can't handle labels with error:\n%v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
log.Println("Ended")
|
log.Println("Ended")
|
||||||
|
1
test/categories_existing.json
Normal file
1
test/categories_existing.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
Loading…
Reference in New Issue
Block a user