Passed
Push — master ( b3e441...fda064 )
by Pauli
03:02
created

DiskNumberMigration::executeMigrationSteps()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 18
rs 9.8666
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\Migration;
14
15
use OCP\IConfig;
0 ignored issues
show
Bug introduced by
The type OCP\IConfig 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...
16
use OCP\IDBConnection;
0 ignored issues
show
Bug introduced by
The type OCP\IDBConnection 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...
17
use OCP\Migration\IOutput;
0 ignored issues
show
Bug introduced by
The type OCP\Migration\IOutput 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...
18
use OCP\Migration\IRepairStep;
0 ignored issues
show
Bug introduced by
The type OCP\Migration\IRepairStep 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...
19
20
class DiskNumberMigration implements IRepairStep {
21
22
	/** @var IDBConnection */
23
	private $db;
24
25
	/** @var IConfig */
26
	private $config;
27
28
	/** @var int[] */
29
	private $obsoleteAlbums;
30
31
	/** @var int[] */
32
	private $mergeFailureAlbums;
33
34
	public function __construct(IDBConnection $connection, IConfig $config) {
35
		$this->db = $connection;
36
		$this->config = $config;
37
		$this->obsoleteAlbums = [];
38
		$this->mergeFailureAlbums = [];
39
	}
40
41
	public function getName() {
42
		return 'Combine multi-disk albums and store disk numbers per track';
43
	}
44
45
	/**
46
	 * @inheritdoc
47
	 */
48
	public function run(IOutput $output) {
49
		$installedVersion = $this->config->getAppValue('music', 'installed_version');
50
51
		if (\version_compare($installedVersion, '0.13.0', '<=')) {
52
			try {
53
				$this->executeMigrationSteps($output);
54
			} catch (\Exception $e) {
55
				$output->warning('Unexpected exception ' . get_class($e) . ' during Music disk-number-migration. ' . 
56
								'The music DB may need to be rebuilt.');
57
			}
58
		}
59
	}
60
61
	private function executeMigrationSteps(IOutput $output) {
62
		$n = $this->copyDiskNumberToTracks();
63
		$output->info("$n tracks were updated with a disk number");
64
65
		$n = $this->combineMultiDiskAlbums();
66
		$output->info("$n tracks were assinged to new albums when combining multi-disk albums");
67
68
		$n = $this->removeObsoleteAlbums();
69
		$output->info("$n obsolete album entries were removed from the database");
70
71
		$n = $this->reEvaluateAlbumHashes();
72
		$output->info("$n albums were updated with new hashes");
73
74
		$n = $this->removeAlbumsWhichFailedMerging();
75
		$output->info("$n albums were rmeoved because merging them failed; these need to be rescanned by the user");
76
77
		$n = $this->removeDiskNumbersFromAlbums();
78
		$output->info("obsolete disk number field was nullified in $n albums");
79
	}
80
81
	/**
82
	 * Copy disk numbers from the albums table to the tracks table
83
	 */
84
	private function copyDiskNumberToTracks() {
85
		$sql = 'UPDATE `*PREFIX*music_tracks` '. 
86
				'SET `disk` = (SELECT `disk` '. 
87
				'              FROM `*PREFIX*music_albums` '.
88
				'              WHERE `*PREFIX*music_tracks`.`album_id` = `*PREFIX*music_albums`.`id`) '.
89
				'WHERE `disk` IS NULL';
90
		return $this->db->executeUpdate($sql);
91
	}
92
93
	/**
94
	 * Move all tracks belonging to separate disks of the same album title to the
95
	 * album entity matching the first of those disks. The album entities matching
96
	 * the rest of the disks become obsolete.
97
	 */
98
	private function combineMultiDiskAlbums() {
99
		$sql = 'SELECT `id`, `user_id`, `album_artist_id`, `name` '.
100
				'FROM `*PREFIX*music_albums` '.
101
				'ORDER BY `user_id`, `album_artist_id`, LOWER(`name`)';
102
103
		$rows = $this->db->executeQuery($sql)->fetchAll();
104
105
		$affectedTracks = 0;
106
		$prevId = null;
107
		$prevUser = null;
108
		$prevArtist = null;
109
		$prevName = null;
110
		foreach ($rows as $row) {
111
			$id = $row['id'];
112
			$user = $row['user_id'];
113
			$artist = $row['album_artist_id'];
114
			$name = \mb_strtolower($row['name']);
115
116
			if ($user === $prevUser && $artist === $prevArtist && $name === $prevName) {
117
				// another disk of the same album => merge
118
				$affectedTracks += $this->moveTracksBetweenAlbums($id, $prevId);
119
				$this->obsoleteAlbums[] = $id;
120
			}
121
			else {
122
				$prevId = $id;
123
				$prevUser = $user;
124
				$prevArtist = $artist;
125
				$prevName = $name;
126
			}
127
		}
128
129
		return $affectedTracks;
130
	}
131
132
	/**
133
	 * Move all tracks from the source album entity to the destination album entity
134
	 * @param int $sourceAlbum ID
135
	 * @param int $destinationAlbum ID
136
	 */
137
	private function moveTracksBetweenAlbums($sourceAlbum, $destinationAlbum) {
138
		$sql = 'UPDATE `*PREFIX*music_tracks` '.
139
				'SET `album_id` = ? '.
140
				'WHERE `album_id` = ?';
141
		return $this->db->executeUpdate($sql, [$destinationAlbum, $sourceAlbum]);
142
	}
143
144
	/**
145
	 * Delete from the albums table those rows which were made obsolete by the previous steps
146
	 */
147
	private function removeObsoleteAlbums() {
148
		$count = count($this->obsoleteAlbums);
149
150
		if ($count > 0) {
151
			$sql = 'DELETE FROM `*PREFIX*music_albums` '.
152
					'WHERE `id` IN '. $this->questionMarks($count);
153
			$count = $this->db->executeUpdate($sql, $this->obsoleteAlbums);
154
		}
155
156
		return $count;
157
	}
158
159
	/**
160
	 * Recalculate the hashes for all albums in the table. The disk number is no longer part
161
	 * of the calculation schema.
162
	 */
163
	private function reEvaluateAlbumHashes() {
164
		$sql = 'SELECT `id`, `name`, `album_artist_id` '.
165
				'FROM `*PREFIX*music_albums`';
166
		$rows = $this->db->executeQuery($sql)->fetchAll();
167
168
		$affectedRows = 0;
169
		foreach ($rows as $row) {
170
			$lowerName = \mb_strtolower($row['name']);
171
			$artist = $row['album_artist_id'];
172
			$hash = \hash('md5', "$lowerName|$artist");
173
174
			try {
175
				$affectedRows += $this->db->executeUpdate(
176
						'UPDATE `*PREFIX*music_albums` SET `hash` = ? WHERE `id` = ?',
177
						[$hash, $row['id']]
178
				);
179
			}
180
			catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
0 ignored issues
show
Bug introduced by
The type Doctrine\DBAL\Exception\...raintViolationException 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...
181
				$this->mergeFailureAlbums[] = $row['id'];
182
			}
183
		}
184
185
		return $affectedRows;
186
	}
