Statistics
| Revision:

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

History | View | Annotate | Download (45.6 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
NGVariant *
269
nntpgrab_plugin_call_plugin_method(NGPlugin *plugin_data, const char *method, NGVariant *parameters, char **errmsg)
270
{
271
    return NULL;
272
}
273

                
274
#if 0 
275
static bool
276
LoadVerificationPacket(DiskFile *diskfile, u64 offset, PACKET_HEADER &header)
277
{
278
  VerificationPacket *packet = new VerificationPacket;
279

                
280
  // Load the packet from disk
281
  if (!packet->Load(diskfile, offset, header))
282
  {
283
    delete packet;
284
    return false;
285
  }
286

                
287
  // What is the fileid
288
  const MD5Hash &fileid = packet->FileId();
289

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

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

                
309
      return true;
310
    }
311
  }
312
  else
313
  {
314
    // Create a new source file for the packet
315
    sourcefile = new Par2RepairerSourceFile(NULL, packet);
316

                
317
    // Record the source file in the source file map
318
    sourcefilemap.insert(pair(fileid, sourcefile));
319

                
320
    return true;
321
  }
322
}
323
#endif
324

                
325
#if 0 
326
gboolean
327
nntpgrab_plugin_par2_load_verification_packet(const char *filename, PAR2Set **par2set)
328
{
329
    DiskFile *file;
330
    VerificationPacket *packet = NULL;
331

                
332
    g_return_val_if_fail(filename != NULL, FALSE);
333
    g_return_val_if_fail(par2set != NULL, FALSE);
334

                
335
    *par2set = NULL;
336

                
337
    file = new DiskFile();
338

                
339
    if (!file->Open(filename)) {
340
        delete file;
341
        return FALSE;
342
    }
343

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

                
346
    // How many useable packets have we found
347
    uint32_t packets = 0;
348

                
349
    // How big is the file
350
    guint64 filesize = file->FileSize();
351
    if (filesize <= 0) {
352
        delete file;
353
        g_print("Filesize == 0\n");
354
        return FALSE;
355
    }
356

                
357
    // Allocate a buffer to read data into
358
    // The buffer should be large enough to hold a whole
359
    // critical packet (i.e. file verification, file description, main,
360
    // and creator), but not necessarily a whole recovery packet.
361
    size_t buffersize = (size_t)min((u64)1048576, filesize);
362
    u8 *buffer = new u8[buffersize];
363

                
364
    // Start at the beginning of the file
365
    guint64 offset = 0;
366

                
367
    // Continue as long as there is at least enough for the packet header
368
    while (offset + sizeof(PACKET_HEADER) <= filesize) {
369
        // Attempt to read the next packet header
370
        PACKET_HEADER header;
371
        if (!file->Read(offset, &header, sizeof(header)))
372
            break;
373

                
374
        // Does this look like it might be a packet
375
        if (packet_magic != header.magic) {
376
            offset++;
377

                
378
            // Is there still enough for at least a whole packet header
379
            while (offset + sizeof(PACKET_HEADER) <= filesize) {
380
                // How much can we read into the buffer
381
                size_t want = (size_t)min((u64)buffersize, filesize-offset);
382

                
383
                // Fill the buffer
384
                if (!file->Read(offset, buffer, want)) {
385
                    offset = filesize;
386
                    break;
387
                }
388

                
389
                // Scan the buffer for the magic value
390
                u8 *current = buffer;
391
                u8 *limit = &buffer[want-sizeof(PACKET_HEADER)];
392
                while (current <= limit && packet_magic != ((PACKET_HEADER*)current)->magic) {
393
                    current++;
394
                }
395

                
396
                // What file offset did we reach
397
                offset += current-buffer;
398

                
399
                // Did we find the magic
400
                if (current <= limit) {
401
                    memcpy(&header, current, sizeof(header));
402
                    break;
403
                }
404
            }
405

                
406
            // Did we reach the end of the file
407
            if (offset + sizeof(PACKET_HEADER) > filesize)
408
            {
409
                break;
410
            }
411
        }
412

                
413
        // We have found the magic
414
        // Check the packet length
415
        if (sizeof(PACKET_HEADER) > header.length || // packet length is too small
416
            0 != (header.length & 3) ||              // packet length is not a multiple of 4
417
            filesize < offset + header.length) {     // packet would extend beyond the end of the file
418
                offset++;
419
                continue;
420
        }
421

                
422
        // Compute the MD5 Hash of the packet
423
        MD5Context context;
424
        context.Update(&header.setid, sizeof(header)-offsetof(PACKET_HEADER, setid));
425

                
426
        // How much more do I need to read to get the whole packet
427
        u64 current = offset+sizeof(PACKET_HEADER);
428
        u64 limit = offset+header.length;
429
        while (current < limit) {
430
            size_t want = (size_t)min((u64)buffersize, limit-current);
431

                
432
            if (!file->Read(current, buffer, want))
433
                break;
434

                
435
            context.Update(buffer, want);
436

                
437
            current += want;
438
        }
439

                
440
        // Did the whole packet get processed
441
        if (current < limit) {
442
            offset++;
443
            continue;
444
        }
445

                
446
        // Check the calculated packet hash against the value in the header
447
        MD5Hash hash;
448
        context.Final(hash);
449
        if (hash != header.hash) {
450
            offset++;
451
            continue;
452
        }
453

                
454
        // The Set ID is now validated, generate the PAR2Set structure
455
        *par2set = (PAR2Set*) g_slice_new0(PAR2Set);
456
        memcpy((*par2set)->set_id, header.setid.hash, sizeof(header.setid.hash));
457

                
458
        if (fileverificationpacket_type == header.type)  {
459
            //if (LoadVerificationPacket(file, offset, header)) {
460
                packets++;
461
            //}
462

                
463
            // Advance to the next packet
464
            offset += header.length;
465
        }
466
    }
467

                
468
    delete [] buffer;
469

                
470
    // We have finished with the file for now
471
    file->Close();
472

                
473
    // Did we actually find any interesting packets
474
    if (packets == 0) {
475
        if ((*par2set)) {
476
            g_slice_free(PAR2Set, (*par2set));
477
        }
478

                
479
        delete packet;
480
        delete file;
481
        return FALSE;
482
    }
483

                
484
    delete packet;
485
    delete file;
486

                
487
    return TRUE;
488
}
489
#endif
490

                
491
char *
492
strip_large_filenames(const char *filename)
493
{
494
    /* par2cmdline strips filenames which are larger than 56 characters. 
495
     * With this function, the same algoritm to strip such filenames can be applied */
496
    char filename_stripped[1024];
497
    int len = strlen(filename);
498

                
499
    if (len < 56) {
500
        return g_strdup(filename);
501
    }
502

                
503
    memset(filename_stripped, 0, sizeof(filename_stripped));
504
    strncpy(filename_stripped, filename, 28);
505
    strcat(filename_stripped, "...");
506
    strncat(filename_stripped, filename + len - 28, sizeof(filename_stripped) - 28 - 3 - 1);
507

                
508
    return g_strdup(filename_stripped);
509
}
510

                
511
struct _par2_repair_file {
512
    char filename[PATH_MAX];
513
    double verify_progress;
514
    int num_packets_found;
515
    int num_blocks_found;
516
    int num_duplicate_blocks_found;
517
    int num_blocks_expected;
518
    gboolean is_missing;
519
    gboolean is_complete;
520
    time_t stamp_last_progress_update;
521
};
522

                
523
static time_t stamp_last_repair_progress_update = 0;
524

                
525
static struct _par2_repair_file *
526
process_message(NGPlugin *plugin_data, GHashTable *files, struct _par2_repair_file *active_file, const char *line, ngboolean more_par2_sets_remaining_in_collection)
527
{
528
    struct _par2_repair_file *file = NULL;
529
    char *filename = NULL;
530
    char *filename_stripped = NULL;
531
    PluginPAR2Priv *priv = plugin_data->priv;
532
    const char *params[7];
533
    char progress_str[16];
534
    char num_packets_found_str[16];
535
    char num_blocks_found_str[16];
536
    char num_blocks_expected_str[16];
537

                
538
    // NOTE: par2cmdline can do wrapping of the filename!
539

                
540
    memset(&progress_str, 0, sizeof(progress_str));
541
    memset(&num_packets_found_str, 0, sizeof(num_packets_found_str));
542
    memset(&num_blocks_found_str, 0, sizeof(num_blocks_found_str));
543
    memset(&num_blocks_expected_str, 0, sizeof(num_blocks_expected_str));
544

                
545
    g_return_val_if_fail(priv->collection_name != NULL, NULL);
546
    g_return_val_if_fail(priv->par2filename != NULL, NULL);
547

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

                
551
        g_return_val_if_fail(end != NULL, NULL);
552

                
553
        filename = g_strndup(line + 9, end - line - 9);
554
        filename_stripped = strip_large_filenames(filename);
555
        file = g_hash_table_lookup(files, filename_stripped);
556
        if (!file) {
557
            file = g_slice_new0(struct _par2_repair_file);
558
            strncpy(file->filename, filename, sizeof(file->filename) - 1);
559
            g_hash_table_insert(files, g_strdup(filename_stripped), file);
560
        }
561

                
562
        g_free(filename_stripped);
563
        g_free(filename);
564

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

                
575
        active_file->verify_progress = g_ascii_strtod(line + 9, NULL);
576
        file = active_file;
577
        if (active_file->stamp_last_progress_update != time(NULL)) {
578
            snprintf(progress_str, sizeof(progress_str) - 1, "%.2f", file->verify_progress);
579

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

                
592
        if (sscanf(line, "Loaded %d new packets including %d recovery blocks", &active_file->num_packets_found, &active_file->num_blocks_found) != 1) {
593
            // line == 'Loaded x new packets'
594
            active_file->num_packets_found = atoi(line + 7);
595
        }
596

                
597
        active_file->verify_progress = 100.0;
598

                
599
        snprintf(num_packets_found_str, sizeof(num_packets_found_str) - 1, "%i", active_file->num_packets_found);
600
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", active_file->num_blocks_found);
601
        params[0] = priv->collection_name;
602
        params[1] = priv->par2filename;
603
        params[2] = active_file->filename;
604
        params[3] = num_packets_found_str;
605
        params[4] = num_blocks_found_str;
606
        params[5] = NULL;
607
        ng_plugin_emit_event(plugin_data, "par2_recovery_file_loaded", params);
608

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

                
616
        snprintf(num_packets_found_str, sizeof(num_packets_found_str) - 1, "%i", active_file->num_packets_found);
617
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", active_file->num_blocks_found);
618
        params[0] = priv->collection_name;
619
        params[1] = priv->par2filename;
620
        params[2] = active_file->filename;
621
        params[3] = num_packets_found_str;
622
        params[4] = num_blocks_found_str;
623
        params[5] = NULL;
624
        ng_plugin_emit_event(plugin_data, "par2_recovery_file_loaded", params);
625

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

                
631
        g_return_val_if_fail(end != NULL, NULL);
632

                
633
        filename = g_strndup(line + 11, end - line - 11);
634
        filename_stripped = strip_large_filenames(filename);
635
        file = g_hash_table_lookup(files, filename_stripped);
636

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

                
646
        g_free(filename_stripped);
647
        g_free(filename);
648

                
649
        g_return_val_if_fail(file != NULL, NULL);
650

                
651
        file->verify_progress = g_ascii_strtod(end + 3, NULL);
652
        if (file->stamp_last_progress_update != time(NULL)) {
653
            snprintf(progress_str, sizeof(progress_str) - 1, "%.2f", file->verify_progress);
654
            params[0] = priv->collection_name;
655
            params[1] = priv->par2filename;
656
            params[2] = file->filename;
657
            params[3] = progress_str;
658
            params[4] = NULL;
659
            ng_plugin_emit_event(plugin_data, "par2_load_progress_update", params);
660

                
661
            file->stamp_last_progress_update = time(NULL);
662
        }
663
    } else if (!strncmp(line, "Target: \"", 9)) {           // Target: "post832.rar" - found.
664
        char *end = strstr(line, "\" - ");
665
        const char *state_str = NULL;
666

                
667
        g_return_val_if_fail(end != NULL, NULL);
668

                
669
        filename = g_strndup(line + 9, end - line - 9);
670
        filename_stripped = strip_large_filenames(filename);
671
        file = g_hash_table_lookup(files, filename_stripped);
672

                
673
        // The file can be missing, add a fallback for that situation
674
        if (!file) {
675
            file = g_slice_new0(struct _par2_repair_file);
676
            strncpy(file->filename, filename, sizeof(file->filename) - 1);
677
            g_hash_table_insert(files, g_strdup(filename_stripped), file);
678
        }
679

                
680
        g_free(filename_stripped);
681
        g_free(filename);
682

                
683
        g_return_val_if_fail(file != NULL, NULL);
684

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

                
704
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", file->num_blocks_found);
705
        snprintf(num_blocks_expected_str, sizeof(num_blocks_expected_str) - 1, "%i", file->num_blocks_expected);
706

                
707
        params[0] = priv->collection_name,
708
        params[1] = priv->par2filename;
709
        params[2] = file->filename;
710
        params[3] = state_str;
711
        params[4] = num_blocks_found_str;
712
        params[5] = num_blocks_expected_str;
713
        params[6] = NULL;
714
        ng_plugin_emit_event(plugin_data, "par2_file_loaded", params);
715
    } 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".
716
        char *end = strstr(line, "\" - ");
717
        const char *state_str;
718

                
719
        g_return_val_if_fail(end != NULL, NULL);
720

                
721
        filename = g_strndup(line + 7, end - line - 7);
722
        filename_stripped = strip_large_filenames(filename);
723
        file = g_hash_table_lookup(files, filename_stripped);
724

                
725
        // The file can be missing, add a fallback for that situation
726
        if (!file) {
727
            file = g_slice_new0(struct _par2_repair_file);
728
            strncpy(file->filename, filename, sizeof(file->filename) - 1);
729
            g_hash_table_insert(files, g_strdup(filename_stripped), file);
730
        }
731

                
732
        g_free(filename);
733

                
734
        g_return_val_if_fail(file != NULL, NULL);
735

                
736
        end += 4;
737
        if (strncmp(end, "no data found.", 14) &&
738
            strncmp(end, "is a match for", 14) &&
739
            !strstr(end, "duplicate data blocks.") &&
740
            !strstr(end, "data blocks from several target files")) {
741

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

                
744
            if (file->num_blocks_found == file->num_blocks_expected) {
745
                state_str = "FOUND";
746
            } else {
747
                state_str = "DAMAGED";
748
            }
749
        } else if (!strncmp(end, "no data found.", 14) ||
750
                    strstr(end, "data blocks from several target files") ||
751
                    strstr(end, "duplicate data blocks.")) {
752

                
753
            state_str = "NO_NEW_BLOCKS_FOUND";
754
        } else if (!strncmp(end, "is a match for", 14)) {
755
            file->num_blocks_found = file->num_blocks_expected = 1;
756
            file->verify_progress = 100.0;
757

                
758
            state_str = "FOUND";
759
        } else {
760
g_print(__FILE__ ":%i unknown state for PAR2 message: %s (end = %s)\n", __LINE__, line, end);
761
            state_str = "MISSING";
762
        }
763

                
764
        g_free(filename_stripped);
765

                
766
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", file->num_blocks_found);
767
        snprintf(num_blocks_expected_str, sizeof(num_blocks_expected_str) - 1, "%i", file->num_blocks_expected);
768

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

                
781
        priv->repair_succeeded = FALSE;
782
        priv->command_completed_normally = TRUE;
783

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

                
797
            params[0] = priv->collection_name;
798
            params[1] = priv->par2filename;
799
            params[2] = progress_str;
800
            params[3] = NULL;
801
            ng_plugin_emit_event(plugin_data, "par2_repair_progress_update", params);
802
            stamp_last_repair_progress_update = time(NULL);
803
        }
