Completed
Push — master ( 6738c2...4998fb )
by Peter
04:34
created

ScanStoragesCommand   B

Complexity

Total Complexity 34

Size/Duplication

Total Lines 320
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 34
c 1
b 0
f 0
lcom 1
cbo 17
dl 0
loc 320
ccs 0
cts 177
cp 0
rs 7.2285

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getItemsOfDeletedFiles() 0 14 4
A isAllowFile() 0 4 1
D execute() 0 106 16
A getFilesByPath() 0 8 1
B configure() 0 39 1
A getItemFromFile() 0 10 3
A checkStorageId() 0 16 4
B getProgress() 0 23 4
1
<?php
2
/**
3
 * AnimeDb package
4
 *
5
 * @package   AnimeDb
6
 * @author    Peter Gribanov <[email protected]>
7
 * @copyright Copyright (c) 2011, Peter Gribanov
8
 * @license   http://opensource.org/licenses/GPL-3.0 GPL v3
9
 */
10
11
namespace AnimeDb\Bundle\CatalogBundle\Command;
12
13
use AnimeDb\Bundle\CatalogBundle\Console\Output\Decorator;
14
use AnimeDb\Bundle\CatalogBundle\Entity\Item;
15
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
16
use Symfony\Component\Console\Input\InputInterface;
17
use Symfony\Component\Console\Output\OutputInterface;
18
use Symfony\Component\Console\Input\InputArgument;
19
use Symfony\Component\Console\Input\InputOption;
20
use AnimeDb\Bundle\CatalogBundle\Entity\Storage;
21
use Symfony\Component\Finder\Finder;
22
use AnimeDb\Bundle\CatalogBundle\Event\Listener\Entity\Storage as StorageListener;
23
use AnimeDb\Bundle\CatalogBundle\Event\Storage\StoreEvents;
24
use AnimeDb\Bundle\CatalogBundle\Event\Storage\UpdateItemFiles;
25
use AnimeDb\Bundle\CatalogBundle\Event\Storage\DetectedNewFiles;
26
use AnimeDb\Bundle\CatalogBundle\Event\Storage\DeleteItemFiles;
27
use AnimeDb\Bundle\CatalogBundle\Repository\Storage as StorageRepository;
28
use Symfony\Component\Finder\SplFileInfo;
29
use AnimeDb\Bundle\CatalogBundle\Console\Output\LazyWrite;
30
use AnimeDb\Bundle\CatalogBundle\Console\Progress\Export;
31
use AnimeDb\Bundle\CatalogBundle\Console\Progress\PresetOutput;
32
use Symfony\Component\Console\Output\NullOutput;
33
use Patchwork\Utf8;
34
35
/**
36
 * Scan storages for new items
37
 *
38
 * @package AnimeDb\Bundle\CatalogBundle\Command
39
 * @author  Peter Gribanov <[email protected]>
40
 */