187
188
	/**
189
	 * Remove any albums which should have got merged to another albums, but for some
190
	 * reason this has not happened. Remove also the contained tracks. The user shall
191
	 * be prompted to rescan these problematic albums/tracks when (s)he opens the Music
192
	 * app.
193
	 */
194
	private function removeAlbumsWhichFailedMerging() {
195
		$count = count($this->mergeFailureAlbums);
196
197
		if ($count > 0) {
198
			$sql = 'DELETE FROM `*PREFIX*music_albums` '.
199
					'WHERE `id` IN '. $this->questionMarks($count);
200
			$count = $this->db->executeUpdate($sql, $this->mergeFailureAlbums);
201
202
			$sql = 'DELETE FROM `*PREFIX*music_tracks` '.
203
					'WHERE `album_id` IN '. $this->questionMarks($count);
204
			$this->db->executeUpdate($sql, $this->mergeFailureAlbums);
205
		}
206
207
		return $count;
208
	}
209
210
	/**
211
	 * Set all disk numbers stored in the albums table as NULL.
212
	 */
213
	private function removeDiskNumbersFromAlbums() {
214
		$sql = 'UPDATE `*PREFIX*music_albums` SET `disk` = NULL';
215
		return $this->db->executeUpdate($sql);
216
	}
217
218
	/**
219
	 * helper creating a string like '(?,?,?)' with the specified number of elements
220
	 * @param int $count
221
	 */
222
	private function questionMarks($count) {
223
		$questionMarks = [];
224
		for ($i = 0; $i < $count; $i++) {
225
			$questionMarks[] = '?';
226
		}
227
		return '(' . \implode(',', $questionMarks) . ')';
228
	}
229
}
230