804
    } else if (!strncmp(line, "Repair complete.", 16)) {
805
        priv->repair_succeeded = TRUE;
806
        priv->command_completed_normally = TRUE;
807

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

                
821
        params[0] = priv->collection_name;
822
        params[1] = priv->par2filename;
823
        if (more_par2_sets_remaining_in_collection) {
824
            params[2] = "TRUE";
825
        } else {
826
            params[2] = "FALSE";
827
        }
828
        params[3] = NULL;
829
        ng_plugin_emit_event(plugin_data, "par2_no_repair_required", params);
830
    } else if (!strncmp(line, "Could not read", 14)) {
831
        priv->repair_succeeded = FALSE;
832
        priv->command_completed_normally = TRUE;
833

                
834
        params[0] = priv->collection_name;
835
        params[1] = priv->par2filename;
836
        params[2] = line;
837
        params[3] = "-1";
838
        params[4] = NULL;
839
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
840
    } else if (!strncmp(line, "The recovery file does not exist", 32)) {
841
        priv->repair_succeeded = FALSE;
842
        priv->command_completed_normally = TRUE;
843

                
844
        params[0] = priv->collection_name;
845
        params[1] = priv->par2filename;
846
        params[2] = "Bug in par2cmdline was triggered";
847
        params[3] = "-1";
848
        params[4] = NULL;
849
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
850
    } else if (strstr(line, "cannot be renamed to")) {
851
        priv->repair_succeeded = FALSE;
852
        priv->command_completed_normally = TRUE;
853

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

                
890
static void
891
par2_repair_file_free(gpointer data)
892
{
893
    g_slice_free(struct _par2_repair_file, data);
894
}
895

                
896
static gboolean
897
verify_par2_extension(const char *filename)
898
{
899
    char *ptr = g_strrstr(filename, ".");
900

                
901
    if (!ptr) {
902
        /* File doesn't have an extension */
903
        return FALSE;
904
    }
905

                
906
    /* Does the filename have an '.par' or '.par2' extension? */
907
    if (!g_ascii_strncasecmp(ptr, ".par2", 5) || !g_strncasecmp(ptr, ".par", 4)) {
908
        return TRUE;
909
    }
910

                
911
    /* Test for files with the extension '.par.1' or '.par2.1' */
912
    ptr = g_strrstr(filename, ".");
913
    if (!ptr) {
914
        return FALSE;
915
    }
916

                
917
    if (!g_ascii_strncasecmp(ptr, ".par2.", 6) || !g_strncasecmp(ptr, ".par.", 5)) {
918
        return TRUE;
919
    }
920

                
921
    return FALSE;
922
}
923

                
924
static gboolean
925
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)
926
{
927
    FILE *fp;
928
    char *cmd;
929
    char buf[1024];
930
    int i;
931
    int c;
932
    struct _par2_repair_file *active_file = NULL;
933
    const char *disable_concurrent_verify_option;
934
    gboolean first_line = TRUE;
935
#ifndef WIN32
936
    gboolean second_line = FALSE;
937
#endif
938
    PluginPAR2Priv *priv;
939
    static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
940
    NGConfigOpts opts;
941
    GList *list;
942
#ifdef WIN32
943
    char *filename;
944
    char *dirname = g_locale_from_utf8(directory, -1, NULL, NULL, NULL);
945
    char *bindir = g_path_get_dirname(__argv[0]);
946
    char *cmd2;
947

                
948
    g_return_val_if_fail(dirname != NULL, FALSE);
949
#endif
950

                
951
    priv = plugin_data->priv;
952

                
953
    if (priv->abort_flag) {
954
        const char *params[5];
955

                
956
        priv->repair_succeeded = FALSE;
957
        params[0] = priv->collection_name;
958
        params[1] = priv->par2filename;
959
        params[2] = "Error: par2cmdline aborted unexpectedly";
960
        params[3] = "-1";
961
        params[4] = NULL;
962
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
963

                
964
        return FALSE;
965
    }
966

                
967
    g_static_mutex_lock(&mutex);
968

                
969
    if (try_to_disable_concurrent_verify) {
970
        disable_concurrent_verify_option = "-t0";
971
    } else {
972
        disable_concurrent_verify_option = "";
973
    }
974

                
975
#ifdef WIN32
976
    filename = g_locale_from_utf8(par2filename, -1, NULL, NULL, NULL);
977

                
978
    g_return_val_if_fail(filename != NULL, FALSE);
979

                
980
    cmd = g_strdup_printf("\"%s\\par2.exe\" r %s \"%s\" \"%s"G_DIR_SEPARATOR_S"*.*\" 2>&1", bindir, disable_concurrent_verify_option, filename, dirname);
981
    g_free(filename);
982
    g_free(dirname);
983
    g_free(bindir);
984
#else
985
    cmd = g_strdup_printf("echo \"pid$$\"; par2 r %s \"%s\" \"%s"G_DIR_SEPARATOR_S"\"*.* 2>&1", disable_concurrent_verify_option, par2filename, directory);
986
#endif
987
    fp = popen(cmd, "r");
988

                
989
#ifdef WIN32
990
    cmd2 = g_locale_to_utf8(cmd, -1, NULL, NULL, NULL);
991
    g_return_val_if_fail(cmd2 != NULL, FALSE);
992
    ng_plugin_emit_log_msg(plugin_data, 1, _("Launched the command: %s\n"), cmd2);
993
    g_free(cmd2);
994
#else
995
    ng_plugin_emit_log_msg(plugin_data, 1, _("Launched the command: %s\n"), cmd);
996
#endif
997

                
998
    if (!fp) {
999
        ng_plugin_emit_log_msg(plugin_data, 1, _("Error while launching from the par2cmdline process\n%s\n%s"), cmd, strerror(errno));
1000
        g_free(cmd);
1001
        g_static_mutex_unlock(&mutex);
1002
        return FALSE;
1003
    }
1004
    g_free(cmd);
1005

                
1006
    priv->collection_name = collection_name;
1007
    priv->par2filename = par2filename;
1008
    priv->par2files = NULL;
1009
    priv->pid = -1;
1010

                
1011
    // Assume the repair was succesfull unless it explicit failed
1012
    priv->repair_succeeded = TRUE;
1013

                
1014
    // Detect abnormal aborts of par2cmdline
1015
    priv->command_completed_normally = FALSE;
1016

                
1017
    memset(buf, 0, sizeof(buf));
1018
    i = 0;
1019
    while ((c = fgetc(fp)) != EOF) {
1020
        if (priv->abort_flag) {
1021
            break;
1022
        }
1023

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

                
1038
                    pclose(fp);
1039

                
1040
                    priv->collection_name = NULL;
1041
                    priv->par2filename = NULL;
1042

                
1043
                    g_static_mutex_unlock(&mutex);
1044

                
1045
                    return FALSE;
1046
                }
1047
            }
