/
nzbparser.go
237 lines (197 loc) · 6.78 KB
/
nzbparser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
package nzbparser
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"sort"
"github.com/Tensai75/nzb-monkey-go/subjectparser"
"golang.org/x/net/html/charset"
)
const (
// xml header for nzb files
Header = `<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE nzb PUBLIC "-//newzBin//DTD NZB 1.1//EN" "http://www.newzbin.com/DTD/nzb/nzb-1.1.dtd">` + "\n"
)
// nzb file structure with additional information
type Nzb struct {
Comment string
Meta map[string]string // meta data as map
Files NzbFiles
TotalFiles int // number of total files
Segments int // number of available segments
TotalSegments int // number of total segments
Bytes int64 // total size of all files
}
// a slice of NzbFiles extended to allow sorting
type NzbFiles []NzbFile
func (s NzbFiles) Len() int { return len(s) }
func (s NzbFiles) Less(i, j int) bool { return s[i].Number < s[j].Number }
func (s NzbFiles) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// individual file structure with additional information
type NzbFile struct {
Groups []string `xml:"groups>group"`
Segments NzbSegments `xml:"segments>segment"`
Poster string `xml:"poster,attr"`
Date int `xml:"date,attr"`
Subject string `xml:"subject,attr"`
Number int `xml:"-"` // number of the file (if indicated in the subject)
Filename string `xml:"-"` // filename of the file (if indicated in the subject)
Basefilename string `xml:"-"` // basefilename of the file (if indicated in the subject)
TotalSegments int `xml:"-"` // number of total segments
Bytes int64 `xml:"-"` // total size of the file
}
// a slice of NzbSegments extended to allow sorting
type NzbSegments []NzbSegment
func (s NzbSegments) Len() int { return len(s) }
func (s NzbSegments) Less(i, j int) bool { return s[i].Number < s[j].Number }
func (s NzbSegments) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// individual segment structure
type NzbSegment struct {
Bytes int `xml:"bytes,attr"`
Number int `xml:"number,attr"`
Id string `xml:",innerxml"`
}
// parse nzb file provided as string
func ParseString(data string) (*Nzb, error) {
return Parse(bytes.NewBufferString(data))
}
// parese nzb file provided as io.Reader buffer
func Parse(buf io.Reader) (*Nzb, error) {
// parse nzb file into temp structure
xnzb := new(xNzb)
decoder := xml.NewDecoder(buf)
decoder.CharsetReader = charset.NewReaderLabel
if err := decoder.Decode(xnzb); err != nil {
return nil, fmt.Errorf("Unable to parse NZB file: %s", err.Error())
}
// convert to nicer format
nzb := new(Nzb)
// copy elements
nzb.Comment = xnzb.Comment
nzb.Files = xnzb.Files
// convert metadata
nzb.Meta = make(map[string]string)
for _, md := range xnzb.Metadata {
nzb.Meta[md.Type] = md.Value
}
// remove duplicate entries
MakeUnique(nzb)
// scan the nzb for the additional information
ScanNzbFile(nzb)
// sort the files and segments
sort.Sort(nzb.Files)
for id := range nzb.Files {
sort.Sort(nzb.Files[id].Segments)
}
return nzb, nil
}
// write nzb struct to nzb xml as string
func WriteString(nzb *Nzb) (string, error) {
file, err := Write(nzb)
return string(file), err
}
// write nzb struct to nzb xml as byte slice
func Write(nzb *Nzb) ([]byte, error) {
// create temp structure
xnzb := new(xNzb)
// copy elements
if nzb.Comment != "" {
xnzb.Comment = " " + nzb.Comment + " "
}
xnzb.Files = nzb.Files
// add namespace
xnzb.Xmlns = "http://www.newzbin.com/DTD/2003/nzb"
// add metadata
for t, v := range nzb.Meta {
xnzb.Metadata = append(xnzb.Metadata, xNzbMeta{Type: t, Value: v})
}
// Marshal and prepend header
if file, err := xml.MarshalIndent(xnzb, "", " "); err != nil {
return []byte(""), err
} else {
return append([]byte(Header), file...), nil
}
}
// scan the nzb struct for additional information
func ScanNzbFile(nzb *Nzb) {
var segments int // total amount of available segments
var totalSegments int // theoretical total amount of segments based on the subject count
var totalBytes int64 // total size of all available segments
var totalFiles int // theoretical total amount of files based on the subject count
for id, file := range nzb.Files {
var totalFileSegments int // theoretical total amount of segments of this file based on the subject count
var totalFileBytes int64 // total size of all available segments of this file
if subject, err := subjectparser.Parse(file.Subject); err == nil {
nzb.Files[id].Number = subject.File
nzb.Files[id].Filename = subject.Filename
nzb.Files[id].Basefilename = subject.Basefilename
totalFileSegments = subject.TotalSegments
if subject.TotalFiles > totalFiles {
totalFiles = subject.TotalFiles
}
}
for _, segment := range file.Segments {
if segment.Number > totalFileSegments {
totalFileSegments = segment.Number
}
totalBytes = totalBytes + int64(segment.Bytes)
totalFileBytes = totalFileBytes + int64(segment.Bytes)
}
segments = segments + file.Segments.Len()
totalSegments = totalSegments + totalFileSegments
nzb.Files[id].TotalSegments = totalFileSegments
nzb.Files[id].Bytes = totalFileBytes
}
if totalFiles < nzb.Files.Len() {
nzb.TotalFiles = nzb.Files.Len()
} else {
nzb.TotalFiles = totalFiles
}
nzb.Segments = segments
nzb.TotalSegments = totalSegments
nzb.Bytes = totalBytes
}
func MakeUnique(nzb *Nzb) {
// check for duplicate file entries and combine segments
var uniqueFiles []NzbFile
fileKeys := make(map[string]int) // helper map for unique keys
for _, file := range nzb.Files {
if i, ok := fileKeys[file.Subject]; ok {
// file already found
// append segments to previous match
uniqueFiles[i].Segments = append(uniqueFiles[i].Segments, file.Segments...)
} else {
// Unique file found. Record position and collect in result.
fileKeys[file.Subject] = len(uniqueFiles)
uniqueFiles = append(uniqueFiles, file)
}
}
nzb.Files = uniqueFiles
// remove duplicate segments
for i, file := range nzb.Files {
var uniqueSegments []NzbSegment
segmentKeys := make(map[string]int) // helper map for unique keys
for _, segment := range file.Segments {
if _, ok := segmentKeys[segment.Id]; !ok {
// Unique key found. Record position and collect in result.
segmentKeys[segment.Id] = len(uniqueSegments)
uniqueSegments = append(uniqueSegments, segment)
}
}
nzb.Files[i].Segments = uniqueSegments
}
}
// temp nzb file struct for (un)marshalling
type xNzb struct {
Comment string `xml:",comment"`
XMLName xml.Name `xml:"nzb"`
Xmlns string `xml:"xmlns,attr"`
Metadata []xNzbMeta `xml:"head>meta"`
Files NzbFiles `xml:"file"`
}
// temp raw meta data for (un)marshalling
type xNzbMeta struct {
Type string `xml:"type,attr"`
Value string `xml:",innerxml"`
}