1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Blacklight; |
4
|
|
|
|
5
|
|
|
use App\Models\Part; |
6
|
|
|
use App\Models\Binary; |
7
|
|
|
use App\Models\Release; |
8
|
|
|
use App\Models\Settings; |
9
|
|
|
use App\Models\Collection; |
10
|
|
|
use Illuminate\Support\Facades\DB; |
11
|
|
|
use Illuminate\Support\Facades\File; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Class for reading and writing NZB files on the hard disk, |
15
|
|
|
* building folder paths to store the NZB files. |
16
|
|
|
* |
17
|
|
|
* |
18
|
|
|
* Class NZB |
19
|
|
|
*/ |
20
|
|
|
class NZB |
21
|
|
|
{ |
22
|
|
|
public const NZB_NONE = 0; // Release has no NZB file yet. |
23
|
|
|
public const NZB_ADDED = 1; // Release had an NZB file created. |
24
|
|
|
|
25
|
|
|
protected const NZB_DTD_NAME = 'nzb'; |
26
|
|
|
protected const NZB_DTD_PUBLIC = '-//newzBin//DTD NZB 1.1//EN'; |
27
|
|
|
protected const NZB_DTD_EXTERNAL = 'http://www.newzbin.com/DTD/nzb/nzb-1.1.dtd'; |
28
|
|
|
protected const NZB_XML_NS = 'http://www.newzbin.com/DTD/2003/nzb'; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Levels deep to store NZB files. |
32
|
|
|
* |
33
|
|
|
* @var int |
34
|
|
|
*/ |
35
|
|
|
protected $nzbSplitLevel; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Path to store NZB files. |
39
|
|
|
* |
40
|
|
|
* @var string |
41
|
|
|
*/ |
42
|
|
|
protected $siteNzbPath; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Group id when writing NZBs. |
46
|
|
|
* |
47
|
|
|
* @var int |
48
|
|
|
*/ |
49
|
|
|
protected $groupID; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var \PDO |
53
|
|
|
*/ |
54
|
|
|
public $pdo; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var bool |
58
|
|
|
*/ |
59
|
|
|
protected $_debug = false; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Base query for selecting collection data for writing NZB files. |
63
|
|
|
* |
64
|
|
|
* @var string |
65
|
|
|
*/ |
66
|
|
|
protected $_collectionsQuery; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Base query for selecting binary data for writing NZB files. |
70
|
|
|
* |
71
|
|
|
* @var string |
72
|
|
|
*/ |
73
|
|
|
protected $_binariesQuery; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Base query for selecting parts data for writing NZB files. |
77
|
|
|
* |
78
|
|
|
* @var string |
79
|
|
|
*/ |
80
|
|
|
protected $_partsQuery; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* String used for head in NZB XML file. |
84
|
|
|
* |
85
|
|
|
* @var string |
86
|
|
|
*/ |
87
|
|
|
protected $_nzbCommentString; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Names of CBP tables. |
91
|
|
|
* |
92
|
|
|
* @var array [string => string] |
93
|
|
|
*/ |
94
|
|
|
protected $_tableNames; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @var string |
98
|
|
|
*/ |
99
|
|
|
protected $_siteCommentString; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* NZB constructor. |
103
|
|
|
*/ |
104
|
|
|
public function __construct() |
105
|
|
|
{ |
106
|
|
|
$nzbSplitLevel = (int) Settings::settingValue('..nzbsplitlevel'); |
107
|
|
|
$this->nzbSplitLevel = $nzbSplitLevel ?? 1; |
108
|
|
|
$this->siteNzbPath = (string) Settings::settingValue('..nzbpath'); |
109
|
|
|
if (! ends_with($this->siteNzbPath, '/')) { |
|
|
|
|
110
|
|
|
$this->siteNzbPath .= '/'; |
111
|
|
|
} |
112
|
|
|
$this->_nzbCommentString = sprintf( |
113
|
|
|
'NZB Generated by: NNTmux %s', |
114
|
|
|
now()->format('F j, Y, g:i a O') |
115
|
|
|
); |
116
|
|
|
$this->_siteCommentString = sprintf( |
117
|
|
|
'NZB downloaded from %s', |
118
|
|
|
Settings::settingValue('site.main.title') |
119
|
|
|
); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Write an NZB to the hard drive for a single release. |
124
|
|
|
* |
125
|
|
|
* |
126
|
|
|
* |
127
|
|
|
* @param \App\Models\Release $release |
128
|
|
|
* |
129
|
|
|
* @return bool |
130
|
|
|
* @throws \Throwable |
131
|
|
|
*/ |
132
|
|
|
public function writeNzbForReleaseId(Release $release): bool |
133
|
|
|
{ |
134
|
|
|
$collections = Collection::whereReleasesId($release->id) |
135
|
|
|
->join('usenet_groups', 'collections.groups_id', '=', 'usenet_groups.id') |
136
|
|
|
->select(['collections.*', DB::raw('UNIX_TIMESTAMP(collections.date) AS udate'), 'usenet_groups.name as groupname']) |
137
|
|
|
->get(); |
138
|
|
|
|
139
|
|
|
if (empty($collections)) { |
140
|
|
|
return false; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
$XMLWriter = new \XMLWriter(); |
144
|
|
|
$XMLWriter->openMemory(); |
145
|
|
|
$XMLWriter->setIndent(true); |
146
|
|
|
$XMLWriter->setIndentString(' '); |
147
|
|
|
|
148
|
|
|
$nzb_guid = ''; |
149
|
|
|
|
150
|
|
|
$XMLWriter->startDocument('1.0', 'UTF-8'); |
151
|
|
|
$XMLWriter->startDtd(self::NZB_DTD_NAME, self::NZB_DTD_PUBLIC, self::NZB_DTD_EXTERNAL); |
152
|
|
|
$XMLWriter->endDtd(); |
153
|
|
|
$XMLWriter->writeComment($this->_nzbCommentString); |
154
|
|
|
|
155
|
|
|
$XMLWriter->startElement('nzb'); |
156
|
|
|
$XMLWriter->writeAttribute('xmlns', self::NZB_XML_NS); |
157
|
|
|
$XMLWriter->startElement('head'); |
158
|
|
|
$XMLWriter->startElement('meta'); |
159
|
|
|
$XMLWriter->writeAttribute('type', 'category'); |
160
|
|
|
$XMLWriter->text($release->category->parent->title.' >'.$release->category->title); |
161
|
|
|
$XMLWriter->endElement(); |
162
|
|
|
$XMLWriter->startElement('meta'); |
163
|
|
|
$XMLWriter->writeAttribute('type', 'name'); |
164
|
|
|
$XMLWriter->text($release->name); |
165
|
|
|
$XMLWriter->endElement(); |
166
|
|
|
$XMLWriter->endElement(); //head |
167
|
|
|
|
168
|
|
|
foreach ($collections as $collection) { |
169
|
|
|
$binaries = Binary::whereCollectionsId($collection->id)->select(['id', 'name', 'totalparts'])->orderBy('name')->get(); |
170
|
|
|
if (empty($binaries)) { |
171
|
|
|
return false; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
$poster = $collection->fromname; |
175
|
|
|
|
176
|
|
|
foreach ($binaries as $binary) { |
177
|
|
|
$parts = Part::whereBinariesId($binary->id)->distinct()->select(['messageid', 'size', 'partnumber'])->orderBy('partnumber')->get(); |
178
|
|
|
if (empty($parts)) { |
179
|
|
|
return false; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
$subject = $binary->name.'(1/'.$binary->totalparts.')'; |
183
|
|
|
$XMLWriter->startElement('file'); |
184
|
|
|
$XMLWriter->writeAttribute('poster', $poster); |
185
|
|
|
$XMLWriter->writeAttribute('date', $collection->udate); |
186
|
|
|
$XMLWriter->writeAttribute('subject', $subject); |
187
|
|
|
$XMLWriter->startElement('groups'); |
188
|
|
|
if (preg_match_all('#(\S+):\S+#', $collection->xref, $matches)) { |
189
|
|
|
$matches = array_values(array_unique($matches[1])); |
190
|
|
|
foreach ($matches as $group) { |
191
|
|
|
$XMLWriter->writeElement('group', $group); |
192
|
|
|
} |
193
|
|
|
} elseif (preg_match_all('#(\S+)#', $collection->xref, $matches)) { |
194
|
|
|
$matches = array_values(array_unique($matches[1])); |
195
|
|
|
foreach ($matches as $group) { |
196
|
|
|
$XMLWriter->writeElement('group', $group); |
197
|
|
|
} |
198
|
|
|
} else { |
199
|
|
|
return false; |
200
|
|
|
} |
201
|
|
|
$XMLWriter->endElement(); //groups |
202
|
|
|
$XMLWriter->startElement('segments'); |
203
|
|
|
foreach ($parts as $part) { |
204
|
|
|
if ($nzb_guid === '') { |
205
|
|
|
$nzb_guid = $part->messageid; |
206
|
|
|
} |
207
|
|
|
$XMLWriter->startElement('segment'); |
208
|
|
|
$XMLWriter->writeAttribute('bytes', $part->size); |
209
|
|
|
$XMLWriter->writeAttribute('number', $part->partnumber); |
210
|
|
|
$XMLWriter->text($part->messageid); |
211
|
|
|
$XMLWriter->endElement(); |
212
|
|
|
} |
213
|
|
|
$XMLWriter->endElement(); //segments |
214
|
|
|
$XMLWriter->endElement(); //file |
215
|
|
|
} |
216
|
|
|
} |
217
|
|
|
$XMLWriter->writeComment($this->_siteCommentString); |
218
|
|
|
$XMLWriter->endElement(); //nzb |
219
|
|
|
$XMLWriter->endDocument(); |
220
|
|
|
$path = ($this->buildNZBPath($release->guid, $this->nzbSplitLevel, true).$release->guid.'.nzb.gz'); |
221
|
|
|
$fp = gzopen($path, 'wb7'); |
222
|
|
|
if (! $fp) { |
|
|
|
|
223
|
|
|
return false; |
224
|
|
|
} |
225
|
|
|
gzwrite($fp, $XMLWriter->outputMemory()); |
226
|
|
|
gzclose($fp); |
227
|
|
|
unset($XMLWriter); |
228
|
|
|
if (! File::isFile($path)) { |
229
|
|
|
echo "ERROR: $path does not exist.\n"; |
230
|
|
|
|
231
|
|
|
return false; |
232
|
|
|
} |
233
|
|
|
// Mark release as having NZB. |
234
|
|
|
DB::transaction(function () use ($release, $nzb_guid) { |
235
|
|
|
$release->update(['nzbstatus' => self::NZB_ADDED]); |
236
|
|
|
if (! empty($nzb_guid)) { |
237
|
|
|
$release->update(['nzb_guid' => DB::raw('UNHEX( '.escapeString(md5($nzb_guid)).' )')]); |
238
|
|
|
} |
239
|
|
|
}, 3); |
240
|
|
|
|
241
|
|
|
// Delete CBP for release that has its NZB created. |
242
|
|
|
DB::transaction(function () use ($release) { |
243
|
|
|
Collection::query()->where('collections.releases_id', $release->id)->delete(); |
244
|
|
|
}, 3); |
245
|
|
|
// Chmod to fix issues some users have with file permissions. |
246
|
|
|
chmod($path, 0777); |
247
|
|
|
|
248
|
|
|
return true; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Build a folder path on the hard drive where the NZB file will be stored. |
253
|
|
|
* |
254
|
|
|
* @param string $releaseGuid The guid of the release. |
255
|
|
|
* @param int $levelsToSplit How many sub-paths the folder will be in. |
256
|
|
|
* @param bool $createIfNotExist Create the folder if it doesn't exist. |
257
|
|
|
* |
258
|
|
|
* @return string $nzbpath The path to store the NZB file. |
259
|
|
|
*/ |
260
|
|
|
public function buildNZBPath($releaseGuid, $levelsToSplit, $createIfNotExist): string |
261
|
|
|
{ |
262
|
|
|
$nzbPath = ''; |
263
|
|
|
|
264
|
|
|
for ($i = 0; $i < $levelsToSplit && $i < 32; $i++) { |
265
|
|
|
$nzbPath .= $releaseGuid[$i].DS; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
$nzbPath = $this->siteNzbPath.$nzbPath; |
269
|
|
|
|
270
|
|
|
if ($createIfNotExist && ! File::isDirectory($nzbPath) && ! File::makeDirectory($nzbPath, 0777, true) && ! File::isDirectory($nzbPath)) { |
271
|
|
|
throw new \RuntimeException(sprintf('Directory "%s" was not created', $nzbPath)); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
return $nzbPath; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Retrieve path + filename of the NZB to be stored. |
279
|
|
|
* |
280
|
|
|
* @param string $releaseGuid The guid of the release. |
281
|
|
|
* @param int $levelsToSplit How many sub-paths the folder will be in. (optional) |
282
|
|
|
* @param bool $createIfNotExist Create the folder if it doesn't exist. (optional) |
283
|
|
|
* |
284
|
|
|
* @return string Path+filename. |
285
|
|
|
*/ |
286
|
|
|
public function getNZBPath($releaseGuid, $levelsToSplit = 0, $createIfNotExist = false): string |
287
|
|
|
{ |
288
|
|
|
if ($levelsToSplit === 0) { |
289
|
|
|
$levelsToSplit = $this->nzbSplitLevel; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
return $this->buildNZBPath($releaseGuid, $levelsToSplit, $createIfNotExist).$releaseGuid.'.nzb.gz'; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Determine is an NZB exists, returning the path+filename, if not return false. |
297
|
|
|
* |
298
|
|
|
* @param string $releaseGuid The guid of the release. |
299
|
|
|
* |
300
|
|
|
* @return false|string On success: (string) Path+file name of the nzb. |
301
|
|
|
* On failure: false . |
302
|
|
|
*/ |
303
|
|
|
public function NZBPath($releaseGuid) |
304
|
|
|
{ |
305
|
|
|
$nzbFile = $this->getNZBPath($releaseGuid); |
306
|
|
|
|
307
|
|
|
return File::isFile($nzbFile) ? $nzbFile : false; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Retrieve various information on a NZB file (the subject, # of pars, |
312
|
|
|
* file extensions, file sizes, file completion, group names, # of parts). |
313
|
|
|
* |
314
|
|
|
* @param string $nzb The NZB contents in a string. |
315
|
|
|
* @param array $options |
316
|
|
|
* 'no-file-key' => True - use numeric array key; False - Use filename as array key. |
317
|
|
|
* 'strip-count' => True - Strip file/part count from file name to make the array key; False - Leave file name as is. |
318
|
|
|
* |
319
|
|
|
* @return array $result Empty if not an NZB or the contents of the NZB. |
320
|
|
|
*/ |
321
|
|
|
public function nzbFileList($nzb, array $options = []): array |
322
|
|
|
{ |
323
|
|
|
$defaults = [ |
324
|
|
|
'no-file-key' => true, |
325
|
|
|
'strip-count' => false, |
326
|
|
|
]; |
327
|
|
|
$options += $defaults; |
328
|
|
|
|
329
|
|
|
$i = 0; |
330
|
|
|
$result = []; |
331
|
|
|
|
332
|
|
|
if (! $nzb) { |
333
|
|
|
return $result; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
$xml = @simplexml_load_string(str_replace("\x0F", '', $nzb)); |
337
|
|
|
if (! $xml || strtolower($xml->getName()) !== 'nzb') { |
338
|
|
|
return $result; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
foreach ($xml->file as $file) { |
342
|
|
|
// Subject. |
343
|
|
|
$title = (string) $file->attributes()->subject; |
344
|
|
|
|
345
|
|
|
if ($options['no-file-key'] === false) { |
346
|
|
|
$i = $title; |
347
|
|
|
if ($options['strip-count']) { |
348
|
|
|
// Strip file / part count to get proper sorting. |
349
|
|
|
$i = preg_replace('#\d+[- ._]?(/|\||[o0]f)[- ._]?\d+?(?![- ._]\d)#i', '', $i); |
350
|
|
|
// Change .rar and .par2 to be sorted before .part0x.rar and .volxxx+xxx.par2 |
351
|
|
|
if (strpos($i, '.par2') !== false && ! preg_match('#\.vol\d+\+\d+\.par2#i', $i)) { |
352
|
|
|
$i = str_replace('.par2', '.vol0.par2', $i); |
353
|
|
|
} elseif (preg_match('#\.rar[^a-z0-9]#i', $i) && ! preg_match('#\.part\d+\.rar$#i', $i)) { |
354
|
|
|
$i = preg_replace('#\.rar(?:[^a-z0-9])#i', '.part0.rar', $i); |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
$result[$i]['title'] = $title; |
360
|
|
|
|
361
|
|
|
// Extensions. |
362
|
|
|
if (preg_match( |
363
|
|
|
'/\.(\d{2,3}|7z|ace|ai7|srr|srt|sub|aiff|asc|avi|audio|bin|bz2|' |
364
|
|
|
.'c|cfc|cfm|chm|class|conf|cpp|cs|css|csv|cue|deb|divx|doc|dot|' |
365
|
|
|
.'eml|enc|exe|file|gif|gz|hlp|htm|html|image|iso|jar|java|jpeg|' |
366
|
|
|
.'jpg|js|lua|m|m3u|mkv|mm|mov|mp3|mp4|mpg|nfo|nzb|odc|odf|odg|odi|odp|' |
367
|
|
|
.'ods|odt|ogg|par2|parity|pdf|pgp|php|pl|png|ppt|ps|py|r\d{2,3}|' |
368
|
|
|
.'ram|rar|rb|rm|rpm|rtf|sfv|sig|sql|srs|swf|sxc|sxd|sxi|sxw|tar|' |
369
|
|
|
.'tex|tgz|txt|vcf|video|vsd|wav|wma|wmv|xls|xml|xpi|xvid|zip7|zip)' |
370
|
|
|
.'[" ](?!([\)|\-]))/i', |
371
|
|
|
$title, |
372
|
|
|
$ext |
373
|
|
|
) |
374
|
|
|
) { |
375
|
|
|
if (preg_match('/\.r\d{2,3}/i', $ext[0])) { |
376
|
|
|
$ext[1] = 'rar'; |
377
|
|
|
} |
378
|
|
|
$result[$i]['ext'] = strtolower($ext[1]); |
379
|
|
|
} else { |
380
|
|
|
$result[$i]['ext'] = ''; |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
$fileSize = $numSegments = 0; |
384
|
|
|
|
385
|
|
|
// Parts. |
386
|
|
|
if (! isset($result[$i]['segments'])) { |
387
|
|
|
$result[$i]['segments'] = []; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
// File size. |
391
|
|
|
foreach ($file->segments->segment as $segment) { |
392
|
|
|
$result[$i]['segments'][] = (string) $segment; |
393
|
|
|
$fileSize += $segment->attributes()->bytes; |
394
|
|
|
$numSegments++; |
395
|
|
|
} |
396
|
|
|
$result[$i]['size'] = $fileSize; |
397
|
|
|
|
398
|
|
|
// File completion. |
399
|
|
|
if (preg_match('/(\d+)\)$/', $title, $parts)) { |
400
|
|
|
$result[$i]['partstotal'] = $parts[1]; |
401
|
|
|
} |
402
|
|
|
$result[$i]['partsactual'] = $numSegments; |
403
|
|
|
|
404
|
|
|
// Groups. |
405
|
|
|
if (! isset($result[$i]['groups'])) { |
406
|
|
|
$result[$i]['groups'] = []; |
407
|
|
|
} |
408
|
|
|
foreach ($file->groups->group as $g) { |
409
|
|
|
$result[$i]['groups'][] = (string) $g; |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
unset($result[$i]['segments']['@attributes']); |
413
|
|
|
if ($options['no-file-key']) { |
414
|
|
|
$i++; |
415
|
|
|
} |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
return $result; |
419
|
|
|
} |
420
|
|
|
} |
421
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.