Fixstructures (#39)

* Fix structures

* torrents v2 file tree interface

* get files vrom both v1 and v2 torrents

* fix typo

* style

* cleanup makefile

* new test fileset with real qBittorrent files and new tests

* counting files using torrent file

* Handle v2 and hybryd torrents
This commit is contained in:
Alexey Kostin 2022-04-17 16:01:18 +03:00 committed by GitHub
parent b66b5f8e75
commit 47e0028bc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 689 additions and 228 deletions

View File

@ -1,11 +1,4 @@
#GOOS=windows GOACH=amd64 go build -o bt2qbt_v${1}_amd64.exe -tags forceposix
#GOOS=windows GOARCH=386 go build -o bt2qbt_v${1}_i386.exe -tags forceposix
#GOOS=linux GOARCH=amd64 go build -o bt2qbt_v${1}_amd64_linux -tags forceposix
#GOOS=linux GOARCH=386 go build -o bt2qbt_v${1}_i386_linux -tags forceposix
#GOOS=darwin GOARCH=amd64 go build -o bt2qbt_v${1}_amd64_macos -tags forceposix
#GOOS=darwin GOARCH=386 go build -o bt2qbt_v${1}_i386_macos -tags forceposix
gotag=1.18.0-bullseye
gotag=1.18.1-bullseye
commit=$(shell git rev-parse HEAD)
@ -15,13 +8,12 @@ buildenvs = -e CGO_ENABLED=0
version = 1.999
ldflags = -ldflags="-X 'main.version=$(version)' -X 'main.commit=$(commit)' -X 'main.buildImage=golang:$(gotag)'"
all: tests build
all: | tests build
tests:
$(dockercmd) golang:$(gotag) go test $(buildtags) ./...
build: | tests windows linux darwin
build: windows linux darwin
windows:
$(dockercmd) $(buildenvs) -e GOOS=windows -e GOARCH=amd64 golang:$(gotag) go build -v $(buildtags) $(ldflags) -o bt2qbt_v$(version)_amd64.exe

View File

@ -17,9 +17,10 @@ func (transfer *TransferStructure) HandleStructures() {
transfer.Fastresume.ActiveTime = transfer.ResumeItem.Runtime
transfer.Fastresume.AddedTime = transfer.ResumeItem.AddedOn
transfer.Fastresume.CompletedTime = transfer.ResumeItem.CompletedOn
transfer.Fastresume.Info = transfer.TorrentFile.Info
transfer.Fastresume.Info = transfer.TorrentFileRaw["info"]
transfer.Fastresume.InfoHash = transfer.ResumeItem.Info
transfer.Fastresume.SeedingTime = transfer.ResumeItem.Runtime
transfer.HandlePriority() // handle priorities before handling pieces and state
transfer.HandleState()
transfer.Fastresume.FinishedTime = int64(time.Since(time.Unix(transfer.ResumeItem.CompletedOn, 0)).Minutes())
@ -30,7 +31,6 @@ func (transfer *TransferStructure) HandleStructures() {
transfer.HandleLabels()
transfer.HandleTrackers()
transfer.HandlePriority() // important handle priorities before handling pieces
/*
pieces maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20,

View File

@ -87,20 +87,19 @@ func (transfer *TransferStructure) HandleState() {
transfer.Fastresume.Paused = 1
transfer.Fastresume.AutoManaged = 0
} else {
if transfer.TorrentFile.Info.Files != nil {
if len(transfer.TorrentFile.Info.Files) > 1 {
var parted bool
for _, prio := range transfer.ResumeItem.Prio {
if byte(prio) == 0 || byte(prio) == 128 {
parted = true
}
}
if parted {
transfer.Fastresume.Paused = 1
transfer.Fastresume.AutoManaged = 0
return
if len(transfer.TorrentFile.GetFileList()) > 1 {
var parted bool
for _, prio := range transfer.Fastresume.FilePriority {
if prio == 0 {
parted = true
break
}
}
if parted {
transfer.Fastresume.Paused = 1
transfer.Fastresume.AutoManaged = 0
return
}
}
transfer.Fastresume.Paused = 0
transfer.Fastresume.AutoManaged = 1
@ -170,6 +169,13 @@ func (transfer *TransferStructure) HandleTrackers() {
}
func (transfer *TransferStructure) HandlePriority() {
if transfer.TorrentFile.IsV2OrHybryd() { // so we need get only odd
trimmedPrio := make([]byte, 0, len(transfer.ResumeItem.Prio)/2)
for i := 0; i < len(transfer.ResumeItem.Prio); i += 2 {
trimmedPrio = append(trimmedPrio, transfer.ResumeItem.Prio[i])
}
transfer.ResumeItem.Prio = trimmedPrio
}
for _, c := range transfer.ResumeItem.Prio {
if i := int(c); (i == 0) || (i == 128) { // if priority not selected
transfer.Fastresume.FilePriority = append(transfer.Fastresume.FilePriority, 0)
@ -195,7 +201,7 @@ func (transfer *TransferStructure) HandlePieces() {
if transfer.Fastresume.Unfinished != nil {
transfer.FillWholePieces(0)
} else {
if len(transfer.TorrentFile.Info.Files) > 0 {
if len(transfer.TorrentFile.GetFileList()) > 0 {
transfer.FillPiecesParted()
} else {
transfer.FillWholePieces(1)
@ -220,7 +226,7 @@ func (transfer *TransferStructure) FillPiecesParted() {
}
var fileOffsets []Offset
bytesLength := int64(0)
for _, file := range transfer.TorrentFile.Info.Files {
for _, file := range transfer.TorrentFile.GetFileListWB() { // need to adapt for torrents v2 version
fileFirstOffset := bytesLength + 1
bytesLength += file.Length
fileLastOffset := bytesLength
@ -272,7 +278,7 @@ func (transfer *TransferStructure) HandleSavePaths() {
}
lastPathName := fileHelpers.Base(transfer.ResumeItem.Path)
if len(transfer.TorrentFile.Info.Files) > 0 {
if len(transfer.TorrentFile.GetFileList()) > 0 {
if lastPathName == torrentName {
transfer.Fastresume.QBtContentLayout = "Original"
transfer.Fastresume.QbtSavePath = fileHelpers.CutLastPath(transfer.ResumeItem.Path, transfer.Opts.PathSeparator)
@ -300,14 +306,8 @@ func (transfer *TransferStructure) HandleSavePaths() {
transfer.Fastresume.QBtContentLayout = "NoSubfolder"
// NoSubfolder always has full mapped files
// so we append all of them
for _, filePath := range transfer.TorrentFile.Info.Files {
var paths []string
if len(filePath.PathUTF8) != 0 {
paths = filePath.PathUTF8
} else {
paths = filePath.Path
}
transfer.Fastresume.MappedFiles = append(transfer.Fastresume.MappedFiles, fileHelpers.Join(paths, transfer.Opts.PathSeparator))
for _, filePath := range transfer.TorrentFile.GetFileList() {
transfer.Fastresume.MappedFiles = append(transfer.Fastresume.MappedFiles, fileHelpers.Normalize(filePath, transfer.Opts.PathSeparator))
}
// and then doing remap if resumeItem contain target field
if maxIndex := transfer.FindHighestIndexOfMappedFiles(); maxIndex >= 0 {

View File

@ -921,11 +921,11 @@ func TestTransferStructure_HandlePieces(t *testing.T) {
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{Length: 5},
&torrentStructures.TorrentFile{Length: 5},
&torrentStructures.TorrentFile{Length: 5},
&torrentStructures.TorrentFile{Length: 5},
&torrentStructures.TorrentFile{Length: 5},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5},
},
PieceLength: 5,
},
@ -954,9 +954,9 @@ func TestTransferStructure_HandlePieces(t *testing.T) {
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{Length: 13},
&torrentStructures.TorrentFile{Length: 7},
&torrentStructures.TorrentFile{Length: 5},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 13},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 7},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5},
},
PieceLength: 5, // 25 total
},
@ -985,9 +985,9 @@ func TestTransferStructure_HandlePieces(t *testing.T) {
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{Length: 9},
&torrentStructures.TorrentFile{Length: 6},
&torrentStructures.TorrentFile{Length: 10},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 9},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 6},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 10},
},
PieceLength: 5, // 25 total
},
@ -1016,9 +1016,9 @@ func TestTransferStructure_HandlePieces(t *testing.T) {
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{Length: 13},
&torrentStructures.TorrentFile{Length: 7},
&torrentStructures.TorrentFile{Length: 5},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 13},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 7},
&torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5},
},
PieceLength: 5, // 25 total
},
@ -1102,25 +1102,93 @@ func TestTransferStructure_HandlePieces(t *testing.T) {
}
func TestTransferStructure_HandlePriority(t *testing.T) {
transferStructure := TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{FilePriority: []int64{}},
ResumeItem: &utorrentStructs.ResumeItem{
Prio: []byte{
byte(0),
byte(128),
byte(2),
byte(5),
byte(8),
byte(9),
byte(15),
byte(127), // unexpected
type HandlePriorityCase struct {
name string
mustFail bool
newTransferStructure *TransferStructure
expected []int64
}
cases := []HandlePriorityCase{
{
name: "001 mustfail",
newTransferStructure: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{FilePriority: []int64{}},
TorrentFile: &torrentStructures.Torrent{Info: &torrentStructures.TorrentInfo{}},
ResumeItem: &utorrentStructs.ResumeItem{
Prio: []byte{},
},
},
mustFail: true,
expected: []int64{0, 0, 1, 1, 1, 6, 6, 0},
},
{
name: "002 check priotiry for v1 torrents",
newTransferStructure: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{FilePriority: []int64{}},
TorrentFile: &torrentStructures.Torrent{Info: &torrentStructures.TorrentInfo{}},
ResumeItem: &utorrentStructs.ResumeItem{
Prio: []byte{
byte(0),
byte(128),
byte(2),
byte(5),
byte(8),
byte(9),
byte(15),
byte(127), // unexpected
},
},
},
expected: []int64{0, 0, 1, 1, 1, 6, 6, 0},
},
{
name: "002 check priotiry for v2 torrents",
newTransferStructure: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{FilePriority: []int64{}},
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
FileTree: map[string]interface{}{},
},
},
ResumeItem: &utorrentStructs.ResumeItem{
Prio: []byte{
byte(0),
byte(128),
byte(128),
byte(128),
byte(2),
byte(128),
byte(5),
byte(128),
byte(8),
byte(128),
byte(9),
byte(128),
byte(15),
byte(128),
byte(127), // unexpected
byte(128),
},
},
},
expected: []int64{0, 0, 1, 1, 1, 6, 6, 0},
},
}
expect := []int64{0, 0, 1, 1, 1, 6, 6, 0}
transferStructure.HandlePriority()
if !reflect.DeepEqual(transferStructure.Fastresume.FilePriority, expect) {
t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n", transferStructure.Fastresume.FilePriority, expect)
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
testCase.newTransferStructure.HandlePriority()
equal := reflect.DeepEqual(testCase.expected, testCase.newTransferStructure.Fastresume.FilePriority)
if !equal && !testCase.mustFail {
changes, err := diff.Diff(testCase.newTransferStructure.Fastresume.FilePriority, testCase.expected, diff.DiscardComplexOrigin())
if err != nil {
t.Error(err.Error())
}
t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n Diff: %v\n", testCase.newTransferStructure.Fastresume.FilePriority, testCase.expected, spew.Sdump(changes))
} else if equal && testCase.mustFail {
t.Fatalf("Unexpected error: structures are equal, but they shouldn't\n Got: %v\n", spew.Sdump(testCase.newTransferStructure.Fastresume.FilePriority))
}
})
}
}
@ -1239,22 +1307,17 @@ func TestTransferStructure_HandleState(t *testing.T) {
Fastresume: &qBittorrentStructures.QBittorrentFastresume{},
ResumeItem: &utorrentStructs.ResumeItem{
Started: 1,
Prio: []byte{
byte(1),
byte(1),
byte(2),
byte(5),
byte(8),
byte(9),
byte(15),
},
},
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
},
},
},
@ -1266,121 +1329,125 @@ func TestTransferStructure_HandleState(t *testing.T) {
{
name: "005 started resume with parted downloaded files",
newTransferStructure: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{},
Fastresume: &qBittorrentStructures.QBittorrentFastresume{
FilePriority: []int64{1, 0, 1, 1, 1, 6, 6},
},
ResumeItem: &utorrentStructs.ResumeItem{
Started: 1,
Prio: []byte{
byte(0),
byte(10),
byte(2),
byte(5),
byte(8),
byte(9),
byte(15),
},
},
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
},
},
},
},
expected: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{Paused: 1, AutoManaged: 0},
Fastresume: &qBittorrentStructures.QBittorrentFastresume{
Paused: 1,
AutoManaged: 0,
FilePriority: []int64{1, 0, 1, 1, 1, 6, 6},
},
},
},
{
name: "006 started resume with parted downloaded files",
newTransferStructure: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{},
Fastresume: &qBittorrentStructures.QBittorrentFastresume{
FilePriority: []int64{1, 0, 1, 1, 1, 6, 6},
},
ResumeItem: &utorrentStructs.ResumeItem{
Started: 1,
Prio: []byte{
byte(1),
byte(128),
byte(2),
byte(5),
byte(8),
byte(9),
byte(15),
},
},
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
},
},
},
},
expected: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{Paused: 1, AutoManaged: 0},
Fastresume: &qBittorrentStructures.QBittorrentFastresume{
Paused: 1,
AutoManaged: 0,
FilePriority: []int64{1, 0, 1, 1, 1, 6, 6},
},
},
},
{
name: "007 started resume with full downloaded files",
newTransferStructure: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{},
Fastresume: &qBittorrentStructures.QBittorrentFastresume{
FilePriority: []int64{1, 0, 1, 1, 1, 6, 6},
},
ResumeItem: &utorrentStructs.ResumeItem{
Started: 1,
Prio: []byte{
byte(1),
byte(128),
byte(2),
byte(5),
byte(8),
byte(9),
byte(15),
},
},
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
},
},
},
},
expected: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{Paused: 1, AutoManaged: 0},
Fastresume: &qBittorrentStructures.QBittorrentFastresume{
Paused: 1,
AutoManaged: 0,
FilePriority: []int64{1, 0, 1, 1, 1, 6, 6},
},
},
},
{
name: "008 started resume with parted downloaded files",
newTransferStructure: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{},
Fastresume: &qBittorrentStructures.QBittorrentFastresume{
FilePriority: []int64{1, 0, 1, 1, 1, 6, 6},
},
ResumeItem: &utorrentStructs.ResumeItem{
Started: 1,
Prio: []byte{
byte(1),
byte(128),
byte(2),
byte(5),
byte(8),
byte(9),
byte(15),
},
},
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{
Files: []*torrentStructures.TorrentFile{
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
&torrentStructures.TorrentFile{Path: []string{`/`}},
},
},
},
},
expected: &TransferStructure{
Fastresume: &qBittorrentStructures.QBittorrentFastresume{Paused: 1, AutoManaged: 0},
Fastresume: &qBittorrentStructures.QBittorrentFastresume{
Paused: 1,
AutoManaged: 0,
FilePriority: []int64{1, 0, 1, 1, 1, 6, 6},
},
},
},
{
@ -1389,15 +1456,6 @@ func TestTransferStructure_HandleState(t *testing.T) {
Fastresume: &qBittorrentStructures.QBittorrentFastresume{},
ResumeItem: &utorrentStructs.ResumeItem{
Started: 0,
Prio: []byte{
byte(1),
byte(128),
byte(2),
byte(5),
byte(8),
byte(9),
byte(15),
},
},
TorrentFile: &torrentStructures.Torrent{
Info: &torrentStructures.TorrentInfo{},

View File

@ -27,3 +27,33 @@ func TestGetStrings(t *testing.T) {
t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n", trackers, expect)
}
}
func TestDecodeTorrentFile(t *testing.T) {
type PathJoinCase struct {
name string
mustFail bool
path string
}
cases := []PathJoinCase{
{
name: "001 not existing file",
path: "notexists.torrent",
mustFail: true,
},
{
name: "002 existing file",
path: "../../test/data/testfileset.torrent",
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
var decoded interface{}
err := DecodeTorrentFile(testCase.path, &decoded)
if err != nil && !testCase.mustFail {
t.Fatalf("Unexpected error: %v", err)
} else if err == nil && testCase.mustFail {
t.Fatalf("Test must fail, but it doesn't")
}
})
}
}

View File

@ -1,65 +1,63 @@
package qBittorrentStructures
import "github.com/rumanzo/bt2qbt/pkg/torrentStructures"
// https://www.libtorrent.org/manual-ref.html
type QBittorrentFastresume struct {
ActiveTime int64 `bencode:"active_time"` // integer. The number of seconds this torrent has been active. i.e. not paused.
AddedTime int64 `bencode:"added_time"`
Allocation string `bencode:"allocation"` // The allocation mode for the storage. Can be either allocate or sparse.
ApplyIpFilter int64 `bencode:"apply_ip_filter"` // integer. 1 if the torrent_flags::apply_ip_filter is set.
AutoManaged int64 `bencode:"auto_managed"` // integer. 1 if the torrent is auto managed, otherwise 0.
BannedPeers []byte `bencode:"banned_peers"` // string. This string has the same format as peers but instead represent IPv4 peers that we have banned.
BannedPeers6 []byte `bencode:"banned_peers6"` // string. This string has the same format as peers6 but instead represent IPv6 peers that we have banned.
CompletedTime int64 `bencode:"completed_time"`
DisableDht int64 `bencode:"disable_dht"` // integer. 1 if the torrent_flags::disable_dht is set.
DisableLsd int64 `bencode:"disable_lsd"` // integer. 1 if the torrent_flags::disable_lsd is set.
DisablePex int64 `bencode:"disable_pex"` // integer. 1 if the torrent_flags::disable_pex is set.
DownloadRateLimit int64 `bencode:"download_rate_limit"` // integer. The download rate limit for this torrent in case one is set, in bytes per second.
FileFormat string `bencode:"file-format"` // string: "libtorrent resume file"
FilePriority []int64 `bencode:"file_priority"` // list of integers. One entry per file in the torrent. Each entry is the priority of the file with the same index.
FileVersion int64 `bencode:"file-version"`
FinishedTime int64 `bencode:"finished_time"`
HttpSeeds []string `bencode:"httpseeds"` // list of strings. List of HTTP seed URLs used by this torrent. The URLs are expected to be properly encoded and not contain any illegal url characters.
Info *torrentStructures.TorrentInfo `bencode:"info,omitempty"` // If this field is present, it should be the info-dictionary of the torrent this resume data is for. Its SHA-1 hash must match the one in the info-hash field. When present, the torrent is loaded from here, meaning the torrent can be added purely from resume data (no need to load the .torrent file separately). This may have performance advantages.
InfoHash string `bencode:"info-hash"` // string, the info hash of the torrent this data is saved for. This is a 20 byte SHA-1 hash of the info section of the torrent if this is a v1 or v1+v2-hybrid torrent.
InfoHash2 string `bencode:"info-hash2"` // string, the v2 info hash of the torrent this data is saved. for, in case it is a v2 or v1+v2-hybrid torrent. This is a 32 byte SHA-256 hash of the info section of the torrent.
LastDownload int64 `bencode:"last_download"` // integer. The number of seconds since epoch when we last downloaded payload from a peer on this torrent.
LastSeenComplete int64 `bencode:"last_seen_complete"`
LastUpload int64 `bencode:"last_upload"` // integer. The number of seconds since epoch when we last uploaded payload to a peer on this torrent.
LibTorrentVersion string `bencode:"libtorrent-version"`
MappedFiles []string `bencode:"mapped_files,omitempty"` // list of strings. If any file in the torrent has been renamed, this entry contains a list of all the filenames. In the same order as in the torrent file.
MaxConnections int64 `bencode:"max_connections"` // integer. The max number of peer connections this torrent may have, if a limit is set.
MaxUploads int64 `bencode:"max_uploads"` // integer. The max number of unchoked peers this torrent may have, if a limit is set.
NumComplete int64 `bencode:"num_complete"`
NumDownloaded int64 `bencode:"num_downloaded"`
NumIncomplete int64 `bencode:"num_incomplete"`
Paused int64 `bencode:"paused"` // integer. 1 if the torrent is paused, 0 otherwise.
Peers int64 `bencode:"peers"` // string. This string contains IPv4 and port pairs of peers we were connected to last session. The endpoints are in compact representation. 4 bytes IPv4 address followed by 2 bytes port. Hence, the length of this string should be divisible by 6.
Peers6 int64 `bencode:"peers6"` // string. This string contains IPv6 and port pairs of peers we were connected to last session. The endpoints are in compact representation. 16 bytes IPv6 address followed by 2 bytes port. The length of this string should be divisible by 18.
Pieces []byte `bencode:"pieces"` // A string with piece flags, one character per piece. Bit 1 means we have that piece. Bit 2 means we have verified that this piece is correct. This only applies when the torrent is in seed_mode.
QBtCategory string `bencode:"qBt-category"`
QBtContentLayout string `bencode:"qBt-contentLayout"` // Original, Subfolder, NoSubfolder
QBtFirstLastPiecePriority string `bencode:"qBt-firstLastPiecePriority"`
QbtName string `bencode:"qBt-name"`
QbtRatioLimit int64 `bencode:"qBt-ratioLimit"`
QbtSavePath string `bencode:"qBt-savePath"`
QbtSeedStatus int64 `bencode:"qBt-seedStatus"`
QbtSeedingTimeLimit int64 `bencode:"qBt-seedingTimeLimit"`
QbtTags []string `bencode:"qBt-tags"`
SavePath string `bencode:"save_path"` // string. The save path where this torrent was saved. This is especially useful when moving torrents with move_storage() since this will be updated.
SeedMode int64 `bencode:"seed_mode"` // integer. 1 if the torrent is in seed mode, 0 otherwise.
SeedingTime int64 `bencode:"seeding_time"` // integer. The number of seconds this torrent has been active and seeding.
SequentialDownload int64 `bencode:"sequential_download"` // integer. 1 if the torrent is in sequential download mode, 0 otherwise.
ShareMode int64 `bencode:"share_mode"` // integer. 1 if the torrent_flags::share_mode is set.
StopWhenReady int64 `bencode:"stop_when_ready"` // integer. 1 if the torrent_flags::stop_when_ready is set.
SuperSeeding int64 `bencode:"super_seeding"` // integer. 1 if the torrent_flags::super_seeding is set.
TotalDownloaded int64 `bencode:"total_downloaded"` // integer. The number of bytes that have been downloaded in total for this torrent.
TotalUploaded int64 `bencode:"total_uploaded"` // integer. The number of bytes that have been uploaded in total for this torrent.
Trackers [][]string `bencode:"trackers"` // list of lists of strings. The top level list lists all tracker tiers. Each second level list is one tier of trackers.
Unfinished *[]interface{} `bencode:"unfinished,omitempty"`
UploadMode int64 `bencode:"upload_mode"` // integer. 1 if the torrent_flags::upload_mode is set.
UploadRateLimit int64 `bencode:"upload_rate_limit"` // integer. In case this torrent has a per-torrent upload rate limit, this is that limit. In bytes per second.
UrlList int64 `bencode:"url-list"` // list of strings. List of url-seed URLs used by this torrent. The URLs are expected to be properly encoded and not contain any illegal url characters.
ActiveTime int64 `bencode:"active_time"` // integer. The number of seconds this torrent has been active. i.e. not paused.
AddedTime int64 `bencode:"added_time"`
Allocation string `bencode:"allocation"` // The allocation mode for the storage. Can be either allocate or sparse.
ApplyIpFilter int64 `bencode:"apply_ip_filter"` // integer. 1 if the torrent_flags::apply_ip_filter is set.
AutoManaged int64 `bencode:"auto_managed"` // integer. 1 if the torrent is auto managed, otherwise 0.
BannedPeers []byte `bencode:"banned_peers"` // string. This string has the same format as peers but instead represent IPv4 peers that we have banned.
BannedPeers6 []byte `bencode:"banned_peers6"` // string. This string has the same format as peers6 but instead represent IPv6 peers that we have banned.
CompletedTime int64 `bencode:"completed_time"`
DisableDht int64 `bencode:"disable_dht"` // integer. 1 if the torrent_flags::disable_dht is set.
DisableLsd int64 `bencode:"disable_lsd"` // integer. 1 if the torrent_flags::disable_lsd is set.
DisablePex int64 `bencode:"disable_pex"` // integer. 1 if the torrent_flags::disable_pex is set.
DownloadRateLimit int64 `bencode:"download_rate_limit"` // integer. The download rate limit for this torrent in case one is set, in bytes per second.
FileFormat string `bencode:"file-format"` // string: "libtorrent resume file"
FilePriority []int64 `bencode:"file_priority"` // list of integers. One entry per file in the torrent. Each entry is the priority of the file with the same index.
FileVersion int64 `bencode:"file-version"`
FinishedTime int64 `bencode:"finished_time"`
HttpSeeds []string `bencode:"httpseeds"` // list of strings. List of HTTP seed URLs used by this torrent. The URLs are expected to be properly encoded and not contain any illegal url characters.
Info interface{} `bencode:"info,omitempty"` // If this field is present, it should be the info-dictionary of the torrent this resume data is for. Its SHA-1 hash must match the one in the info-hash field. When present, the torrent is loaded from here, meaning the torrent can be added purely from resume data (no need to load the .torrent file separately). This may have performance advantages.
InfoHash string `bencode:"info-hash"` // string, the info hash of the torrent this data is saved for. This is a 20 byte SHA-1 hash of the info section of the torrent if this is a v1 or v1+v2-hybrid torrent.
InfoHash2 string `bencode:"info-hash2"` // string, the v2 info hash of the torrent this data is saved. for, in case it is a v2 or v1+v2-hybrid torrent. This is a 32 byte SHA-256 hash of the info section of the torrent.
LastDownload int64 `bencode:"last_download"` // integer. The number of seconds since epoch when we last downloaded payload from a peer on this torrent.
LastSeenComplete int64 `bencode:"last_seen_complete"`
LastUpload int64 `bencode:"last_upload"` // integer. The number of seconds since epoch when we last uploaded payload to a peer on this torrent.
LibTorrentVersion string `bencode:"libtorrent-version"`
MappedFiles []string `bencode:"mapped_files,omitempty"` // list of strings. If any file in the torrent has been renamed, this entry contains a list of all the filenames. In the same order as in the torrent file.
MaxConnections int64 `bencode:"max_connections"` // integer. The max number of peer connections this torrent may have, if a limit is set.
MaxUploads int64 `bencode:"max_uploads"` // integer. The max number of unchoked peers this torrent may have, if a limit is set.
NumComplete int64 `bencode:"num_complete"`
NumDownloaded int64 `bencode:"num_downloaded"`
NumIncomplete int64 `bencode:"num_incomplete"`
Paused int64 `bencode:"paused"` // integer. 1 if the torrent is paused, 0 otherwise.
Peers string `bencode:"peers"` // string. This string contains IPv4 and port pairs of peers we were connected to last session. The endpoints are in compact representation. 4 bytes IPv4 address followed by 2 bytes port. Hence, the length of this string should be divisible by 6.
Peers6 string `bencode:"peers6"` // string. This string contains IPv6 and port pairs of peers we were connected to last session. The endpoints are in compact representation. 16 bytes IPv6 address followed by 2 bytes port. The length of this string should be divisible by 18.
Pieces []byte `bencode:"pieces"` // A string with piece flags, one character per piece. Bit 1 means we have that piece. Bit 2 means we have verified that this piece is correct. This only applies when the torrent is in seed_mode.
QBtCategory string `bencode:"qBt-category"`
QBtContentLayout string `bencode:"qBt-contentLayout"` // Original, Subfolder, NoSubfolder
QBtFirstLastPiecePriority int64 `bencode:"qBt-firstLastPiecePriority"`
QbtName string `bencode:"qBt-name"`
QbtRatioLimit int64 `bencode:"qBt-ratioLimit"`
QbtSavePath string `bencode:"qBt-savePath"`
QbtSeedStatus int64 `bencode:"qBt-seedStatus"`
QbtSeedingTimeLimit int64 `bencode:"qBt-seedingTimeLimit"`
QbtTags []string `bencode:"qBt-tags"`
SavePath string `bencode:"save_path"` // string. The save path where this torrent was saved. This is especially useful when moving torrents with move_storage() since this will be updated.
SeedMode int64 `bencode:"seed_mode"` // integer. 1 if the torrent is in seed mode, 0 otherwise.
SeedingTime int64 `bencode:"seeding_time"` // integer. The number of seconds this torrent has been active and seeding.
SequentialDownload int64 `bencode:"sequential_download"` // integer. 1 if the torrent is in sequential download mode, 0 otherwise.
ShareMode int64 `bencode:"share_mode"` // integer. 1 if the torrent_flags::share_mode is set.
StopWhenReady int64 `bencode:"stop_when_ready"` // integer. 1 if the torrent_flags::stop_when_ready is set.
SuperSeeding int64 `bencode:"super_seeding"` // integer. 1 if the torrent_flags::super_seeding is set.
TotalDownloaded int64 `bencode:"total_downloaded"` // integer. The number of bytes that have been downloaded in total for this torrent.
TotalUploaded int64 `bencode:"total_uploaded"` // integer. The number of bytes that have been uploaded in total for this torrent.
Trackers [][]string `bencode:"trackers"` // list of lists of strings. The top level list lists all tracker tiers. Each second level list is one tier of trackers.
Unfinished *[]interface{} `bencode:"unfinished,omitempty"`
UploadMode int64 `bencode:"upload_mode"` // integer. 1 if the torrent_flags::upload_mode is set.
UploadRateLimit int64 `bencode:"upload_rate_limit"` // integer. In case this torrent has a per-torrent upload rate limit, this is that limit. In bytes per second.
UrlList []string `bencode:"url-list"` // list of strings. List of url-seed URLs used by this torrent. The URLs are expected to be properly encoded and not contain any illegal url characters.
}

View File

@ -0,0 +1,56 @@
package qBittorrentStructures
import (
"github.com/rumanzo/bt2qbt/pkg/helpers"
"testing"
)
func TestDecodeFastresumeFile(t *testing.T) {
type PathJoinCase struct {
name string
mustFail bool
path string
}
cases := []PathJoinCase{
{
name: "001 not existing file",
path: "notexists.fastresume",
mustFail: true,
},
{
name: "002 testdir hybryd",
path: "../../test/data/testdir_hybrid.fastresume",
},
{
name: "003 testdir v1",
path: "../../test/data/testdir_v1.fastresume",
},
{
name: "004 testdir v2",
path: "../../test/data/testdir_v2.fastresume",
},
{
name: "005 single hybryd",
path: "../../test/data/testfile1_single_hybrid.fastresume",
},
{
name: "006 single v1",
path: "../../test/data/testfile1_single_v1.fastresume",
},
{
name: "007 single v2",
path: "../../test/data/testfile1_single_v2.fastresume",
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
var decoded QBittorrentFastresume
err := helpers.DecodeTorrentFile(testCase.path, &decoded)
if err != nil && !testCase.mustFail {
t.Fatalf("Unexpected error: %v", err)
} else if err == nil && testCase.mustFail {
t.Fatalf("Test must fail, but it doesn't")
}
})
}
}

View File

@ -0,0 +1,85 @@
package torrentStructures
import (
"github.com/rumanzo/bt2qbt/pkg/fileHelpers"
"sort"
)
func (t *Torrent) IsV2OrHybryd() bool {
if t.Info.FileTree != nil {
return true
}
return false
}
// GetFileListWB function that return struct with filelists with bytes from torrent file
func (t *Torrent) GetFileListWB() []FilepathLength {
if t.FilePathLength == nil {
if t.IsV2OrHybryd() { // torrents with v2 or hybrid scheme
result := getFileListV2(t.Info.FileTree)
t.FilePathLength = &result
return *t.FilePathLength
} else { // torrent v1 with FileTree
result := getFileListV1(t)
t.FilePathLength = &result
return *t.FilePathLength
}
} else {
return *t.FilePathLength
}
}
func (t *Torrent) GetFileList() []string {
if t.FilePathLength == nil {
t.GetFileListWB()
}
if t.FilePaths == nil {
t.FilePaths = &[]string{}
for _, fb := range *t.FilePathLength {
*t.FilePaths = append(*t.FilePaths, fb.Path)
}
}
return *t.FilePaths
}
func getFileListV1(t *Torrent) []FilepathLength {
var files []FilepathLength
for _, file := range t.Info.Files {
if file.PathUTF8 != nil {
files = append(files, FilepathLength{
Path: fileHelpers.Join(file.PathUTF8, `/`),
Length: file.Length,
})
} else {
files = append(files, FilepathLength{
Path: fileHelpers.Join(file.Path, `/`),
Length: file.Length,
})
}
}
return files
}
func getFileListV2(f interface{}) []FilepathLength {
nfiles := []FilepathLength{}
// sort map previously
keys := make([]string, 0, len(f.(map[string]interface{})))
for k := range f.(map[string]interface{}) {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := f.(map[string]interface{})[k]
if len(k) == 0 { // it's means that next will be structure with length and piece root
nfiles = append(nfiles, FilepathLength{Path: "", Length: v.(map[string]interface{})["length"].(int64)})
return nfiles
}
s := getFileListV2(v)
for _, fpl := range s {
nfiles = append(nfiles, FilepathLength{Path: fileHelpers.Join(append([]string{k}, fpl.Path), `/`), Length: fpl.Length})
}
}
return nfiles
}

View File

@ -0,0 +1,227 @@
package torrentStructures
import (
"github.com/davecgh/go-spew/spew"
"github.com/r3labs/diff/v2"
"github.com/rumanzo/bt2qbt/pkg/helpers"
"reflect"
"testing"
)
func TestDecodeRealTorrents(t *testing.T) {
type PathJoinCase struct {
name string
mustFail bool
path string
}
cases := []PathJoinCase{
{
name: "001 not existing file",
path: "notexists.torrent",
mustFail: true,
},
{
name: "002 existing file",
path: "../../test/data/testfileset.torrent",
},
{
name: "003 testdir hybryd",
path: "../../test/data/testdir_hybrid.torrent",
},
{
name: "004 testdir v1",
path: "../../test/data/testdir_v1.torrent",
},
{
name: "005 testdir v2",
path: "../../test/data/testdir_v2.torrent",
},
{
name: "006 single hybryd",
path: "../../test/data/testfile1_single_hybrid.torrent",
},
{
name: "007 single v1",
path: "../../test/data/testfile1_single_v1.torrent",
},
{
name: "008 single v2",
path: "../../test/data/testfile1_single_v2.torrent",
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
var torrent Torrent
err := helpers.DecodeTorrentFile(testCase.path, &torrent)
if err != nil && !testCase.mustFail {
t.Fatalf("Unexpected error: %v", err)
} else if err == nil && testCase.mustFail {
t.Fatalf("Test must fail, but it doesn't")
}
})
}
}
func TestTorrent_GetFileList(t *testing.T) {
type PathJoinCase struct {
name string
path string
expected []string
mustFail bool
}
cases := []PathJoinCase{
{
name: "001 testdir v2 mustfail",
path: "../../test/data/testdir_v2.torrent",
mustFail: true,
expected: []string{},
},
{
name: "002 testdir v2",
path: "../../test/data/testdir_v2.torrent",
expected: []string{
"dir1/testfile1.txt",
"dir2/testfile1.txt",
"dir2/testfile2.txt",
"dir3/testfile1.txt",
"dir3/testfile2.txt",
"dir3/testfile3.txt",
"testfile1.txt",
"testfile2.txt",
"testfile3.txt",
},
},
{
name: "003 testdir v1",
path: "../../test/data/testdir_v1.torrent",
expected: []string{
"testfile1.txt",
"testfile2.txt",
"testfile3.txt",
"dir1/testfile1.txt",
"dir2/testfile1.txt",
"dir2/testfile2.txt",
"dir3/testfile1.txt",
"dir3/testfile2.txt",
"dir3/testfile3.txt",
},
},
{
name: "004 testdir hybrid",
path: "../../test/data/testdir_hybrid.torrent",
expected: []string{
"dir1/testfile1.txt",
"dir2/testfile1.txt",
"dir2/testfile2.txt",
"dir3/testfile1.txt",
"dir3/testfile2.txt",
"dir3/testfile3.txt",
"testfile1.txt",
"testfile2.txt",
"testfile3.txt",
},
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
var torrent Torrent
err := helpers.DecodeTorrentFile(testCase.path, &torrent)
if err != nil {
t.Fatalf("Unexpected error with decoding torrent file: %v", err)
}
list := torrent.GetFileList()
equal := reflect.DeepEqual(list, testCase.expected)
if !equal && !testCase.mustFail {
changes, err := diff.Diff(list, testCase.expected, diff.DiscardComplexOrigin())
if err != nil {
t.Error(err.Error())
}
t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n Diff: %v\n", list, testCase.expected, spew.Sdump(changes))
} else if equal && testCase.mustFail {
t.Fatalf("Unexpected error: structures are equal, but they shouldn't\n Got: %v\n", spew.Sdump(list))
}
})
}
}
func TestTorrent_GetFileListWB(t *testing.T) {
type PathJoinCase struct {
name string
path string
expected []FilepathLength
mustFail bool
}
cases := []PathJoinCase{
{
name: "001 testdir v2 mustfail",
path: "../../test/data/testdir_v2.torrent",
mustFail: true,
expected: []FilepathLength{},
},
{
name: "001 testdir v2",
path: "../../test/data/testdir_v2.torrent",
expected: []FilepathLength{
FilepathLength{Path: "dir1/testfile1.txt", Length: 33},
FilepathLength{Path: "dir2/testfile1.txt", Length: 33},
FilepathLength{Path: "dir2/testfile2.txt", Length: 33},
FilepathLength{Path: "dir3/testfile1.txt", Length: 33},
FilepathLength{Path: "dir3/testfile2.txt", Length: 33},
FilepathLength{Path: "dir3/testfile3.txt", Length: 33},
FilepathLength{Path: "testfile1.txt", Length: 33},
FilepathLength{Path: "testfile2.txt", Length: 33},
FilepathLength{Path: "testfile3.txt", Length: 33},
},
},
{
name: "003 testdir v1",
path: "../../test/data/testdir_v1.torrent",
expected: []FilepathLength{
FilepathLength{Path: "testfile1.txt", Length: 33},
FilepathLength{Path: "testfile2.txt", Length: 33},
FilepathLength{Path: "testfile3.txt", Length: 33},
FilepathLength{Path: "dir1/testfile1.txt", Length: 33},
FilepathLength{Path: "dir2/testfile1.txt", Length: 33},
FilepathLength{Path: "dir2/testfile2.txt", Length: 33},
FilepathLength{Path: "dir3/testfile1.txt", Length: 33},
FilepathLength{Path: "dir3/testfile2.txt", Length: 33},
FilepathLength{Path: "dir3/testfile3.txt", Length: 33},
},
},
{
name: "004 testdir hybrid",
path: "../../test/data/testdir_hybrid.torrent",
expected: []FilepathLength{
FilepathLength{Path: "dir1/testfile1.txt", Length: 33},
FilepathLength{Path: "dir2/testfile1.txt", Length: 33},
FilepathLength{Path: "dir2/testfile2.txt", Length: 33},
FilepathLength{Path: "dir3/testfile1.txt", Length: 33},
FilepathLength{Path: "dir3/testfile2.txt", Length: 33},
FilepathLength{Path: "dir3/testfile3.txt", Length: 33},
FilepathLength{Path: "testfile1.txt", Length: 33},
FilepathLength{Path: "testfile2.txt", Length: 33},
FilepathLength{Path: "testfile3.txt", Length: 33},
},
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
var torrent Torrent
err := helpers.DecodeTorrentFile(testCase.path, &torrent)
if err != nil {
t.Fatalf("Unexpected error with decoding torrent file: %v", err)
}
list := torrent.GetFileListWB()
equal := reflect.DeepEqual(list, testCase.expected)
if !equal && !testCase.mustFail {
changes, err := diff.Diff(list, testCase.expected, diff.DiscardComplexOrigin())
if err != nil {
t.Error(err.Error())
}
t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n Diff: %v\n", list, testCase.expected, spew.Sdump(changes))
} else if equal && testCase.mustFail {
t.Fatalf("Unexpected error: structures are equal, but they shouldn't\n Got: %v\n", spew.Sdump(list))
}
})
}
}

View File

@ -1,35 +1,32 @@
package torrentStructures
type Torrent struct {
Announce string `bencode:"announce"`
Comment string `bencode:"comment"`
CreatedBy string `bencode:"created by"`
CreationDate int64 `bencode:"creation date"`
Info *TorrentInfo `bencode:"info"`
Publisher string `bencode:"publisher,omitempty"`
PublisherUrl string `bencode:"publisher-url,omitempty"`
PieceLayers *map[byte][]byte `bencode:"piece layers"`
Announce string `bencode:"announce"`
Comment string `bencode:"comment"`
CreatedBy string `bencode:"created by"`
CreationDate int64 `bencode:"creation date"`
Info *TorrentInfo `bencode:"info"`
Publisher string `bencode:"publisher,omitempty"`
PublisherUrl string `bencode:"publisher-url,omitempty"`
PieceLayers *map[string]interface{} `bencode:"piece layers"`
FilePathLength *[]FilepathLength `bencode:"-"` // service field
FilePaths *[]string `bencode:"-"` // service field
}
type TorrentInfo struct {
FileDuration []int64 `bencode:"file-duration,omitempty"`
FileMedia []int64 `bencode:"file-media,omitempty"`
Files []*TorrentFile `bencode:"files,omitempty"`
FileTree map[string]*[]TorrentFileTree `bencode:"file tree,omitempty"`
Length int64 `bencode:"length,omitempty"`
MetaVersion int64 `bencode:"meta version,omitempty"`
Md5sum string `bencode:"md5sum,omitempty"`
Name string `bencode:"name,omitempty"`
NameUTF8 string `bencode:"name.utf-8,omitempty"`
PieceLength int64 `bencode:"piece length,omitempty"`
Pieces []byte `bencode:"pieces,omitempty"`
Private uint8 `bencode:"private,omitempty"`
Profiles []*TorrentProfile `bencode:"profiles,omitempty"`
}
type TorrentFileTree struct {
Length int64 `bencode:"length,omitempty"`
PiecesRoot []byte `bencode:"pieces root"`
FileDuration []int64 `bencode:"file-duration,omitempty"`
FileMedia []int64 `bencode:"file-media,omitempty"`
Files []*TorrentFile `bencode:"files,omitempty"`
FileTree map[string]interface{} `bencode:"file tree,omitempty"`
Length int64 `bencode:"length,omitempty"`
MetaVersion int64 `bencode:"meta version,omitempty"`
Md5sum string `bencode:"md5sum,omitempty"`
Name string `bencode:"name,omitempty"`
NameUTF8 string `bencode:"name.utf-8,omitempty"`
PieceLength int64 `bencode:"piece length,omitempty"`
Pieces []byte `bencode:"pieces,omitempty"`
Private uint8 `bencode:"private,omitempty"`
Profiles []*TorrentProfile `bencode:"profiles,omitempty"`
}
type TorrentFile struct {
@ -45,3 +42,8 @@ type TorrentProfile struct {
Vcodec []byte `bencode:"vcodec,omitempty"`
Width int64 `bencode:"width,omitempty"`
}
type FilepathLength struct {
Path string
Length int64
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
d10:created by18:qBittorrent v4.4.213:creation datei1650146608e4:infod5:filesld6:lengthi33e4:pathl13:testfile1.txteed6:lengthi33e4:pathl13:testfile2.txteed6:lengthi33e4:pathl13:testfile3.txteed6:lengthi33e4:pathl4:dir113:testfile1.txteed6:lengthi33e4:pathl4:dir213:testfile1.txteed6:lengthi33e4:pathl4:dir213:testfile2.txteed6:lengthi33e4:pathl4:dir313:testfile1.txteed6:lengthi33e4:pathl4:dir313:testfile2.txteed6:lengthi33e4:pathl4:dir313:testfile3.txteee4:name7:testdir12:piece lengthi16384e6:pieces20:ã9Ép¶ç õAOͯw»såÕ64ee

Binary file not shown.

View File

@ -0,0 +1,10 @@
d10:created by18:qBittorrent v4.4.213:creation datei1650146556e4:infod9:file treed4:dir1d13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee4:dir2d13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile2.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee4:dir3d13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile2.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile3.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile2.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile3.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee12:meta versioni2e4:name7:testdir12:piece lengthi16384ee12:piece layersdee

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
d10:created by18:qBittorrent v4.4.213:creation datei1650146462e4:infod9:file treed13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1
x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee12:meta versioni2e4:name13:testfile1.txt12:piece lengthi16384ee12:piece layersdee