Test Setup Failed
Push — master ( 7f1ec0...39e837 )
by Phan
04:24
created

Song::getObjectStoragePublicUrl()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 12
nc 3
nop 1
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\Builder;
11
use Illuminate\Database\Eloquent\Collection;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
15
use Lastfm;
16
use YouTube;
17
18
/**
19
 * @property string path
20
 * @property string title
21
 * @property Album  album
22
 * @property Artist artist
23
 * @property string s3_params
24
 * @property float  length
25
 * @property string lyrics
26
 * @property int    track
27
 * @property int    album_id
28
 * @property int    id
29
 * @property int    artist_id
30
 */
31
class Song extends Model
32
{
33
    use SupportsDeleteWhereIDsNotIn;
34
35
    protected $guarded = [];
36
37
    /**
38
     * Attributes to be hidden from JSON outputs.
39
     * Here we specify to hide lyrics as well to save some bandwidth (actually, lots of it).
40
     * Lyrics can then be queried on demand.
41
     *
42
     * @var array
43
     */
44
    protected $hidden = ['lyrics', 'updated_at', 'path', 'mtime'];
45
46
    /**
47
     * @var array
48
     */
49
    protected $casts = [
50
        'length' => 'float',
51
        'mtime' => 'int',
52
        'track' => 'int',
53
    ];
54
55
    /**
56
     * Indicates if the IDs are auto-incrementing.
57
     *
58
     * @var bool
59
     */
60
    public $incrementing = false;
61
62
    /**
63
     * A song belongs to an artist.
64
     *
65
     * @return BelongsTo
66
     */
67
    public function artist()
68
    {
69
        return $this->belongsTo(Artist::class);
70
    }
71
72
    /**
73
     * A song belongs to a album.
74
     *
75
     * @return BelongsTo
76
     */
77
    public function album()
78
    {
79
        return $this->belongsTo(Album::class);
80
    }
81
82
    /**
83
     * A song can belong to many playlists.
84
     *
85
     * @return BelongsToMany
86
     */
87
    public function playlists()
88
    {
89
        return $this->belongsToMany(Playlist::class);
90
    }
91
92
    /**
93
     * Scrobble the song using Last.fm service.
94
     *
95
     * @param User   $user
96
     * @param string $timestamp The UNIX timestamp in which the song started playing.
97
     *
98
     * @return mixed
99
     */
100
    public function scrobble(User $user, $timestamp)
101
    {
102
        // Don't scrobble the unknown guys. No one knows them.
103
        if ($this->artist->is_unknown) {
104
            return false;
105
        }
106
107
        // If the current user hasn't connected to Last.fm, don't do shit.
108
        if (!$user->connectedToLastfm()) {
109
            return false;
110
        }
111
112
        return Lastfm::scrobble(
113
            $this->artist->name,
114
            $this->title,
115
            $timestamp,
116
            $this->album->name === Album::UNKNOWN_NAME ? '' : $this->album->name,
117
            $user->lastfm_session_key
118
        );
119
    }
120
121
    /**
122
     * Get a Song record using its path.
123
     *
124
     * @param string $path
125
     *
126
     * @return Song|null
127
     */
128
    public static function byPath($path)
129
    {
130
        return self::find(File::getHash($path));
131
    }
132
133
    /**
134
     * Update song info.
135
     *
136
     * @param array $ids
137
     * @param array $data The data array, with these supported fields:
138
     *                    - title
139
     *                    - artistName
140
     *                    - albumName
141
     *                    - lyrics
142
     *                    All of these are optional, in which case the info will not be changed
143
     *                    (except for lyrics, which will be emptied).
144
     *
145
     * @return array
146
     */
147
    public static function updateInfo($ids, $data)
148
    {
149
        /*
150
         * A collection of the updated songs.
151
         *
152
         * @var Collection
153
         */
154
        $updatedSongs = collect();
155
156
        $ids = (array) $ids;
157
        // If we're updating only one song, take into account the title, lyrics, and track number.
158
        $single = count($ids) === 1;
159
160
        foreach ($ids as $id) {
161
            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...
162
                continue;
163
            }
164
165
            $updatedSongs->push($song->updateSingle(
166
                $single ? trim($data['title']) : $song->title,
167
                trim($data['albumName'] ?: $song->album->name),
168
                trim($data['artistName']) ?: $song->artist->name,
169
                $single ? trim($data['lyrics']) : $song->lyrics,
170
                $single ? (int) $data['track'] : $song->track,
171
                (int) $data['compilationState']
172
            ));
173
        }
174
175
        // Our library may have been changed. Broadcast an event to tidy it up if need be.
176
        if ($updatedSongs->count()) {
177
            event(new LibraryChanged());
178
        }
179
180
        return [
181
            'artists' => Artist::whereIn('id', $updatedSongs->pluck('artist_id'))->get(),
182
            'albums' => Album::whereIn('id', $updatedSongs->pluck('album_id'))->get(),
183
            'songs' => $updatedSongs,
184
        ];
185
    }
186
187
    /**
188
     * Update a single song's info.
189
     *
190
     * @param string $title
191
     * @param string $albumName
192
     * @param string $artistName
193
     * @param string $lyrics
194
     * @param int    $track
195
     * @param int    $compilationState
196
     *
197
     * @return self
198
     */
199
    public function updateSingle($title, $albumName, $artistName, $lyrics, $track, $compilationState)