1048

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

                
1060
            i = 0;
1061
            active_file = process_message(plugin_data, files, active_file, buf, more_par2_sets_remaining_in_collection);
1062
// imported_funcs.emit_repair_message(buf);
1063
            memset(buf, 0, sizeof(buf));
1064
        } else if (c == '\n') {
1065
            // Ignore
1066
        } else {
1067
            buf[i] = (char) c;
1068
            i++;
1069
        }
1070
    }
1071

                
1072
    pclose(fp);
1073

                
1074
    if (!priv->command_completed_normally) {
1075
        /* Abnormal abort of par2cmdline detected, send a notification to the frontend */
1076
        const char *params[5];
1077

                
1078
        priv->repair_succeeded = FALSE;
1079
        params[0] = priv->collection_name;
1080
        params[1] = priv->par2filename;
1081
        params[2] = "Error: par2cmdline aborted unexpectedly";
1082
        params[3] = "-1";
1083
        params[4] = NULL;
1084
        ng_plugin_emit_event(plugin_data, "par2_repair_failure", params);
1085
    }
1086

                
1087
    /* Remove the PAR2 files after a successful verification/recovery if it's enabled */
1088
    opts = plugin_data->core_funcs.config_get_opts();
1089
    list = priv->par2files;
1090
    while (list) {
1091
        char *filename = list->data;
1092

                
1093
        /* 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 */
1094
        if (priv->repair_succeeded && !more_par2_sets_remaining_in_collection && opts.auto_remove_files_after_repair) {
1095
            if (verify_par2_extension(filename)) {
1096
                char *path = g_build_filename(directory, filename, NULL);
1097
                ng_plugin_emit_log_msg(plugin_data, NG_LOG_LEVEL_DEBUG, _("Now automatically removing file '%s'"), path);
1098
                g_unlink(path);
1099
                g_free(path);
1100
            } else {
1101
                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);
1102
            }
1103
        }
