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.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()

View File

@ -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")

View File

@ -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",

View File

@ -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 cfg.Section("BitTorrent").HasKey("Session\\Tags") {
oldtags = cfg.Section("BitTorrent").Key("Session\\Tags").String() if !categoriesIsNew {
for _, tag := range strings.Split(oldtags, ", ") { dataRaw, err := ioutil.ReadAll(file)
if exists, t := helpers.CheckExists(tag, newtags); !exists { if err != nil {
newtags = append(newtags, t) 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))
} }
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)
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 { if err != nil {
fmt.Printf("Unexpected error while save qBittorrent config.ini. Error:\n%v\n", err) 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 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")

View File

@ -0,0 +1 @@
{}