Passed
Push — master ( 784085...51f739 )
by Pauli
02:41
created

PlaylistImport::doConfigure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 18
c 1
b 0
f 0
dl 0
loc 21
rs 9.6666
cc 1
nc 1
nop 0
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 Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2021
11
 */
12
13
namespace OCA\Music\Command;
14
15
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
16
use OCA\Music\BusinessLayer\PlaylistBusinessLayer;
17
use OCA\Music\Db\Playlist;
18
use OCA\Music\Utility\PlaylistFileService;
19
20
use OCP\Files\File;
21
use OCP\Files\Folder;
22
use OCP\Files\IRootFolder;
23
use OCP\Files\Node;
24
use OCP\Files\NotFoundException;
25
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Output\OutputInterface;
29
30
class PlaylistImport extends BaseCommand {
31
	/** @var IRootFolder */
32
	private $rootFolder;
33
	/** @var PlaylistBusinessLayer */
34
	private $businessLayer;
35
	/** @var PlaylistFileService */
36
	private $playlistFileService;
37
38
	public function __construct(
39
			\OCP\IUserManager $userManager,
40
			\OCP\IGroupManager $groupManager,
41
			IRootFolder $rootFolder,
42
			PlaylistBusinessLayer $playlistBusinessLayer,
43
			PlaylistFileService $playlistFileService) {
44
		$this->rootFolder = $rootFolder;
45
		$this->businessLayer = $playlistBusinessLayer;
46
		$this->playlistFileService = $playlistFileService;
47
		parent::__construct($userManager, $groupManager);
48
	}
49
50
	protected function doConfigure() : void {
51
		$this
52
			->setName('music:playlist-import')
53
			->setDescription('import user playlist(s) from file(s)')
54
			->addOption(
55
				'file',
56
				null,
57
				InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
58
				'path of the playlist file, relative to the user home folder; * and ? are treated as wildcards within the file name but not on the directory name'
59
			)
60
			->addOption(
61
				'overwrite',
62
				null,
63
				InputOption::VALUE_NONE,
64
				'overwrite the target playlist if it already exists'
65
			)
66
			->addOption(
67
				'append',
68
				null,
69
				InputOption::VALUE_NONE,
70
				'append imported tracks to an existing playlist if found'
71
			)
72
		;
73
	}
74
75
	protected function doExecute(InputInterface $input, OutputInterface $output, array $users) : void {
76
		$files = $input->getOption('file');
77
		$overwrite = (bool)$input->getOption('overwrite');
78
		$append = (bool)$input->getOption('append');
79
80
		if (empty($files)) {
81
			throw new \InvalidArgumentException('At least one <error>file</error> argument must be given');
82
		}
83
84
		if ($overwrite && $append) {
85
			throw new \InvalidArgumentException('The options <error>overwrite</error> and <error>append</error> are mutually exclusive');
86
		}
87
88
		if ($input->getOption('all')) {
89
			$this->userManager->callForAllUsers(function($user) use ($output, $files, $overwrite, $append) {
90
				$this->executeForUser($user->getUID(), $files, $overwrite, $append, $output);
91
			});
92
		} else {
93
			foreach ($users as $userId) {
94
				$this->executeForUser($userId, $files, $overwrite, $append, $output);
95
			}
96
		}
97
	}
98
99
	private function executeForUser(string $userId, array $files, bool $overwrite, bool $append, OutputInterface $output) : void {
100
		$output->writeln("Importing playlist(s) for <info>$userId</info>...");
101
102
		$userFolder = $this->rootFolder->getUserFolder($userId);
103
		$files = self::resolveWildcards($files, $userFolder, $output);
104
105
		foreach ($files as $filePath) {
106
			$name = \pathinfo($filePath, PATHINFO_FILENAME);
107
			$existingLists = $this->businessLayer->findAllByName($name, $userId);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type array; however, parameter $name of OCA\Music\BusinessLayer\...sLayer::findAllByName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

107
			$existingLists = $this->businessLayer->findAllByName(/** @scrutinizer ignore-type */ $name, $userId);
Loading history...
108
			if (\count($existingLists) === 0) {
109
				$playlist = $this->businessLayer->create($name, $userId);
110
			} elseif (!$overwrite && !$append) {
111
				$output->writeln("  The playlist <error>$name</error> already exists, give argument <info>overwrite</info> or <info>append</info>");
112
				$playlist = null;
113
			} else {
114
				$playlist = $existingLists[0];
115
			}
116
117
			if ($playlist !== null) {
118
				$this->importPlaylist($filePath, $playlist, $userId, $userFolder, $overwrite, $output);
119
			}
120
		}
121
	}
122
123
	private function importPlaylist(string $filePath, Playlist $playlist, string $userId, Folder $userFolder, bool $overwrite, OutputInterface $output) : void {
124
		try {
125
			$id = $playlist->getId();
126
			$result = $this->playlistFileService->importFromFile($playlist->getId(), $userId, $userFolder, $filePath, $overwrite ? 'overwrite' : 'append');
127
			$output->writeln("  <info>{$result['imported_count']}</info> tracks were imported to playlist <info>$id</info> from <info>$filePath</info>");
128
		} catch (BusinessLayerException $ex) {
129
			$output->writeln("  User <info>$userId</info> has no playlist with id <error>$id</error>");
130
		} catch (\OCP\Files\NotFoundException $ex) {
131
			$output->writeln("  Invalid file path <error>$filePath</error>");
132
		} catch (\UnexpectedValueException $ex) {
133
			$output->writeln("  The file <error>$filePath</error> is not a supported playlist file");
134
		}
135
	}
136
137
	/**
138
	 * @param string[] $paths
139
	 * @return string[]
140
	 */
141
	private static function resolveWildcards(array $paths, Folder $userFolder, OutputInterface $output) : array {
142
		$result = [];
143
144
		foreach ($paths as $path) {
145
			list('basename' => $basename, 'dirname' => $dirname) = \pathinfo($path);
146
			if (\strpos($basename, '?') === false && \strpos($basename, '*') === false) {
147
				// no wildcards, take the path as such
148
				$result[] = $path;
149
			} else {
150
				try {
151
					$dir = $userFolder->get($dirname);
152
				} catch (NotFoundException $e) {
153
					$dir = null;
154
				}
155
				if (!($dir instanceof Folder)) {
156
					$output->writeln("  Invalid directory path <error>$dirname</error>");
157
				} else {
158
					$matches = [];
159
					foreach ($dir->getDirectoryListing() as $node) {
160
						if ($node instanceof File && self::fileMatchesPattern($node, $basename)) {
161
							$matches[] = $userFolder->getRelativePath($node->getPath());
162
						}
163
					}
164
					if (\count($matches) == 0) {
165
						$output->writeln("  The path pattern <error>$path</error> matched no files");
166
					} else {
167
						$result = \array_merge($result, $matches);
168
					}
169
				}
170
			}
171
		}
172
173
		return $result;
174
	}
175
176
	private static function fileMatchesPattern(File $file, string $pattern) : bool {
177
		// convert the pattern to regex
178
		$pattern = \preg_quote($pattern);				// escape regex meta characters
179
		$pattern = \str_replace('\*', '.*', $pattern);	// convert * to its regex equivaleant
180
		$pattern = \str_replace('\?', '.', $pattern);	// convert ? to its regex equivaleant
181
		$pattern = "/^$pattern$/";						// the pattern should match the name from begin to end
182
183
		return (\preg_match($pattern, $file->getName()) === 1);
184
	}
185
}
186