1104

                
1105
        g_free(filename);
1106

                
1107
        list = g_list_next(list);
1108
    }
1109
    g_list_free(priv->par2files);
1110

                
1111
    priv->collection_name = NULL;
1112
    priv->par2filename = NULL;
1113

                
1114
    g_static_mutex_unlock(&mutex);
1115

                
1116
    return priv->repair_succeeded;
1117
}
1118

                
1119
static gboolean
1120
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)
1121
{
1122
    char *path;
1123
    GHashTable *files = NULL;
1124
    GDir *dir;
1125
    GError *error = NULL;
1126
    const char *filename;
1127
    char *filename_stripped;
1128
    gboolean invalid_option_detected = FALSE;
1129
    gboolean repair_succeeded;
1130

                
1131
    path = g_build_filename(directory, par2filename, NULL);
1132
    g_return_val_if_fail(g_file_test(path, G_FILE_TEST_EXISTS), FALSE);
1133

                
1134
    // Build a list of all the files which are in the directory
1135
    dir = g_dir_open(directory, 0, &error);
1136
    if (!dir) {
1137
        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);
1138
        g_error_free(error);
1139
        return FALSE;
1140
    }
1141

                
1142
    files = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, par2_repair_file_free);
1143

                
1144
    while ((filename = g_dir_read_name(dir))) {
1145
        struct _par2_repair_file *file = g_slice_new0(struct _par2_repair_file);
1146

                
1147
        strncpy(file->filename, filename, sizeof(file->filename) - 1);
1148
        filename_stripped = strip_large_filenames(filename);
1149
        g_hash_table_insert(files, filename_stripped, file);
1150
        /* No need to g_free(filename_stripped) here as that will be done by the GHashTable itself */
1151
    }
