Passed
Pull Request — master (#806)
by
unknown
07:49
created

DiskNumberMigration::removeDiskNumbersFromAlbums()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 2020
11
 */
12
13
namespace OCA\Music\Migration;
14
15
use OCP\IConfig;
16
use OCP\IDBConnection;
17
use OCP\Migration\IOutput;
18
use OCP\Migration\IRepairStep;
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 removed 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 = isset($row['name']) ? \mb_strtolower($row['name']) : null;
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
			} else {
121
				$prevId = $id;
122
				$prevUser = $user;
123
				$prevArtist = $artist;
124
				$prevName = $name;
125
			}
126
		}
127
128
		return $affectedTracks;
129
	}
130
131
	/**
132
	 * Move all tracks from the source album entity to the destination album entity
133
	 * @param int $sourceAlbum ID
134
	 * @param int $destinationAlbum ID
135
	 */
136
	private function moveTracksBetweenAlbums($sourceAlbum, $destinationAlbum) {
137
		$sql = 'UPDATE `*PREFIX*music_tracks` '.
138
				'SET `album_id` = ? '.
139
				'WHERE `album_id` = ?';
140
		return $this->db->executeUpdate($sql, [$destinationAlbum, $sourceAlbum]);
141
	}
142
143
	/**
144
	 * Delete from the albums table those rows which were made obsolete by the previous steps
145
	 */
146
	private function removeObsoleteAlbums() {
147
		$count = \count($this->obsoleteAlbums);
148
149
		if ($count > 0) {
150
			$sql = 'DELETE FROM `*PREFIX*music_albums` '.
151
					'WHERE `id` IN '. $this->questionMarks($count);
152
			$count = $this->db->executeUpdate($sql, $this->obsoleteAlbums);
153
		}
154
155
		return $count;
156
	}
157
158
	/**
159
	 * Recalculate the hashes for all albums in the table. The disk number is no longer part
160
	 * of the calculation schema.
161
	 */
162
	private function reEvaluateAlbumHashes() {
163
		$sql = 'SELECT `id`, `name`, `album_artist_id` '.
164
				'FROM `*PREFIX*music_albums`';
165
		$rows = $this->db->executeQuery($sql)->fetchAll();
166
167
		$affectedRows = 0;
168
		foreach ($rows as $row) {
169
			$lowerName = \mb_strtolower($row['name'] ?? '');
170
			$artist = $row['album_artist_id'];
171
			$hash = \hash('md5', "$lowerName|$artist");
172
173
			try {
174
				$affectedRows += $this->db->executeUpdate(
175
						'UPDATE `*PREFIX*music_albums` SET `hash` = ? WHERE `id` = ?',
176
						[$hash, $row['id']]
177
				);
178
			} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
179
				$this->mergeFailureAlbums[] = $row['id'];
180
			}
181
		}
182
183
		return $affectedRows;
184
	}
185
186
	/**
187
	 * Remove any albums which should have got merged to another albums, but for some
188
	 * reason this has not happened. Remove also the contained tracks. The user shall
189
	 * be prompted to rescan these problematic albums/tracks when (s)he opens the Music
190
	 * app.
191
	 */
192
	private function removeAlbumsWhichFailedMerging() {
193
		$count = \count($this->mergeFailureAlbums);
194
195
		if ($count > 0) {
196
			$sql = 'DELETE FROM `*PREFIX*music_albums` '.
197
					'WHERE `id` IN '. $this->questionMarks($count);
198
			$count = $this->db->executeUpdate($sql, $this->mergeFailureAlbums);
199
200
			$sql = 'DELETE FROM `*PREFIX*music_tracks` '.
201
					'WHERE `album_id` IN '. $this->questionMarks($count);
202
			$this->db->executeUpdate($sql, $this->mergeFailureAlbums);
203
		}
204
205
		return $count;
206
	}
207
208
	/**
209
	 * Set all disk numbers stored in the albums table as NULL.
210
	 */
211
	private function removeDiskNumbersFromAlbums() {
212
		$sql = 'UPDATE `*PREFIX*music_albums` SET `disk` = NULL';
213
		return $this->db->executeUpdate($sql);
214
	}
215
216
	/**
217
	 * helper creating a string like '(?,?,?)' with the specified number of elements
218
	 * @param int $count
219
	 */
220
	private function questionMarks($count) {
221
		$questionMarks = [];
222
		for ($i = 0; $i < $count; $i++) {
223
			$questionMarks[] = '?';
224
		}
225
		return '(' . \implode(',', $questionMarks) . ')';
226
	}
227
}
228