Passed
Push — feature/playlist_improvements ( 417478...9b747b )
by Pauli
14:15
created

PlaylistFileService::doParseFile()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 21
c 0
b 0
f 0
nc 10
nop 1
dl 0
loc 35
rs 8.9617
ccs 0
cts 4
cp 0
crap 42
1
<?php
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 2020
11
 */
12
13
namespace OCA\Music\Utility;
14
15
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
16
use \OCA\Music\AppFramework\Core\Logger;
17
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
18
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
19
20
use \OCP\Files\File;
0 ignored issues
show
Bug introduced by
The type OCP\Files\File was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use \OCP\Files\Folder;
0 ignored issues
show
Bug introduced by
The type OCP\Files\Folder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
23
/**
24
 * Class responsible of exporting playlists to file and importing playlist
25
 * contents from file.
26
 */
27
class PlaylistFileService {
28
	private $playlistBusinessLayer;
29
	private $trackBusinessLayer;
30
	private $userFolder;
31
	private $userId;
32
	private $logger;
33
34
	public function __construct(
35
			PlaylistBusinessLayer $playlistBusinessLayer,
36
			TrackBusinessLayer $trackBusinessLayer,
37
			Folder $userFolder,
38
			$userId,
39
			Logger $logger) {
40
		$this->playlistBusinessLayer = $playlistBusinessLayer;
41
		$this->trackBusinessLayer = $trackBusinessLayer;
42
		$this->userFolder = $userFolder;
43
		$this->userId = $userId;
44
		$this->logger = $logger;
45
	}
46
47
	/**
48
	 * export the playlist to a file
49
	 * @param int $id playlist ID
50
	 * @param string $folderPath parent folder path
51
	 * @param string $collisionMode action to take on file name collision,
52
	 *								supported values:
53
	 *								- 'overwrite' The existing file will be overwritten
54
	 *								- 'keepboth' The new file is named with a suffix to make it unique
55
	 *								- 'abort' (default) The operation will fail
56
	 * @return string path of the written file
57
	 * @throws BusinessLayerException if playlist with ID not found
58
	 * @throws \OCP\Files\NotFoundException if the $folderPath is not a valid folder
59
	 * @throws \RuntimeException on name conflict if $collisionMode == 'abort'
60
	 * @throws \OCP\Files\NotPermittedException if the user is not allowed to write to the given folder
61
	 */
62
	public function exportToFile($id, $folderPath, $collisionMode) {
63
		$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
64
		$tracks = $this->playlistBusinessLayer->getPlaylistTracks($id, $this->userId);
65
		$targetFolder = Util::getFolderFromRelativePath($this->userFolder, $folderPath);
66
67
		// Name the file according the playlist. File names cannot contain the '/' character on Linux, and in
68
		// owncloud/Nextcloud, the whole name must fit 250 characters, including the file extension. Reserve
69
		// another 5 characters to fit the postfix like " (xx)" on name collisions. If there are more than 100
70
		// exports of the same playlist with overly long name, then this function will fail but we can live
71
		// with that :).
72
		$filename = \str_replace('/', '-', $playlist->getName());
73
		$filename = Util::truncate($filename, 250 - 5 - 5);
74
		$filename .= '.m3u8';
75
76
		if ($targetFolder->nodeExists($filename)) {
77
			switch ($collisionMode) {
78
				case 'overwrite':
79
					$targetFolder->get($filename)->delete();
80
					break;
81
				case 'keepboth':
82
					$filename = $targetFolder->getNonExistingName($filename);
83
					break;
84
				default:
85
					throw new \RuntimeException('file already exists');
86
			}
87
		}
88
89
		$content = "#EXTM3U\n#EXTENC: UTF-8\n";
90
		foreach ($tracks as $track) {
91
			$nodes = $this->userFolder->getById($track->getFileId());
92
			if (\count($nodes) > 0) {
93
				$content .= "#EXTINF:{$track->getLength()},{$track->getTitle()}\n";
94
				$content .= Util::relativePath($targetFolder->getPath(), $nodes[0]->getPath()) . "\n";
95
			}
96
		}
97
		$file = $targetFolder->newFile($filename);
98
		$file->putContent($content);
99
100
		return $this->userFolder->getRelativePath($file->getPath());
101
	}
102
	
103
	/**
104
	 * export the playlist to a file
105
	 * @param int $id playlist ID
106
	 * @param string $filePath path of the file to import
107
	 * @return array with three keys:
108
	 * 			- 'playlist': The Playlist entity after the modification
109
	 * 			- 'imported_count': An integer showing the number of tracks imported
110
	 * 			- 'failed_count': An integer showing the number of tracks in the file which could not be imported
111
	 * @throws BusinessLayerException if playlist with ID not found
112
	 * @throws \OCP\Files\NotFoundException if the $filePath is not a valid file
113
	 */
114
	public function importFromFile($id, $filePath) {
115
		$parsed = $this->doParseFile($this->userFolder->get($filePath));
116
		$trackFiles = $parsed['files'];
117
		$invalidPaths = $parsed['invalid_paths'];
118
119
		$trackIds = [];
120
		foreach ($trackFiles as $trackFile) {
121
			if ($track = $this->trackBusinessLayer->findByFileId($trackFile->getId(), $this->userId)) {
122
				$trackIds[] = $track->getId();
123
			} else {
124
				$invalidPaths[] = $trackFile->getPath();
125
			}
126
		}
127
128
		$playlist = $this->playlistBusinessLayer->addTracks($trackIds, $id, $this->userId);
129
130
		if (\count($invalidPaths) > 0) {
131
			$this->logger->log('Some files were not found from the user\'s music library: '
132
								. \json_encode($invalidPaths, JSON_PARTIAL_OUTPUT_ON_ERROR), 'warn');
133
		}
134
135
		return [
136
			'playlist' => $playlist,
137
			'imported_count' => \count($trackIds),
138
			'failed_count' => \count($invalidPaths)
139
		];
140
	}
141
142
	/**
143
	 * 
144
	 * @param int $fileId
145
	 * @throws \OCP\Files\NotFoundException
146
	 * @return array
147
	 */
148
	public function parseFile($fileId) {
149
		$nodes = $this->userFolder->getById($fileId);
150
		if (\count($nodes) > 0) {
151
			return $this->doParseFile($nodes[0]);
152
		} else {
153
			throw new \OCP\Files\NotFoundException();
0 ignored issues
show
Bug introduced by
The type OCP\Files\NotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
154
		}
155
	}
156
157
	private function doParseFile(File $file) {
158
		$trackFiles = [];
159
		$invalidPaths = [];
160
161
		// By default, files with extension .m3u8 are interpreted as UTF-8 and files with extension
162
		// .m3u as ISO-8859-1. These can be overridden with the tag '#EXTENC' in the file contents.
163
		$encoding = Util::endsWith($file->getPath(), '.m3u8', /*ignoreCase=*/true) ? 'UTF-8' : 'ISO-8859-1';
164
165
		$cwd = $this->userFolder->getRelativePath($file->getParent()->getPath());
166
		$fp = $file->fopen('r');
167
		while ($line = \fgets($fp)) {
168
			$line = \trim($line);
169
			if (Util::startsWith($line, '#')) {
170
				// comment line
171
				if (Util::startsWith($line, '#EXTENC:')) {
172
					// update the used encoding with the explicitly defined one
173
					$encoding = \trim(\substr($line, \strlen('#EXTENC:')));
174
				}
175
			}
176
			else {
177
				$line = \mb_convert_encoding($line, \mb_internal_encoding(), $encoding);
178
179
				$path = Util::resolveRelativePath($cwd, $line);
180
				try {
181
					$trackFiles[] = $this->userFolder->get($path);
182
				} catch (\OCP\Files\NotFoundException $ex) {
183
					$invalidPaths[] = $path;
184
				}
185
			}
186
		}
187
		\fclose($fp);
188
189
		return [
190
			'files' => $trackFiles,
191
			'invalid_paths' => $invalidPaths
192
		];
193
	}
194
}
195