200
    {
201
        if ($artistName === Artist::VARIOUS_NAME) {
202
            // If the artist name is "Various Artists", it's a compilation song no matter what.
203
            $compilationState = 1;
204
            // and since we can't determine the real contributing artist, it's "Unknown"
205
            $artistName = Artist::UNKNOWN_NAME;
206
        }
207
208
        $artist = Artist::get($artistName);
209
210
        switch ($compilationState) {
211
            case 1: // ALL, or forcing compilation status to be Yes
212
                $isCompilation = true;
213
                break;
214
            case 2: // Keep current compilation status
215
                $isCompilation = $this->album->artist_id === Artist::VARIOUS_ID;
216
                break;
217
            default:
218
                $isCompilation = false;
219
                break;
220
        }
221
222
        $album = Album::get($artist, $albumName, $isCompilation);
223
224
        $this->artist_id = $artist->id;
225
        $this->album_id = $album->id;
226
        $this->title = $title;
227
        $this->lyrics = $lyrics;
228
        $this->track = $track;
229
230
        $this->save();
231
232
        // Clean up unnecessary data from the object
233
        unset($this->album);
234
        unset($this->artist);
235
        // and make sure the lyrics is shown
236
        $this->makeVisible('lyrics');
237
238
        return $this;
239
    }
240
241
    /**
242
     * Scope a query to only include songs in a given directory.
243
     *
244
     * @param Builder $query
245
     * @param string  $path  Full path of the directory
246
     *
247
     * @return Builder
248
     */
249
    public function scopeInDirectory($query, $path)
250
    {
251
        // Make sure the path ends with a directory separator.
252
        $path = rtrim(trim($path), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
253
254
        return $query->where('path', 'LIKE', "$path%");
255
    }
256
257
    /**
258
     * Get all songs favored by a user.
259
     *
260
     * @param User $user
261
     * @param bool $toArray
262
     *
263
     * @return Collection|array
264
     */
265
    public static function getFavorites(User $user, $toArray = false)
266
    {
267
        $songs = Interaction::whereUserIdAndLike($user->id, true)
268
            ->with('song')
269
            ->get()
270
            ->pluck('song');
271
272
        return $toArray ? $songs->toArray() : $songs;
273
    }
274
275
    /**
276
     * Get the song's Object Storage url for streaming or downloading.
277
     *
278
     * @param AwsClient $s3
279
     *
280
     * @return string
281
     */
282
    public function getObjectStoragePublicUrl(AwsClient $s3 = null)
283
    {
284
        // If we have a cached version, just return it.
285
        if ($cached = Cache::get("OSUrl/{$this->id}")) {
286
            return $cached;
287
        }
288
289
        // Otherwise, we query S3 for the presigned request.
290
        if (!$s3) {
291
            $s3 = AWS::createClient('s3');
292
        }
293
294
        $cmd = $s3->getCommand('GetObject', [
295
            'Bucket' => $this->s3_params['bucket'],
296
            'Key' => $this->s3_params['key'],
297
        ]);
298
299
        // Here we specify that the request is valid for 1 hour.
300
        // We'll also cache the public URL for future reuse.
301
        $request = $s3->createPresignedRequest($cmd, '+1 hour');
302
        $url = (string) $request->getUri();
303
        Cache::put("OSUrl/{$this->id}", $url, 60);
304
305
        return $url;
306
    }
307
308
    /**
309
     * Get the YouTube videos related to this song.
310
     *
311
     * @param string $youTubePageToken The YouTube page token, for pagination purpose.
312
     *
313
     * @return false|object
314
     */
315
    public function getRelatedYouTubeVideos($youTubePageToken = '')
316
    {
317
        return YouTube::searchVideosRelatedToSong($this, $youTubePageToken);
318
    }
319
320
    /**
321
     * Sometimes the tags extracted from getID3 are HTML entity encoded.
322
     * This makes sure they are always sane.
323
     *
324
     * @param string $value
325
     */
326
    public function setTitleAttribute($value)
327
    {
328
        $this->attributes['title'] = html_entity_decode($value);
329
    }
330
331
    /**
332
     * Some songs don't have a title.
333
     * Fall back to the file name (without extension) for such.
334
     *
335
     * @param  string $value
336
     *
337
     * @return string
338
     */
339
    public function getTitleAttribute($value)
340
    {
341
        return $value ?: pathinfo($this->path, PATHINFO_FILENAME);
342
    }
343
344
    /**
345
     * Prepare the lyrics for displaying.
346
     *
347
     * @param $value
348
     *
349
     * @return string
350
     */
351
    public function getLyricsAttribute($value)
352
    {
353
        // We don't use nl2br() here, because the function actually preserves line breaks -
354
        // it just _appends_ a "<br />" after each of them. This would cause our client
355
        // implementation of br2nl to fail with duplicated line breaks.
356
        return str_replace(["\r\n", "\r", "\n"], '<br />', $value);
357
    }
358
359
    /**
360
     * Get the bucket and key name of an S3 object.
361
     *
362
     * @return bool|array
363
     */
364
    public function getS3ParamsAttribute()
365
    {
366
        if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) {
367
            return false;
368
        }
369
370
        list($bucket, $key) = explode('/', $matches[1], 2);
371
372
        return compact('bucket', 'key');
373
    }
374
375
    /**
376
     * Return the ID of the song when it's converted to string.
377
     *
378
     * @return string
379
     */
380
    public function __toString()
381
    {
382
        return $this->id;
383
    }
384
}
385