/** * Generate the downloadable path for a song. * * @param Song $song * * @return string */ protected function fromSong(Song $song) { if ($s3Params = $song->s3_params) { // The song is hosted on Amazon S3. // We download it back to our local server first. $localPath = rtrim(sys_get_temp_dir(), '/') . '/' . basename($s3Params['key']); $url = $song->getObjectStoragePublicUrl(); abort_unless($url, 404); // The following function require allow_url_fopen to be ON. // We're just assuming that to be the case here. copy($url, $localPath); } else { // The song is hosted locally. Make sure the file exists. abort_unless(file_exists($song->path), 404); $localPath = $song->path; } // The BinaryFileResponse factory only accept ASCII-only file names. if (ctype_print($localPath)) { return $localPath; } // For those with high-byte characters in names, we copy it into a safe name // as a workaround. $newPath = rtrim(sys_get_temp_dir(), '/') . '/' . utf8_decode(basename($song->path)); if ($s3Params) { // If the file is downloaded from S3, we rename it directly. // This will save us some disk space. rename($localPath, $newPath); } else { // Else we copy it to another file to not mess up the original one. copy($localPath, $newPath); } return $newPath; }
public function testMultipleSongs() { $songs = Song::take(2)->get(); $mocked = Download::shouldReceive('from')->once()->andReturn($this->mediaPath . '/blank.mp3'); // should be a zip file, but we're testing here… $this->get("api/download/songs?songs[]={$songs[0]->id}&songs[]={$songs[1]->id}")->seeStatusCode(200); }
public function testSync() { $media = new Media(); $media->sync($this->mediaPath); // Standard mp3 files under root path should be recognized $this->seeInDatabase('songs', ['path' => $this->mediaPath . '/full.mp3']); // Ogg files and audio files in subdirectories should be recognized $this->seeInDatabase('songs', ['path' => $this->mediaPath . '/subdir/back-in-black.ogg']); // Non-audio files shouldn't be recognized $this->notSeeInDatabase('songs', ['path' => $this->mediaPath . '/rubbish.log']); // Broken/corrupted audio files shouldn't be recognized $this->notSeeInDatabase('songs', ['path' => $this->mediaPath . '/fake.mp3']); // Artists should be created $this->seeInDatabase('artists', ['name' => 'Cuckoo']); $this->seeInDatabase('artists', ['name' => 'Koel']); // Albums should be created $this->seeInDatabase('albums', ['name' => 'Koel Testing Vol. 1']); // Albums and artists should be correctly linked $album = Album::whereName('Koel Testing Vol. 1')->first(); $this->assertEquals('Koel', $album->artist->name); $currentCover = $album->cover; $song = Song::orderBy('id', 'desc')->first(); // Modified file should be recognized touch($song->path, $time = time()); $media->sync($this->mediaPath); $song = Song::find($song->id); $this->assertEquals($time, $song->mtime); // Albums with a non-default cover should have their covers overwritten $this->assertEquals($currentCover, Album::find($album->id)->cover); }
/** * Remove a song whose info matches with data sent from AWS. * * @param RemoveSongRequest $request * * @return \Illuminate\Http\JsonResponse */ public function remove(RemoveSongRequest $request) { abort_unless($song = Song::byPath("s3://{$request->bucket}/{$request->key}"), 404); $song->delete(); event(new LibraryChanged()); return response()->json(); }
public function testSearchVideosRelatedToSong() { $this->createSampleMediaSet(); $song = Song::first(); // We test on the facade here YouTubeFacade::shouldReceive('searchVideosRelatedToSong')->once(); $this->visit("/api/youtube/search/song/{$song->id}"); }
public function testWatchSingleFileDeleted() { $this->expectsEvents(LibraryChanged::class); $this->createSampleMediaSet(); $song = Song::orderBy('id', 'desc')->first(); (new Media())->syncByWatchRecord(new InotifyWatchRecord("DELETE {$song->path}")); $this->notSeeInDatabase('songs', ['id' => $song->id]); }
/** * Fired every time a LibraryChanged event is triggered. * Remove empty albums and artists from our system. */ 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(); }
public function testWatchSingleFileDeleted() { $this->expectsEvents(LibraryChanged::class); $this->createSampleMediaSet(); $song = Song::orderBy('id', 'desc')->first(); $record = m::mock(FSWatchRecord::class, ['isDeleted' => true, 'getPath' => $song->path, 'isFile' => true, 'isValidEvent' => true], ["{$song->path} IsFile"]); (new Media())->syncFSWatchRecord($record); $this->notSeeInDatabase('songs', ['id' => $song->id]); }
public function testScrobble() { $this->withoutEvents(); $this->createSampleMediaSet(); $song = Song::first(); $ts = time(); m::mock(Lastfm::class, ['enabled' => true])->shouldReceive('scrobble')->with($song->album->artist->name, $song->title, $ts, $song->album->name, 'bar'); $this->post("/api/{$song->id}/scrobble/{$ts}"); }
/** * @param Menu $menuModel * @param Slider $slider * @param Alphabet $alphabet * @param Song $song * @param Performer $performer * @param Lessons $lessons * @param Request $request */ public function __construct(Menu $menuModel, Slider $slider, Alphabet $alphabet, Song $song, Performer $performer, Lessons $lessons, Request $request) { $this->performer = $performer; #Models performer $this->song = $song; #Models song $this->lessons = $lessons; #Models lessons $this->request = $request; #request $this->data['menu']['left'] = $menuModel->getLeftMenu(); $this->data['menu']['right'] = $menuModel->getRightMenu(); $this->data['slider'] = $slider->getActive(); $this->data['alphabet'] = $alphabet->getActive(); $this->data['countPerformer'] = count($performer->getActive()); $this->data['countSong'] = count($song->getActive()); $this->data['countLessons'] = count($lessons->getAll()); $URL = $_SERVER['REQUEST_URI']; $this->data['url_lang'] = substr($URL, 1, 2); }
/** * BaseStreamer constructor. * * @param $song Song|string A Song object, or its ID. */ public function __construct($song) { $this->song = $song instanceof Song ? $song : Song::findOrFail($song); if (!file_exists($this->song->path)) { abort(404); } // Hard code the content type instead of relying on PHP's fileinfo() // or even Symfony's MIMETypeGuesser, since they appear to be wrong sometimes. $this->contentType = 'audio/' . pathinfo($this->song->path, PATHINFO_EXTENSION); // Turn off error reporting to make sure our stream isn't interfered. @error_reporting(0); }
public function testBatchLikeAndUnlike() { $user = factory(User::class)->create(); $songs = Song::orderBy('id')->take(2)->get(); $songIds = array_pluck($songs->toArray(), 'id'); $this->actingAs($user)->post('api/interaction/batch/like', ['ids' => $songIds]); foreach ($songs as $song) { $this->seeInDatabase('interactions', ['user_id' => $user->id, 'song_id' => $song->id, 'liked' => 1]); } $this->actingAs($user)->post('api/interaction/batch/unlike', ['ids' => $songIds]); foreach ($songs as $song) { $this->seeInDatabase('interactions', ['user_id' => $user->id, 'song_id' => $song->id, 'liked' => 0]); } }
public function testSyncPlaylist() { $user = factory(User::class)->create(); $playlist = factory(Playlist::class)->create(['user_id' => $user->id]); $songs = Song::orderBy('id')->take(4)->get(); $playlist->songs()->attach(array_pluck($songs->toArray(), 'id')); $removedSong = $songs->pop(); $this->actingAs($user)->put("api/playlist/{$playlist->id}/sync", ['songs' => array_pluck($songs->toArray(), 'id')]); // We should still see the first 3 songs, but not the removed one foreach ($songs as $song) { $this->seeInDatabase('playlist_song', ['playlist_id' => $playlist->id, 'song_id' => $song->id]); } $this->notSeeInDatabase('playlist_song', ['playlist_id' => $playlist->id, 'song_id' => $removedSong->id]); }
/** * Register any other events for your application. * * @param \Illuminate\Contracts\Events\Dispatcher $events */ public function boot(DispatcherContract $events) { parent::boot($events); // Generate a unique hash for a song from its path to be the ID Song::creating(function ($song) { $song->id = Media::getHash($song->path); }); // Remove the cover file if the album is deleted Album::deleted(function ($album) { if ($album->hasCover) { @unlink(app()->publicPath() . '/public/img/covers/' . $album->cover); } }); }
public function testSingleUpdateAllInfoYesCompilation() { $admin = factory(User::class, 'admin')->create(); $this->createSampleMediaSet(); $song = Song::orderBy('id', 'desc')->first(); $this->actingAs($admin)->put('/api/songs', ['songs' => [$song->id], 'data' => ['title' => 'Foo Bar', 'artistName' => 'John Cena', 'albumName' => 'One by One', 'lyrics' => 'Lorem ipsum dolor sic amet.', 'track' => 1, 'compilationState' => 1]])->seeStatusCode(200); $compilationAlbum = Album::whereArtistIdAndName(Artist::VARIOUS_ID, 'One by One')->first(); $this->assertNotNull($compilationAlbum); $contributingArtist = Artist::whereName('John Cena')->first(); $this->assertNotNull($contributingArtist); $this->seeInDatabase('songs', ['id' => $song->id, 'contributing_artist_id' => $contributingArtist->id, 'album_id' => $compilationAlbum->id, 'lyrics' => 'Lorem ipsum dolor sic amet.', 'track' => 1]); // Now try changing stuff and make sure things work. // Case 1: Keep compilation state and artist the same $this->actingAs($admin)->put('/api/songs', ['songs' => [$song->id], 'data' => ['title' => 'Barz Qux', 'artistName' => 'John Cena', 'albumName' => 'Two by Two', 'lyrics' => 'Lorem ipsum dolor sic amet.', 'track' => 1, 'compilationState' => 2]])->seeStatusCode(200); $compilationAlbum = Album::whereArtistIdAndName(Artist::VARIOUS_ID, 'Two by Two')->first(); $this->assertNotNull($compilationAlbum); $contributingArtist = Artist::whereName('John Cena')->first(); $this->assertNotNull($contributingArtist); $this->seeInDatabase('songs', ['id' => $song->id, 'contributing_artist_id' => $contributingArtist->id, 'album_id' => $compilationAlbum->id]); // Case 2: Keep compilation state, but change the artist. $this->actingAs($admin)->put('/api/songs', ['songs' => [$song->id], 'data' => ['title' => 'Barz Qux', 'artistName' => 'Foo Fighters', 'albumName' => 'One by One', 'lyrics' => 'Lorem ipsum dolor sic amet.', 'track' => 1, 'compilationState' => 2]])->seeStatusCode(200); $compilationAlbum = Album::whereArtistIdAndName(Artist::VARIOUS_ID, 'One by One')->first(); $this->assertNotNull($compilationAlbum); $contributingArtist = Artist::whereName('Foo Fighters')->first(); $this->assertNotNull($contributingArtist); $this->seeInDatabase('songs', ['id' => $song->id, 'contributing_artist_id' => $contributingArtist->id, 'album_id' => $compilationAlbum->id]); // Case 3: Change compilation state only $this->actingAs($admin)->put('/api/songs', ['songs' => [$song->id], 'data' => ['title' => 'Barz Qux', 'artistName' => 'Foo Fighters', 'albumName' => 'One by One', 'lyrics' => 'Lorem ipsum dolor sic amet.', 'track' => 1, 'compilationState' => 0]])->seeStatusCode(200); $artist = Artist::whereName('Foo Fighters')->first(); $this->assertNotNull($artist); $album = Album::whereArtistIdAndName($artist->id, 'One by One')->first(); $this->seeInDatabase('songs', ['id' => $song->id, 'contributing_artist_id' => null, 'album_id' => $album->id]); // Case 3: Change compilation state and artist // Remember to set the compliation state back to 1 $this->actingAs($admin)->put('/api/songs', ['songs' => [$song->id], 'data' => ['title' => 'Barz Qux', 'artistName' => 'Foo Fighters', 'albumName' => 'One by One', 'lyrics' => 'Lorem ipsum dolor sic amet.', 'track' => 1, 'compilationState' => 1]])->put('/api/songs', ['songs' => [$song->id], 'data' => ['title' => 'Twilight of the Thunder God', 'artistName' => 'Amon Amarth', 'albumName' => 'Twilight of the Thunder God', 'lyrics' => 'Thor! Nanananananana Batman.', 'track' => 1, 'compilationState' => 0]])->seeStatusCode(200); $artist = Artist::whereName('Amon Amarth')->first(); $this->assertNotNull($artist); $album = Album::whereArtistIdAndName($artist->id, 'Twilight of the Thunder God')->first(); $this->assertNotNull($album); $this->seeInDatabase('songs', ['id' => $song->id, 'contributing_artist_id' => null, 'album_id' => $album->id, 'lyrics' => 'Thor! Nanananananana Batman.']); }
/** * Bootstrap any application services. * * @return void */ public function boot() { // when a user changes an item .... Item::updated(function ($item) { // .... update the 'changer' field on the parent model (plan) $plan = Plan::find($item->plan_id); $plan->update(['changer' => Auth::user()->first_name]); }); /** * Provide the data of the last update to the list of songs */ $lastSongUpdated_at = Song::select('updated_at')->orderby('updated_at', 'desc')->first()->updated_at; view()->share('lastSongUpdated_at', $lastSongUpdated_at); // provide the name of the current Main Presenter to all views view()->share('serverSideMainPresenter', getMainPresenter()); // provide the PATH to the (custom) logos to all views if (strtolower(env('USE_CUSTOM_LOGOS')) == 'yes') { view()->share('logoPath', 'images/custom/'); } else { view()->share('logoPath', 'images/'); } // provide a list (array) of user-id's with Admin rights to all views (for page feedback messages) // (the condition is needed in order to avoid an error in artisan when no migration has happened yet!) if (\Schema::hasTable('users')) { view()->share('administrators', findAdmins('id')); } // detect mobile users $isMobileUser = false; $useragent = 'local'; if (isset($_SERVER['HTTP_USER_AGENT'])) { $useragent = $_SERVER['HTTP_USER_AGENT']; } if (preg_match('/(android|bb\\d+|meego).+mobile|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i', $useragent) || preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\\-|your|zeto|zte\\-/i', substr($useragent, 0, 4))) { $isMobileUser = true; } // provide this to all views view()->share('isMobileUser', $isMobileUser); }
public function testMultipleUpdateSomeInfo() { $this->createSampleMediaSet(); $originalSongs = Song::orderBy('id', 'desc')->take(3)->get(); $songIds = $originalSongs->pluck('id')->toArray(); $this->actingAs(factory(User::class, 'admin')->create())->put('/api/songs', ['songs' => $songIds, 'data' => ['title' => 'Foo Bar', 'artistName' => 'John Cena', 'albumName' => '', 'lyrics' => 'Lorem ipsum dolor sic amet.', 'track' => 1]])->seeStatusCode(200); $songs = Song::orderBy('id', 'desc')->take(3)->get(); // Even though the album name doesn't change, a new artist should have been created // and thus, a new album with the same name was created as well. $this->assertEquals($songs[0]->album->name, $originalSongs[0]->album->name); $this->assertNotEquals($songs[0]->album->id, $originalSongs[0]->album->id); $this->assertEquals($songs[1]->album->name, $originalSongs[1]->album->name); $this->assertNotEquals($songs[1]->album->id, $originalSongs[1]->album->id); $this->assertEquals($songs[2]->album->name, $originalSongs[2]->album->name); $this->assertNotEquals($songs[2]->album->id, $originalSongs[2]->album->id); // And of course, the new artist is... $this->assertEquals('John Cena', $songs[0]->album->artist->name); // JOHN CENA!!! $this->assertEquals('John Cena', $songs[1]->album->artist->name); // JOHN CENA!!! $this->assertEquals('John Cena', $songs[2]->album->artist->name); // And... JOHN CENAAAAAAAAAAA!!! }
/** * Check if a media file is new or changed. * A file is considered existing and unchanged only when: * - its hash (ID) can be found in the database, and * - its last modified time is the same with that of the comparing file. * * @param SplFileInfo $file * * @return bool */ protected function isNewOrChanged(SplFileInfo $file) { return !Song::whereIdAndMtime($this->getHash($file->getPathname()), $file->getMTime())->count(); }
/** * Sync the song with all available media info against the database. * * @param array $tags The (selective) tags to sync (if the song exists) * @param bool $force Whether to force syncing, even if the file is unchaged * * @return bool|Song A Song object on success, * true if file exists but is unmodified, * or false on an error. */ public function sync($tags, $force = false) { // If the file is not new or changed and we're not forcing update, don't do anything. if (!$this->isNewOrChanged() && !$force) { return true; } // If the file is invalid, don't do anything. if (!($info = $this->getInfo())) { return false; } if ($this->isChanged() || $force) { // This is a changed file, or the user is forcing updates. // We cater for the tags by removing those not specified. $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; } else { $album = Album::get(Artist::get($info['artist']), $info['album']); } if (!empty($info['cover']) && !$album->has_cover) { try { $album->generateCover($info['cover']); } catch (Exception $e) { Log::error($e); } } $info['album_id'] = $album->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']); $song = Song::updateOrCreate(['id' => $this->hash], $info); $song->save(); return $song; }
/** * 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(); }
public function testGetObjectStoragePublicUrl() { $song = Song::first(); $song->path = 's3://foo/bar.mp3'; $fakeUrl = 'http://aws.com/foo/bar.mp3'; $client = m::mock(AwsClient::class, ['getCommand' => null, 'createPresignedRequest' => m::mock(Request::class, ['getUri' => $fakeUrl])]); Cache::shouldReceive('get')->once()->with("OSUrl/{$song->id}"); Cache::shouldReceive('put')->once()->with("OSUrl/{$song->id}", $fakeUrl, 60); $this->assertEquals($fakeUrl, $song->getObjectStoragePublicUrl($client)); }
/** * Update songs info. * * @param SongUpdateRequest $request * * @return \Illuminate\Http\JsonResponse */ public function update(SongUpdateRequest $request) { return response()->json(Song::updateInfo($request->songs, $request->data)); }
/** * Search for YouTube videos related to a song (using its title and artist name). * * @param Request $request * @param Song $song * * @return \Illuminate\Http\JsonResponse */ public function searchVideosRelatedToSong(Request $request, Song $song) { return response()->json($song->getRelatedYouTubeVideos($request->pageToken)); }
/** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($society, $id, $mode = "view") { $data['soc'] = Society::find($society); $data['song'] = Song::find($id); $data['lyrics'] = ""; $data['chords'] = $this->_getChords($data['song']->lyrics); $lines = explode(PHP_EOL, $data['song']->lyrics); foreach ($lines as $line) { $line = str_replace("\r", '', $line); $line = str_replace("\n", '', $line); if (strpos($line, ']')) { $line = "<chordline>" . $line . "</chordline>"; $line = str_replace('[', '<chord>', $line); $line = str_replace(']', '</chord>', $line); } $line = $line . "<br>"; $line = str_replace('{', '<strong>', $line); $line = str_replace('}', '</strong>', $line); $data['lyrics'] = $data['lyrics'] . $line; } if ($mode == "view") { return View::make('songs.show', $data); } else { $this->pdf($data); } }
/** * Download a song or multiple songs. * * @param SongRequest $request * * @return \Symfony\Component\HttpFoundation\BinaryFileResponse */ public function download(SongRequest $request) { $songs = Song::whereIn('id', $request->songs)->get(); return response()->download(Download::from($songs)); }
/** * Sync the song with all available media info against the database. * * @param array $tags The (selective) tags to sync (if the song exists) * @param bool $force Whether to force syncing, even if the file is unchanged * * @return bool|Song A Song object on success, * true if file exists but is unmodified, * or false on an error. */ public function sync($tags, $force = false) { // If the file is not new or changed and we're not forcing update, don't do anything. if (!$this->isNewOrChanged() && !$force) { return true; } // If the file is invalid, don't do anything. if (!($info = $this->getInfo())) { return false; } // Fixes #366. If the file is new, we use all tags by simply setting $force to false. if ($this->isNew()) { $force = false; } $artist = null; if ($this->isChanged() || $force) { // This is a changed file, or the user is forcing updates. // In such a case, the user must have specified a list of tags to sync. // A sample command could be: ./artisan koel:sync --force --tags=artist,album,lyrics // We cater for these tags by removing those not specified. // There's a special case with 'album' though. // If 'compilation' tag is specified, 'album' must be counted in as well. // But if 'album' isn't specified, we don't want to update normal albums. // This variable is to keep track of this state. $changeCompilationAlbumOnly = false; if (in_array('compilation', $tags, true) && !in_array('album', $tags, true)) { $tags[] = 'album'; $changeCompilationAlbumOnly = true; } $info = array_intersect_key($info, array_flip($tags)); // If the "artist" tag is specified, use it. // Otherwise, re-use the existing model value. $artist = isset($info['artist']) ? Artist::get($info['artist']) : $this->song->album->artist; $isCompilation = (bool) array_get($info, 'compilation'); // If the "album" tag is specified, use it. // Otherwise, re-use the existing model value. if (isset($info['album'])) { $album = $changeCompilationAlbumOnly ? $this->song->album : Album::get($artist, $info['album'], $isCompilation); } else { $album = $this->song->album; } } else { // The file is newly added. $isCompilation = (bool) array_get($info, 'compilation'); $artist = Artist::get($info['artist']); $album = Album::get($artist, $info['album'], $isCompilation); } if (!$album->has_cover) { // If the album has no cover, we try to get the cover image from existing tag data if (!empty($info['cover'])) { try { $album->generateCover($info['cover']); } catch (Exception $e) { Log::error($e); } } elseif ($cover = $this->getCoverFileUnderSameDirectory()) { $album->copyCoverFile($cover); } } $info['album_id'] = $album->id; // If the song is part of a compilation, make sure we properly set its // artist and contributing artist attributes. if ($isCompilation) { $info['contributing_artist_id'] = $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', 'albumartist', 'album', 'cover', 'compilation']); return Song::updateOrCreate(['id' => $this->hash], $info); }
/** * Download all songs in a playlist. * * @param Request $request * * @return \Symfony\Component\HttpFoundation\BinaryFileResponse */ public function download(Request $request) { return response()->download(Download::from(Song::getFavorites($request->user()))); }
/** * Scrobble a song. * * @param Song $song * @param string $timestamp The UNIX timestamp when the song started playing. * * @return \Illuminate\Http\JsonResponse */ public function scrobble(Song $song, $timestamp) { return response()->json($song->scrobble($timestamp)); }
/** * Sync media using a watch record. * * @param WatchRecordInterface $record The watch record. * @param SyncMedia|null $syncCommand The SyncMedia command object, to log to console if executed by artisan. */ public function syncByWatchRecord(WatchRecordInterface $record, SyncMedia $syncCommand = null) { Log::info("New watch record received: '{$record}'"); $path = $record->getPath(); if ($record->isFile()) { Log::info("'{$path}' is a file."); // If the file has been deleted... if ($record->isDeleted()) { // ...and it has a record in our database, remove it. if ($song = Song::byPath($path)) { $song->delete(); Log::info("{$path} deleted."); event(new LibraryChanged()); } else { Log::info("{$path} doesn't exist in our database--skipping."); } } elseif ($record->isNewOrModified()) { $result = (new File($path))->sync($this->tags); Log::info($result instanceof Song ? "Synchronized {$path}" : "Invalid file {$path}"); } return; } // Record is a directory. Log::info("'{$path}' is a directory."); if ($record->isDeleted()) { // The directory is removed. We remove all songs in it. if ($count = Song::inDirectory($path)->delete()) { Log::info("Deleted {$count} song(s) under {$path}"); event(new LibraryChanged()); } else { Log::info("{$path} is empty--no action needed."); } } elseif ($record->isNewOrModified()) { foreach ($this->gatherFiles($path) as $file) { (new File($file))->sync($this->tags); } Log::info("Synced all song(s) under {$path}"); } }
public function testUpdateNowPlaying() { $this->withoutEvents(); $this->createSampleMediaSet(); $user = factory(User::class)->create(['preferences' => ['lastfm_session_key' => 'bar']]); $song = Song::first(); $lastfm = m::mock(Lastfm::class, ['enabled' => true]); $lastfm->shouldReceive('updateNowPlaying')->with($song->album->artist->name, $song->title, $song->album->name, $song->length, 'bar'); (new UpdateLastfmNowPlaying($lastfm))->handle(new SongStartedPlaying($song, $user)); }