Statistics
| Revision:

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

History | View | Annotate | Download (44 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
#define popen pt_popen
55
#define pclose pt_pclose
56

                
57
HANDLE my_pipein[2], my_pipeout[2], my_pipeerr[2];
58
char   my_popenmode = ' ';
59

                
60
static int
61
my_pipe(HANDLE *readwrite)
62
{
63
    SECURITY_ATTRIBUTES sa;
64

                
65
    sa.nLength = sizeof(sa);
66
    sa.bInheritHandle = 1;
67
    sa.lpSecurityDescriptor = NULL;
68

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

                
74
    return 0;
75
}
76

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

                
85
    my_pipein[0]   = INVALID_HANDLE_VALUE;
86
    my_pipein[1]   = INVALID_HANDLE_VALUE;
87
    my_pipeout[0]  = INVALID_HANDLE_VALUE;
88
    my_pipeout[1]  = INVALID_HANDLE_VALUE;
89
    my_pipeerr[0]  = INVALID_HANDLE_VALUE;
90
    my_pipeerr[1]  = INVALID_HANDLE_VALUE;
91

                
92
    if (!mode || !*mode)
93
        goto finito;
94

                
95
    my_popenmode = *mode;
96
    if (my_popenmode != 'r' && my_popenmode != 'w')
97
        goto finito;
98

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

                
105
    if (my_pipe(my_pipein)  == -1 || my_pipe(my_pipeout) == -1)
106
        goto finito;
107
    if (!catch_stderr && my_pipe(my_pipeerr) == -1)
108
      goto finito;
109

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

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

                
131
    if (!success)
132
        goto finito;
133

                
134
    CloseHandle(my_pipein[0]);  my_pipein[0]  = INVALID_HANDLE_VALUE;
135
    CloseHandle(my_pipeout[1]); my_pipeout[1] = INVALID_HANDLE_VALUE;
136
    CloseHandle(my_pipeerr[1]); my_pipeerr[1] = INVALID_HANDLE_VALUE;
137

                
138
    if (my_popenmode == 'r')
139
        fptr = _fdopen(_open_osfhandle((long)my_pipeout[0],_O_BINARY),"r");
140
    else
141
        fptr = _fdopen(_open_osfhandle((long)my_pipein[1],_O_BINARY),"w");
142

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

                
159
    return fptr;
160
}
161

                
162
static int
163
pt_pclose(FILE *file)
164
{
165
    if (!file) {
166
        return -1;
167
    }
168

                
169
    fclose(file);
170

                
171
    CloseHandle(my_pipeerr[0]);
172
    if (my_popenmode == 'r')
173
        CloseHandle(my_pipein[1]);
174
    else
175
       CloseHandle(my_pipeout[0]);
176

                
177
    return 0;
178
}
179
#endif
180

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

                
190
    ng_plugin_set_required_event(plugin_data, "collection_downloaded");
191

                
192
    ng_plugin_register_function(plugin_data,
193
                                "par2_repair_files",
194
                                NG_PLUGIN_FUNCTION(nntpgrab_plugin_par2_repair_files),
195
                                ng_plugin_marshal_BOOLEAN__STRING_STRING_STRING,
196
                                G_TYPE_BOOLEAN,
197
                                3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
198

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

                
209
ngboolean
210
nntpgrab_plugin_load(NGPlugin *plugin_data, char **errmsg)
211
{
212
    GError *err = NULL;
213
    PluginPAR2Priv *priv;
214

                
215
    plugin_data->priv = g_slice_new0(PluginPAR2Priv);
216
    priv = plugin_data->priv;
217
    priv->abort_flag = FALSE;
218
    priv->thread_pool = g_thread_pool_new(par2_thread_func, plugin_data, 1, FALSE, &err);
219

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

                
226
    ng_plugin_connect_event(plugin_data, "collection_downloaded", NG_PLUGIN_FUNCTION(on_collection_downloaded), NULL);
227

                
228
    return TRUE;
229
}
230

                
231
ngboolean
232
nntpgrab_plugin_can_unload(NGPlugin *plugin_data, char **reason)
233
{
234
    return TRUE;
235
}
236

                
237
void
238
nntpgrab_plugin_unload(NGPlugin *plugin_data)
239
{
240
    PluginPAR2Priv *priv = plugin_data->priv;
241

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

                
250
    g_slice_free(PluginPAR2Priv, plugin_data->priv);
251
    plugin_data->priv = NULL;
252
}
253

                
254
void
255
nntpgrab_plugin_destroy(NGPlugin *plugin_data)
256
{
257
}
258

                
259
int
260
nntpgrab_plugin_get_version(void)
261
{
262
    return NNTPGRAB_PLUGIN_API_VERSION;
263
}
264

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

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

                
278
  // What is the fileid
279
  const MD5Hash &fileid = packet->FileId();
280

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

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

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

                
308
    // Record the source file in the source file map
309
    sourcefilemap.insert(pair(fileid, sourcefile));
310

                
311
    return true;
312
  }
