Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit 68fb3b0

Browse files
authored
✨ [Spotify] Create Friendship Playlists (#95)
* Added backend * Added frontend * lol * nope * Added factory * Improved api * Codacy
1 parent 22d8bd5 commit 68fb3b0

23 files changed

+12991
-386
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ npm-debug.log
1313
yarn-error.log
1414
.editorconfig
1515
.DS_Store
16-
public/css/cover.css
16+
public/css/*.css
17+
public/js/*.js
1718
public/mix-manifest.json
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Backend\Spotify;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\User;
7+
use Illuminate\Support\Facades\DB;
8+
use App\SpotifyTrack;
9+
use Illuminate\Support\Collection;
10+
use App\SpotifyFriendshipPlaylist;
11+
use Illuminate\Database\RecordsNotFoundException;
12+
use Carbon\Carbon;
13+
use App\Exceptions\SpotifyTokenExpiredException;
14+
15+
abstract class FriendshipPlaylistController extends Controller {
16+
17+
public static function getCommonTracks(User $user, User $friend): Collection {
18+
$topTracksUser = self::getTopTrackIds($user, 300);
19+
$topTracksFriend = self::getTopTrackIds($friend, 300);
20+
$likedTracksUser = self::getLikedTrackIds($user, 300);
21+
$likedTracksFriend = self::getLikedTrackIds($friend, 300);
22+
23+
$topTrackIds = $topTracksUser->intersect($topTracksFriend);
24+
$bothLikedTrackIds = $likedTracksUser->intersect($likedTracksFriend);
25+
$userLikedTrackIds = $topTracksFriend->intersect($likedTracksUser);
26+
$friendLikedTrackIds = $topTracksUser->intersect($likedTracksFriend);
27+
28+
$trackIds = $topTrackIds->union($bothLikedTrackIds)
29+
->union($userLikedTrackIds)
30+
->union($friendLikedTrackIds);
31+
32+
return SpotifyTrack::whereIn('id', $trackIds)->get();
33+
}
34+
35+
private static function getTopTrackIds(User $user, int $limit = 50): Collection {
36+
return DB::table('spotify_play_activities')
37+
->join('spotify_tracks', 'spotify_tracks.track_id', '=', 'spotify_play_activities.track_id')
38+
->where('user_id', $user->id)
39+
->groupBy('spotify_tracks.id')
40+
->select(['spotify_tracks.id', DB::raw('COUNT(*) AS cnt')])
41+
->orderByDesc('cnt')
42+
->limit($limit)
43+
->get()
44+
->pluck('id');
45+
}
46+
47+
private static function getLikedTrackIds(User $user, int $limit = 50): Collection {
48+
return DB::table('spotify_track_ratings')
49+
->where('user_id', $user->id)
50+
->where('rating', 1)
51+
->select('track_id')
52+
->limit($limit)
53+
->pluck('track_id');
54+
}
55+
56+
/**
57+
* @param User $user
58+
* @param User $friend
59+
* @param bool $createIfDoesntExist
60+
*
61+
* @return array|object
62+
* @throws SpotifyTokenExpiredException
63+
*/
64+
public static function getFriendshipPlaylist(User $user, User $friend, bool $createIfDoesntExist = false): object|array {
65+
$playlistId = SpotifyFriendshipPlaylist::where('user_id', $user->id)
66+
->where('friend_id', $friend->id)
67+
->first()?->playlist_id;
68+
69+
if($playlistId != null) {
70+
return SpotifyController::getApi($user)->getPlaylist($playlistId);
71+
}
72+
73+
if($createIfDoesntExist) {
74+
self::createFriendshipPlaylist($user, $friend);
75+
return self::getFriendshipPlaylist($user, $friend, false);
76+
}
77+
78+
throw new RecordsNotFoundException;
79+
}
80+
81+
/**
82+
* @param User $user
83+
* @param User $friend
84+
*
85+
* @return SpotifyFriendshipPlaylist
86+
* @throws SpotifyTokenExpiredException
87+
*/
88+
private static function createFriendshipPlaylist(User $user, User $friend): SpotifyFriendshipPlaylist {
89+
$playlist = SpotifyController::getApi($user)
90+
->createPlaylist([
91+
'name' => 'Freundschaftsplaylist von ' . $user->username . ' und ' . $friend->username,
92+
'description' => 'Freundschaftsplaylist - generiert von KStats auf k118.de',
93+
'public' => false
94+
]);
95+
96+
SpotifyFriendshipPlaylist::updateOrCreate([
97+
'user_id' => $user->id,
98+
'friend_id' => $friend->id,
99+
], [
100+
'playlist_id' => $playlist->id
101+
]);
102+
103+
return self::refreshFriendshipPlaylist($user, $friend);
104+
}
105+
106+
/**
107+
* @param User $user
108+
* @param User $friend
109+
*
110+
* @return SpotifyFriendshipPlaylist
111+
* @throws SpotifyTokenExpiredException
112+
*/
113+
private static function refreshFriendshipPlaylist(User $user, User $friend): SpotifyFriendshipPlaylist {
114+
$friendshipPlaylist = SpotifyFriendshipPlaylist::where('user_id', $user->id)
115+
->where('friend_id', $friend->id)
116+
->first();
117+
118+
if($friendshipPlaylist?->playlist_id == null) {
119+
throw new RecordsNotFoundException();
120+
}
121+
122+
$tracks = self::getCommonTracks($user, $friend)->map(function($spotifyTack) {
123+
return 'spotify:track:' . $spotifyTack->track_id;
124+
});
125+
126+
SpotifyController::getApi($user)->replacePlaylistTracks($friendshipPlaylist?->playlist_id, $tracks->toArray());
127+
$friendshipPlaylist->update(['last_refreshed' => Carbon::now()]);
128+
129+
return $friendshipPlaylist;
130+
}
131+
132+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Backend\Spotify;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\User;
7+
use SpotifyWebAPI\SpotifyWebAPI;
8+
use SpotifyWebAPI\Session;
9+
use App\Exceptions\SpotifyTokenExpiredException;
10+
11+
abstract class SpotifyController extends Controller {
12+
13+
/**
14+
* @param User $user
15+
*
16+
* @return SpotifyWebAPI
17+
* @throws SpotifyTokenExpiredException
18+
*/
19+
public static function getApi(User $user): SpotifyWebAPI {
20+
if($user?->socialProfile?->spotify_accessToken == null) {
21+
throw new SpotifyTokenExpiredException();
22+
}
23+
24+
$session = new Session(
25+
clientId: config('services.spotify.client_id'),
26+
clientSecret: config('services.spotify.client_secret'),
27+
);
28+
$session->setAccessToken($user->socialProfile->spotify_accessToken);
29+
30+
return new SpotifyWebAPI([], $session);
31+
}
32+
33+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Frontend\Spotify;
4+
5+
use App\Http\Controllers\Controller;
6+
use Illuminate\View\View;
7+
use Illuminate\Http\Request;
8+
use App\User;
9+
use Illuminate\Validation\Rule;
10+
use App\Http\Controllers\Backend\Spotify\FriendshipPlaylistController as FriendshipPlaylistBackend;
11+
use Illuminate\Http\RedirectResponse;
12+
use App\SpotifyFriendshipPlaylist;
13+
use App\Exceptions\SpotifyTokenExpiredException;
14+
15+
class FriendshipPlaylistController extends Controller {
16+
17+
public function renderFriendshipPlaylists(): View {
18+
return view('spotify.friendship.overview');
19+
}
20+
21+
public function createFriendshipPlaylist(Request $request): RedirectResponse {
22+
$validated = $request->validate([
23+
'friend_id' => [
24+
'required',
25+
'exists:users,id',
26+
Rule::in(auth()->user()->friends->pluck('id'))
27+
]
28+
]);
29+
30+
$friend = User::find($validated['friend_id']);
31+
32+
try {
33+
FriendshipPlaylistBackend::getFriendshipPlaylist(
34+
user: auth()->user(),
35+
friend: $friend,
36+
createIfDoesntExist: true
37+
);
38+
} catch(SpotifyTokenExpiredException) {
39+
return back()->with('alert-danger', __('spotify.token-invalid'));
40+
}
41+
42+
return redirect()->route('spotify.friendship-playlists.show', ['friendId' => $friend->id]);
43+
}
44+
45+
public function renderList(int $friendId): View|RedirectResponse {
46+
$friend = User::findOrFail($friendId);
47+
48+
if(!auth()->user()->friends->contains('id', $friend->id)) {
49+
abort(404);
50+
}
51+
52+
$playlistId = SpotifyFriendshipPlaylist::where('user_id', auth()->user()->id)
53+
->where('friend_id', $friend->id)
54+
->first()?->playlist_id;
55+
56+
if($playlistId == null) {
57+
return back()->with('alert-danger', 'Ihr besitzt aktuell keine gemeinsame Playlist.');
58+
}
59+
60+
return view('spotify.friendship.show', [
61+
'friend' => $friend,
62+
'tracks' => FriendshipPlaylistBackend::getCommonTracks(auth()->user(), $friend),
63+
'playlistId' => $playlistId
64+
]);
65+
}
66+
67+
}

app/Http/Controllers/SpotifyController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ public function renderExplore(): Renderable {
533533
->where('preview_url', '<>', null)
534534
->orderByDesc('popularity')
535535
->first();
536-
$trackReason = __('spotify.explore.reason.trend');
536+
$trackReason = __('spotify.explore.reason.trend');
537537
}
538538

539539

app/SpotifyFriendshipPlaylist.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace App;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
9+
class SpotifyFriendshipPlaylist extends Model {
10+
11+
use HasFactory;
12+
13+
protected $fillable = ['user_id', 'friend_id', 'playlist_id', 'last_refreshed'];
14+
protected $casts = ['last_refreshed'];
15+
16+
public function user(): BelongsTo {
17+
return $this->belongsTo(User::class, 'user_id', 'id');
18+
}
19+
20+
public function friend(): BelongsTo {
21+
return $this->belongsTo(User::class, 'friend_id', 'id');
22+
}
23+
}

app/User.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public function spotifySessions(): HasMany {
4444
return $this->hasMany(SpotifySession::class, 'user_id', 'id');
4545
}
4646

47+
public function spotifyFriendshipPlaylists(): HasMany {
48+
return $this->hasMany(SpotifyFriendshipPlaylist::class, 'user_id', 'id');
49+
}
50+
4751
public function friends(): BelongsToMany {
4852
return $this->belongsToMany(User::class, 'friendships', 'user_id', 'friend_id');
4953
}

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
"type": "project",
55
"require": {
66
"php": "^8.0",
7+
"ext-gd": "*",
78
"ext-imap": "*",
89
"ext-json": "*",
9-
"ext-gd": "*",
1010
"ext-mbstring": "*",
1111
"abraham/twitteroauth": "^2.0",
1212
"doctrine/dbal": "^3.0",
1313
"fideloper/proxy": "^4.0",
1414
"guzzlehttp/guzzle": "^7.0.1",
1515
"irazasyed/telegram-bot-sdk": "^3.3",
16+
"jwilsson/spotify-web-api-php": "^5.0",
1617
"laravel/framework": "^8.0",
1718
"laravel/socialite": "^5.0",
1819
"laravel/tinker": "^2.0",

0 commit comments

Comments
 (0)