Passed
Push — feature/playlist_improvements ( 8abdb6...764662 )
by Pauli
13:41 queued 01:22
created

PlaylistFileService::exportToFile()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 25
c 2
b 0
f 0
nc 10
nop 3
dl 0
loc 39
ccs 0
cts 25
cp 0
crap 42
rs 8.8977
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\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...
21
22
/**
23
 * Class responsible of exporting playlists to file and importing playlist
24
 * contents from file.
25
 */
26
class PlaylistFileService {
27
	private $playlistBusinessLayer;
28
	private $trackBusinessLayer;
29
	private $userFolder;
30
	private $userId;
31
	private $logger;
32
33
	public function __construct(
34
			PlaylistBusinessLayer $playlistBusinessLayer,
35
			TrackBusinessLayer $trackBusinessLayer,
36
			Folder $userFolder,
37
			$userId,
38
			Logger $logger) {
39
		$this->playlistBusinessLayer = $playlistBusinessLayer;
40
		$this->trackBusinessLayer = $trackBusinessLayer;
41
		$this->userFolder = $userFolder;
42
		$this->userId = $userId;
43
		$this->logger = $logger;
44
	}
45
46
	/**
47
	 * export the playlist to a file
48
	 * @param int $id playlist ID
49
	 * @param string $folderPath parent folder path
50
	 * @param string $collisionMode action to take on file name collision,
51
	 *								supported values:
52
	 *								- 'overwrite' The existing file will be overwritten
53
	 *								- 'keepboth' The new file is named with a suffix to make it unique
54
	 *								- 'abort' (default) The operation will fail
55
	 * @return string path of the written file
56
	 * @throws BusinessLayerException if playlist with ID not found
57
	 * @throws \OCP\Files\NotFoundException if the $folderPath is not a valid folder
58
	 * @throws \RuntimeException on name conflict if $collisionMode == 'abort'
59
	 * @throws \OCP\Files\NotPermittedException if the user is not allowed to write to the given folder
60
	 */
61
	public function exportToFile($id, $folderPath, $collisionMode) {
62
		$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
63
		$tracks = $this->playlistBusinessLayer->getPlaylistTracks($id, $this->userId);
64
		$targetFolder = Util::getFolderFromRelativePath($this->userFolder, $folderPath);
65
66
		// Name the file according the playlist. File names cannot contain the '/' character on Linux, and in
67
		// owncloud/Nextcloud, the whole name must fit 250 characters, including the file extension. Reserve
68
		// another 5 characters to fit the postfix like " (xx)" on name collisions. If there are more than 100
69
		// exports of the same playlist with overly long name, then this function will fail but we can live
70
		// with that :).
71
		$filename = \str_replace('/', '-', $playlist->getName());
72
		$filename = Util::truncate($filename, 250 - 5 - 5);
73
		$filename .= '.m3u8';
74
75
		if ($targetFolder->nodeExists($filename)) {
76
			switch ($collisionMode) {
77
				case 'overwrite':
78
					$targetFolder->get($filename)->delete();
79
					break;
80
				case 'keepboth':
81
					$filename = $targetFolder->getNonExistingName($filename);
82
					break;
83
				default:
84
					throw new \RuntimeException('file already exists');
85
			}
86
		}
87
88
		$content = "#EXTM3U\n#EXTENC: UTF-8\n";
89
		foreach ($tracks as $track) {
90
			$nodes = $this->userFolder->getById($track->getFileId());
91
			if (\count($nodes) > 0) {
92
				$content .= "#EXTINF:{$track->getLength()},{$track->getTitle()}\n";
93
				$content .= Util::relativePath($targetFolder->getPath(), $nodes[0]->getPath()) . "\n";
94
			}
95
		}
96
		$file = $targetFolder->newFile($filename);
97
		$file->putContent($content);
98
99
		return $this->userFolder->getRelativePath($file->getPath());
100
	}
101
	
102
	/**
103
	 * export the playlist to a file
104
	 * @param int $id playlist ID
105
	 * @param string $filePath path of the file to import
106
	 * @return array with three keys:
107
	 * 			- 'playlist': The Playlist entity after the modification
108
	 * 			- 'imported_count': An integer showing the number of tracks imported
109
	 * 			- 'failed_count': An integer showing the number of tracks in the file which could not be imported
110
	 * @throws BusinessLayerException if playlist with ID not found
111
	 * @throws \OCP\Files\NotFoundException if the $filePath is not a valid file
112
	 */
113
	public function importFromFile($id, $filePath) {
114
		$file = $this->userFolder->get($filePath);
115
116
		$trackIds = [];
117
		$invalidPaths = [];
118
119
		// By default, files with extension .m3u8 are interpreted as UTF-8 and files with extension
120
		// .m3u as iso-8859-1. These can be overridden with the tag '#EXTENC' in the file contents.
121
		$encoding = Util::endsWith($filePath, '.m3u8', /*ignoreCase=*/true) ? 'UTF-8' : 'iso-8859-1';
122
123
		$cwd = \dirname($filePath);
124
		$fp = $file->fopen('r');
125
		while ($line = \fgets($fp)) {
126
			$line = \trim($line);
127
			if (Util::startsWith($line, '#')) {
128
				// comment line
129
				if (Util::startsWith($line, '#EXTENC:')) {
130
					// update the used encoding with the explicitly defined one
131
					$encoding = \trim(\substr($line, \strlen('#EXTENC:')));
132
				}
133
			}
134
			else {
135
				$line = \mb_convert_encoding($line, \mb_internal_encoding(), $encoding);
136
137
				$path = Util::resolveRelativePath($cwd, $line);
138
				try {
139
					$trackFile = $this->userFolder->get($path);
140
					if ($track = $this->trackBusinessLayer->findByFileId($trackFile->getId(), $this->userId)) {
141
						$trackIds[] = $track->getId();
142
					} else {
143
						$invalidPaths[] = $path;
144
					}
145
				}
146
				catch (\OCP\Files\NotFoundException $ex) {
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...
147
					$invalidPaths[] = $path;
148
				}
149
			}
150
		}
151
		\fclose($fp);
152
153
		$playlist = $this->playlistBusinessLayer->addTracks($trackIds, $id, $this->userId);
154
155
		if (\count($invalidPaths) > 0) {
156
			$this->logger->log('Some files were not found from the user\'s music library: '
157
								. \json_encode($invalidPaths, JSON_PARTIAL_OUTPUT_ON_ERROR), 'warn');
158
		}
159
160
		return [
161
			'playlist' => $playlist,
162
			'imported_count' => \count($trackIds),
163
			'failed_count' => \count($invalidPaths)
164
		];
165
	}	
166
}
167