Skip to content

Commit 01f1b4e

Browse files
authored
Merge pull request #38 from CalPinSW/add-playback-controls
Add playback controls
2 parents bd33521 + 455bef8 commit 01f1b4e

File tree

13 files changed

+143
-41
lines changed

13 files changed

+143
-41
lines changed

.github/workflows/ci-pipeline.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ on:
44
paths-ignore:
55
- "diagrams/*"
66
- "**/README.md"
7-
pull_request:
8-
paths-ignore:
9-
- "diagrams/*"
10-
- "**/README.md"
117

128
jobs:
139
build:

backend/src/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def create_app():
2020
SESSION_COOKIE_SAMESITE="None",
2121
SESSION_COOKIE_SECURE="True",
2222
)
23+
2324
cors = CORS(
2425
app,
2526
resources={

backend/src/controllers/spotify.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from flask import Blueprint, make_response, request
22
from src.dataclasses.playback_info import PlaybackInfo
3+
from src.dataclasses.playback_request import StartPlaybackRequest
34
from src.dataclasses.playlist import Playlist
45
from src.spotify import SpotifyClient
56

@@ -125,4 +126,23 @@ def add_album_to_playlist():
125126
access_token=access_token, playlist_id=playlist_id, album_id=album_id
126127
)
127128

129+
@spotify_controller.route("pause_playback", methods=["PUT"])
130+
def pause_playback():
131+
access_token = request.cookies.get("spotify_access_token")
132+
return spotify.pause_playback(access_token)
133+
134+
@spotify_controller.route("start_playback", methods=["PUT"])
135+
def start_playback():
136+
access_token = request.cookies.get("spotify_access_token")
137+
request_body = request.json
138+
start_playback_request_body = (
139+
StartPlaybackRequest.model_validate(request_body) if request_body else None
140+
)
141+
return spotify.start_playback(access_token, start_playback_request_body)
142+
143+
@spotify_controller.route("pause_or_start_playback", methods=["PUT"])
144+
def pause_or_start_playback():
145+
access_token = request.cookies.get("spotify_access_token")
146+
return spotify.pause_or_start_playback(access_token)
147+
128148
return spotify_controller

backend/src/dataclasses/playback_info.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class PlaybackInfo(BaseModel):
2323
track_duration: float
2424
album_progress: float
2525
album_duration: float
26+
is_playing: bool
2627

2728
def get_formatted_artists(self) -> str:
2829
return ", ".join(self.track_artists)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from pydantic import BaseModel
2+
from typing import List, Optional
3+
4+
5+
class StartPlaybackRequestPositionOffset(BaseModel):
6+
position: int
7+
8+
9+
class StartPlaybackRequestUriOffset(BaseModel):
10+
uri: str
11+
12+
13+
class StartPlaybackRequest(BaseModel):
14+
context_uri: Optional[str] = None
15+
uris: Optional[List[str]] = None
16+
offset: Optional[
17+
StartPlaybackRequestPositionOffset | StartPlaybackRequestUriOffset
18+
] = None
19+
position_ms: Optional[int] = None

backend/src/spotify.py

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from flask import Response, make_response, redirect
66
from src.dataclasses.album import Album
77
from src.dataclasses.playback_info import PlaybackInfo, PlaylistProgression
8+
from src.dataclasses.playback_request import StartPlaybackRequest
89
from src.dataclasses.playback_state import PlaybackState
910
from src.dataclasses.playlist import Playlist
1011
from src.dataclasses.playlist_info import CurrentUserPlaylists, SimplifiedPlaylist
@@ -15,7 +16,7 @@
1516
from src.dataclasses.user import User
1617
from src.exceptions.Unauthorized import UnauthorizedException
1718
from src.flask_config import Config
18-
from src.utils.response_creator import ResponseCreator
19+
from src.utils.response_creator import add_cookies_to_response
1920

2021
scope = [
2122
"user-library-read",
@@ -28,6 +29,7 @@
2829
"playlist-read-collaborative",
2930
"playlist-modify-private",
3031
"playlist-modify-public",
32+
"user-modify-playback-state",
3133
]
3234

3335

@@ -93,12 +95,9 @@ def refresh_access_token(self, refresh_token):
9395
token_response = TokenResponse.model_validate(api_response)
9496
access_token = token_response.access_token
9597
user_info = self.get_current_user(access_token)
96-
resp = (
97-
ResponseCreator()
98-
.with_cookies(
99-
{"spotify_access_token": access_token, "user_id": user_info.id}
100-
)
101-
.create()
98+
resp = add_cookies_to_response(
99+
make_response(),
100+
{"spotify_access_token": access_token, "user_id": user_info.id},
102101
)
103102
return resp
104103

@@ -119,16 +118,13 @@ def request_access_token(self, code):
119118
token_response = TokenResponse.model_validate(api_response)
120119
access_token = token_response.access_token
121120
user_info = self.get_current_user(access_token)
122-
resp = (
123-
ResponseCreator(redirect(f"{Config().FRONTEND_URL}/"))
124-
.with_cookies(
125-
{
126-
"spotify_access_token": access_token,
127-
"spotify_refresh_token": token_response.refresh_token,
128-
"user_id": user_info.id,
129-
}
130-
)
131-
.create()
121+
resp = add_cookies_to_response(
122+
make_response(redirect(f"{Config().FRONTEND_URL}/")),
123+
{
124+
"spotify_access_token": access_token,
125+
"spotify_refresh_token": token_response.refresh_token,
126+
"user_id": user_info.id,
127+
},
132128
)
133129
return resp
134130

@@ -293,7 +289,7 @@ def get_album(self, access_token, id):
293289
album = Album.model_validate(api_album)
294290
return album
295291

296-
def get_current_playback(self, access_token):
292+
def get_current_playback(self, access_token) -> PlaybackState | None:
297293
response = requests.get(
298294
f"https://api.spotify.com/v1/me/player",
299295
auth=BearerAuth(access_token),
@@ -345,6 +341,7 @@ def get_my_current_playback(self, access_token) -> PlaybackInfo | None:
345341
"track_duration": api_playback.item.duration_ms,
346342
"album_progress": album_progress,
347343
"album_duration": album_duration,
344+
"is_playing": api_playback.is_playing,
348345
}
349346
)
350347

@@ -366,11 +363,13 @@ def get_playlist_progression(self, access_token, api_playback: PlaybackInfo):
366363
}
367364
)
368365

