Test Setup Failed
Pull Request — master (#623)
by Alejandro Carstens
13:00
created

Song::album()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace App\Models;
4
5
use App\Events\LibraryChanged;
6
use App\Traits\SupportsDeleteWhereIDsNotIn;
7
use AWS;
8
use Aws\AwsClient;
9
use Cache;
10
use Illuminate\Database\Eloquent\Model;
11
use Lastfm;
12
use YouTube;
13
use Musixmatch;
14
15
/**
16
 * @property string path
17
 * @property string title
18
 * @property Album  album
19
 * @property Artist artist
20
 * @property string s3_params
21
 * @property float  length
22
 * @property string lyrics
23
 * @property int    track
24
 * @property int    album_id
25
 * @property int    id
26
 * @property int    artist_id
27
 */
28
class Song extends Model
29
{
30
    use SupportsDeleteWhereIDsNotIn;
31
32
    protected $guarded = [];
33
34
    /**
35
     * Attributes to be hidden from JSON outputs.
36
     * Here we specify to hide lyrics as well to save some bandwidth (actually, lots of it).
37
     * Lyrics can then be queried on demand.
38
     *
39
     * @var array
40
     */
41
    protected $hidden = ['lyrics', 'updated_at', 'path', 'mtime'];
42
43
    /**
44
     * @var array
45
     */
46
    protected $casts = [
47
        'length' => 'float',
48
        'mtime' => 'int',
49
        'track' => 'int',
50
    ];
51
52
    /**
53
     * Indicates if the IDs are auto-incrementing.
54
     *
55
     * @var bool
56
     */
57
    public $incrementing = false;
58
59
    public function artist()
60
    {
61
        return $this->belongsTo(Artist::class);
62
    }
63
64
    public function album()
65
    {
66
        return $this->belongsTo(Album::class);
67
    }
68
69
    public function playlists()
70
    {
71
        return $this->belongsToMany(Playlist::class);
72
    }
73
74
    /**
75
     * Scrobble the song using Last.fm service.
76
     *
77
     * @param User   $user
78
     * @param string $timestamp The UNIX timestamp in which the song started playing.
79
     *
80
     * @return mixed
81
     */
82
    public function scrobble(User $user, $timestamp)
83
    {
84
        // Don't scrobble the unknown guys. No one knows them.
85
        if ($this->artist->is_unknown) {
86
            return false;
87
        }
88
89
        // If the current user hasn't connected to Last.fm, don't do shit.
90
        if (!$user->connectedToLastfm()) {
91
            return false;
92
        }
93
94
        return Lastfm::scrobble(
95
            $this->artist->name,
96
            $this->title,
97
            $timestamp,
98
            $this->album->name === Album::UNKNOWN_NAME ? '' : $this->album->name,
99
            $user->lastfm_session_key
100
        );
101
    }
102
103
    /**
104
     * Get a Song record using its path.
105
     *
106
     * @param string $path
107
     *
108
     * @return Song|null
109
     */
110
    public static function byPath($path)
111
    {
112
        return self::find(File::getHash($path));
113
    }
114
115
    /**
116
     * Update song info.
117
     *
118
     * @param array $ids
119
     * @param array $data The data array, with these supported fields:
120
     *                    - title
121
     *                    - artistName
122
     *                    - albumName
123
     *                    - lyrics
124
     *                    All of these are optional, in which case the info will not be changed
125
     *                    (except for lyrics, which will be emptied).
126
     *
127
     * @return array
128
     */
129
    public static function updateInfo($ids, $data)
130
    {
131
        /*
132
         * A collection of the updated songs.
133
         *
134
         * @var \Illuminate\Support\Collection
135
         */
136
        $updatedSongs = collect();
137
138
        $ids = (array) $ids;
139
        // If we're updating only one song, take into account the title, lyrics, and track number.
140
        $single = count($ids) === 1;
141
142
        foreach ($ids as $id) {
143
            if (!$song = self::with('album', 'album.artist')->find($id)) {
0 ignored issues
show
Bug introduced by
The method find does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
144
                continue;
145
            }
146
147
            $updatedSongs->push($song->updateSingle(
148
                $single ? trim($data['title']) : $song->title,
149
                trim($data['albumName'] ?: $song->album->name),
150
                trim($data['artistName']) ?: $song->artist->name,
151
                $single ? trim($data['lyrics']) : $song->lyrics,
152
                $single ? (int) $data['track'] : $song->track,
153
                (int) $data['compilationState']
154
            ));
155
        }
156
157
        // Our library may have been changed. Broadcast an event to tidy it up if need be.
158
        if ($updatedSongs->count()) {
159
            event(new LibraryChanged());
160
        }
161
162
        return [
163
            'artists' => Artist::whereIn('id', $updatedSongs->pluck('artist_id'))->get(),
164
            'albums' => Album::whereIn('id', $updatedSongs->pluck('album_id'))->get(),
165
            'songs' => $updatedSongs,
166
        ];
167
    }
168
169
    /**
170
     * Update a single song's info.
171
     *
172
     * @param string $title
173
     * @param string $albumName
174
     * @param string $artistName
175
     * @param string $lyrics
176
     * @param int    $track
177
     * @param int    $compilationState
178
     *
179
     * @return self
180
     */
181
    public function updateSingle($title, $albumName, $artistName, $lyrics, $track, $compilationState)
182
    {
183
        if ($artistName === Artist::VARIOUS_NAME) {
184
            // If the artist name is "Various Artists", it's a compilation song no matter what.
185
            $compilationState = 1;
186
            // and since we can't determine the real contributing artist, it's "Unknown"
187
            $artistName = Artist::UNKNOWN_NAME;
188
        }
189
190
        $artist = Artist::get($artistName);
191
192
        switch ($compilationState) {
193
            case 1: // ALL, or forcing compilation status to be Yes
194
                $isCompilation = true;
195
                break;
196
            case 2: // Keep current compilation status
197
                $isCompilation = $this->album->artist_id === Artist::VARIOUS_ID;
198
                break;
199
            default:
200
                $isCompilation = false;
201
                break;
202
        }
203
204
        $album = Album::get($artist, $albumName, $isCompilation);
205
206
        $this->artist_id = $artist->id;
207
        $this->album_id = $album->id;
208
        $this->title = $title;
209
        $this->lyrics = $lyrics;
210
        $this->track = $track;
211
212
        $this->save();
213
214
        // Clean up unnecessary data from the object
215
        unset($this->album);
216
        unset($this->artist);
217
        // and make sure the lyrics is shown
218
        $this->makeVisible('lyrics');
219
220
        return $this;
221
    }
222
223
    /**
224
     * Scope a query to only include songs in a given directory.
225
     *
226
     * @param \Illuminate\Database\Eloquent\Builder $query
227
     * @param string                                $path  Full path of the directory
228
     *
229
     * @return \Illuminate\Database\Eloquent\Builder
230
     */
231
    public function scopeInDirectory($query, $path)
232
    {
233
        // Make sure the path ends with a directory separator.
234
        $path = rtrim(trim($path), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
235
236
        return $query->where('path', 'LIKE', "$path%");
237
    }
238
239
    /**
240
     * Get all songs favored by a user.
241
     *
242
     * @param User $user
243
     * @param bool $toArray
244
     *
245
     * @return \Illuminate\Database\Eloquent\Collection|array
246
     */
247
    public static function getFavorites(User $user, $toArray = false)
248
    {
249
        $songs = Interaction::whereUserIdAndLike($user->id, true)
250
            ->with('song')
251
            ->get()
252
            ->pluck('song');
253
254
        return $toArray ? $songs->toArray() : $songs;
255
    }
256
257
    /**
258
     * Get the song's Object Storage url for streaming or downloading.
259
     *
260
     * @param AwsClient $s3
261
     *
262
     * @return string
263
     */
264
    public function getObjectStoragePublicUrl(AwsClient $s3 = null)
265
    {
266
        // If we have a cached version, just return it.
267
        if ($cached = Cache::get("OSUrl/{$this->id}")) {
268
            return $cached;
269
        }
270
271
        // Otherwise, we query S3 for the presigned request.
272
        if (!$s3) {
273
            $s3 = AWS::createClient('s3');
274
        }
275
276
        $cmd = $s3->getCommand('GetObject', [
277
            'Bucket' => $this->s3_params['bucket'],
278
            'Key' => $this->s3_params['key'],
279
        ]);
280
281
        // Here we specify that the request is valid for 1 hour.
282
        // We'll also cache the public URL for future reuse.
283
        $request = $s3->createPresignedRequest($cmd, '+1 hour');
284
        $url = (string) $request->getUri();
285
        Cache::put("OSUrl/{$this->id}", $url, 60);
286
287
        return $url;
288
    }
289
290
    /**
291
     * Get the YouTube videos related to this song.
292
     *
293
     * @param string $youTubePageToken The YouTube page token, for pagination purpose.
294
     *
295
     * @return @return object|false
0 ignored issues
show
Documentation introduced by
The doc-type @return could not be parsed: Unknown type name "@return" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
296
     */
297
    public function getRelatedYouTubeVideos($youTubePageToken = '')
298
    {
299
        return YouTube::searchVideosRelatedToSong($this, $youTubePageToken);
300
    }
301
    
302
    /**
303
     * Get Lyrics related to this song.
304
     * 
305
     * @return object|false
306
     */
307
    public function getSongLyrics()
308
    {
309
        return Musixmatch::searchLyricsRelatedToSong($this);
310
    }
311
    
312
    /**
313
     * Sometimes the tags extracted from getID3 are HTML entity encoded.
314
     * This makes sure they are always sane.
315
     *
316
     * @param $value
317
     */
318
    public function setTitleAttribute($value)
319
    {
320
        $this->attributes['title'] = html_entity_decode($value);
321
    }
322
323
    /**
324
     * Some songs don't have a title.
325
     * Fall back to the file name (without extension) for such.
326
     *
327
     * @param  $value
328
     *
329
     * @return string
330
     */
331
    public function getTitleAttribute($value)
332
    {
333
        return $value ?: pathinfo($this->path, PATHINFO_FILENAME);
334
    }
335
336
    /**
337
     * Prepare the lyrics for displaying.
338
     *
339
     * @param $value
340
     *
341
     * @return string
342
     */
343
    public function getLyricsAttribute($value)
344
    {
345
        // We don't use nl2br() here, because the function actually preserves line breaks -
346
        // it just _appends_ a "<br />" after each of them. This would cause our client
347
        // implementation of br2nl to fail with duplicated line breaks.
348
        return str_replace(["\r\n", "\r", "\n"], '<br />', $value);
349
    }
350
    
351
    /**
352
     * Determine if the song has lyrics.
353
     *
354
     * @return bool
355
     */
356
    public function hasLyrics()
357
    {
358
        return (bool) $this->lyrics;
359
    }
360
    
361
    /**
362
     * Determine if the song is an AWS S3 Object.
363
     *
364
     * @return bool
365
     */
366
    public function isS3ObjectAttribute()
367
    {
368
        return starts_with($this->path, 's3://');
369
    }
370
371
    /**
372
     * Get the bucket and key name of an S3 object.
373
     *
374
     * @return bool|array
375
     */
376
    public function getS3ParamsAttribute()
377
    {
378
        if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) {
379
            return false;
380
        }
381
382
        list($bucket, $key) = explode('/', $matches[1], 2);
383
384
        return compact('bucket', 'key');
385
    }
386
}
387