diff --git a/app/Http/Controllers/API/SongController.php b/app/Http/Controllers/API/SongController.php
index 5c0a06da..f7b437fe 100644
--- a/app/Http/Controllers/API/SongController.php
+++ b/app/Http/Controllers/API/SongController.php
@@ -46,7 +46,7 @@ class SongController extends Controller
return response()->json([
'lyrics' => $song->lyrics,
'album_info' => $song->album->getInfo(),
- 'artist_info' => $song->album->artist->getInfo(),
+ 'artist_info' => $song->artist->getInfo(),
]);
}
diff --git a/app/Listeners/TidyLibrary.php b/app/Listeners/TidyLibrary.php
index 0523ed37..b9ca0b1b 100644
--- a/app/Listeners/TidyLibrary.php
+++ b/app/Listeners/TidyLibrary.php
@@ -2,9 +2,7 @@
namespace App\Listeners;
-use App\Models\Album;
-use App\Models\Artist;
-use App\Models\Song;
+use Media;
class TidyLibrary
{
@@ -17,16 +15,10 @@ class TidyLibrary
/**
* Fired every time a LibraryChanged event is triggered.
- * Remove empty albums and artists from our system.
+ * Tidies up our lib.
*/
public function handle()
{
- $inUseAlbums = Song::select('album_id')->groupBy('album_id')->get()->lists('album_id');
- $inUseAlbums[] = Album::UNKNOWN_ID;
- Album::whereNotIn('id', $inUseAlbums)->delete();
-
- $inUseArtists = Album::select('artist_id')->groupBy('artist_id')->get()->lists('artist_id');
- $inUseArtists[] = Artist::UNKNOWN_ID;
- Artist::whereNotIn('id', $inUseArtists)->delete();
+ Media::tidy();
}
}
diff --git a/app/Listeners/UpdateLastfmNowPlaying.php b/app/Listeners/UpdateLastfmNowPlaying.php
index 1059a219..bc2c0d2b 100644
--- a/app/Listeners/UpdateLastfmNowPlaying.php
+++ b/app/Listeners/UpdateLastfmNowPlaying.php
@@ -34,13 +34,13 @@ class UpdateLastfmNowPlaying
{
if (!$this->lastfm->enabled() ||
!($sessionKey = $event->user->getLastfmSessionKey()) ||
- $event->song->album->artist->isUnknown()
+ $event->song->artist->isUnknown()
) {
return;
}
$this->lastfm->updateNowPlaying(
- $event->song->album->artist->name,
+ $event->song->artist->name,
$event->song->title,
$event->song->album->name === Album::UNKNOWN_NAME ? null : $event->song->album->name,
$event->song->length,
diff --git a/app/Models/Album.php b/app/Models/Album.php
index f0369f6f..f0ad7b86 100644
--- a/app/Models/Album.php
+++ b/app/Models/Album.php
@@ -6,11 +6,12 @@ use App\Facades\Lastfm;
use Illuminate\Database\Eloquent\Model;
/**
- * @property string cover The path to the album's cover
- * @property bool has_cover If the album has a cover image
+ * @property string cover The path to the album's cover
+ * @property bool has_cover If the album has a cover image
* @property int id
- * @property string name Name of the album
- * @property Artist artist The album's artist
+ * @property string name Name of the album
+ * @property bool is_compilation If the album is a compilation from multiple artists
+ * @property Artist artist The album's artist
*/
class Album extends Model
{
@@ -20,6 +21,7 @@ class Album extends Model
protected $guarded = ['id'];
protected $hidden = ['created_at', 'updated_at'];
+ protected $casts = ['is_compilation' => 'bool'];
public function artist()
{
@@ -40,21 +42,23 @@ class Album extends Model
* Get an album using some provided information.
*
* @param Artist $artist
- * @param $name
+ * @param string $name
+ * @param bool $isCompilation
*
* @return self
*/
- public static function get(Artist $artist, $name)
+ public static function get(Artist $artist, $name, $isCompilation = false)
{
- // If an empty name is provided, turn it into our "Unknown Album"
- $name = $name ?: self::UNKNOWN_NAME;
+ // If this is a compilation album, its artist must be "Various Artists"
+ if ($isCompilation) {
+ $artist = Artist::getVarious();
+ }
- $album = self::firstOrCreate([
+ return self::firstOrCreate([
'artist_id' => $artist->id,
- 'name' => $name,
+ 'name' => $name ?: self::UNKNOWN_NAME,
+ 'is_compilation' => $isCompilation,
]);
-
- return $album;
}
/**
diff --git a/app/Models/Artist.php b/app/Models/Artist.php
index 038b99aa..4bb1c015 100644
--- a/app/Models/Artist.php
+++ b/app/Models/Artist.php
@@ -16,6 +16,8 @@ class Artist extends Model
{
const UNKNOWN_ID = 1;
const UNKNOWN_NAME = 'Unknown Artist';
+ const VARIOUS_ID = 2;
+ const VARIOUS_NAME = 'Various Artists';
protected $guarded = ['id'];
@@ -31,6 +33,21 @@ class Artist extends Model
return $this->id === self::UNKNOWN_ID;
}
+ public function isVarious()
+ {
+ return $this->id === self::VARIOUS_ID;
+ }
+
+ /**
+ * Get the "Various Artists" object.
+ *
+ * @return Artist
+ */
+ public static function getVarious()
+ {
+ return self::find(self::VARIOUS_ID);
+ }
+
/**
* Sometimes the tags extracted from getID3 are HTML entity encoded.
* This makes sure they are always sane.
diff --git a/app/Models/ContributingArtist.php b/app/Models/ContributingArtist.php
new file mode 100644
index 00000000..9f4c1847
--- /dev/null
+++ b/app/Models/ContributingArtist.php
@@ -0,0 +1,8 @@
+getID3->analyze($this->path);
- if (isset($info['error'])) {
+ if (isset($info['error']) || !isset($info['playtime_seconds'])) {
return;
}
@@ -89,10 +89,6 @@ class File
// Read on.
getid3_lib::CopyTagsToComments($info);
- if (!isset($info['playtime_seconds'])) {
- return;
- }
-
$track = 0;
// Apparently track number can be stored with different indices as the following.
@@ -109,6 +105,7 @@ class File
$props = [
'artist' => '',
'album' => '',
+ 'part_of_a_compilation' => false,
'title' => '',
'length' => $info['playtime_seconds'],
'track' => (int) $track,
@@ -122,6 +119,10 @@ class File
return $props;
}
+ // getID3's 'part_of_a_compilation' value can either be null, ['0'], or ['1']
+ // We convert it into a boolean here.
+ $props['part_of_a_compilation'] = !!array_get($comments, 'part_of_a_compilation', [false])[0];
+
// We prefer id3v2 tags over others.
if (!$artist = array_get($info, 'tags.id3v2.artist', [null])[0]) {
$artist = array_get($comments, 'artist', [''])[0];
@@ -175,9 +176,11 @@ class File
$info = array_intersect_key($info, array_flip($tags));
$artist = isset($info['artist']) ? Artist::get($info['artist']) : $this->song->album->artist;
- $album = isset($info['album']) ? Album::get($artist, $info['album']) : $this->song->album;
+ $album = isset($info['album'])
+ ? Album::get($artist, $info['album'], array_get($info, 'part_of_a_compilation'))
+ : $this->song->album;
} else {
- $album = Album::get(Artist::get($info['artist']), $info['album']);
+ $album = Album::get(Artist::get($info['artist']), $info['album'], array_get($info, 'part_of_a_compilation'));
}
if (!empty($info['cover']) && !$album->has_cover) {
@@ -190,8 +193,13 @@ class File
$info['album_id'] = $album->id;
+ // If the song is part of a compilation, we set its artist as the contributing artist.
+ if (array_get($info, 'part_of_a_compilation')) {
+ $info['contributing_artist_id'] = Artist::get($info['artist'])->id;
+ }
+
// Remove these values from the info array, so that we can just use the array as model's input data.
- array_forget($info, ['artist', 'album', 'cover']);
+ array_forget($info, ['artist', 'album', 'cover', 'part_of_a_compilation']);
$song = Song::updateOrCreate(['id' => $this->hash], $info);
$song->save();
diff --git a/app/Models/Song.php b/app/Models/Song.php
index dd3d460a..970b63b3 100644
--- a/app/Models/Song.php
+++ b/app/Models/Song.php
@@ -31,6 +31,7 @@ class Song extends Model
'length' => 'float',
'mtime' => 'int',
'track' => 'int',
+ 'contributing_artist_id' => 'int',
];
/**
@@ -40,6 +41,11 @@ class Song extends Model
*/
public $incrementing = false;
+ public function contributingArtist()
+ {
+ return $this->belongsTo(ContributingArtist::class);
+ }
+
public function album()
{
return $this->belongsTo(Album::class);
@@ -60,7 +66,7 @@ class Song extends Model
public function scrobble($timestamp)
{
// Don't scrobble the unknown guys. No one knows them.
- if ($this->album->artist->isUnknown()) {
+ if ($this->artist->isUnknown()) {
return false;
}
@@ -70,7 +76,7 @@ class Song extends Model
}
return Lastfm::scrobble(
- $this->album->artist->name,
+ $this->artist->name,
$this->title,
$timestamp,
$this->album->name === Album::UNKNOWN_NAME ? '' : $this->album->name,
@@ -242,4 +248,18 @@ class Song extends Model
// implementation of br2nl to fail with duplicated linebreaks.
return str_replace(["\r\n", "\r", "\n"], '
', $value);
}
+
+ /**
+ * Get the correct artist of the song.
+ * If it's part of a compilation, that would be the contributing artist.
+ * Otherwise, it's the album artist.
+ *
+ * @return Artist
+ */
+ public function getArtistAttribute()
+ {
+ return $this->contributing_artist_id
+ ? $this->contributingArtist
+ : $this->album->artist;
+ }
}
diff --git a/app/Services/Media.php b/app/Services/Media.php
index b16e565d..588f1448 100644
--- a/app/Services/Media.php
+++ b/app/Services/Media.php
@@ -5,6 +5,8 @@ namespace App\Services;
use App\Console\Commands\SyncMedia;
use App\Events\LibraryChanged;
use App\Libraries\WatchRecord\WatchRecordInterface;
+use App\Models\Album;
+use App\Models\Artist;
use App\Models\File;
use App\Models\Setting;
use App\Models\Song;
@@ -20,7 +22,17 @@ class Media
*
* @var array
*/
- protected $allTags = ['artist', 'album', 'title', 'length', 'track', 'lyrics', 'cover', 'mtime'];
+ protected $allTags = [
+ 'artist',
+ 'album',
+ 'title',
+ 'length',
+ 'track',
+ 'lyrics',
+ 'cover',
+ 'mtime',
+ 'part_of_a_compilation',
+ ];
/**
* Tags to be synced.
@@ -190,4 +202,29 @@ class Media
{
return File::getHash($path);
}
+
+ /**
+ * Tidy up the library by deleting empty albums and artists.
+ */
+ public function tidy()
+ {
+ $inUseAlbums = Song::select('album_id')->groupBy('album_id')->get()->lists('album_id')->toArray();
+ $inUseAlbums[] = Album::UNKNOWN_ID;
+ Album::whereNotIn('id', $inUseAlbums)->delete();
+
+ $inUseArtists = Album::select('artist_id')->groupBy('artist_id')->get()->lists('artist_id')->toArray();
+
+ $contributingArtists = Song::distinct()
+ ->select('contributing_artist_id')
+ ->groupBy('contributing_artist_id')
+ ->get()
+ ->lists('contributing_artist_id')
+ ->toArray();
+
+ $inUseArtists = array_merge($inUseArtists, $contributingArtists);
+ $inUseArtists[] = Artist::UNKNOWN_ID;
+ $inUseArtists[] = Artist::VARIOUS_ID;
+
+ Artist::whereNotIn('id', $inUseArtists)->delete();
+ }
}
diff --git a/database/migrations/2016_04_15_121215_add_is_complilation_into_albums.php b/database/migrations/2016_04_15_121215_add_is_complilation_into_albums.php
new file mode 100644
index 00000000..b38a4330
--- /dev/null
+++ b/database/migrations/2016_04_15_121215_add_is_complilation_into_albums.php
@@ -0,0 +1,31 @@
+boolean('is_compilation')->nullable()->defaults(false)->after('cover');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('albums', function (Blueprint $table) {
+ $table->dropColumn('is_compilation');
+ });
+ }
+}
diff --git a/database/migrations/2016_04_15_125237_add_contributing_artist_id_into_songs.php b/database/migrations/2016_04_15_125237_add_contributing_artist_id_into_songs.php
new file mode 100644
index 00000000..66007cb6
--- /dev/null
+++ b/database/migrations/2016_04_15_125237_add_contributing_artist_id_into_songs.php
@@ -0,0 +1,32 @@
+integer('contributing_artist_id')->unsigned()->nullable()->after('album_id');
+ $table->foreign('contributing_artist_id')->references('id')->on('artists')->onDelete('cascade');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('songs', function (Blueprint $table) {
+ $table->dropColumn('contributing_artist_id');
+ });
+ }
+}
diff --git a/database/migrations/2016_04_16_082627_create_various_artists.php b/database/migrations/2016_04_16_082627_create_various_artists.php
new file mode 100644
index 00000000..ebeee337
--- /dev/null
+++ b/database/migrations/2016_04_16_082627_create_various_artists.php
@@ -0,0 +1,49 @@
+name === Artist::VARIOUS_NAME) {
+ goto ret;
+ }
+
+ // There's an existing artist with that special ID, but it's not our Various Artist
+ // We move it to the end of the table.
+ $latestArtist = Artist::orderBy('id', 'DESC')->first();
+ $existingArtist->id = $latestArtist->id + 1;
+ $existingArtist->save();
+ }
+
+ Artist::create([
+ 'id' => Artist::VARIOUS_ID,
+ 'name' => Artist::VARIOUS_NAME,
+ ]);
+
+ ret:
+ Artist::reguard();
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ }
+}
diff --git a/resources/assets/js/components/main-wrapper/extra/index.vue b/resources/assets/js/components/main-wrapper/extra/index.vue
index 27ed16e2..a549e45d 100644
--- a/resources/assets/js/components/main-wrapper/extra/index.vue
+++ b/resources/assets/js/components/main-wrapper/extra/index.vue
@@ -12,7 +12,7 @@