Track::toSubsonicApi()   B
last analyzed

Complexity

Conditions 7
Paths 16

Size

Total Lines 34
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 32
c 1
b 0
f 0
nc 16
nop 2
dl 0
loc 34
rs 8.4746
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 Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2016 - 2025
13
 */
14
15
namespace OCA\Music\Db;
16
17
use OCA\Music\Utility\StringUtil;
18
use OCA\Music\Utility\Util;
19
use OCP\IL10N;
20
use OCP\IURLGenerator;
21
22
/**
23
 * @method string getTitle()
24
 * @method void setTitle(string $title)
25
 * @method ?int getNumber()
26
 * @method void setNumber(?int $number)
27
 * @method ?int getDisk()
28
 * @method void setDisk(?int $disk)
29
 * @method ?int getYear()
30
 * @method void setYear(?int $year)
31
 * @method int getArtistId()
32
 * @method void setArtistId(int $artistId)
33
 * @method int getAlbumId()
34
 * @method void setAlbumId(int $albumId)
35
 * @method ?int getLength()
36
 * @method void setLength(?int $length)
37
 * @method int getFileId()
38
 * @method void setFileId(int $fileId)
39
 * @method ?int getBitrate()
40
 * @method void setBitrate(?int $bitrate)
41
 * @method string getMimetype()
42
 * @method void setMimetype(string $mimetype)
43
 * @method ?string getMbid()
44
 * @method void setMbid(?string $mbid)
45
 * @method ?string getStarred()
46
 * @method void setStarred(?string $timestamp)
47
 * @method int getRating()
48
 * @method void setRating(int $rating)
49
 * @method ?int getGenreId()
50
 * @method void setGenreId(?int $genreId)
51
 * @method int getPlayCount()
52
 * @method void setPlayCount(int $count)
53
 * @method ?string getLastPlayed()
54
 * @method void setLastPlayed(?string $timestamp)
55
 * @method int getDirty()
56
 * @method void setDirty(int $dirty)
57
 *
58
 * @method string getFilename()
59
 * @method int getSize()
60
 * @method int getFileModTime()
61
 * @method ?string getAlbumName()
62
 * @method ?string getArtistName()
63
 * @method ?string getGenreName()
64
 * @method int getFolderId()
65
 */
