Passed
Push — master ( 041459...b27899 )
by Pauli
03:55
created

Scan   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 149
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 102
dl 0
loc 149
rs 10
c 1
b 0
f 0
wmc 26

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A doConfigure() 0 39 1
A doExecute() 0 25 6
A searchArt() 0 17 3
C scanUser() 0 48 15
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Thomas Müller <[email protected]>
10
 * @author Bart Visscher <[email protected]>
11
 * @author Leizh <[email protected]>
12
 * @author Pauli Järvinen <[email protected]>
13
 * @copyright Thomas Müller 2013
14
 * @copyright Bart Visscher 2013
15
 * @copyright Leizh 2014
16
 * @copyright Pauli Järvinen 2017 - 2025
17
 */
18
19
namespace OCA\Music\Command;
20
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Input\InputOption;
23
use Symfony\Component\Console\Output\OutputInterface;
24
25
use OCP\IGroupManager;
26
use OCP\IUserManager;
27
28
use OCA\Music\Service\Scanner;
29
30
class Scan extends BaseCommand {
31
32
	private Scanner $scanner;
33
34
	public function __construct(IUserManager $userManager, IGroupManager $groupManager, Scanner $scanner) {
35
		$this->scanner = $scanner;
36
		parent::__construct($userManager, $groupManager);
37
	}
38
39
	protected function doConfigure() : void {
40
		$this
41
			->setName('music:scan')
42
			->setDescription('scan and index any unindexed or dirty audio files')
43
			->addOption(
44
					'debug',
45
					null,
46
					InputOption::VALUE_NONE,
47
					'will run the scan in debug mode, showing memory and time consumption'
48
			)
49
			->addOption(
50
					'clean-obsolete',
51
					null,
52
					InputOption::VALUE_NONE,
53
					'also remove any obsolete file references from the library'
54
			)
55
			->addOption(
56
					'rescan',
57
					null,
58
					InputOption::VALUE_NONE,
59
					'rescan also any previously scanned tracks'
60
			)
61
			->addOption(
62
					'skip-dirty',
63
					null,
64
					InputOption::VALUE_NONE,
65
					'do not rescan the files marked "dirty" or having timestamp after the latest scan time'
66
			)
67
			->addOption(
68
					'skip-art',
69
					null,
70
					InputOption::VALUE_NONE,
71
					'do not search for album and artist cover art images'
72
			)
73
			->addOption(
74
					'folder',
75
					null,
76
					InputOption::VALUE_OPTIONAL,
77
					'scan only files within this folder (path is relative to the user home folder)'
78
			)
79
		;
80
	}
81
82
	protected function doExecute(InputInterface $input, OutputInterface $output, array $users) : void {
83
		if (!$input->getOption('debug')) {
84
			$this->scanner->listen(Scanner::class, 'update', fn($path) => $output->writeln("Scanning <info>$path</info>"));
85
			$this->scanner->listen(Scanner::class, 'exclude', fn($path) => $output->writeln("!! Removing <info>$path</info>"));
86
		}
87
88
		if ($input->getOption('rescan') && $input->getOption('skip-dirty')) {
89
			throw new \InvalidArgumentException('The options <error>rescan</error> and <error>skip-dirty</error> are mutually exclusive');
90
		}
91
92
		if ($input->getOption('all')) {
93
			$users = $this->userManager->search('');
94
			$users = \array_map(fn($u) => $u->getUID(), $users);
95
		}
96
97
		foreach ($users as $user) {
98
			$this->scanUser(
99
					$user,
100
					$output,
101
					$input->getOption('rescan'),
102
					$input->getOption('skip-dirty'),
103
					$input->getOption('skip-art'),
104
					$input->getOption('clean-obsolete'),
105
					$input->getOption('folder'),
106
					$input->getOption('debug')
107
			);
108
		}
109
	}
110
111
	protected function scanUser(
112
			string $user, OutputInterface $output, bool $rescan, bool $skipDirty, bool $skipArt,
113
			bool $cleanObsolete, ?string $folder, bool $debug) : void {
114
115
		$output->writeln("Check library scan status for <info>$user</info>"  . ($folder ? " in path '$folder'..." : '...'));
116
		$startTime = \hrtime(true);
117
		\extract($this->scanner->getStatusOfLibraryFiles($user, $folder)); // populate $unscannedFiles, $obsoleteFiles, $dirtyFiles, $scannedCount
118
		$statusTime = (int)((\hrtime(true) - $startTime) / 1000000);
119
		$unscannedCount = \count($unscannedFiles);
120
		$dirtyCount = \count($dirtyFiles);
121
		$obsoleteCount = \count($obsoleteFiles);
122
123
		$output->writeln("  Status got in $statusTime ms");
124
		$output->writeln("  Scanned files: $scannedCount");
125
		$output->writeln("  Unscanned files: $unscannedCount");
126
		$output->writeln("  Dirty files: $dirtyCount" . (($dirtyCount && $skipDirty) ? ' (skipped)' : ''));
127
		$output->writeln("  Obsolete files: $obsoleteCount" . (($obsoleteCount && !$cleanObsolete) ? ' (use --clean-obsolete to remove)' : ''));
128
		$output->writeln("");
129
130
		if ($cleanObsolete && !empty($obsoleteFiles)) {
131
			if ($this->scanner->deleteAudio($obsoleteFiles, [$user])) {
132
				$output->writeln("The obsolete files no longer available in the the library of <info>$user</info> were removed");
133
			} else {
134
				$output->writeln("<error>Failed</error> to remove any obsolete files of <info>$user</info>!");
135
			}
136
		}
137
138
		if ($rescan) {
139
			$filesToScan = $this->scanner->getAllMusicFileIds($user, $folder);
140
		} else {
141
			$filesToScan = $unscannedFiles;
142
			if (!$skipDirty) {
143
				$filesToScan = \array_merge($filesToScan, $dirtyFiles);
144
			}
145
		}
146
		$output->writeln('Total ' . \count($filesToScan) . ' files to scan' . ($folder ? " in '$folder'" : ''));
147
148
		if (\count($filesToScan)) {
149
			$stats = $this->scanner->scanFiles($user, $filesToScan, $debug ? $output : null);
150
			$output->writeln("Added or updated {$stats['count']} files in database of <info>$user</info>");
151
			$output->writeln('  Time consumed to analyze files: ' . ($stats['anlz_time'] / 1000) . ' s');
152
			$output->writeln('  Time consumed to update DB: ' . ($stats['db_time'] / 1000) . ' s');
153
		}
154
155
		if ($skipArt) {
156
			$output->writeln("Cover art search skipped");
157
		} else {
158
			$this->searchArt($user, $output);
159
		}
160
	}
161
162
	private function searchArt(string $user, OutputInterface $output) : void {
163
		$output->writeln("");
164
		$output->writeln("Searching cover images for albums with no cover art set...");
165
		$startTime = \hrtime(true);
166
		if ($this->scanner->findAlbumCovers($user)) {
167
			$output->writeln("  Some album cover image(s) were found and added");
168
		}
169
		$albumCoverTime = (int)((\hrtime(true) - $startTime) / 1000000);
170
		$output->writeln("  Search took $albumCoverTime ms");
171
172
		$output->writeln("Searching cover images for artists with no cover art set...");
173
		$startTime = \hrtime(true);
174
		if ($this->scanner->findArtistCovers($user)) {
175
			$output->writeln("  Some artist cover image(s) were found and added");
176
		}
177
		$artistCoverTime = (int)((\hrtime(true) - $startTime) / 1000000);
178
		$output->writeln("  Search took $artistCoverTime ms");
179
	}
180
}
181