1152

                
1153
    g_dir_close(dir);
1154

                
1155
    ng_plugin_emit_log_msg(plugin_data, 1, "%s", _("Now starting PAR2 repair"));
1156

                
1157
    repair_succeeded = par2_start_par2cmdline(plugin_data, collection_name, directory, path, files, TRUE, &invalid_option_detected, more_par2_sets_remaining_in_collection);
1158
    if (!repair_succeeded && invalid_option_detected) {
1159
        /* The regular par2cmdline was detected, try again with the -t0 option */
1160
        repair_succeeded = par2_start_par2cmdline(plugin_data, collection_name, directory, path, files, FALSE, NULL, more_par2_sets_remaining_in_collection);
1161
    }
1162

                
1163
    if (repair_succeeded) {
1164
        ng_plugin_emit_log_msg(plugin_data, 1, "%s", _("PAR2 repair finished successfully"));
1165
    } else {
1166
        ng_plugin_emit_log_msg(plugin_data, 1, "%s", _("PAR2 repair failed"));
1167
    }
1168

                
1169
    g_free(path);
1170
    g_hash_table_destroy(files);
1171

                
1172
    return repair_succeeded;
1173
}
1174

                
1175
typedef struct _par2_file_seen {
1176
    char filename_without_extension[256];
1177
    char filename[256];
1178
} PAR2FileSeen;
1179

                
1180
/* APARTE THREAD ! */
1181
struct _par2_thread_data {
1182
    NGPlugin *plugin_data;
1183
    char collection_name[256];
1184
};
1185

                
1186
static void
1187
par2_thread_func(gpointer data, gpointer user_data)
1188
{
1189
    struct _par2_thread_data *thread_data = (struct _par2_thread_data *) data;
1190
    NGConfigOpts opts;
1191
    char *directory;
1192
    GList *list;
1193
    GList *list2;
1194
    GList *files = NULL;
1195
    GDir *dir;
1196
    GError *err = NULL;
1197
    const char *filename;
1198
    gboolean repair_successful;
1199
    char *errmsg = NULL;
1200

                
1201
    /* Do we need to repair? */
1202
    opts = thread_data->plugin_data->core_funcs.config_get_opts();
1203
    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) {
1204
        g_slice_free(struct _par2_thread_data, thread_data);
1205
        return;
1206
    }