313
}
314
#endif
315

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

                
323
    g_return_val_if_fail(filename != NULL, FALSE);
324
    g_return_val_if_fail(par2set != NULL, FALSE);
325

                
326
    *par2set = NULL;
327

                
328
    file = new DiskFile();
329

                
330
    if (!file->Open(filename)) {
331
        delete file;
332
        return FALSE;
333
    }
334

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

                
337
    // How many useable packets have we found
338
    uint32_t packets = 0;
339

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

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

                
355
    // Start at the beginning of the file
356
    guint64 offset = 0;
357

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

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

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

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

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

                
387
                // What file offset did we reach
388
                offset += current-buffer;
389

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

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

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

                
413
        // Compute the MD5 Hash of the packet
414
        MD5Context context;
415
        context.Update(&header.setid, sizeof(header)-offsetof(PACKET_HEADER, setid));
416

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

                
423
            if (!file->Read(current, buffer, want))
424
                break;
425

                
426
            context.Update(buffer, want);
427

                
428
            current += want;
429
        }
430

                
431
        // Did the whole packet get processed
432
        if (current < limit) {
433
            offset++;
434
            continue;
435
        }
436

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

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

                
449
        if (fileverificationpacket_type == header.type)  {
450
            //if (LoadVerificationPacket(file, offset, header)) {
451
                packets++;
452
            //}
453

                
454
            // Advance to the next packet
455
            offset += header.length;
456
        }
457
    }
458

                
459
    delete [] buffer;
460

                
461
    // We have finished with the file for now
462
    file->Close();
463

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

                
470
        delete packet;
471
        delete file;
472
        return FALSE;
473
    }
474

                
475
    delete packet;
476
    delete file;
477
#endif
478

                
479
    return TRUE;
480
}
481

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

                
490
    if (len < 56) {
491
        return g_strdup(filename);
492
    }
493

                
494
    memset(filename_stripped, 0, sizeof(filename_stripped));
495
    strncpy(filename_stripped, filename, 28);
496
    strcat(filename_stripped, "...");
497
    strncat(filename_stripped, filename + len - 28, sizeof(filename_stripped) - 28 - 3 - 1);
498

                
499
    return g_strdup(filename_stripped);
500
}
501

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

                
514
static time_t stamp_last_repair_progress_update = 0;
515

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

                
529
    // NOTE: par2cmdline can do wrapping of the filename!
530

                
531
    memset(&progress_str, 0, sizeof(progress_str));
532
    memset(&num_packets_found_str, 0, sizeof(num_packets_found_str));
533
    memset(&num_blocks_found_str, 0, sizeof(num_blocks_found_str));
534
    memset(&num_blocks_expected_str, 0, sizeof(num_blocks_expected_str));
535

                
536
    g_return_val_if_fail(priv->collection_name != NULL, NULL);
537
    g_return_val_if_fail(priv->par2filename != NULL, NULL);
538

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

                
542
        g_return_val_if_fail(end != NULL, NULL);
543

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

                
553
        g_free(filename_stripped);
554
        g_free(filename);
555

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

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

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

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

                
588
        active_file->verify_progress = 100.0;
589

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

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

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

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

                
622
        g_return_val_if_fail(end != NULL, NULL);
623

                
624
        filename = g_strndup(line + 11, end - line - 11);
625
        filename_stripped = strip_large_filenames(filename);
626
        file = g_hash_table_lookup(files, filename_stripped);
627

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

                
637
        g_free(filename_stripped);
638
        g_free(filename);
639

                
640
        g_return_val_if_fail(file != NULL, NULL);
641

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

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

                
658
        g_return_val_if_fail(end != NULL, NULL);
659

                
660
        filename = g_strndup(line + 9, end - line - 9);
661
        filename_stripped = strip_large_filenames(filename);
662
        file = g_hash_table_lookup(files, filename_stripped);
663

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

                
671
        g_free(filename_stripped);
672
        g_free(filename);
673

                
674
        g_return_val_if_fail(file != NULL, NULL);
675

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

                
695
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", file->num_blocks_found);
696
        snprintf(num_blocks_expected_str, sizeof(num_blocks_expected_str) - 1, "%i", file->num_blocks_expected);
