| Total Complexity | 40 |
| Total Lines | 412 |
| Duplicated Lines | 0 % |
| Changes | 7 | ||
| Bugs | 0 | Features | 0 |
Complex classes like TrackMapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use TrackMapper, and based on these observations, apply Extract Interface, too.
| 1 | <?php declare(strict_types=1); |
||
| 22 | class TrackMapper extends BaseMapper { |
||
| 23 | public function __construct(IDBConnection $db) { |
||
| 24 | parent::__construct($db, 'music_tracks', Track::class, 'title'); |
||
| 25 | } |
||
| 26 | |||
| 27 | /** |
||
| 28 | * Override the base implementation to include data from multiple tables |
||
| 29 | * |
||
| 30 | * {@inheritdoc} |
||
| 31 | * @see BaseMapper::selectEntities() |
||
| 32 | */ |
||
| 33 | protected function selectEntities(string $condition, string $extension=null) : string { |
||
| 34 | return "SELECT `*PREFIX*music_tracks`.*, `file`.`name` AS `filename`, `file`.`size`, |
||
| 35 | `album`.`name` AS `album_name`, `artist`.`name` AS `artist_name`, `genre`.`name` AS `genre_name` |
||
| 36 | FROM `*PREFIX*music_tracks` |
||
| 37 | INNER JOIN `*PREFIX*filecache` `file` |
||
| 38 | ON `*PREFIX*music_tracks`.`file_id` = `file`.`fileid` |
||
| 39 | INNER JOIN `*PREFIX*music_albums` `album` |
||
| 40 | ON `*PREFIX*music_tracks`.`album_id` = `album`.`id` |
||
| 41 | INNER JOIN `*PREFIX*music_artists` `artist` |
||
| 42 | ON `*PREFIX*music_tracks`.`artist_id` = `artist`.`id` |
||
| 43 | LEFT JOIN `*PREFIX*music_genres` `genre` |
||
| 44 | ON `*PREFIX*music_tracks`.`genre_id` = `genre`.`id` |
||
| 45 | WHERE $condition $extension"; |
||
| 46 | } |
||
| 47 | |||
| 48 | /** |
||
| 49 | * Overridden from the base implementation to add support for sorting by artist. |
||
| 50 | * |
||
| 51 | * {@inheritdoc} |
||
| 52 | * @see BaseMapper::formatSortingClause() |
||
| 53 | */ |
||
| 54 | protected function formatSortingClause(int $sortBy) : ?string { |
||
| 55 | if ($sortBy === SortBy::Parent) { |
||
| 56 | return 'ORDER BY LOWER(`artist_name`), LOWER(`title`)'; |
||
| 57 | } else { |
||
| 58 | return parent::formatSortingClause($sortBy); |
||
| 59 | } |
||
| 60 | } |
||
| 61 | |||
| 62 | /** |
||
| 63 | * Returns all tracks of the given artist (both album and track artists are considered) |
||
| 64 | * @param integer $artistId |
||
| 65 | * @param string $userId |
||
| 66 | * @return Track[] |
||
| 67 | */ |
||
| 68 | public function findAllByArtist($artistId, $userId) { |
||
| 69 | $sql = $this->selectUserEntities( |
||
| 70 | '`artist_id` = ? OR `album_id` IN (SELECT `id` from `*PREFIX*music_albums` WHERE `album_artist_id` = ?) ', |
||
| 71 | 'ORDER BY LOWER(`title`)'); |
||
| 72 | $params = [$userId, $artistId, $artistId]; |
||
| 73 | return $this->findEntities($sql, $params); |
||
| 74 | } |
||
| 75 | |||
| 76 | /** |
||
| 77 | * @param integer $albumId |
||
| 78 | * @param string $userId |
||
| 79 | * @param integer|null $artistId |
||
| 80 | * @return Track[] |
||
| 81 | */ |
||
| 82 | public function findAllByAlbum($albumId, $userId, $artistId = null) { |
||
| 83 | $condition = '`album_id` = ?'; |
||
| 84 | $params = [$userId, $albumId]; |
||
| 85 | |||
| 86 | if ($artistId !== null) { |
||
| 87 | $condition .= ' AND `artist_id` = ? '; |
||
| 88 | $params[] = $artistId; |
||
| 89 | } |
||
| 90 | |||
| 91 | $sql = $this->selectUserEntities($condition, |
||
| 92 | 'ORDER BY `*PREFIX*music_tracks`.`disk`, `number`, LOWER(`title`)'); |
||
| 93 | return $this->findEntities($sql, $params); |
||
| 94 | } |
||
| 95 | |||
| 96 | /** |
||
| 97 | * @param integer $folderId |
||
| 98 | * @param string $userId |
||
| 99 | * @return Track[] |
||
| 100 | */ |
||
| 101 | public function findAllByFolder($folderId, $userId) { |
||
| 105 | } |
||
| 106 | |||
| 107 | /** |
||
| 108 | * @param int $genreId |
||
| 109 | * @param string $userId |
||
| 110 | * @param int|null $limit |
||
| 111 | * @param int|null $offset |
||
| 112 | * @return Track[] tracks |
||
| 113 | */ |
||
| 114 | public function findAllByGenre($genreId, $userId, $limit=null, $offset=null) { |
||
| 115 | $sql = $this->selectUserEntities('`genre_id` = ?', 'ORDER BY LOWER(`title`)'); |
||
| 116 | $params = [$userId, $genreId]; |
||
| 117 | return $this->findEntities($sql, $params, $limit, $offset); |
||
| 118 | } |
||
| 119 | |||
| 120 | /** |
||
| 121 | * @param string $userId |
||
| 122 | * @return int[] |
||
| 123 | */ |
||
| 124 | public function findAllFileIds($userId) { |
||
| 125 | $sql = 'SELECT `file_id` FROM `*PREFIX*music_tracks` WHERE `user_id` = ?'; |
||
| 126 | $result = $this->execute($sql, [$userId]); |
||
| 127 | |||
| 128 | return \array_map(function ($i) { |
||
| 129 | return (int)$i['file_id']; |
||
| 130 | }, $result->fetchAll()); |
||
| 131 | } |
||
| 132 | |||
| 133 | /** |
||
| 134 | * Find a track of user matching a file ID |
||
| 135 | * @param integer $fileId |
||
| 136 | * @param string $userId |
||
| 137 | * @return Track |
||
| 138 | * @throws \OCP\AppFramework\Db\DoesNotExistException if not found |
||
| 139 | */ |
||
| 140 | public function findByFileId($fileId, $userId) { |
||
| 144 | } |
||
| 145 | |||
| 146 | /** |
||
| 147 | * Find tracks of user with multiple file IDs |
||
| 148 | * @param integer[] $fileIds |
||
| 149 | * @param string[] $userIds |
||
| 150 | * @return Track[] |
||
| 151 | */ |
||
| 152 | public function findByFileIds($fileIds, $userIds) { |
||
| 153 | $sql = $this->selectEntities( |
||
| 154 | '`*PREFIX*music_tracks`.`user_id` IN ' . $this->questionMarks(\count($userIds)) . |
||
| 155 | ' AND `file_id` IN '. $this->questionMarks(\count($fileIds))); |
||
| 156 | $params = \array_merge($userIds, $fileIds); |
||
| 157 | return $this->findEntities($sql, $params); |
||
| 158 | } |
||
| 159 | |||
| 160 | /** |
||
| 161 | * Finds tracks of all users matching one or multiple file IDs |
||
| 162 | * @param integer[] $fileIds |
||
| 163 | * @return Track[] |
||
| 164 | */ |
||
| 165 | public function findAllByFileIds($fileIds) { |
||
| 166 | $sql = $this->selectEntities('`file_id` IN '. |
||
| 167 | $this->questionMarks(\count($fileIds))); |
||
| 168 | return $this->findEntities($sql, $fileIds); |
||
| 169 | } |
||
| 170 | |||
| 171 | /** |
||
| 172 | * @param integer $artistId |
||
| 173 | * @return integer |
||
| 174 | */ |
||
| 175 | public function countByArtist($artistId) { |
||
| 176 | $sql = 'SELECT COUNT(*) AS `count` FROM `*PREFIX*music_tracks` WHERE `artist_id` = ?'; |
||
| 177 | $result = $this->execute($sql, [$artistId]); |
||
| 178 | $row = $result->fetch(); |
||
| 179 | return (int)$row['count']; |
||
| 180 | } |
||
| 181 | |||
| 182 | /** |
||
| 183 | * @param integer $albumId |
||
| 184 | * @return integer |
||
| 185 | */ |
||
| 186 | public function countByAlbum($albumId) { |
||
| 191 | } |
||
| 192 | |||
| 193 | /** |
||
| 194 | * @param integer $albumId |
||
| 195 | * @return integer Duration in seconds |
||
| 196 | */ |
||
| 197 | public function totalDurationOfAlbum($albumId) { |
||
| 198 | $sql = 'SELECT SUM(`length`) AS `duration` FROM `*PREFIX*music_tracks` WHERE `album_id` = ?'; |
||
| 199 | $result = $this->execute($sql, [$albumId]); |
||
| 200 | $row = $result->fetch(); |
||
| 201 | return (int)$row['duration']; |
||
| 202 | } |
||
| 203 | |||
| 204 | /** |
||
| 205 | * Get durations of the given tracks. |
||
| 206 | * @param integer[] $trackIds |
||
| 207 | * @return array {int => int} where keys are track IDs and values are corresponding durations |
||
| 208 | */ |
||
| 209 | public function getDurations($trackIds) { |
||
| 221 | } |
||
| 222 | |||
| 223 | /** |
||
| 224 | * @param string $name |
||
| 225 | * @param string $userId |
||
| 226 | * @return Track[] |
||
| 227 | */ |
||
| 228 | public function findAllByNameRecursive($name, $userId) { |
||
| 236 | } |
||
| 237 | |||
| 238 | /** |
||
| 239 | * Returns all tracks specified by name and/or artist name |
||
| 240 | * @param string|null $name the name of the track |
||
| 241 | * @param string|null $artistName the name of the artist |
||
| 242 | * @param bool $fuzzy match names using case-insensitive substring search |
||
| 243 | * @param string $userId the name of the user |
||
| 244 | * @return \OCA\Music\Db\Track[] Tracks matching the criteria |
||
| 245 | */ |
||
| 246 | public function findAllByNameAndArtistName($name, $artistName, $fuzzy, $userId) { |
||
| 276 | } |
||
| 277 | } |
||
| 278 | |||
| 279 | /** |
||
| 280 | * Find most frequently played tracks |
||
| 281 | * @return Track[] |
||
| 282 | */ |
||
| 283 | public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array { |
||
| 286 | } |
||
| 287 | |||
| 288 | /** |
||
| 289 | * Find most recently played tracks |
||
| 290 | * @return Track[] |
||
| 291 | */ |
||
| 292 | public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array { |
||
| 293 | $sql = $this->selectUserEntities('`last_played` IS NOT NULL', 'ORDER BY `last_played` DESC'); |
||
| 294 | return $this->findEntities($sql, [$userId], $limit, $offset); |
||
| 295 | } |
||
| 296 | |||
| 297 | /** |
||
| 298 | * Find least recently played tracks |
||
| 299 | * @return Track[] |
||
| 300 | */ |
||
| 301 | public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array { |
||
| 302 | $sql = $this->selectUserEntities(null, 'ORDER BY `last_played` ASC'); |
||
| 303 | return $this->findEntities($sql, [$userId], $limit, $offset); |
||
| 304 | } |
||
| 305 | |||
| 306 | /** |
||
| 307 | * Finds all track IDs of the user along with the parent folder ID of each track |
||
| 308 | * @param string $userId |
||
| 309 | * @return array where keys are folder IDs and values are arrays of track IDs |
||
| 310 | */ |
||
| 311 | public function findTrackAndFolderIds($userId) { |
||
| 312 | $sql = 'SELECT `track`.`id` AS id, `file`.`name` AS `filename`, `file`.`parent` AS parent |
||
| 313 | FROM `*PREFIX*music_tracks` `track` |
||
| 314 | JOIN `*PREFIX*filecache` `file` |
||
| 315 | ON `track`.`file_id` = `file`.`fileid` |
||
| 316 | WHERE `track`.`user_id` = ?'; |
||
| 317 | |||
| 318 | $rows = $this->execute($sql, [$userId])->fetchAll(); |
||
| 319 | |||
| 320 | // Sort the results according the file names. This can't be made using ORDERBY in the |
||
| 321 | // SQL query because then we couldn't use the "natural order" comparison algorithm |
||
| 322 | \usort($rows, function ($a, $b) { |
||
| 323 | return \strnatcasecmp($a['filename'], $b['filename']); |
||
| 324 | }); |
||
| 325 | |||
| 326 | // group the files to parent folder "buckets" |
||
| 327 | $result = []; |
||
| 328 | foreach ($rows as $row) { |
||
| 329 | $result[(int)$row['parent']][] = (int)$row['id']; |
||
| 330 | } |
||
| 331 | |||
| 332 | return $result; |
||
| 333 | } |
||
| 334 | |||
| 335 | /** |
||
| 336 | * Find names and parents of the file system nodes with given IDs within the given storage |
||
| 337 | * @param int[] $nodeIds |
||
| 338 | * @param string $storageId |
||
| 339 | * @return array where keys are the node IDs and values are associative arrays |
||
| 340 | * like { 'name' => string, 'parent' => int }; |
||
| 341 | */ |
||
| 342 | public function findNodeNamesAndParents($nodeIds, $storageId) { |
||
| 343 | $result = []; |
||
| 344 | |||
| 345 | if (!empty($nodeIds)) { |
||
| 346 | $sql = 'SELECT `fileid`, `name`, `parent` '. |
||
| 347 | 'FROM `*PREFIX*filecache` `filecache` '. |
||
| 348 | 'JOIN `*PREFIX*storages` `storages` '. |
||
| 349 | 'ON `filecache`.`storage` = `storages`.`numeric_id` '. |
||
| 350 | 'WHERE `storages`.`id` = ? '. |
||
| 351 | 'AND `filecache`.`fileid` IN '. $this->questionMarks(\count($nodeIds)); |
||
| 352 | |||
| 353 | $rows = $this->execute($sql, \array_merge([$storageId], $nodeIds))->fetchAll(); |
||
| 354 | |||
| 355 | foreach ($rows as $row) { |
||
| 356 | $result[$row['fileid']] = [ |
||
| 357 | 'name' => $row['name'], |
||
| 358 | 'parent' => (int)$row['parent'] |
||
| 359 | ]; |
||
| 360 | } |
||
| 361 | } |
||
| 362 | |||
| 363 | return $result; |
||
| 364 | } |
||
| 365 | |||
| 366 | /** |
||
| 367 | * Returns all genre IDs associated with the given artist |
||
| 368 | * @param int $artistId |
||
| 369 | * @param string $userId |
||
| 370 | * @return int[] |
||
| 371 | */ |
||
| 372 | public function getGenresByArtistId($artistId, $userId) { |
||
| 373 | $sql = 'SELECT DISTINCT(`genre_id`) FROM `*PREFIX*music_tracks` WHERE |
||
| 374 | `genre_id` IS NOT NULL AND `user_id` = ? AND `artist_id` = ?'; |
||
| 375 | $rows = $this->execute($sql, [$userId, $artistId]); |
||
| 376 | return $rows->fetchAll(\PDO::FETCH_COLUMN); |
||
| 377 | } |
||
| 378 | |||
| 379 | /** |
||
| 380 | * Returns all tracks IDs of the user, organized by the genre_id. |
||
| 381 | * @param string $userId |
||
| 382 | * @return array where keys are genre IDs and values are arrays of track IDs |
||
| 383 | */ |
||
| 384 | public function mapGenreIdsToTrackIds($userId) { |
||
| 395 | } |
||
| 396 | |||
| 397 | /** |
||
| 398 | * Returns file IDs of the tracks which do not have genre scanned. This is not the same |
||
| 399 | * thing as unknown genre, which means that the genre has been scanned but was not found |
||
| 400 | * from the track metadata. |
||
| 401 | * @param string $userId |
||
| 402 | * @return int[] |
||
| 403 | */ |
||
| 404 | public function findFilesWithoutScannedGenre($userId) { |
||
| 405 | $sql = 'SELECT `track`.`file_id` FROM `*PREFIX*music_tracks` `track` |
||
| 406 | INNER JOIN `*PREFIX*filecache` `file` |
||
| 407 | ON `track`.`file_id` = `file`.`fileid` |
||
| 408 | WHERE `genre_id` IS NULL and `user_id` = ?'; |
||
| 409 | $rows = $this->execute($sql, [$userId]); |
||
| 410 | return $rows->fetchAll(\PDO::FETCH_COLUMN); |
||
| 411 | } |
||
| 412 | |||
| 413 | /** |
||
| 414 | * Update "last played" timestamp and increment the total play count of the track. |
||
| 415 | * The DB row is updated *without* updating the `updated` column. |
||
| 416 | * @return bool true if the track was found and updated, false otherwise |
||
| 417 | */ |
||
| 418 | public function recordTrackPlayed(int $trackId, string $userId, \DateTime $timeOfPlay) : bool { |
||
| 419 | $sql = 'UPDATE `*PREFIX*music_tracks` |
||
| 420 | SET `last_played` = ?, `play_count` = `play_count` + 1 |
||
| 421 | WHERE `user_id` = ? AND `id` = ?'; |
||
| 422 | $params = [$timeOfPlay->format(BaseMapper::SQL_DATE_FORMAT), $userId, $trackId]; |
||
| 423 | $result = $this->execute($sql, $params); |
||
| 424 | return ($result->rowCount() > 0); |
||
| 425 | } |
||
| 426 | |||
| 427 | /** |
||
| 428 | * @see \OCA\Music\Db\BaseMapper::findUniqueEntity() |
||
| 429 | * @param Track $track |
||
| 430 | * @return Track |
||
| 431 | */ |
||
| 432 | protected function findUniqueEntity(Entity $track) : Entity { |
||
| 434 | } |
||
| 435 | } |
||
| 436 |