1207

                
1208
    /* Find out all the unique PAR2 sets in this collection */
1209
    directory = g_build_filename(opts.download_directory, thread_data->collection_name, NULL);
1210
    dir = g_dir_open(directory, 0, &err);
1211
    if (!dir) {
1212
        ng_plugin_emit_log_msg(thread_data->plugin_data, NG_LOG_LEVEL_WARNING, _("Unable to open directory '%s': %s"), directory, err->message);
1213
        g_error_free(err);
1214
        g_free(directory);
1215
        g_slice_free(struct _par2_thread_data, thread_data);
1216
        return;
1217
    }
1218

                
1219
    while ((filename = g_dir_read_name(dir))) {
1220
        char *extension;
1221
        char filename_without_extension[256];
1222
        gboolean filename_seen = FALSE;
1223
        char *ptr;
1224

                
1225
        extension = g_strrstr(filename, ".");
1226
        if (!extension) {
1227
            /* File without extension, ignore */
1228
            continue;
1229
        }
1230

                
1231
        if (g_ascii_strncasecmp(extension, ".par", 4) != 0) {
1232
            /* Different extension, ignore */
1233
            continue;
1234
        }
1235

                
1236
        memset(filename_without_extension, 0, sizeof(filename_without_extension));
1237
        strncpy(filename_without_extension, filename, extension - filename);
1238

                
1239
        // Strip any .volXX+YY part
1240
        ptr = g_strrstr(filename_without_extension, ".");
1241
        if (ptr && !g_strncasecmp(ptr, ".vol", 4)) {
1242
            *ptr = '\0';
1243
        }
1244

                
1245
        list2 = files;
1246
        while (list2) {
1247
            PAR2FileSeen *file = (PAR2FileSeen*) list2->data;
1248

                
1249
            if (!strcmp(filename_without_extension, file->filename_without_extension)) {
1250
                // We've already seen this PAR2 set, ignore
1251
                filename_seen = TRUE;
1252
                break;
1253
            }
1254

                
1255
            list2 = g_list_next(list2);
1256
        }
1257

                
1258
        if (!filename_seen) {
1259
            PAR2FileSeen *file_seen = g_slice_new0(PAR2FileSeen);
1260

                
1261
            strncpy(file_seen->filename_without_extension, filename_without_extension, sizeof(file_seen->filename_without_extension) - 1);
1262
            strncpy(file_seen->filename, filename, sizeof(file_seen->filename) - 1);
1263

                
1264
            files = g_list_append(files, file_seen);
1265
        }
1266
    }
