Modern qBittorrent doesn't save tags to config file. It uses categories.json instead.

Rewritted handleLabels function. Tests
This commit is contained in:
rumanzo 2022-04-03 19:18:10 +03:00
parent 46103167ef
commit e166194b56
7 changed files with 109 additions and 67 deletions

View File

@ -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.HiRed("Check that the qBittorrent is turned off and the directory %v and config %v is backed up.\n",
opts.QBitDir, opts.Config)
color.HiRed("Check that the qBittorrent is turned off and the directory %v and %v is backed up.\n",
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")
fmt.Println("Press Enter to start")
fmt.Scanln()

View File

@ -15,7 +15,7 @@ import (
type Opts 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)"`
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"`
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'"`
@ -29,7 +29,7 @@ func PrepareOpts() *Opts {
switch OS := runtime.GOOS; OS {
case "windows":
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")
case "linux":
usr, err := user.Current()
@ -37,7 +37,7 @@ func PrepareOpts() *Opts {
panic(err)
}
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")
case "darwin":
usr, err := user.Current()
@ -45,7 +45,7 @@ func PrepareOpts() *Opts {
panic(err)
}
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")
}
return opts
@ -86,12 +86,6 @@ func OptsCheck(opts *Opts) error {
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 opts.SearchPaths == nil {
return fmt.Errorf("on linux systems you must define search path for torrents")

View File

@ -33,7 +33,7 @@ func TestOptionsArgs(t *testing.T) {
args: []string{
"-s", "/dir",
"-d", "/dir",
"-c", "/dir/q.conf",
"-c", "/dir/q.json",
"-r", "dir1,dir2", "-r", "dir3,dir4",
"--sep", "/",
"-t", "/dir5", "-t", "/dir6/",
@ -42,7 +42,7 @@ func TestOptionsArgs(t *testing.T) {
expected: &Opts{
BitDir: "/dir",
QBitDir: "/dir",
Config: "/dir/q.conf",
Categories: "/dir/q.json",
Replaces: []string{"dir1,dir2", "dir3,dir4"},
PathSeparator: "/",
SearchPaths: []string{"/dir5", "/dir6/"},
@ -54,7 +54,7 @@ func TestOptionsArgs(t *testing.T) {
args: []string{
"--source", "/dir",
"--destination", "/dir",
"--config", "/dir/q.conf",
"--categories", "/dir/q.json",
"--replace", "dir1,dir2", "-r", "dir3,dir4",
"--sep", "/",
"--search", "/dir5", "-t", "/dir6/",
@ -63,7 +63,7 @@ func TestOptionsArgs(t *testing.T) {
expected: &Opts{
BitDir: "/dir",
QBitDir: "/dir",
Config: "/dir/q.conf",
Categories: "/dir/q.json",
Replaces: []string{"dir1,dir2", "dir3,dir4"},
PathSeparator: "/",
SearchPaths: []string{"/dir5", "/dir6/"},
@ -96,7 +96,7 @@ func TestOptionsHandle(t *testing.T) {
opts: &Opts{
BitDir: "/dir",
QBitDir: "/dir",
Config: "/dir/q.conf",
Categories: "/dir/q.json",
Replaces: []string{"dir1,dir2", "dir3,dir4"},
PathSeparator: "/",
SearchPaths: []string{"/dir5", "/dir6/"},
@ -142,7 +142,7 @@ func TestOptionsChecks(t *testing.T) {
opts: &Opts{
BitDir: "/dir",
QBitDir: "/dir",
Config: "/dir/q.conf",
Categories: "/dir/q.json",
Replaces: []string{"dir1,dir2", "dir3,dir4"},
PathSeparator: "/",
SearchPaths: []string{"/dir5", "/dir6/"},
@ -155,7 +155,6 @@ func TestOptionsChecks(t *testing.T) {
opts: &Opts{
BitDir: "../../test/data",
QBitDir: "../../test/data",
Config: "../../test/data/testfileset.torrent",
SearchPaths: []string{},
},
mustFail: false,
@ -165,23 +164,14 @@ func TestOptionsChecks(t *testing.T) {
opts: &Opts{
BitDir: "/dir",
QBitDir: "/dir",
Config: "/dir/q.conf",
Categories: "/dir/q.json",
Replaces: []string{"dir1,dir2,dir4", "dir4"},
SearchPaths: []string{"/dir5", "/dir6/"},
},
mustFail: true,
},
{
name: "004 Must fail do not exists config test",
opts: &Opts{
BitDir: "../../test/data",
QBitDir: "../../test/data",
Config: "/dir/q.conf",
},
mustFail: true,
},
{
name: "005 Must fail do not exists qbitdir test",
name: "004 Must fail do not exists qbitdir test",
opts: &Opts{
BitDir: "../../test/data",
QBitDir: "/dir",

View File

@ -1,50 +1,65 @@
package transfer
import (
"encoding/json"
"errors"
"fmt"
"github.com/go-ini/ini"
"github.com/rumanzo/bt2qbt/internal/options"
"github.com/rumanzo/bt2qbt/pkg/helpers"
"io/ioutil"
"os"
"strings"
"time"
)
func ProcessLabels(opts *options.Opts, newtags []string) {
var oldtags 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")
func ProcessLabels(opts *options.Opts, newtags []string) error {
categories := map[string]map[string]string{}
//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)
// check if categories is new file. If it exists it must be unmarshaled. Default categories file contains only {}
var categoriesIsNew bool
file, err := os.OpenFile(opts.Categories, os.O_RDWR, 0644)
if errors.Is(err, os.ErrNotExist) {
categoriesIsNew = true
} 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 cfg.Section("BitTorrent").HasKey("Session\\Tags") {
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 !categoriesIsNew {
dataRaw, err := ioutil.ReadAll(file)
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
}

View 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)
})
}

View File

@ -95,7 +95,7 @@ func HandleResumeItems(opts *options.Opts, resumeItems map[string]*utorrentStruc
if opts.WithoutTags == false {
if resumeItem.Labels != nil {
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)
}
}
@ -125,7 +125,10 @@ func HandleResumeItems(opts *options.Opts, resumeItems map[string]*utorrentStruc
numJob++
}
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()
log.Println("Ended")

View File

@ -0,0 +1 @@
{}