697

                
698
        params[0] = priv->collection_name,
699
        params[1] = priv->par2filename;
700
        params[2] = file->filename;
701
        params[3] = state_str;
702
        params[4] = num_blocks_found_str;
703
        params[5] = num_blocks_expected_str;
704
        params[6] = NULL;
705
        ng_plugin_emit_event(plugin_data, "par2_file_loaded", params);
706
    } 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".
707
        char *end = strstr(line, "\" - ");
708
        const char *state_str;
709

                
710
        g_return_val_if_fail(end != NULL, NULL);
711

                
712
        filename = g_strndup(line + 7, end - line - 7);
713
        filename_stripped = strip_large_filenames(filename);
714
        file = g_hash_table_lookup(files, filename_stripped);
715

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

                
723
        g_free(filename);
724

                
725
        g_return_val_if_fail(file != NULL, NULL);
726

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

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

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

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

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

                
755
        g_free(filename_stripped);
756

                
757
        snprintf(num_blocks_found_str, sizeof(num_blocks_found_str) - 1, "%i", file->num_blocks_found);
758
        snprintf(num_blocks_expected_str, sizeof(num_blocks_expected_str) - 1, "%i", file->num_blocks_expected);
759

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

                
772
        priv->repair_succeeded = FALSE;
773
        priv->command_completed_normally = TRUE;
774

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

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

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

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

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

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

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

                
881
static void
882
par2_repair_file_free(gpointer data)
883
{
884
    g_slice_free(struct _par2_repair_file, data);
885
}
886

                
887
static gboolean
888
verify_par2_extension(const char *filename)
889
{
890
    char *ptr = g_strrstr(filename, ".");
891

                
892
    if (!ptr) {
893
        /* File doesn't have an extension */
894
        return FALSE;
895
    }
896

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

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

                
908
    if (!g_ascii_strncasecmp(ptr, ".par2.", 6) || !g_strncasecmp(ptr, ".par.", 5)) {
909
        return TRUE;
910
    }
911

                
912
    return FALSE;
913
}
914

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

                
939
    g_return_val_if_fail(dirname != NULL, FALSE);
940
#endif
941

                
942
    priv = plugin_data->priv;
943

                
944
    if (priv->abort_flag) {
945
        const char *params[5];
946

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

                
955
        return FALSE;
956
    }
957

                
958
    g_static_mutex_lock(&mutex);
959

                
960
    if (try_to_disable_concurrent_verify) {
961
        disable_concurrent_verify_option = "-t0";
962
    } else {
963
        disable_concurrent_verify_option = "";
964
    }
965

                
966
#ifdef WIN32
967
    filename = g_locale_from_utf8(par2filename, -1, NULL, NULL, NULL);
968

                
969
    g_return_val_if_fail(filename != NULL, FALSE);
970

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

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

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

                
997
    priv->collection_name = collection_name;
998
    priv->par2filename = par2filename;
999
    priv->par2files = NULL;
1000
    priv->pid = -1;
1001

                
1002
    // Assume the repair was succesfull unless it explicit failed
1003
    priv->repair_succeeded = TRUE;
1004

                
1005
    // Detect abnormal aborts of par2cmdline
1006
    priv->command_completed_normally = FALSE;
1007

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

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

                
1029
                    pclose(fp);
1030

                
1031
                    priv->collection_name = NULL;
1032
                    priv->par2filename = NULL;
1033

                
1034
                    g_static_mutex_unlock(&mutex);
1035

                
1036
                    return FALSE;
1037
                }
1038
            }
1039

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

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

                
1063
    pclose(fp);
1064

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

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

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

                
1084
        /* 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 */
1085
        if (priv->repair_succeeded && !more_par2_sets_remaining_in_collection && opts.auto_remove_files_after_repair) {
1086
            if (verify_par2_extension(filename)) {
1087
                char *path = g_build_filename(directory, filename, NULL);
1088
                ng_plugin_emit_log_msg(plugin_data, NG_LOG_LEVEL_DEBUG, _("Now automatically removing file '%s'"), path);
1089
                g_unlink(path);
1090
                g_free(path);
1091
            } else {
1092
                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);
1093
            }
1094
        }
1095

                
1096
        g_free(filename);
1097

                
1098
        list = g_list_next(list);
1099
    }
1100
    g_list_free(priv->par2files);
1101

                
1102
    priv->collection_name = NULL;
1103
    priv->par2filename = NULL;
1104

                
1105
    g_static_mutex_unlock(&mutex);
1106

                
1107
    return priv->repair_succeeded;