1267

                
1268
    g_dir_close(dir);
1269

                
1270
    if (!files) {
1271
        ng_plugin_emit_log_msg(thread_data->plugin_data, NG_LOG_LEVEL_DEBUG, _("No .par2 files found for collection '%s'"), thread_data->collection_name);
1272
    }
1273

                
1274
    /* Perform the repair */
1275
    list = files;
1276
    repair_successful = TRUE;
1277
    while (list) {
1278
        PAR2FileSeen *file_seen = (PAR2FileSeen*) list->data;
1279

                
1280
        if (!nntpgrab_plugin_par2_repair_files(thread_data->plugin_data, thread_data->collection_name, directory, file_seen->filename, (list->next != NULL))) {
1281
            repair_successful = FALSE;
1282
        }
1283

                
1284
        g_slice_free(PAR2FileSeen, file_seen);
1285
        list = g_list_next(list);
1286
    }
1287
    g_list_free(files);
1288

                
1289
    /* Automatically remove the collection if necessary and automatic unpack is disabled */
1290
    if (repair_successful) {
1291
        if (opts.enable_auto_unpack) {
1292
            ng_plugin_emit_log_msg(thread_data->plugin_data, NG_LOG_LEVEL_INFO, _("No need to remove collection '%s' as automatic unpack is enabled"), thread_data->collection_name);
1293
        } else if (opts.auto_remove_collections_after_download) {
1294
            ng_plugin_emit_log_msg(thread_data->plugin_data, NG_LOG_LEVEL_INFO, _("Now trying to remove collection '%s'"), thread_data->collection_name);
1295

                
1296
            if (!thread_data->plugin_data->core_funcs.schedular_del_file_from_queue(thread_data->collection_name, NULL, &errmsg)) {
1297
                ng_plugin_emit_log_msg(thread_data->plugin_data, NG_LOG_LEVEL_WARNING, _("Removing collection '%s' failed: %s"), thread_data->collection_name, errmsg);
1298
                g_free(errmsg);
1299
            }
1300
        }
1301
    } else {
1302
        ng_plugin_emit_log_msg(thread_data->plugin_data, NG_LOG_LEVEL_INFO, _("PAR2 repair was unsuccesful, no need to remove collection '%s'"), thread_data->collection_name);
1303
    }
1304

                
1305
    /* Manually save the download queue to update the administration */
1306
    thread_data->plugin_data->core_funcs.schedular_save_queue(NULL);
1307

                
1308
    g_free(directory);
1309
    g_slice_free(struct _par2_thread_data, thread_data);
1310
}
1311

                
1312
static void
1313
on_collection_downloaded(NGPlugin *plugin_data, const char *collection_name, gpointer data)
1314
{
1315
    struct _par2_thread_data *thread_data = g_slice_new0(struct _par2_thread_data);
1316

                
1317
    thread_data->plugin_data = plugin_data;
1318
    strncpy(thread_data->collection_name, collection_name, sizeof(thread_data->collection_name) - 1);
1319

                
1320
    g_thread_pool_push(((PluginPAR2Priv *) plugin_data->priv)->thread_pool, thread_data, NULL);
1321
}
1322