41
class ScanStoragesCommand extends ContainerAwareCommand
42
{
43
    /**
44
     * Allowable extension
45
     *
46
     * @var array
47
     */
48
    protected $allow_ext = [
49
        'avi',
50
        'mkv',
51
        'm1v',
52
        'm2v',
53
        'm4v',
54
        'mov',
55
        'qt',
56
        'mpeg',
57
        'mpg',
58
        'mpe',
59
        'ogg',
60
        'rm',
61
        'wmv',
62
        'asf',
63
        'wm',
64
        'm2ts',
65
        'mts',
66
        'm2t',
67
        'mp4',
68
        'mov',
69
        '3gp',
70
        '3g2',
71
        'k3g',
72
        'mp2',
73
        'mpv2',
74
        'mod',
75
        'vob',
76
        'f4v',
77
        'ismv'
78
    ];
79
80
    protected function configure()
81
    {
82
        $this
83
            ->setName('animedb:scan-storage')
84
            ->setDescription('Scan storages for new items')
85
            ->addArgument(
86
                'storage',
87
                InputArgument::OPTIONAL,
88
                'Id scanned storage'
89
            )
90
            ->addOption(
91
                'force',
92
                'f',
93
                InputOption::VALUE_NONE,
94
                'Ignore the last modified storage'
95
            )
96
            ->addOption(
97
                'no-progress',
98
                null,
99
                InputOption::VALUE_NONE,
100
                'Disable progress bar'
101
            )
102
            ->addOption(
103
                'export',
104
                null,
105
                InputOption::VALUE_REQUIRED,
106
                'Export progress to file (disables progress as --no-progress)'
107
            )
108
            ->setHelp(<<<EOT
109
Example scan all storages:
110
111
<info>php app/console animedb:scan-storage</info>
112
113
Example scan storage with id <info>1</info>:
114
115
<info>php app/console animedb:scan-storage 1</info>
116
EOT
117
            );
118
    }
119
120
    /**
121
     * @param InputInterface $input
122
     * @param OutputInterface $output
123
     *
124
     * @return int
125
     */
126
    protected function execute(InputInterface $input, OutputInterface $output) {
127
        $start = time();
128
129
        $em = $this->getContainer()->get('doctrine.orm.entity_manager');
130
        $dispatcher = $this->getContainer()->get('event_dispatcher');
131
        /* @var $rep StorageRepository */
132
        $rep = $em->getRepository('AnimeDbCatalogBundle:Storage');
133
134
        $progress = $this->getProgress($input, $output);
135
        $lazywrite = new LazyWrite($output);
136
        $lazywrite->setLazyWrite(!$input->getOption('no-progress'));
137
138
        // get list storages
139
        if ($id = $input->getArgument('storage')) {
140
            $storage = $rep->find($id);
141
            if (!($storage instanceof Storage)) {
142
                throw new \InvalidArgumentException('Not found the storage with id: '.$id);
143
            }
144
            if (!$storage->isWritable()) {
145
                throw new \InvalidArgumentException('Storage "'.$storage->getName().'" can not be scanned');
146
            }
147
            $storages = [$storage];
148
        } else {
149
            $storages = $rep->getList(Storage::getTypesWritable());
150
        }
151
152
        /* @var $storage Storage */
153
        foreach ($storages as $storage) {
154
            $output->writeln('Scan storage <info>'.$storage->getName().'</info>:');
155
156
            $path = $storage->getPath();
157
            $path = Utf8::wrapPath($path); // wrap path for current fs
158
159
            if (!file_exists($path)) {
160
                $output->writeln('Storage is not available');
161
                continue;
162
            }
163
164
            // check storage id
165
            $owner = $this->checkStorageId($path, $storage, $rep);
166
            if ($owner instanceof Storage) {
167
                $output->writeln('Path <info>'.$storage->getPath().'</info> reserved storage <info>'
168
                    .$owner->getName().'</info>');
169
                continue;
170
            }
171
172
            // storage not modified
173
            if (!$input->getOption('force') &&
174
                $storage->getFileModified() &&
175
                filemtime($path) == $storage->getFileModified()->getTimestamp()
176
            ) {
177
                $output->writeln('Storage is not modified');
178
                continue;
179
            }
180
181
            $files = $this->getFilesByPath($path);
182
            $total = $files->count();
183
            // total files +1% for check of delete files
184
            $progress->start(ceil($total+($total*0.01)));
185
            $progress->display();
186
187
            /* @var $file SplFileInfo */
188
            foreach ($files as $file) {
189
                // ignore not supported files
190
                if ($file->isFile() && !$this->isAllowFile($file)) {
191
                    $progress->advance();
192
                    continue;
193
                }
194
195
                // item is exists and modified
196
                if ($item = $this->getItemFromFile($storage, $file)) {
197
                    if ($item->getDateUpdate()->getTimestamp() < $file->getPathInfo()->getMTime()) {
198
                        $dispatcher->dispatch(StoreEvents::UPDATE_ITEM_FILES, new UpdateItemFiles($item));
0 ignored issues
show
Bug introduced by
It seems like $item defined by $this->getItemFromFile($storage, $file) on line 196 can also be of type boolean; however, AnimeDb\Bundle\CatalogBu...temFiles::__construct() does only seem to accept object<AnimeDb\Bundle\CatalogBundle\Entity\Item>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
199
                        $lazywrite->writeln('Changes are detected in files of item <info>'.$item->getName().'</info>');
200
                    }
201
                } else {
202
                    // remove wrap prefix
203
                    list(, $file) = explode('://', $file->getPathname(), 2);
204
                    $file = new SplFileInfo($file, '', '');
205
206
                    // it is a new item
207
                    $dispatcher->dispatch(StoreEvents::DETECTED_NEW_FILES, new DetectedNewFiles($storage, $file));
208
                    $lazywrite->writeln('Detected files for new item <info>'.$file->getFilename().'</info>');
209
                }
210
                $progress->advance();
211
            }
212
            $em->refresh($storage);
213
214
            // check of delete file for item
215
            foreach ($this->getItemsOfDeletedFiles($storage, $files) as $item) {
216
                $dispatcher->dispatch(StoreEvents::DELETE_ITEM_FILES, new DeleteItemFiles($item));
217
                $lazywrite->writeln('<error>Files for item "'.$item->getName().'" is not found</error>');
218
            }
219
            $progress->advance();
220
            $progress->finish();
221
            $lazywrite->writeAll();
222
223
            // update date modified
224
            $storage->setFileModified(new \DateTime(date('Y-m-d H:i:s', filemtime($path))));
225
            $em->persist($storage);
226
            $output->writeln('');
227
        }
228
        $em->flush();
229
230
        $output->writeln('Time: <info>'.(time()-$start).'</info> s.');
231
    }
232
233
    /**
234
     * Get items of deleted files
235
     *
236
     * @param Storage $storage
237
     * @param Finder $finder
238
     *
239
     * @return array
240
     */
241
    protected function getItemsOfDeletedFiles(Storage $storage, Finder $finder)
242
    {
243
        $items = [];
244
        // check of delete file for item
245
        foreach ($storage->getItems() as $item) {
246
            foreach ($finder as $file) {
247
                if (pathinfo($item->getPath(), PATHINFO_BASENAME) == $file->getFilename()) {
248
                    continue 2;
249
                }
250
            }
251
            $items[] = $item;
252
        }
253
        return $items;
254
    }
255
256
    /**
257
     * Get item from files
258
     *
259
     * @param Storage $storage
260
     * @param SplFileInfo $file
261
     *
262
     * @return Item|bool
263
     */
264
    protected function getItemFromFile(Storage $storage, SplFileInfo $file)
265
    {
266
        /* @var $item Item */
267
        foreach ($storage->getItems() as $item) {
268
            if (pathinfo($item->getPath(), PATHINFO_BASENAME) == $file->getFilename()) {
269
                return $item;
270
            }
271
        }
272
        return false;
273
    }
274
275
    /**
276
     * Get files by path
277
     *
278
     * @param string $path
279
     *
280
     * @return Finder
281
     */
282
    protected function getFilesByPath($path)
283
    {
284
        return Finder::create()
285
            ->in($path)
286
            ->ignoreUnreadableDirs()
287
            ->depth('== 0')
288
            ->notName('.*');
289
    }
290
291
    /**
292
     * Is allow file
293
     *
294
     * @param SplFileInfo $file
295
     *
296
     * @return bool
297
     */
298
    protected function isAllowFile(SplFileInfo $file)
299
    {
300
        return in_array(strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION)), $this->allow_ext);