1108
}
1109

                
1110
static gboolean
1111
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)
1112
{
1113
    char *path;
1114
    GHashTable *files = NULL;
1115
    GDir *dir;
1116
    GError *error = NULL;
1117
    const char *filename;
1118
    char *filename_stripped;
1119
    gboolean invalid_option_detected = FALSE;
1120
    gboolean repair_succeeded;
1121

                
1122
    path = g_build_filename(directory, par2filename, NULL);
1123
    g_return_val_if_fail(g_file_test(path, G_FILE_TEST_EXISTS), FALSE);
1124

                
1125
    // Build a list of all the files which are in the directory
1126
    dir = g_dir_open(directory, 0, &error);
1127
    if (!dir) {
1128
        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);
1129
        g_error_free(error);
1130
        return FALSE;
1131
    }
1132

                
1133
    files = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, par2_repair_file_free);
1134

                
1135
    while ((filename = g_dir_read_name(dir))) {
1136
        struct _par2_repair_file *file = g_slice_new0(struct _par2_repair_file);
1137

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

                
1144
    g_dir_close(dir);
1145

                
1146
    ng_plugin_emit_log_msg(plugin_data, 1, "%s", _("Now starting PAR2 repair"));
1147

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

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

                
1160
    g_free(path);
1161
    g_hash_table_destroy(files);
1162

                
1163
    return repair_succeeded;
1164
}
1165

                
1166
typedef struct _par2_file_seen {
1167
    char filename_without_extension[256];
1168
    char filename[256];
1169
} PAR2FileSeen;
1170

                
1171
/* APARTE THREAD ! */
1172
struct _par2_thread_data {
1173
    NGPlugin *plugin_data;
1174
    char collection_name[256];
1175
};
1176

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

                
1190
    /* Do we need to repair? */
1191
    opts = thread_data->plugin_data->core_funcs.config_get_opts();
1192
    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) {
1193
        g_slice_free(struct _par2_thread_data, thread_data);
1194
        return;
1195
    }
1196

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

                
1208
    while ((filename = g_dir_read_name(dir))) {
1209
        char *extension;
1210
        char filename_without_extension[256];
1211
        gboolean filename_seen = FALSE;
1212
        char *ptr;
1213

                
1214
        extension = g_strrstr(filename, ".");
1215
        if (!extension) {
1216
            /* File without extension, ignore */
1217
            continue;
1218
        }
1219

                
1220
        if (g_ascii_strncasecmp(extension, ".par", 4) != 0) {
1221
            /* Different extension, ignore */
1222
            continue;
1223
        }
1224

                
1225
        memset(filename_without_extension, 0, sizeof(filename_without_extension));
1226
        strncpy(filename_without_extension, filename, extension - filename);
1227

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

                
1234
        list2 = files;
1235
        while (list2) {
1236
            PAR2FileSeen *file = (PAR2FileSeen*) list2->data;
1237

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

                
1244
            list2 = g_list_next(list2);
1245
        }
1246

                
1247
        if (!filename_seen) {
1248
            PAR2FileSeen *file_seen = g_slice_new0(PAR2FileSeen);
1249

                
1250
            strncpy(file_seen->filename_without_extension, filename_without_extension, sizeof(file_seen->filename_without_extension) - 1);
1251
            strncpy(file_seen->filename, filename, sizeof(file_seen->filename) - 1);
1252

                
1253
            files = g_list_append(files, file_seen);
1254
        }
1255
    }
1256

                
1257
    g_dir_close(dir);
1258

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

                
1263
    /* Perform the repair */
1264
    list = files;
1265
    while (list) {
1266
        PAR2FileSeen *file_seen = (PAR2FileSeen*) list->data;
1267

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

                
1270
        g_slice_free(PAR2FileSeen, file_seen);
1271
        list = g_list_next(list);
1272
    }
1273
    g_list_free(files);
1274

                
1275
    g_free(directory);
1276
    g_slice_free(struct _par2_thread_data, thread_data);
1277
}
1278

                
1279
static void
1280
on_collection_downloaded(NGPlugin *plugin_data, const char *collection_name, gpointer data)
1281
{
1282
    struct _par2_thread_data *thread_data = g_slice_new0(struct _par2_thread_data);
1283

                
1284
    thread_data->plugin_data = plugin_data;
1285
    strncpy(thread_data->collection_name, collection_name, sizeof(thread_data->collection_name) - 1);
1286

                
1287
    g_thread_pool_push(((PluginPAR2Priv *) plugin_data->priv)->thread_pool, thread_data, NULL);
1288
}
1289