|
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 2025 |
|
11
|
|
|
*/ |
|
12
|
|
|
|
|
13
|
|
|
namespace OCA\Music\Service; |
|
14
|
|
|
|
|
15
|
|
|
use OCA\Music\AppFramework\Core\Logger; |
|
16
|
|
|
use OCA\Music\Db\Track; |
|
17
|
|
|
use OCA\Music\Db\TrackMapper; |
|
18
|
|
|
use OCA\Music\Utility\ArrayUtil; |
|
19
|
|
|
|
|
20
|
|
|
use OCP\AppFramework\Db\DoesNotExistException; |
|
21
|
|
|
use OCP\Files\FileInfo; |
|
22
|
|
|
use OCP\Files\Folder; |
|
23
|
|
|
|
|
24
|
|
|
class FileSystemService { |
|
25
|
|
|
|
|
26
|
|
|
private TrackMapper $mapper; |
|
27
|
|
|
private Logger $logger; |
|
28
|
|
|
|
|
29
|
|
|
public function __construct(TrackMapper $trackMapper, Logger $logger) { |
|
30
|
|
|
$this->mapper = $trackMapper; |
|
31
|
|
|
$this->logger = $logger; |
|
32
|
|
|
} |
|
33
|
|
|
|
|
34
|
|
|
/** |
|
35
|
|
|
* Returns all folders of the user containing indexed tracks, along with the contained track IDs |
|
36
|
|
|
* @return array of entries like {id: int, name: string, parent: ?int, trackIds: int[]} |
|
37
|
|
|
*/ |
|
38
|
|
|
public function findAllFolders(string $userId, Folder $musicFolder) : array { |
|
39
|
|
|
// All tracks of the user, grouped by their parent folders. Some of the parent folders |
|
40
|
|
|
// may be owned by other users and are invisible to this user (in case of shared files). |
|
41
|
|
|
$trackIdsByFolder = $this->mapper->findTrackAndFolderIds($userId); |
|
42
|
|
|
$foldersLut = $this->getFoldersLut($trackIdsByFolder, $userId, $musicFolder); |
|
43
|
|
|
return \array_map( |
|
44
|
|
|
fn($id, $folderInfo) => \array_merge($folderInfo, ['id' => $id]), |
|
45
|
|
|
\array_keys($foldersLut), $foldersLut |
|
46
|
|
|
); |
|
47
|
|
|
} |
|
48
|
|
|
|
|
49
|
|
|
/** |
|
50
|
|
|
* @param Track[] $tracks (in|out) |
|
51
|
|
|
*/ |
|
52
|
|
|
public function injectFolderPathsToTracks(array $tracks, string $userId, Folder $musicFolder) : void { |
|
53
|
|
|
$folderIds = \array_map(fn($t) => $t->getFolderId(), $tracks); |
|
54
|
|
|
$folderIds = \array_unique($folderIds); |
|
55
|
|
|
$trackIdsByFolder = \array_fill_keys($folderIds, []); // track IDs are not actually used here so we can use empty arrays |
|
56
|
|
|
|
|
57
|
|
|
$foldersLut = $this->getFoldersLut($trackIdsByFolder, $userId, $musicFolder); |
|
58
|
|
|
|
|
59
|
|
|
// recursive helper to get folder's path and cache all parent paths on the way |
|
60
|
|
|
$getFolderPath = function(int $id, array &$foldersLut) use (&$getFolderPath) : string { |
|
61
|
|
|
// setup the path if not cached already |
|
62
|
|
|
if (!isset($foldersLut[$id]['path'])) { |
|
63
|
|
|
$parentId = $foldersLut[$id]['parent']; |
|
64
|
|
|
if ($parentId === null) { |
|
65
|
|
|
$foldersLut[$id]['path'] = ''; |
|
66
|
|
|
} else { |
|
67
|
|
|
$foldersLut[$id]['path'] = $getFolderPath($parentId, $foldersLut) . '/' . $foldersLut[$id]['name']; |
|
68
|
|
|
} |
|
69
|
|
|
} |
|
70
|
|
|
return $foldersLut[$id]['path']; |
|
71
|
|
|
}; |
|
72
|
|
|
|
|
73
|
|
|
foreach ($tracks as $track) { |
|
74
|
|
|
$track->setFolderPath($getFolderPath($track->getFolderId(), $foldersLut)); |
|
75
|
|
|
} |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
/** |
|
79
|
|
|
* Find all direct and indirect sub folders of the given folder. The result will include also the start folder. |
|
80
|
|
|
* NOTE: This does not return the mounted or shared folders even in case the $folderId points to user home directory. |
|
81
|
|
|
* @return int[] |
|
82
|
|
|
*/ |
|
83
|
|
|
public function findAllDescendantFolders(int $folderId) : array { |
|
84
|
|
|
$descendants = []; |
|
85
|
|
|
$foldersToProcess = [$folderId]; |
|
86
|
|
|
|
|
87
|
|
|
while(\count($foldersToProcess)) { |
|
88
|
|
|
$descendants = \array_merge($descendants, $foldersToProcess); |
|
89
|
|
|
$foldersToProcess = $this->mapper->findSubFolderIds($foldersToProcess); |
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
return $descendants; |
|
93
|
|
|
} |
|
94
|
|
|
|
|
95
|
|
|
/** |
|
96
|
|
|
* Get folder info lookup table, for the given tracks. The table will contain all the predecessor folders |
|
97
|
|
|
* between those tracks and the root music folder (inclusive). |
|
98
|
|
|
* |
|
99
|
|
|
* @param array $trackIdsByFolder Keys are folder IDs and values are arrays of track IDs |
|
100
|
|
|
* @return array Keys are folder IDs and values are arrays like ['name' : string, 'parent' : int, 'trackIds' : int[]] |
|
101
|
|
|
*/ |
|
102
|
|
|
private function getFoldersLut(array $trackIdsByFolder, string $userId, Folder $musicFolder) : array { |
|
103
|
|
|
// Get the folder names and direct parent folder IDs directly from the DB. |
|
104
|
|
|
// This is significantly more efficient than using the Files API because we need to |
|
105
|
|
|
// run only single DB query instead of one per folder. |
|
106
|
|
|
$folderNamesAndParents = $this->mapper->findNodeNamesAndParents(\array_keys($trackIdsByFolder)); |
|
107
|
|
|
|
|
108
|
|
|
// Compile the look-up-table entries from our two intermediary arrays |
|
109
|
|
|
$lut = []; |
|
110
|
|
|
foreach ($trackIdsByFolder as $folderId => $trackIds) { |
|
111
|
|
|
// $folderId is not found from $folderNamesAndParents if it's a dummy ID created as placeholder on a malformed playlist |
|
112
|
|
|
$nameAndParent = $folderNamesAndParents[$folderId] ?? ['name' => '', 'parent' => null]; |
|
113
|
|
|
$lut[$folderId] = \array_merge($nameAndParent, ['trackIds' => $trackIds]); |
|
114
|
|
|
} |
|
115
|
|
|
|
|
116
|
|
|
// the root folder should have null parent; here we also ensure it's included |
|
117
|
|
|
$rootFolderId = $musicFolder->getId(); |
|
118
|
|
|
$rootTracks = $lut[$rootFolderId]['trackIds'] ?? []; |
|
119
|
|
|
$lut[$rootFolderId] = ['name' => '', 'parent' => null, 'trackIds' => $rootTracks]; |
|
120
|
|
|
|
|
121
|
|
|
// External mounts and shared files/folders need some special handling. But if there are any, they should be found |
|
122
|
|
|
// right under the top-level folder. |
|
123
|
|
|
$this->addExternalMountsToFoldersLut($lut, $userId, $musicFolder); |
|
124
|
|
|
|
|
125
|
|
|
// Add the intermediate folders which do not directly contain any tracks |
|
126
|
|
|
$this->addMissingParentsToFoldersLut($lut); |
|
127
|
|
|
|
|
128
|
|
|
return $lut; |
|
129
|
|
|
} |
|
130
|
|
|
|
|
131
|
|
|
/** |
|
132
|
|
|
* Add externally mounted folders and shared files and folders to the folder LUT if there are any under the $musicFolder |
|
133
|
|
|
* |
|
134
|
|
|
* @param array $lut (in|out) Keys are folder IDs and values are arrays like ['name' : string, 'parent' : int, 'trackIds' : int[]] |
|
135
|
|
|
*/ |
|
136
|
|
|
private function addExternalMountsToFoldersLut(array &$lut, string $userId, Folder $musicFolder) : void { |
|
137
|
|
|
$nodesUnderRoot = $musicFolder->getDirectoryListing(); |
|
138
|
|
|
$homeStorageId = $musicFolder->getStorage()->getId(); |
|
139
|
|
|
$rootFolderId = $musicFolder->getId(); |
|
140
|
|
|
|
|
141
|
|
|
foreach ($nodesUnderRoot as $node) { |
|
142
|
|
|
if ($node->getStorage()->getId() != $homeStorageId) { |
|
143
|
|
|
// shared file/folder or external mount |
|
144
|
|
|
if ($node->getType() == FileInfo::TYPE_FOLDER) { |
|
145
|
|
|
// The mount point folders are always included in the result. At this time, we don't know if |
|
146
|
|
|
// they actually contain any tracks, unless they have direct track children. If there are direct tracks, |
|
147
|
|
|
// then the parent ID is incorrectly set and needs to be overridden. |
|
148
|
|
|
$trackIds = $lut[$node->getId()]['trackIds'] ?? []; |
|
149
|
|
|
$lut[$node->getId()] = ['name' => $node->getName(), 'parent' => $rootFolderId, 'trackIds' => $trackIds]; |
|
150
|
|
|
|
|
151
|
|
|
} else if ($node->getMimePart() == 'audio') { |
|
152
|
|
|
// shared audio file, check if it's actually a scanned file in our library |
|
153
|
|
|
try { |
|
154
|
|
|
$sharedTrack = $this->mapper->findByFileId($node->getId(), $userId); |
|
155
|
|
|
$trackId = $sharedTrack->getId(); |
|
156
|
|
|
foreach ($lut as $folderId => &$entry) { |
|
157
|
|
|
$trackIdIdx = \array_search($trackId, $entry['trackIds']); |
|
158
|
|
|
if ($trackIdIdx !== false) { |
|
159
|
|
|
// move the track from it's actual parent (in other user's storage) to our root |
|
160
|
|
|
unset($entry['trackIds'][$trackIdIdx]); |
|
161
|
|
|
$lut[$rootFolderId]['trackIds'][] = $trackId; |
|
162
|
|
|
|
|
163
|
|
|
// remove the former parent folder if it has no more tracks and it's not one of the mount point folders |
|
164
|
|
|
if (\count($entry['trackIds']) == 0 && empty(\array_filter($nodesUnderRoot, fn($n) => $n->getId() == $folderId))) { |
|
165
|
|
|
unset($lut[$folderId]); |
|
166
|
|
|
} |
|
167
|
|
|
break; |
|
168
|
|
|
} |
|
169
|
|
|
} |
|
170
|
|
|
} catch (DoesNotExistException $e) { |
|
171
|
|
|
// ignore |
|
172
|
|
|
} |
|
173
|
|
|
} |
|
174
|
|
|
} |
|
175
|
|
|
} |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
/** |
|
179
|
|
|
* Add any missing intermediary folder to the LUT. For this function to work correctly, the pre-condition is that the LUT contains |
|
180
|
|
|
* a root node which is predecessor of all other contained nodes and has 'parent' set as null. |
|
181
|
|
|
* |
|
182
|
|
|
* @param array $lut (in|out) Keys are folder IDs and values are arrays like ['name' : string, 'parent' : int, 'trackIds' : int[]] |
|
183
|
|
|
*/ |
|
184
|
|
|
private function addMissingParentsToFoldersLut(array &$lut) : void { |
|
185
|
|
|
$foldersToProcess = $lut; |
|
186
|
|
|
|
|
187
|
|
|
while (\count($foldersToProcess)) { |
|
188
|
|
|
$parentIds = \array_unique(\array_column($foldersToProcess, 'parent')); |
|
189
|
|
|
// do not process root even if it's included in $foldersToProcess |
|
190
|
|
|
$parentIds = \array_filter($parentIds, fn($i) => $i !== null); |
|
191
|
|
|
$parentIds = ArrayUtil::diff($parentIds, \array_keys($lut)); |
|
192
|
|
|
$parentFolders = $this->mapper->findNodeNamesAndParents($parentIds); |
|
193
|
|
|
|
|
194
|
|
|
$foldersToProcess = []; |
|
195
|
|
|
foreach ($parentFolders as $folderId => $nameAndParent) { |
|
196
|
|
|
$foldersToProcess[] = $lut[$folderId] = \array_merge($nameAndParent, ['trackIds' => []]); |
|
197
|
|
|
} |
|
198
|
|
|
} |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
} |