301
    }
302
303
    /**
304
     * Update storage id
305
     *
306
     * @param string $path
307
     * @param Storage $storage
308
     * @param StorageRepository $rep
309
     *
310
     * @return Storage|bool
311
     */
312
    protected function checkStorageId($path, Storage $storage, StorageRepository $rep)
313
    {
314
        if (!file_exists($path.StorageListener::ID_FILE)) {
315
            // path is free. reserve for me
316
            file_put_contents($path.StorageListener::ID_FILE, $storage->getId());
317
        } elseif (file_get_contents($path.StorageListener::ID_FILE) == $storage->getId()) {
318
            // it is my path. do nothing
319
        } elseif (!($duplicate = $rep->find(file_get_contents($path.StorageListener::ID_FILE)))) {
320
            // this path is reserved storage that was removed and now this path is free
321
            file_put_contents($path.StorageListener::ID_FILE, $storage->getId());
322
        } else {
323
            return $duplicate;
324
        }
325
326
        return true;
327
    }
328
329
    /**
330
     * Get progress
331
     *
332
     * @param InputInterface $input
333
     * @param OutputInterface $output
334
     *
335
     * @return Decorator
336
     */
337
    protected function getProgress(InputInterface $input, OutputInterface $output)
338
    {
339
        if ($input->getOption('no-progress')) {
340
            $output = new NullOutput();
341
        }
342
343
        // export progress only for one storage
344
        if (!$input->getArgument('storage')) {
345
            $input->setOption('export', null);
346
        }
347
348
        if ($export_file = $input->getOption('export')) {
349
            // progress is redirected to the export file
350
            $input->setOption('no-progress', true);
351
            $progress = new Export($this->getHelperSet()->get('progress'), new NullOutput(), $export_file);
352
        } else {
353
            $progress = new PresetOutput($this->getHelperSet()->get('progress'), $output);
354
        }
355
356
        $progress->setBarCharacter('<comment>=</comment>');
357
358
        return $progress;
359
    }
360
}
361