Song::updateInfo()   B
last analyzed

Complexity

Conditions 9
Paths 6

Size

Total Lines 37
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 9.0139

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 37
ccs 17
cts 18
cp 0.9444
rs 8.0555
cc 9
nc 6
nop 2
crap 9.0139
1
<?php
2
3
namespace App\Models;
4
5
use App\Events\LibraryChanged;
6
use App\Traits\SupportsDeleteWhereIDsNotIn;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Relations\BelongsTo;
10
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
11
use Illuminate\Database\Eloquent\Relations\HasMany;
12
use Illuminate\Support\Collection;
13
14
/**
15
 * @property string $path
16
 * @property string $title
17
 * @property Album  $album
18
 * @property Artist $artist
19
 * @property string[] $s3_params
20
 * @property float  $length
21
 * @property string $lyrics
22
 * @property int    $track
23
 * @property int    $disc
24
 * @property int    $album_id
25
 * @property string $id
26
 * @property int    $artist_id
27
 * @property int    $mtime
28
 *
29
 * @method static self updateOrCreate(array $where, array $params)
30
 * @method static Builder select(string $string)
31
 * @method static Builder inDirectory(string $path)
32
 * @method static self first()
33
 * @method static Collection orderBy(...$args)
34
 */
35
class Song extends Model
36
{
37
    use SupportsDeleteWhereIDsNotIn;
38
39
    protected $guarded = [];
40
41
    /**
42
     * Attributes to be hidden from JSON outputs.
43
     * Here we specify to hide lyrics as well to save some bandwidth (actually, lots of it).
44
     * Lyrics can then be queried on demand.
45
     *
46
     * @var array
47
     */
48
    protected $hidden = ['lyrics', 'updated_at', 'path', 'mtime'];
49
50
    /**
51
     * @var array
52
     */
53
    protected $casts = [
54
        'length' => 'float',
55
        'mtime' => 'int',
56
        'track' => 'int',
57
        'disc' => 'int',
58
    ];
59
60
    /**
61
     * Indicates if the IDs are auto-incrementing.
62
     *
63
     * @var bool
64
     */
65
    public $incrementing = false;
66
67 6
    public function artist(): BelongsTo
68
    {
69 6
        return $this->belongsTo(Artist::class);
70
    }
71
72 12
    public function album(): BelongsTo
73
    {
74 12
        return $this->belongsTo(Album::class);
75
    }
76
77
    public function playlists(): BelongsToMany
78
    {
79
        return $this->belongsToMany(Playlist::class);
80
    }
81
82
    public function interactions(): HasMany
83
    {
84
        return $this->hasMany(Interaction::class);
85
    }
86
87
    /**
88
     * Update song info.
89
     *
90
     * @param string[] $ids
91
     * @param string[] $data The data array, with these supported fields:
92
     *                       - title
93
     *                       - artistName
94
     *                       - albumName
95
     *                       - lyrics
96
     *                       All of these are optional, in which case the info will not be changed
97
     *                       (except for lyrics, which will be emptied).
98
     */
99 5
    public static function updateInfo(array $ids, array $data): Collection
100
    {
101
        /*
102
         * A collection of the updated songs.
103
         *
104
         * @var Collection
105
         */
106 5
        $updatedSongs = collect();
107
108 5
        $ids = (array) $ids;
109
        // If we're updating only one song, take into account the title, lyrics, and track number.
110 5
        $single = count($ids) === 1;
111
112 5
        foreach ($ids as $id) {
113
            /** @var Song $song */
114 5
            $song = self::with('album', 'album.artist')->find($id);
115
116 5
            if (!$song) {
117
                continue;
118
            }
119
120 5
            $updatedSongs->push($song->updateSingle(
121 5
                $single ? trim($data['title']) : $song->title,
122 5
                trim($data['albumName'] ?: $song->album->name),
123 5
                trim($data['artistName']) ?: $song->artist->name,
124 5
                $single ? trim($data['lyrics']) : $song->lyrics,
125 5
                $single ? (int) $data['track'] : $song->track,
126 5
                (int) $data['compilationState']
127
            ));
128
        }
129
130
        // Our library may have been changed. Broadcast an event to tidy it up if need be.
131 5
        if ($updatedSongs->count()) {
132 5
            event(new LibraryChanged());
133
        }
134
135 5
        return $updatedSongs;
136
    }
137
138 5
    public function updateSingle(
139
        string $title,
140
        string $albumName,
141
        string $artistName,
142
        string $lyrics,
143
        int $track,
144
        int $compilationState
145
    ): self {
146 5
        if ($artistName === Artist::VARIOUS_NAME) {
147
            // If the artist name is "Various Artists", it's a compilation song no matter what.
148
            $compilationState = 1;
149
            // and since we can't determine the real contributing artist, it's "Unknown"
150
            $artistName = Artist::UNKNOWN_NAME;
151
        }
152
153 5
        $artist = Artist::get($artistName);
154
155
        switch ($compilationState) {
156 5
            case 1: // ALL, or forcing compilation status to be Yes
157 1
                $isCompilation = true;
158 1
                break;
159 5
            case 2: // Keep current compilation status
160 1
                $isCompilation = $this->album->artist_id === Artist::VARIOUS_ID;
161 1
                break;
162
            default:
163 5
                $isCompilation = false;
164 5
                break;
165
        }
166
167 5
        $album = Album::get($artist, $albumName, $isCompilation);
168
169 5
        $this->artist_id = $artist->id;
170 5
        $this->album_id = $album->id;
171 5
        $this->title = $title;
172 5
        $this->lyrics = $lyrics;
173 5
        $this->track = $track;
174
175 5
        $this->save();
176
177
        // Clean up unnecessary data from the object
178 5
        unset($this->album);
179 5
        unset($this->artist);
180
        // and make sure the lyrics is shown
181 5
        $this->makeVisible('lyrics');
182
183 5
        return $this;
184
    }
185
186
    /**
187
     * Scope a query to only include songs in a given directory.
188
     */
189 1
    public function scopeInDirectory(Builder $query, string $path): Builder
190
    {
191
        // Make sure the path ends with a directory separator.
192 1
        $path = rtrim(trim($path), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
193
194 1
        return $query->where('path', 'LIKE', "$path%");
195
    }
196
197
    /**
198
     * Sometimes the tags extracted from getID3 are HTML entity encoded.
199
     * This makes sure they are always sane.
200
     */
201 51
    public function setTitleAttribute(string $value): void
202
    {
203 51
        $this->attributes['title'] = html_entity_decode($value);
204 51
    }
205
206
    /**
207
     * Some songs don't have a title.
208
     * Fall back to the file name (without extension) for such.
209
     */
210 15
    public function getTitleAttribute(?string $value): string
211
    {
212 15
        return $value ?: pathinfo($this->path, PATHINFO_FILENAME);
213
    }
214
215
    /**
216
     * Prepare the lyrics for displaying.
217
     */
218 8
    public function getLyricsAttribute(string $value): string
219
    {
220
        // We don't use nl2br() here, because the function actually preserves line breaks -
221
        // it just _appends_ a "<br />" after each of them. This would cause our client
222
        // implementation of br2nl to fail with duplicated line breaks.
223 8
        return str_replace(["\r\n", "\r", "\n"], '<br />', $value);
224
    }
225
226
    /**
227
     * Get the bucket and key name of an S3 object.
228
     *
229
     * @return string[]|null
230
     */
231 9
    public function getS3ParamsAttribute(): ?array
232
    {
233 9
        if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) {
234 7
            return null;
235
        }
236
237 2
        list($bucket, $key) = explode('/', $matches[1], 2);
238
239 2
        return compact('bucket', 'key');
240
    }
241
242
    /**
243
     * Return the ID of the song when it's converted to string.
244
     */
245 2
    public function __toString()
246
    {
247 2
        return $this->id;
248
    }
249
}
250