66
class Track extends Entity {
67
	public string $title = '';
68
	public ?int $number = null;
69
	public ?int $disk = null;
70
	public ?int $year = null;
71
	public ?int $artistId = null;
72
	public ?int $albumId = null;
73
	public ?int $length = null;
74
	public int $fileId = 0;
75
	public ?int $bitrate = null;
76
	public string $mimetype = '';
77
	public ?string $mbid = null;
78
	public ?string $starred = null;
79
	public int $rating = 0;
80
	public ?int $genreId = null;
81
	public int $playCount = 0;
82
	public ?string $lastPlayed = null;
83
	public int $dirty = 0;
84
85
	// not from the music_tracks table but still part of the standard content of this entity:
86
	public string $filename = '';
87
	public int $size = 0;
88
	public int $fileModTime = 0;
89
	public ?string $albumName = null;
90
	public ?string $artistName = null;
91
	public ?string $genreName = null;
92
	public int $folderId = 0;
93
94
	// the rest of the variables are injected separately when needed
95
	private ?Album $album = null;
96
	private ?int $numberOnPlaylist = null;
97
	private ?string $folderPath = null;
98
	private ?string $lyrics = null;
99
100
	public function __construct() {
101
		$this->addType('number', 'int');
102
		$this->addType('disk', 'int');
103
		$this->addType('year', 'int');
104
		$this->addType('artistId', 'int');
105
		$this->addType('albumId', 'int');
106
		$this->addType('length', 'int');
107
		$this->addType('bitrate', 'int');
108
		$this->addType('fileId', 'int');
109
		$this->addType('genreId', 'int');
110
		$this->addType('playCount', 'int');
111
		$this->addType('rating', 'int');
112
		$this->addType('dirty', 'int');
113
		$this->addType('size', 'int');
114
		$this->addType('fileModTime', 'int');
115
		$this->addType('folderId', 'int');
116
	}
117
118
	public function getAlbum() : ?Album {
119
		return $this->album;
120
	}
121
122
	public function setAlbum(?Album $album) : void {
123
		$this->album = $album;
124
	}
125
126
	public function getNumberOnPlaylist() : ?int {
127
		return $this->numberOnPlaylist;
128
	}
129
130
	public function setNumberOnPlaylist(int $number) : void {
131
		$this->numberOnPlaylist = $number;
132
	}
133
134
	public function setFolderPath(string $path) : void {
135
		$this->folderPath = $path;
136
	}
137
138
	public function setLyrics(?string $lyrics) : void {
139
		$this->lyrics = $lyrics;
140
	}
141
142
	public function getPath() : ?string {
143
		return ($this->folderPath ?? '') . '/' . $this->filename;
144
	}
145
146
	public function getUri(IURLGenerator $urlGenerator) : string {
147
		return $urlGenerator->linkToRoute(
148
			'music.shivaApi.track',
149
			['id' => $this->id]
150
		);
151
	}
152
153
	public function getArtistWithUri(IURLGenerator $urlGenerator) : array {
154
		return [
155
			'id' => $this->artistId,
156
			'uri' => $urlGenerator->linkToRoute(
157
				'music.shivaApi.artist',
158
				['id' => $this->artistId]
159
			)
160
		];
161
	}
162
163
	public function getAlbumWithUri(IURLGenerator $urlGenerator) : array {
164
		return [
165
			'id' => $this->albumId,
166
			'uri' => $urlGenerator->linkToRoute(
167
				'music.shivaApi.album',
168
				['id' => $this->albumId]
169
			)
170
		];
171
	}
172
173
	public function getArtistNameString(IL10N $l10n) : string {
174
		return $this->getArtistName() ?: Artist::unknownNameString($l10n);
175
	}
176
177
	public function getAlbumNameString(IL10N $l10n) : string {
178
		return $this->getAlbumName() ?: Album::unknownNameString($l10n);
179
	}
180
181
	public function getGenreNameString(IL10N $l10n) : string {
182
		return $this->getGenreName() ?: Genre::unknownNameString($l10n);
183
	}
184
185
	public function toCollection() : array {
186
		return [
187
			'title' => $this->getTitle(),
188
			'number' => $this->getNumber(),
189
			'disk' => $this->getDisk(),
190
			'artistId' => $this->getArtistId(),
191
			'length' => $this->getLength(),
192
			'files' => [$this->getMimetype() => $this->getFileId()],
193
			'id' => $this->getId(),
194
		];
195
	}
196
197
	public function toShivaApi(IURLGenerator $urlGenerator) : array {
198
		return [
199
			'title' => $this->getTitle(),
200
			'ordinal' => $this->getAdjustedTrackNumber(),
201
			'artist' => $this->getArtistWithUri($urlGenerator),
202
			'album' => $this->getAlbumWithUri($urlGenerator),
203
			'length' => $this->getLength(),
204
			'files' => [$this->getMimetype() => $urlGenerator->linkToRoute(
205
				'music.musicApi.download',
206
				['fileId' => $this->getFileId()]
207
			)],
208
			'bitrate' => $this->getBitrate(),
209
			'id' => $this->getId(),
210
			'slug' => $this->slugify('title'),
211
			'uri' => $this->getUri($urlGenerator)
212
		];
213
	}
214
215
	public function toAmpacheApi(
216
			IL10N $l10n,
217
			callable $createPlayUrl,
218
			callable $createImageUrl,
219
			callable $renderAlbumOrArtistRef,
220
			string $genreKey,
221
			bool $includeArtists) : array {
222
		$album = $this->getAlbum();
223
224
		$result = [
225
			'id' => (string)$this->getId(),
226
			'title' => $this->getTitle() ?: '',
227
			'name' => $this->getTitle() ?: '',
228
			'artist' => $renderAlbumOrArtistRef($this->getArtistId() ?: 0, $this->getArtistNameString($l10n)),
229
			'albumartist' => $renderAlbumOrArtistRef($album->getAlbumArtistId() ?: 0, $album->getAlbumArtistNameString($l10n)),
230
			'album' => $renderAlbumOrArtistRef($album->getId() ?: 0, $album->getNameString($l10n)),
231
			'url' => $createPlayUrl($this),
232
			'time' => $this->getLength(),
233
			'year' => $this->getYear(),
234
			'track' => $this->getAdjustedTrackNumber(), // TODO: maybe there should be a user setting to select plain or adjusted number
235
			'playlisttrack' => $this->getAdjustedTrackNumber(),
236
			'disk' => $this->getDisk(),
237
			'filename' => $this->getFilename(),
238
			'format' => $this->getFileExtension(),
239
			'stream_format' => $this->getFileExtension(),
240
			'bitrate' => $this->getBitrate(),
241
			'stream_bitrate' => $this->getBitrate(),
242
			'mime' => $this->getMimetype(),
243
			'stream_mime' => $this->getMimetype(),
244
			'size' => $this->getSize(),
245
			'art' => $createImageUrl($this),
246
			'rating' => $this->getRating(),
247
			'preciserating' => $this->getRating(),
248
			'playcount' => $this->getPlayCount(),
249
			'flag' => !empty($this->getStarred()),
250
			'language' => null,
251
			'lyrics' => $this->lyrics,
252
			'mode' => null, // cbr/vbr
253
			'rate' => null, // sample rate [Hz]
254
			'replaygain_album_gain' => null,
255
			'replaygain_album_peak' => null,
256
			'replaygain_track_gain' => null,
257
			'replaygain_track_peak' => null,
258
			'r128_album_gain' => null,
259
			'r128_track_gain' => null,
260
		];
261
262
		$result['has_art'] = !empty($result['art']);
263
264
		$genreId = $this->getGenreId();
265
		if ($genreId !== null) {
266
			$result[$genreKey] = [[
267
				'id' => (string)$genreId,
268
				'text' => $this->getGenreNameString($l10n),
269
				'count' => 1
270
			]];
271
		}
272
273
		if ($includeArtists) {
274
			// Add another property `artists`. Apparently, it exists to support multiple artists per song
275
			// but we don't have such possibility and this is always just a 1-item array.
276
			$result['artists'] = [$result['artist']];
277
		}
278
	
279
		return $result;
280
	}
281
282
	/**
283
	 * The same API format is used both on "old" and "new" API methods. The "new" API adds some
284
	 * new fields for the songs, but providing some extra fields shouldn't be a problem for the
285
	 * older clients. The $track entity must have the Album reference injected prior to calling this.
286
	 * 
287
	 * @param string[] $ignoredArticles
288
	 */
289
	public function toSubsonicApi(IL10N $l10n, array $ignoredArticles) : array {
290
		$albumId = $this->getAlbumId();
291
		$album = $this->getAlbum();
292
		$hasCoverArt = ($album !== null && !empty($album->getCoverFileId()));
293
294
		return [
295
			'id' => 'track-' . $this->getId(),
296
			'parent' => 'album-' . $albumId,
297
			'discNumber' => $this->getDisk(),
298
			'title' => $this->getTitle(),
299
			'artist' => $this->getArtistNameString($l10n),
300
			'isDir' => false,
301
			'album' => $this->getAlbumNameString($l10n),
302
			'year' => $this->getYear(),
303
			'size' => $this->getSize(),
304
			'contentType' => $this->getMimetype(),
305
			'suffix' => $this->getFileExtension(),
306
			'duration' => $this->getLength() ?? 0,
307
			'bitRate' => empty($this->getBitrate()) ? null : (int)\round($this->getBitrate()/1000), // convert bps to kbps
308
			'path' => $this->getPath(),
309
			'isVideo' => false,
310
			'albumId' => 'album-' . $albumId,
311
			'artistId' => 'artist-' . $this->getArtistId(),
312
			'type' => 'music',
313
			'created' => Util::formatZuluDateTime($this->getCreated()),
314
			'track' => $this->getAdjustedTrackNumber(false), // DSub would get confused of playlist numbering, https://github.com/owncloud/music/issues/994
315
			'starred' => Util::formatZuluDateTime($this->getStarred()),
316
			'userRating' => $this->getRating() ?: null,
317
			'averageRating' => $this->getRating() ?: null,
318
			'genre' => empty($this->getGenreId()) ? null : $this->getGenreNameString($l10n),
319
			'coverArt' => !$hasCoverArt ? null : 'album-' . $albumId,
320
			'playCount' => $this->getPlayCount(),
321
			'played' => Util::formatZuluDateTime($this->getLastPlayed()) ?? '', // OpenSubsonic
322
			'sortName' => StringUtil::splitPrefixAndBasename($this->getTitle(), $ignoredArticles)['basename'], // OpenSubsonic
323
		];
324
	}
325
326
	public function getAdjustedTrackNumber(bool $enablePlaylistNumbering=true) : ?int {
327
		// Unless disabled, the number on playlist overrides the track number if it is set.
328
		if ($enablePlaylistNumbering && $this->numberOnPlaylist !== null) {
329
			$trackNumber = $this->numberOnPlaylist;
330
		} else {
331
			// On single-disk albums, the track number is given as-is.
332
			// On multi-disk albums, the disk-number is applied to the track number.
333
			// In case we have no Album reference, the best we can do is to apply the
334
			// disk number if it is greater than 1. For disk 1, we don't know if this
335
			// is a multi-disk album or not.
336
			$numberOfDisks = ($this->album) ? $this->album->getNumberOfDisks() : null;
337
			$trackNumber = $this->getNumber();
338
339
			if ($this->disk > 1 || $numberOfDisks > 1) {
340
				$trackNumber = $trackNumber ?: 0;
341
				$trackNumber += (100 * $this->disk);
342
			}
343
		}
344
345
		return $trackNumber;
346
	}
347
348
	public function getFileExtension() : string {
349
		$parts = Util::explode('.', $this->getFilename());
350
		return empty($parts) ? '' : \end($parts);
351
	}
352
353
	/**
354
	 * Get an instance which has all the mandatory fields set to valid but empty values
355
	 */
356
	public static function emptyInstance() : Track {
357
		$track = new self();
358
359
		$track->id = -1;
360
		$track->title = '';
361
		$track->artistId = -1;
362
		$track->albumId = -1;
363
		$track->fileId = -1;
364
		$track->mimetype = '';
365
		$track->playCount = 0;
366
		$track->dirty = 0;
367
368
		$track->filename = '';
369
		$track->size = 0;
370
		$track->fileModTime = 0;
371
		$track->folderId = -1;
372
373
		return $track;
374
	}
375
}
376