369-
def search_albums(self, access_token, search=None, offset=0) -> List[Album]:
366+
def search_albums(
367+
self, access_token, search=None, offset=0, limit=50
368+
) -> List[Album]:
370369
if search:
371370
response = requests.get(
372371
f"https://api.spotify.com/v1/albums/{id}",
373-
data={"q": search, "type": "album", "limit": 50, "offset": offset},
372+
data={"q": search, "type": "album", "limit": limit, "offset": offset},
374373
headers={
375374
"content-type": "application/json",
376375
},
@@ -456,6 +455,45 @@ def is_album_in_playlist(self, access_token, playlist_id, album: Album) -> bool:
456455
album_track_ids = [track.id for track in album.tracks.items]
457456
return all(e in playlist_track_ids for e in album_track_ids)
458457

458+
def pause_playback(self, access_token) -> Response:
459+
response = requests.put(
460+
url="https://api.spotify.com/v1/me/player/pause",
461+
headers={
462+
"content-type": "application/json",
463+
},
464+
auth=BearerAuth(access_token),
465+
)
466+
return self.response_handler(
467+
make_response("", response.status_code), jsonify=False
468+
)
469+
470+
def start_playback(
471+
self, access_token, start_playback_request_body: StartPlaybackRequest = None
472+
) -> Response:
473+
if not start_playback_request_body:
474+
data = None
475+
else:
476+
data = start_playback_request_body.model_dump_json(exclude_none=True)
477+
478+
response = requests.put(
479+
url="https://api.spotify.com/v1/me/player/play",
480+
data=data,
481+
headers={
482+
"content-type": "application/json",
483+
},
484+
auth=BearerAuth(access_token),
485+
)
486+
return self.response_handler(
487+
make_response("", response.status_code), jsonify=False
488+
)
489+
490+
def pause_or_start_playback(self, access_token) -> Response:
491+
is_playing = self.get_current_playback(access_token).is_playing
492+
if is_playing:
493+
return self.pause_playback(access_token)
494+
else:
495+
return self.start_playback(access_token)
496+
459497

460498
def get_playlist_duration(playlist_info: List[PlaylistTrackObject]) -> int:
461499
return sum(track.track.duration_ms for track in playlist_info)

backend/src/tests/mock_builders/playback_info_builder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def playback_info_builder(
1111
track_duration=180000,
1212
album_progress=1000000,
1313
album_duration=18000000,
14+
is_playing=True,
1415
):
1516
return PlaybackInfo.model_validate(
1617
{
@@ -26,5 +27,6 @@ def playback_info_builder(
2627
"track_duration": track_duration,
2728
"album_progress": album_progress,
2829
"album_duration": album_duration,
30+
"is_playing": is_playing,
2931
}
3032
)
Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
from flask import Response, make_response
1+
from flask import Response
22

33

4-
class ResponseCreator:
5-
def __init__(self):
6-
self.response = make_response()
7-
8-
def with_cookies(self, cookie_dict: dict):
9-
for key, value in cookie_dict.items():
10-
self.response.set_cookie(key, value, samesite="None", secure=True)
11-
return self
12-
13-
def create(self) -> Response:
14-
return self.response
4+
def add_cookies_to_response(response: Response, cookie_dict: dict):
5+
for key, value in cookie_dict.items():
6+
response.set_cookie(key, value, samesite="None", secure=True)
7+
return response

compose.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ services:
44
build:
55
context: frontend
66
dockerfile: Dockerfile
7-
target: production
7+
target: development
88
ports:
99
- "8080:8080"
1010
env_file:
@@ -21,7 +21,7 @@ services:
2121
build:
2222
context: backend
2323
dockerfile: Dockerfile
24-
target: production
24+
target: development
2525
ports:
2626
- "5000:5000"
2727
env_file:

frontend/src/api/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,25 @@ export const addAlbumToPlaylist = async (
104104
albumId,
105105
});
106106
};
107+
108+
export const pausePlayback = async (): Promise<Response> => {
109+
return jsonRequest(`spotify/pause_playback`, RequestMethod.PUT);
110+
};
111+
112+
export const startPlayback = async (
113+
context_uri?: string,
114+
uris?: string[],
115+
offset?: {position: number} | {uri: string},
116+
position_ms?: number
117+
): Promise<Response> => {
118+
return jsonRequest(`spotify/start_playback`, RequestMethod.PUT, {
119+
context_uri,
120+
uris,
121+
offset,
122+
position_ms
123+
});
124+
};
125+
126+
export const pauseOrStartPlayback = async (): Promise<Response> => {
127+
return jsonRequest(`spotify/pause_or_start_playback`, RequestMethod.PUT);
128+
};

0 commit comments

Comments
 (0)