Statistics
| Revision:

root / trunk / plugins / par2 / plugin_par2.c @ 1857

History | View | Annotate | Download (44.1 KB)

1
/* 
2
    Copyright (C) 2005-2010  Erik van Pienbroek
3

                
4
    This program is free software; you can redistribute it and/or modify
5
    it under the terms of the GNU General Public License as published by
6
    the Free Software Foundation; either version 2 of the License, or
7
    (at your option) any later version.
8

                
9
    This program is distributed in the hope that it will be useful,
10
    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
    GNU General Public License for more details.
13

                
14
    You should have received a copy of the GNU General Public License
15
    along with this program; if not, write to the Free Software
16
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
*/
18

                
19
#include 
20
#include 
21
#include 
22
#include 
23
#include 
24
#include 
25
#include 
26
#include 
27
#ifndef WIN32
28
#include 
29
#endif
30

                
31
#include "nntpgrab_plugin.h"
32
#include "marshalers.h"
33
#include "config.h"
34

                
35
static gboolean nntpgrab_plugin_par2_repair_files(NGPlugin *plugin_data, const char *collection_name, const char *directory, const char *par2filename, ngboolean more_par2_sets_remaining_in_collection);
36
static void on_collection_downloaded(NGPlugin *plugin_data, const char *collection_name, gpointer data);
37
static void par2_thread_func(gpointer data, gpointer user_data);
38

                
39
typedef struct _plugin_par2_priv {
40
    GThreadPool *thread_pool;
41
    const char *collection_name;
42
    const char *par2filename;
43
    GList *par2files;
44
    gboolean repair_succeeded;
45
    gboolean command_completed_normally;
46
    gboolean abort_flag;
47
    int pid;
48
} PluginPAR2Priv;
49

                
50
#ifdef WIN32
51
#include 
52
#include 
53

                
54
#undef popen
55
#undef pclose
56

                
57
#define popen pt_popen
58
#define pclose pt_pclose
59

                
60
HANDLE my_pipein[2], my_pipeout[2], my_pipeerr[2];
61
char   my_popenmode = ' ';
62

                
63
static int
64
my_pipe(HANDLE *readwrite)
65
{
66
    SECURITY_ATTRIBUTES sa;
67

                
68
    sa.nLength = sizeof(sa);
69
    sa.bInheritHandle = 1;
70
    sa.lpSecurityDescriptor = NULL;
71

                
72
    if (!CreatePipe (&readwrite[0],&readwrite[1],&sa,1 << 13)) {
73
        errno = EMFILE;
74
        return -1;
75
    }
76

                
77
    return 0;
78
}
79

                
80
static FILE *
81
pt_popen(const char *cmd, const char *mode)
82
{
83
    FILE *fptr = (FILE *)0;
84
    PROCESS_INFORMATION piProcInfo;
85
    STARTUPINFO siStartInfo;
86
    int success, catch_stderr;
87

                
88
    my_pipein[0]   = INVALID_HANDLE_VALUE;
89
    my_pipein[1]   = INVALID_HANDLE_VALUE;
90
    my_pipeout[0]  = INVALID_HANDLE_VALUE;
91
    my_pipeout[1]  = INVALID_HANDLE_VALUE;
92
    my_pipeerr[0]  = INVALID_HANDLE_VALUE;
93
    my_pipeerr[1]  = INVALID_HANDLE_VALUE;
94

                
95
    if (!mode || !*mode)
96
        goto finito;
97

                
98
    my_popenmode = *mode;
99
    if (my_popenmode != 'r' && my_popenmode != 'w')
100
        goto finito;
101

                
102
    catch_stderr = strstr((char *) cmd, "2>&1") != NULL;
103
    if (catch_stderr) {
104
        char *ptr = strstr((char*) cmd, "2>&1");
105
        *ptr = '\0';
106
    }
107

                
108
    if (my_pipe(my_pipein)  == -1 || my_pipe(my_pipeout) == -1)
109
        goto finito;
110
    if (!catch_stderr && my_pipe(my_pipeerr) == -1)
111
      goto finito;
112

                
113
    ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
114
    siStartInfo.cb             = sizeof(STARTUPINFO);
115
    siStartInfo.hStdInput      = my_pipein[0];
116
    siStartInfo.hStdOutput     = my_pipeout[1];
117
    if (catch_stderr)
118
        siStartInfo.hStdError  = my_pipeout[1];
119
    else
120
        siStartInfo.hStdError  = my_pipeerr[1];
121
    siStartInfo.dwFlags        = STARTF_USESTDHANDLES;
122

                
123
    success = CreateProcess(NULL,
124
       (LPTSTR)cmd,       // command line
125
       NULL,              // process security attributes
126
       NULL,              // primary thread security attributes
127
       TRUE,              // handles are inherited
128
       DETACHED_PROCESS,  // creation flags
129
       NULL,              // use parent's environment
130
       NULL,              // use parent's current directory
131
       &siStartInfo,      // STARTUPINFO pointer
132
       &piProcInfo);      // receives PROCESS_INFORMATION
133

                
134
    if (!success)
135
        goto finito;
136

                
137
    CloseHandle(my_pipein[0]);  my_pipein[0]  = INVALID_HANDLE_VALUE;
138
    CloseHandle(my_pipeout[1]); my_pipeout[1] = INVALID_HANDLE_VALUE;
139
    CloseHandle(my_pipeerr[1]); my_pipeerr[1] = INVALID_HANDLE_VALUE;
140

                
141
    if (my_popenmode == 'r')
142
        fptr = _fdopen(_open_osfhandle((intptr_t)my_pipeout[0],_O_BINARY),"r");
143
    else
144
        fptr = _fdopen(_open_osfhandle((intptr_t)my_pipein[1],_O_BINARY),"w");
145

                
146
finito:
147
    if (!fptr) {
148
        if (my_pipein[0]  != INVALID_HANDLE_VALUE)
149
            CloseHandle(my_pipein[0]);
150
        if (my_pipein[1]  != INVALID_HANDLE_VALUE)
151
            CloseHandle(my_pipein[1]);
152
        if (my_pipeout[0] != INVALID_HANDLE_VALUE)
153
            CloseHandle(my_pipeout[0]);
154
        if (my_pipeout[1] != INVALID_HANDLE_VALUE)
155
            CloseHandle(my_pipeout[1]);
156
        if (my_pipeerr[0] != INVALID_HANDLE_VALUE)
157
            CloseHandle(my_pipeerr[0]);
158
        if (my_pipeerr[1] != INVALID_HANDLE_VALUE)
159
            CloseHandle(my_pipeerr[1]);
160
    }
161

                
162
    return fptr;
163
}
164

                
165
static int
166
pt_pclose(FILE *file)
167
{
168
    if (!file) {
169
        return -1;
170
    }
171

                
172
    fclose(file);
173

                
174
    CloseHandle(my_pipeerr[0]);
175
    if (my_popenmode == 'r')
176
        CloseHandle(my_pipein[1]);
177
    else
178
       CloseHandle(my_pipeout[0]);
179

                
180
    return 0;
181
}
182
#endif
183

                
184
void
185
nntpgrab_plugin_initialize(NGPlugin *plugin_data)
186
{
187
    ng_plugin_set_name(plugin_data, "PAR2");
188
    ng_plugin_set_version(plugin_data, PACKAGE_VERSION);
189
    ng_plugin_set_author(plugin_data, "Erik van Pienbroek");
190
    ng_plugin_set_url(plugin_data, "https://www.nntpgrab.nl");
191
    ng_plugin_set_description(plugin_data, "Automatically verify and repair files using PAR2 after a collection has been downloaded");
192

                
193
    ng_plugin_set_required_event(plugin_data, "collection_downloaded");
194

                
195
    ng_plugin_register_function(plugin_data,
196
                                "par2_repair_files",
197
                                NG_PLUGIN_FUNCTION(nntpgrab_plugin_par2_repair_files),
198
                                ng_plugin_marshal_BOOLEAN__STRING_STRING_STRING,
199
                                G_TYPE_BOOLEAN,
200
                                3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
201

                
202
    ng_plugin_create_event(plugin_data, "par2_begin_verify", 2);
203
    ng_plugin_create_event(plugin_data, "par2_load_progress_update", 4);
204
    ng_plugin_create_event(plugin_data, "par2_recovery_file_loaded", 5);
205
    ng_plugin_create_event(plugin_data, "par2_file_loaded", 6);
206
    ng_plugin_create_event(plugin_data, "par2_repair_progress_update", 3);
207
    ng_plugin_create_event(plugin_data, "par2_repair_failure", 4);
208
    ng_plugin_create_event(plugin_data, "par2_repair_success", 3);
209
    ng_plugin_create_event(plugin_data, "par2_no_repair_required", 3);
210
}
211

                
212
ngboolean
213
nntpgrab_plugin_load(NGPlugin *plugin_data, char **errmsg)
214
{
215
    GError *err = NULL;
216
    PluginPAR2Priv *priv;
217

                
218
    plugin_data->priv = g_slice_new0(PluginPAR2Priv);
219
    priv = plugin_data->priv;
220
    priv->abort_flag = FALSE;
221
    priv->thread_pool = g_thread_pool_new(par2_thread_func, plugin_data, 1, FALSE, &err);
222

                
223
    if (!priv->thread_pool) {
224
        *errmsg = g_strdup_printf(_("%s:%i Unable to create PAR2 thread pool: %s"), __FILE__, __LINE__, err->message);
225
        g_error_free(err);
226
        return FALSE;
227
    }
228

                
229
    ng_plugin_connect_event(plugin_data, "collection_downloaded", NG_PLUGIN_FUNCTION(on_collection_downloaded), NULL);
230

                
231
    return TRUE;
232
}
233

                
234
ngboolean
235
nntpgrab_plugin_can_unload(NGPlugin *plugin_data, char **reason)
236
{
237
    return TRUE;
238
}
239

                
240
void
241
nntpgrab_plugin_unload(NGPlugin *plugin_data)
242
{
243
    PluginPAR2Priv *priv = plugin_data->priv;
244

                
245
    priv->abort_flag = TRUE;
246
#ifndef WIN32
247
    if (priv->pid > 0) {
248
        kill(priv->pid, SIGTERM);
249
    }
250
#endif
251
    g_thread_pool_free(priv->thread_pool, TRUE, TRUE);
252

                
253
    g_slice_free(PluginPAR2Priv, plugin_data->priv);
254
    plugin_data->priv = NULL;
255
}
256

                
257
void
258
nntpgrab_plugin_destroy(NGPlugin *plugin_data)
259
{
260
}
261

                
262
int
263
nntpgrab_plugin_get_version(void)
264
{
265
    return NNTPGRAB_PLUGIN_API_VERSION;
266
}
267

                
268
#if 0 
269
static bool
270
LoadVerificationPacket(DiskFile *diskfile, u64 offset, PACKET_HEADER &header)
271
{
272
  VerificationPacket *packet = new VerificationPacket;
273

                
274
  // Load the packet from disk
275
  if (!packet->Load(diskfile, offset, header))
276
  {
277
    delete packet;
278
    return false;
279
  }
280

                
281
  // What is the fileid
282
  const MD5Hash &fileid = packet->FileId();
283

                
284
  // Look up the fileid in the source file map for an existing source file entry
285
  map::iterator sfmi = sourcefilemap.find(fileid);
286
  Par2RepairerSourceFile *sourcefile = (sfmi == sourcefilemap.end()) ? 0 :sfmi->second;
287

                
288
  // Was there an existing source file
289
  if (sourcefile)
290
  {
291
    // Does the source file already have a verification packet
292
    if (sourcefile->GetVerificationPacket())
293
    {
294
      // Yes. We don't need another copy.
295
      delete packet;
296
      return false;
297
    }
298
    else
299
    {
300
      // No. Store the packet in the source file
301
      sourcefile->SetVerificationPacket(packet);
302

                
303
      return true;
304
    }
305
  }
306
  else
307
  {
308
    // Create a new source file for the packet
309
    sourcefile = new Par2RepairerSourceFile(NULL, packet);
310

                
311
    // Record the source file in the source file map
312
    sourcefilemap.insert(pair(fileid, sourcefile));
313

                
314
    return true;
315
  }
316
}
317
#endif
318

                
319
gboolean
320
nntpgrab_plugin_par2_load_verification_packet(const char *filename, PAR2Set **par2set)
321
{
322
#if 0 
323
    DiskFile *file;
324
    VerificationPacket *packet = NULL;
325

                
326
    g_return_val_if_fail(filename != NULL, FALSE);
327
    g_return_val_if_fail(par2set != NULL, FALSE);
328

                
329
    *par2set = NULL;
330

                
331
    file = new DiskFile();
332

                
333
    if (!file->Open(filename)) {
334
        delete file;
335
        return FALSE;
336
    }
337

                
338
    g_print("Now reading '%s' for PAR2 verification data..\n", filename);
339

                
340
    // How many useable packets have we found
341
    uint32_t packets = 0;
342

                
343
    // How big is the file
344
    guint64 filesize = file->FileSize();
345
    if (filesize <= 0) {
346
        delete file;
347
        g_print("Filesize == 0\n");
348
        return FALSE;
349
    }
350

                
351
    // Allocate a buffer to read data into
352
    // The buffer should be large enough to hold a whole
353
    // critical packet (i.e. file verification, file description, main,
354
    // and creator), but not necessarily a whole recovery packet.
355
    size_t buffersize = (size_t)min((u64)1048576, filesize);
356
    u8 *buffer = new u8[buffersize];
357

                
358
    // Start at the beginning of the file
359
    guint64 offset = 0;
360

                
361
    // Continue as long as there is at least enough for the packet header
362
    while (offset + sizeof(PACKET_HEADER) <= filesize) {
363
        // Attempt to read the next packet header
364
        PACKET_HEADER header;
365
        if (!file->Read(offset, &header, sizeof(header)))
366
            break;
367

                
368
        // Does this look like it might be a packet
369
        if (packet_magic != header.magic) {
370
            offset++;
371

                
372
            // Is there still enough for at least a whole packet header
373
            while (offset + sizeof(PACKET_HEADER) <= filesize) {
374
                // How much can we read into the buffer
375
                size_t want = (size_t)min((u64)buffersize, filesize-offset);
376

                
377
                // Fill the buffer
378
                if (!file->Read(offset, buffer, want)) {
379
                    offset = filesize;
380
                    break;
381
                }
382

                
383
                // Scan the buffer for the magic value
384
                u8 *current = buffer;
385
                u8 *limit = &buffer[want-sizeof(PACKET_HEADER)];
386
                while (current <= limit && packet_magic != ((PACKET_HEADER*)current)->magic) {
387
                    current++;
388
                }
389

                
390
                // What file offset did we reach
391
                offset += current-buffer;
392

                
393
                // Did we find the magic
394
                if (current <= limit) {
395
                    memcpy(&header, current, sizeof(header));
396
                    break;
397
                }
398
            }
399

                
400
            // Did we reach the end of the file
401
            if (offset + sizeof(PACKET_HEADER) > filesize)
402
            {
403
                break;
404
            }
405
        }
406

                
407
        // We have found the magic
408
        // Check the packet length
409
        if (sizeof(PACKET_HEADER) > header.length || // packet length is too small
410
            0 != (header.length & 3) ||              // packet length is not a multiple of 4
411
            filesize < offset + header.length) {     // packet would extend beyond the end of the file
412
                offset++;
413
                continue;
414
        }
415

                
416
        // Compute the MD5 Hash of the packet
417
        MD5Context context;
418
        context.Update(&header.setid, sizeof(header)-offsetof(PACKET_HEADER, setid));
419

                
420
        // How much more do I need to read to get the whole packet
421
        u64 current = offset+sizeof(PACKET_HEADER);
422
        u64 limit = offset+header.length;
423
        while (current < limit) {
424
            size_t want = (size_t)min((u64)buffersize, limit-current);
425

                
426
            if (!file->Read(current, buffer, want))
427
                break;
428

                
429
            context.Update(buffer, want);
430

                
431
            current += want;
432
        }
433

                
434
        // Did the whole packet get processed
435
        if (current < limit) {
436
            offset++;
437
            continue;
438
        }
439

                
440
        // Check the calculated packet hash against the value in the header
441
        MD5Hash hash;
442
        context.Final(hash);
443
        if (hash != header.hash) {
444
            offset++;
445
            continue;
446
        }
447

                
448
        // The Set ID is now validated, generate the PAR2Set structure
449
        *par2set = (PAR2Set*) g_slice_new0(PAR2Set);
450
        memcpy((*par2set)->set_id, header.setid.hash, sizeof(header.setid.hash));
451

                
452
        if (fileverificationpacket_type == header.type)  {
453
            //if (LoadVerificationPacket(file, offset, header)) {
454
                packets++;
455
            //}
456

                
457
            // Advance to the next packet
458
            offset += header.length;
459
        }
460
    }
461

                
462
    delete [] buffer;
463

                
464
    // We have finished with the file for now
465
    file->Close();
466

                
467
    // Did we actually find any interesting packets
468
    if (packets == 0) {
469
        if ((*par2set)) {
470
            g_slice_free(PAR2Set, (*par2set));
471
        }
472

                
473
        delete packet;
474
        delete file;
475
        return FALSE;
476
    }
477

                
478
    delete packet;
479
    delete file;
480
#endif
481

                
482
    return TRUE;
483
}
484

                
485
char *
486
strip_large_filenames(const char *filename)
487
{
488
    /* par2cmdline strips filenames which are larger than 56 characters. 
489
     * With this function, the same algoritm to strip such filenames can be applied */
490
    char filename_stripped[1024];
491
    int len = strlen(filename);
492

                
493
    if (len < 56) {
494
        return g_strdup(filename);
495
    }
496

                
497
    memset(filename_stripped, 0, sizeof(filename_stripped));
498
    strncpy(filename_stripped, filename, 28);
499
    strcat(filename_stripped, "...");
500
    strncat(filename_stripped, filename + len - 28, sizeof(filename_stripped) - 28 - 3 - 1);
501

                
502
    return g_strdup(filename_stripped);
503
}
504

                
505
struct _par2_repair_file {
506
    char filename[PATH_MAX];
507
    double verify_progress;
508
    int num_packets_found;
509
    int num_blocks_found;
510
    int num_duplicate_blocks_found;
511
    int num_blocks_expected;
512
    gboolean is_missing;
513
    gboolean is_complete;
514
    time_t stamp_last_progress_update;
515
};
516

                
517
static time_t stamp_last_repair_progress_update = 0;
518

                
519
static struct _par2_repair_file *
520
process_message(NGPlugin *plugin_data, GHashTable *files, struct _par2_repair_file *active_file, const char *line, ngboolean more_par2_sets_remaining_in_collection)
521
{
522
    struct _par2_repair_file *file = NULL;
523
    char *filename = NULL;
524
    char *filename_stripped = NULL;
525
    PluginPAR2Priv *priv = plugin_data->priv;
526
    const char *params[7];
527
    char progress_str[16];
528
    char num_packets_found_str[16];
529
    char num_blocks_found_str[16];
530
    char num_blocks_expected_str[16];
531

                
532
    // NOTE: par2cmdline can do wrapping of the filename!
533

                
534
    memset(&progress_str, 0, sizeof(progress_str));
535
    memset(&num_packets_found_str, 0, sizeof(num_packets_found_str));
536
    memset(&num_blocks_found_str, 0, sizeof(num_blocks_found_str));
537
    memset(&num_blocks_expected_str, 0, sizeof(num_blocks_expected_str));
538

                
539
    g_return_val_if_fail(priv->collection_name != NULL, NULL);
540
    g_return_val_if_fail(priv->par2filename != NULL, NULL);
541

                
542
    if (!strncmp(line, "Loading \"", 9)) {                  // Loading "Top 40 dossier _1994 final_.par2".
543
        char *end = strstr(line + 10, "\"");
544

                
545
        g_return_val_if_fail(end != NULL, NULL);
546

                
547
        filename = g_strndup(line + 9, end - line - 9);
548
        filename_stripped = strip_large_filenames(filename);
549
        file = g_hash_table_lookup(files, filename_stripped);
550
        if (!file) {
551
            file = g_slice_new0(struct _par2_repair_file);
552
            strncpy(file->filename, filename, sizeof(file->filename) - 1);
553
            g_hash_table_insert(files, g_strdup(filename_stripped), file);
554
        }
555

                
556
        g_free(filename_stripped);
557
        g_free(filename);
558

                
559
        snprintf(progress_str, sizeof(progress_str) - 1, "%.2f", file->verify_progress);
560
        params[0] = priv->collection_name;
561
        params[1] = priv->par2filename;
562
        params[2] = file->filename;
563
        params[3] = progress_str;
564
        params[4] = NULL;
565
        ng_plugin_emit_event(plugin_data, "par2_load_progress_update", params);
566
    } else if (!strncmp(line, "Loading: ", 9)) {            // Loading: 97.9%
567
        g_return_val_if_fail(active_file != NULL, NULL);
568

                
569
        active_file->verify_progress = g_ascii_strtod(line + 9, NULL);
570
        file = active_file;
571
        if (active_file->stamp_last_progress_update != time(NULL)) {
572
            snprintf(progress_str, sizeof(progress_str) - 1, "%.2f", file->verify_progress);
573

                
574
            params[0] = priv->collection_name;
575
            params[1] = priv->par2filename;
576
            params[2] = active_file->filename;
577
            params[3] = progress_str;
578
            params[4] = NULL;
579
            ng_plugin_emit_event(plugin_data, "par2_load_progress_update", params);
580
            active_file->stamp_last_progress_update = time(NULL);
581
        }
582
    } else if (!strncmp(line, "Loaded ", 7)) {              // Loaded 4 new packets
583
                                                            // Loaded 12 new packets including 12 recovery blocks
584
        g_return_val_if_fail(active_file != NULL, NULL);
585

                
586
        if (sscanf(line, "Loaded %d new packets including %d recovery blocks", &active_file->num_packets_found, &active_file->num_blocks_found) != 1) {
587
            // line == 'Loaded x new packets'
588
            active_file->num_packets_found = atoi(line + 7);
589
        }
590

                
591
        active_file->verify_progress = 100.0;
592

                
593
        snprintf(num_packets_found_str, sizeof(num_packets_found_str) - 1, "%i", active_file->num_packets_found);
594
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", active_file->num_blocks_found);
595
        params[0] = priv->collection_name;
596
        params[1] = priv->par2filename;
597
        params[2] = active_file->filename;
598
        params[3] = num_packets_found_str;
599
        params[4] = num_blocks_found_str;
600
        params[5] = NULL;
601
        ng_plugin_emit_event(plugin_data, "par2_recovery_file_loaded", params);
602

                
603
        /* Maintain a list of PAR2 files which can be removed after the verification/reparation has succeeded */
604
        priv->par2files = g_list_append(priv->par2files, g_strdup(active_file->filename));
605
    } else if (!strncmp(line, "No new packets found", 20)) {
606
        g_return_val_if_fail(active_file != NULL, NULL);
607
        active_file->num_packets_found = 0;
608
        active_file->verify_progress = 100.0;
609

                
610
        snprintf(num_packets_found_str, sizeof(num_packets_found_str) - 1, "%i", active_file->num_packets_found);
611
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", active_file->num_blocks_found);
612
        params[0] = priv->collection_name;
613
        params[1] = priv->par2filename;
614
        params[2] = active_file->filename;
615
        params[3] = num_packets_found_str;
616
        params[4] = num_blocks_found_str;
617
        params[5] = NULL;
618
        ng_plugin_emit_event(plugin_data, "par2_recovery_file_loaded", params);
619

                
620
        /* Maintain a list of PAR2 files which can be removed after the verification/reparation has succeeded */
621
        priv->par2files = g_list_append(priv->par2files, g_strdup(active_file->filename));
622
    } else if (!strncmp(line, "Scanning: \"", 11)) {        // Scanning: "post832.rar": 0.5%
623
        char *end = strstr(line + 12, "\"");
624

                
625
        g_return_val_if_fail(end != NULL, NULL);
626

                
627
        filename = g_strndup(line + 11, end - line - 11);
628
        filename_stripped = strip_large_filenames(filename);
629
        file = g_hash_table_lookup(files, filename_stripped);
630

                
631
        /* On Win32 environments, it can happen that the filename is saved in a different 
632
         * character encoding than UTF-8. This can confuse the code above, so add a ugly
633
         * workaround for these kind of situations */
634
        if (!file) {
635
            file = g_slice_new0(struct _par2_repair_file);
636
            strncpy(file->filename, filename, sizeof(file->filename) - 1);
637
            g_hash_table_insert(files, g_strdup(filename_stripped), file);
638
        }
639

                
640
        g_free(filename_stripped);
641
        g_free(filename);
642

                
643
        g_return_val_if_fail(file != NULL, NULL);
644

                
645
        file->verify_progress = g_ascii_strtod(end + 3, NULL);
646
        if (file->stamp_last_progress_update != time(NULL)) {
647
            snprintf(progress_str, sizeof(progress_str) - 1, "%.2f", file->verify_progress);
648
            params[0] = priv->collection_name;
649
            params[1] = priv->par2filename;
650
            params[2] = file->filename;
651
            params[3] = progress_str;
652
            params[4] = NULL;
653
            ng_plugin_emit_event(plugin_data, "par2_load_progress_update", params);
654

                
655
            file->stamp_last_progress_update = time(NULL);
656
        }
657
    } else if (!strncmp(line, "Target: \"", 9)) {           // Target: "post832.rar" - found.
658
        char *end = strstr(line, "\" - ");
659
        const char *state_str = NULL;
660

                
661
        g_return_val_if_fail(end != NULL, NULL);
662

                
663
        filename = g_strndup(line + 9, end - line - 9);
664
        filename_stripped = strip_large_filenames(filename);
665
        file = g_hash_table_lookup(files, filename_stripped);
666

                
667
        // The file can be missing, add a fallback for that situation
668
        if (!file) {
669
            file = g_slice_new0(struct _par2_repair_file);
670
            strncpy(file->filename, filename, sizeof(file->filename) - 1);
671
            g_hash_table_insert(files, g_strdup(filename_stripped), file);
672
        }
673

                
674
        g_free(filename_stripped);
675
        g_free(filename);
676

                
677
        g_return_val_if_fail(file != NULL, NULL);
678

                
679
        end += 4;
680
        if (!strncmp(end, "found", 5)) {
681
            file->is_complete = TRUE;
682
            state_str = "FOUND";
683
        } else if (!strncmp(end, "missing", 7)) {
684
            file->is_missing = TRUE;
685
            state_str = "MISSING";
686
        } else if (!strncmp(end, "damaged", 7)) {
687
            if (strstr(end, "from several target files")) {
688
                state_str = "NO_NEW_BLOCKS_FOUND";
689
            } else {
690
                g_return_val_if_fail (sscanf(end, "damaged. Found %d of %d data blocks.", &file->num_blocks_found, &file->num_blocks_expected) == 2, NULL);
691
                state_str = "DAMAGED";
692
            }
693
        } else {
694
            g_print(__FILE__ ":%i reason = %s\n", __LINE__, end);
695
            state_str = "MISSING";
696
        }
697

                
698
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", file->num_blocks_found);
699
        snprintf(num_blocks_expected_str, sizeof(num_blocks_expected_str) - 1, "%i", file->num_blocks_expected);
700

                
701
        params[0] = priv->collection_name,
702
        params[1] = priv->par2filename;
703
        params[2] = file->filename;
704
        params[3] = state_str;
705
        params[4] = num_blocks_found_str;
706
        params[5] = num_blocks_expected_str;
707
        params[6] = NULL;
708
        ng_plugin_emit_event(plugin_data, "par2_file_loaded", params);
709
    } else if (!strncmp(line, "File: \"", 7)) {             // File: "1994_026_CDS__TW_ Prodigy - No Good _Start The Dance_ _Original Mix_.mp3" - found 12 of 13 data blocks from "1994_026[CDS][TW] Prodigy - No Good (Start The Dance) (Original Mix).mp3".
710
        char *end = strstr(line, "\" - ");
711
        const char *state_str;
712

                
713
        g_return_val_if_fail(end != NULL, NULL);
714

                
715
        filename = g_strndup(line + 7, end - line - 7);
716
        filename_stripped = strip_large_filenames(filename);
717
        file = g_hash_table_lookup(files, filename_stripped);
718

                
719
        // The file can be missing, add a fallback for that situation
720
        if (!file) {
721
            file = g_slice_new0(struct _par2_repair_file);
722
            strncpy(file->filename, filename, sizeof(file->filename) - 1);
723
            g_hash_table_insert(files, g_strdup(filename_stripped), file);
724
        }
725

                
726
        g_free(filename);
727

                
728
        g_return_val_if_fail(file != NULL, NULL);
729

                
730
        end += 4;
731
        if (strncmp(end, "no data found.", 14) &&
732
            strncmp(end, "is a match for", 14) &&
733
            !strstr(end, "duplicate data blocks.") &&
734
            !strstr(end, "data blocks from several target files")) {
735

                
736
            g_return_val_if_fail (sscanf(end, "found %d of %d data blocks", &file->num_blocks_found, &file->num_blocks_expected) == 2, NULL);
737

                
738
            if (file->num_blocks_found == file->num_blocks_expected) {
739
                state_str = "FOUND";
740
            } else {
741
                state_str = "DAMAGED";
742
            }
743
        } else if (!strncmp(end, "no data found.", 14) ||
744
                    strstr(end, "data blocks from several target files") ||
745
                    strstr(end, "duplicate data blocks.")) {
746

                
747
            state_str = "NO_NEW_BLOCKS_FOUND";
748
        } else if (!strncmp(end, "is a match for", 14)) {
749
            file->num_blocks_found = file->num_blocks_expected = 1;
750
            file->verify_progress = 100.0;
751

                
752
            state_str = "FOUND";
753
        } else {
754
g_print(__FILE__ ":%i unknown state for PAR2 message: %s (end = %s)\n", __LINE__, line, end);
755
            state_str = "MISSING";
756
        }
757

                
758
        g_free(filename_stripped);
759

                
760
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", file->num_blocks_found);
761
        snprintf(num_blocks_expected_str, sizeof(num_blocks_expected_str) - 1, "%i", file->num_blocks_expected);
762

                
763
        params[0] = priv->collection_name;
764
        params[1] = priv->par2filename;
765
        params[2] = file->filename;
766
        params[3] = state_str;
767
        params[4] = num_blocks_found_str;
768
        params[5] = num_blocks_expected_str;
769
        params[6] = NULL;
770
        ng_plugin_emit_event(plugin_data, "par2_file_loaded", params);
771
    } else if (!strncmp(line, "You need ", 9)) {         // You need 3000 more recovery blocks to be able to repair
772
        char num_blocks_required_str[16];
773
        int num_blocks_required = atoi(line + 9);
774

                
775
        priv->repair_succeeded = FALSE;
776
        priv->command_completed_normally = TRUE;
777

                
778
        memset(&num_blocks_required_str, 0, sizeof(num_blocks_required_str));
779
        snprintf(num_blocks_required_str, sizeof(num_blocks_required_str) - 1, "%i", num_blocks_required);
780
        params[0] = priv->collection_name;
781
        params[1] = priv->par2filename;
782
        params[2] = "";
783
        params[3] = num_blocks_required_str;
784
        params[4] = NULL;
785
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
786
    } else if (!strncmp(line, "Repairing:", 10)) {
787
        double progress = g_ascii_strtod(line + 11, NULL);
788
        if (stamp_last_repair_progress_update != time(NULL)) {
789
            snprintf(progress_str, sizeof(progress_str) - 1, "%.2f", progress);
790

                
791
            params[0] = priv->collection_name;
792
            params[1] = priv->par2filename;
793
            params[2] = progress_str;
794
            params[3] = NULL;
795
            ng_plugin_emit_event(plugin_data, "par2_repair_progress_update", params);
796
            stamp_last_repair_progress_update = time(NULL);
797
        }
798
    } else if (!strncmp(line, "Repair complete.", 16)) {
799
        priv->repair_succeeded = TRUE;
800
        priv->command_completed_normally = TRUE;
801

                
802
        params[0] = priv->collection_name;
803
        params[1] = priv->par2filename;
804
        if (more_par2_sets_remaining_in_collection) {
805
            params[2] = "TRUE";
806
        } else {
807
            params[2] = "FALSE";
808
        }
809
        params[3] = NULL;
810
        ng_plugin_emit_event(plugin_data, "par2_repair_success", params);
811
    } else if (!strncmp(line, "All files are correct, repair is not required.", 46)) {
812
        priv->repair_succeeded = TRUE;
813
        priv->command_completed_normally = TRUE;
814

                
815
        params[0] = priv->collection_name;
816
        params[1] = priv->par2filename;
817
        if (more_par2_sets_remaining_in_collection) {
818
            params[2] = "TRUE";
819
        } else {
820
            params[2] = "FALSE";
821
        }
822
        params[3] = NULL;
823
        ng_plugin_emit_event(plugin_data, "par2_no_repair_required", params);
824
    } else if (!strncmp(line, "Could not read", 14)) {
825
        priv->repair_succeeded = FALSE;
826
        priv->command_completed_normally = TRUE;
827

                
828
        params[0] = priv->collection_name;
829
        params[1] = priv->par2filename;
830
        params[2] = line;
831
        params[3] = "-1";
832
        params[4] = NULL;
833
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
834
    } else if (!strncmp(line, "The recovery file does not exist", 32)) {
835
        priv->repair_succeeded = FALSE;
836
        priv->command_completed_normally = TRUE;
837

                
838
        params[0] = priv->collection_name;
839
        params[1] = priv->par2filename;
840
        params[2] = "Bug in par2cmdline was triggered";
841
        params[3] = "-1";
842
        params[4] = NULL;
843
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
844
    } else if (strstr(line, "cannot be renamed to")) {
845
        priv->repair_succeeded = FALSE;
846
        priv->command_completed_normally = TRUE;
847

                
848
        params[0] = priv->collection_name;
849
        params[1] = priv->par2filename;
850
        params[2] = "Error: par2cmdline wasn't able to rename a file";
851
        params[3] = "-1";
852
        params[4] = NULL;
853
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
854
    } else if (!strncmp(line, "pid", 3)) {
855
        priv->pid = atoi(line + 3);
856
    } else {
857
// g_print("line = %s - active_file = %#x\n", line, active_file);
858
    }
859
    /* 
860
    Loading "Top 40 dossier _1994 final_.par2".
861
    Loaded 5 new packets including 1 recovery blocks
862
    Repair is required.
863
    Repair is not possible.
864
    You need 3000 more recovery blocks to be able to repair.
865
    Loading: 99.9%
866
    Loaded 712 new packets
867
    No new packets found
868
    There are 355 recoverable files and 0 other files.
869
    The block size used was 1223232 bytes.
870
    There are a total of 3000 data blocks.
871
    The total size of the data files is 3453446279 bytes.
872
    Verifying source files:
873
    Scanning: "post832.rar": 0.5%
874
    Target: "post832.rar" - found.
875
    Target: "1994_001[CD][ME] Marco Borsato - Dromen Zijn Bedrog.mp3" - missing.
876
    Scanning extra files:
877
    File: "1994_026_CDS__TW_ Prodigy - No Good _Start The Dance_ _Original Mix_.mp3" - found 12 of 13 data blocks from "1994_026[CDS][TW] Prodigy - No Good (Start The Dance) (Original Mix).mp3".
878
    File: "vrllvhnbnz.part032.rar" - no data found.
879
    All files are correct, repair is not required.
880
    */
881
    return file;
882
}
883

                
884
static void
885
par2_repair_file_free(gpointer data)
886
{
887
    g_slice_free(struct _par2_repair_file, data);
888
}
889

                
890
static gboolean
891
verify_par2_extension(const char *filename)
892
{
893
    char *ptr = g_strrstr(filename, ".");
894

                
895
    if (!ptr) {
896
        /* File doesn't have an extension */
897
        return FALSE;
898
    }
899

                
900
    /* Does the filename have an '.par' or '.par2' extension? */
901
    if (!g_ascii_strncasecmp(ptr, ".par2", 5) || !g_strncasecmp(ptr, ".par", 4)) {
902
        return TRUE;
903
    }
904

                
905
    /* Test for files with the extension '.par.1' or '.par2.1' */
906
    ptr = g_strrstr(filename, ".");
907
    if (!ptr) {
908
        return FALSE;
909
    }
910

                
911
    if (!g_ascii_strncasecmp(ptr, ".par2.", 6) || !g_strncasecmp(ptr, ".par.", 5)) {
912
        return TRUE;
913
    }
914

                
915
    return FALSE;
916
}
917

                
918
static gboolean
919
par2_start_par2cmdline(NGPlugin *plugin_data, const char *collection_name, const char *directory, const char *par2filename, GHashTable *files, gboolean try_to_disable_concurrent_verify, gboolean *invalid_option_detected, ngboolean more_par2_sets_remaining_in_collection)
920
{
921
    FILE *fp;
922
    char *cmd;
923
    char buf[1024];
924
    int i;
925
    int c;
926
    struct _par2_repair_file *active_file = NULL;
927
    const char *disable_concurrent_verify_option;
928
    gboolean first_line = TRUE;
929
#ifndef WIN32
930
    gboolean second_line = FALSE;
931
#endif
932
    PluginPAR2Priv *priv;
933
    static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
934
    NGConfigOpts opts;
935
    GList *list;
936
#ifdef WIN32
937
    char *filename;
938
    char *dirname = g_locale_from_utf8(directory, -1, NULL, NULL, NULL);
939
    char *bindir = g_path_get_dirname(__argv[0]);
940
    char *cmd2;
941

                
942
    g_return_val_if_fail(dirname != NULL, FALSE);
943
#endif
944

                
945
    priv = plugin_data->priv;
946

                
947
    if (priv->abort_flag) {
948
        const char *params[5];
949

                
950
        priv->repair_succeeded = FALSE;
951
        params[0] = priv->collection_name;
952
        params[1] = priv->par2filename;
953
        params[2] = "Error: par2cmdline aborted unexpectedly";
954
        params[3] = "-1";
955
        params[4] = NULL;
956
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
957

                
958
        return FALSE;
959
    }
960

                
961
    g_static_mutex_lock(&mutex);
962

                
963
    if (try_to_disable_concurrent_verify) {
964
        disable_concurrent_verify_option = "-t0";
965
    } else {
966
        disable_concurrent_verify_option = "";
967
    }
968

                
969
#ifdef WIN32
970
    filename = g_locale_from_utf8(par2filename, -1, NULL, NULL, NULL);
971

                
972
    g_return_val_if_fail(filename != NULL, FALSE);
973

                
974
    cmd = g_strdup_printf("\"%s\\par2.exe\" r %s \"%s\" \"%s"G_DIR_SEPARATOR_S"*.*\" 2>&1", bindir, disable_concurrent_verify_option, filename, dirname);
975
    g_free(filename);
976
    g_free(dirname);
977
    g_free(bindir);
978
#else
979
    cmd = g_strdup_printf("echo \"pid$$\"; par2 r %s \"%s\" \"%s"G_DIR_SEPARATOR_S"\"*.* 2>&1", disable_concurrent_verify_option, par2filename, directory);
980
#endif
981
    fp = popen(cmd, "r");
982

                
983
#ifdef WIN32
984
    cmd2 = g_locale_to_utf8(cmd, -1, NULL, NULL, NULL);
985
    g_return_val_if_fail(cmd2 != NULL, FALSE);
986
    ng_plugin_emit_log_msg(plugin_data, 1, _("Launched the command: %s\n"), cmd2);
987
    g_free(cmd2);
988
#else
989
    ng_plugin_emit_log_msg(plugin_data, 1, _("Launched the command: %s\n"), cmd);
990
#endif
991

                
992
    if (!fp) {
993
        ng_plugin_emit_log_msg(plugin_data, 1, _("Error while launching from the par2cmdline process\n%s\n%s"), cmd, strerror(errno));
994
        g_free(cmd);
995
        g_static_mutex_unlock(&mutex);
996
        return FALSE;
997
    }
998
    g_free(cmd);
999

                
1000
    priv->collection_name = collection_name;
1001
    priv->par2filename = par2filename;
1002
    priv->par2files = NULL;
1003
    priv->pid = -1;
1004

                
1005
    // Assume the repair was succesfull unless it explicit failed
1006
    priv->repair_succeeded = TRUE;
1007

                
1008
    // Detect abnormal aborts of par2cmdline
1009
    priv->command_completed_normally = FALSE;
1010

                
1011
    memset(buf, 0, sizeof(buf));
1012
    i = 0;
1013
    while ((c = fgetc(fp)) != EOF) {
1014
        if (priv->abort_flag) {
1015
            break;
1016
        }
1017

                
1018
        if (c == '\r' || (c == '\n' && i > 0)) {
1019
            if (first_line) {
1020
                first_line = FALSE;
1021
#ifndef WIN32
1022
                second_line = TRUE;
1023
            } else if (second_line) {
1024
                second_line = FALSE;
1025
#endif
1026
                /* Check if the given options were valid */
1027
                if (!strncmp(buf, "Invalid option specified", 24)) {
1028
                    if (invalid_option_detected) {
1029
                        *invalid_option_detected = TRUE;
1030
                    }
1031

                
1032
                    pclose(fp);
1033

                
1034
                    priv->collection_name = NULL;
1035
                    priv->par2filename = NULL;
1036

                
1037
                    g_static_mutex_unlock(&mutex);
1038

                
1039
                    return FALSE;
1040
                }
1041
            }
1042

                
1043
            /* Translate the line to UTF-8 if it isn't already */
1044
            if (!g_utf8_validate(buf, -1, NULL)) {
1045
                /* Filename is probably in Windows-1252 charset */
1046
                char *tmp = g_convert(buf, -1, "utf-8", "windows-1252", NULL, NULL, NULL);
1047
                if (tmp) {
1048
                    memset(buf, 0, sizeof(buf));
1049
                    strncpy(buf, tmp, sizeof(buf) - 1);
1050
                    g_free(tmp);
1051
                }
1052
            }
1053

                
1054
            i = 0;
1055
            active_file = process_message(plugin_data, files, active_file, buf, more_par2_sets_remaining_in_collection);
1056
// imported_funcs.emit_repair_message(buf);
1057
            memset(buf, 0, sizeof(buf));
1058
        } else if (c == '\n') {
1059
            // Ignore
1060
        } else {
1061
            buf[i] = (char) c;
1062
            i++;
1063
        }
1064
    }
1065

                
1066
    pclose(fp);
1067

                
1068
    if (!priv->command_completed_normally) {
1069
        /* Abnormal abort of par2cmdline detected, send a notification to the frontend */
1070
        const char *params[5];
1071

                
1072
        priv->repair_succeeded = FALSE;
1073
        params[0] = priv->collection_name;
1074
        params[1] = priv->par2filename;
1075
        params[2] = "Error: par2cmdline aborted unexpectedly";
1076
        params[3] = "-1";
1077
        params[4] = NULL;
1078
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
1079
    }
1080

                
1081
    /* Remove the PAR2 files after a successfull verification/recovery if it's enabled */
1082
    opts = plugin_data->core_funcs.config_get_opts();
1083
    list = priv->par2files;
1084
    while (list) {
1085
        char *filename = list->data;
1086

                
1087
        /* Make sure that the file really is a PAR/PAR2 file. Due to a bug in par2cmdline, regular files might get marked as PAR2 recovery files */
1088
        if (priv->repair_succeeded && !more_par2_sets_remaining_in_collection && opts.auto_remove_files_after_repair) {
1089
            if (verify_par2_extension(filename)) {
1090
                char *path = g_build_filename(directory, filename, NULL);
1091
                ng_plugin_emit_log_msg(plugin_data, NG_LOG_LEVEL_DEBUG, _("Now automatically removing file '%s'"), path);
1092
                g_unlink(path);
1093
                g_free(path);
1094
            } else {
1095
                ng_plugin_emit_log_msg(plugin_data, NG_LOG_LEVEL_DEBUG, _("File '%s' was marked as a PAR2 recovery file by par2cmdline while it actually isn't. Ignoring"), filename);
1096
            }
1097
        }
1098

                
1099
        g_free(filename);
1100

                
1101
        list = g_list_next(list);
1102
    }
1103
    g_list_free(priv->par2files);
1104

                
1105
    priv->collection_name = NULL;
1106
    priv->par2filename = NULL;
1107

                
1108
    g_static_mutex_unlock(&mutex);
1109

                
1110
    return priv->repair_succeeded;
1111
}
1112

                
1113
static gboolean
1114
nntpgrab_plugin_par2_repair_files(NGPlugin *plugin_data, const char *collection_name, const char *directory, const char *par2filename, ngboolean more_par2_sets_remaining_in_collection)
1115
{
1116
    char *path;
1117
    GHashTable *files = NULL;
1118
    GDir *dir;
1119
    GError *error = NULL;
1120
    const char *filename;
1121
    char *filename_stripped;
1122
    gboolean invalid_option_detected = FALSE;
1123
    gboolean repair_succeeded;
1124

                
1125
    path = g_build_filename(directory, par2filename, NULL);
1126
    g_return_val_if_fail(g_file_test(path, G_FILE_TEST_EXISTS), FALSE);
1127

                
1128
    // Build a list of all the files which are in the directory
1129
    dir = g_dir_open(directory, 0, &error);
1130
    if (!dir) {
1131
        ng_plugin_emit_log_msg(plugin_data, 1, _("PAR2 repair can't be started because the directory with the\ndownloaded files can't be opened: %s"), error->message);
1132
        g_error_free(error);
1133
        return FALSE;
1134
    }
1135

                
1136
    files = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, par2_repair_file_free);
1137

                
1138
    while ((filename = g_dir_read_name(dir))) {
1139
        struct _par2_repair_file *file = g_slice_new0(struct _par2_repair_file);
1140

                
1141
        strncpy(file->filename, filename, sizeof(file->filename) - 1);
1142
        filename_stripped = strip_large_filenames(filename);
1143
        g_hash_table_insert(files, filename_stripped, file);
1144
        /* No need to g_free(filename_stripped) here as that will be done by the GHashTable itself */
1145
    }
1146

                
1147
    g_dir_close(dir);
1148

                
1149
    ng_plugin_emit_log_msg(plugin_data, 1, "%s", _("Now starting PAR2 repair"));
1150

                
1151
    repair_succeeded = par2_start_par2cmdline(plugin_data, collection_name, directory, path, files, TRUE, &invalid_option_detected, more_par2_sets_remaining_in_collection);
1152
    if (!repair_succeeded && invalid_option_detected) {
1153
        /* The regular par2cmdline was detected, try again with the -t0 option */
1154
        repair_succeeded = par2_start_par2cmdline(plugin_data, collection_name, directory, path, files, FALSE, NULL, more_par2_sets_remaining_in_collection);
1155
    }
1156

                
1157
    if (repair_succeeded) {
1158
        ng_plugin_emit_log_msg(plugin_data, 1, "%s", _("PAR2 repair finished successfully"));
1159
    } else {
1160
        ng_plugin_emit_log_msg(plugin_data, 1, "%s", _("PAR2 repair failed"));
1161
    }
1162

                
1163
    g_free(path);
1164
    g_hash_table_destroy(files);
1165

                
1166
    return repair_succeeded;
1167
}
1168

                
1169
typedef struct _par2_file_seen {
1170
    char filename_without_extension[256];
1171
    char filename[256];
1172
} PAR2FileSeen;
1173

                
1174
/* APARTE THREAD ! */
1175
struct _par2_thread_data {
1176
    NGPlugin *plugin_data;
1177
    char collection_name[256];
1178
};
1179

                
1180
static void
1181
par2_thread_func(gpointer data, gpointer user_data)
1182
{
1183
    struct _par2_thread_data *thread_data = (struct _par2_thread_data *) data;
1184
    NGConfigOpts opts;
1185
    char *directory;
1186
    GList *list;
1187
    GList *list2;
1188
    GList *files = NULL;
1189
    GDir *dir;
1190
    GError *err = NULL;
1191
    const char *filename;
1192

                
1193
    /* Do we need to repair? */
1194
    opts = thread_data->plugin_data->core_funcs.config_get_opts();
1195
    if (!opts.enable_par2_repair || thread_data->plugin_data->core_funcs.schedular_get_state() != SCHEDULAR_STATE_RUNNING || ((PluginPAR2Priv*)thread_data->plugin_data->priv)->abort_flag == TRUE) {
1196
        g_slice_free(struct _par2_thread_data, thread_data);
1197
        return;
1198
    }
1199

                
1200
    /* Find out all the unique PAR2 sets in this collection */
1201
    directory = g_build_filename(opts.download_directory, thread_data->collection_name, NULL);
1202
    dir = g_dir_open(directory, 0, &err);
1203
    if (!dir) {
1204
        ng_plugin_emit_log_msg(thread_data->plugin_data, NG_LOG_LEVEL_WARNING, _("Unable to open directory '%s': %s"), directory, err->message);
1205
        g_error_free(err);
1206
        g_free(directory);
1207
        g_slice_free(struct _par2_thread_data, thread_data);
1208
        return;
1209
    }
1210

                
1211
    while ((filename = g_dir_read_name(dir))) {
1212
        char *extension;
1213
        char filename_without_extension[256];
1214
        gboolean filename_seen = FALSE;
1215
        char *ptr;
1216

                
1217
        extension = g_strrstr(filename, ".");
1218
        if (!extension) {
1219
            /* File without extension, ignore */
1220
            continue;
1221
        }
1222

                
1223
        if (g_ascii_strncasecmp(extension, ".par", 4) != 0) {
1224
            /* Different extension, ignore */
1225
            continue;
1226
        }
1227

                
1228
        memset(filename_without_extension, 0, sizeof(filename_without_extension));
1229
        strncpy(filename_without_extension, filename, extension - filename);
1230

                
1231
        // Strip any .volXX+YY part
1232
        ptr = g_strrstr(filename_without_extension, ".");
1233
        if (ptr && !g_strncasecmp(ptr, ".vol", 4)) {
1234
            *ptr = '\0';
1235
        }
1236

                
1237
        list2 = files;
1238
        while (list2) {
1239
            PAR2FileSeen *file = (PAR2FileSeen*) list2->data;
1240

                
1241
            if (!strcmp(filename_without_extension, file->filename_without_extension)) {
1242
                // We've already seen this PAR2 set, ignore
1243
                filename_seen = TRUE;
1244
                break;
1245
            }
1246

                
1247
            list2 = g_list_next(list2);
1248
        }
1249

                
1250
        if (!filename_seen) {
1251
            PAR2FileSeen *file_seen = g_slice_new0(PAR2FileSeen);
1252

                
1253
            strncpy(file_seen->filename_without_extension, filename_without_extension, sizeof(file_seen->filename_without_extension) - 1);
1254
            strncpy(file_seen->filename, filename, sizeof(file_seen->filename) - 1);
1255

                
1256
            files = g_list_append(files, file_seen);
1257
        }
1258
    }
1259

                
1260
    g_dir_close(dir);
1261

                
1262
    if (!files) {
1263
        ng_plugin_emit_log_msg(thread_data->plugin_data, NG_LOG_LEVEL_DEBUG, _("No .par2 files found for collection '%s'"), thread_data->collection_name);
1264
    }
1265

                
1266
    /* Perform the repair */
1267
    list = files;
1268
    while (list) {
1269
        PAR2FileSeen *file_seen = (PAR2FileSeen*) list->data;
1270

                
1271
        nntpgrab_plugin_par2_repair_files(thread_data->plugin_data, thread_data->collection_name, directory, file_seen->filename, (list->next != NULL));
1272

                
1273
        g_slice_free(PAR2FileSeen, file_seen);
1274
        list = g_list_next(list);
1275
    }
1276
    g_list_free(files);
1277

                
1278
    g_free(directory);
1279
    g_slice_free(struct _par2_thread_data, thread_data);
1280
}
1281

                
1282
static void
1283
on_collection_downloaded(NGPlugin *plugin_data, const char *collection_name, gpointer data)
1284
{
1285
    struct _par2_thread_data *thread_data = g_slice_new0(struct _par2_thread_data);
1286

                
1287
    thread_data->plugin_data = plugin_data;
1288
    strncpy(thread_data->collection_name, collection_name, sizeof(thread_data->collection_name) - 1);
1289

                
1290
    g_thread_pool_push(((PluginPAR2Priv *) plugin_data->priv)->thread_pool, thread_data, NULL);
1291
}
1292