Skip to content

Commit

Permalink
Add pause/play functionality to the playback footer
Browse files Browse the repository at this point in the history
  • Loading branch information
CalPinSW committed Jul 24, 2024
1 parent bd33521 commit 8f82157
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 11 deletions.
13 changes: 13 additions & 0 deletions backend/src/controllers/spotify.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from flask import Blueprint, make_response, request
from src.dataclasses.playback_info import PlaybackInfo
from src.dataclasses.playback_request import StartPlaybackRequest
from src.dataclasses.playlist import Playlist
from src.spotify import SpotifyClient

Expand Down Expand Up @@ -125,4 +126,16 @@ def add_album_to_playlist():
access_token=access_token, playlist_id=playlist_id, album_id=album_id
)

@spotify_controller.route("pause_playback", methods=["PUT"])
def pause_playback():
access_token = request.cookies.get("spotify_access_token")
return spotify.pause_playback(access_token)

@spotify_controller.route("start_playback", methods=["PUT"])
def start_playback():
access_token = request.cookies.get("spotify_access_token")
request_body = request.json
start_playback_request_body = StartPlaybackRequest.model_validate(request_body)
return spotify.start_playback(access_token, start_playback_request_body)

return spotify_controller
1 change: 1 addition & 0 deletions backend/src/dataclasses/playback_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class PlaybackInfo(BaseModel):
track_duration: float
album_progress: float
album_duration: float
is_playing: bool

def get_formatted_artists(self) -> str:
return ", ".join(self.track_artists)
Expand Down
19 changes: 19 additions & 0 deletions backend/src/dataclasses/playback_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from pydantic import BaseModel
from typing import List, Optional


class StartPlaybackRequestPositionOffset(BaseModel):
position: int


class StartPlaybackRequestUriOffset(BaseModel):
uri: str


class StartPlaybackRequest(BaseModel):
context_uri: Optional[str] = None
uris: Optional[List[str]] = None
offset: Optional[
StartPlaybackRequestPositionOffset | StartPlaybackRequestUriOffset
] = None
position_ms: Optional[int] = None
46 changes: 43 additions & 3 deletions backend/src/spotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from flask import Response, make_response, redirect
from src.dataclasses.album import Album
from src.dataclasses.playback_info import PlaybackInfo, PlaylistProgression
from src.dataclasses.playback_request import StartPlaybackRequest
from src.dataclasses.playback_state import PlaybackState
from src.dataclasses.playlist import Playlist
from src.dataclasses.playlist_info import CurrentUserPlaylists, SimplifiedPlaylist
Expand All @@ -28,6 +29,7 @@
"playlist-read-collaborative",
"playlist-modify-private",
"playlist-modify-public",
"user-modify-playback-state",
]


Expand Down Expand Up @@ -293,7 +295,7 @@ def get_album(self, access_token, id):
album = Album.model_validate(api_album)
return album

def get_current_playback(self, access_token):
def get_current_playback(self, access_token) -> PlaybackState | None:
response = requests.get(
f"https://api.spotify.com/v1/me/player",
auth=BearerAuth(access_token),
Expand Down Expand Up @@ -345,6 +347,7 @@ def get_my_current_playback(self, access_token) -> PlaybackInfo | None:
"track_duration": api_playback.item.duration_ms,
"album_progress": album_progress,
"album_duration": album_duration,
"is_playing": api_playback.is_playing,
}
)

Expand All @@ -366,11 +369,13 @@ def get_playlist_progression(self, access_token, api_playback: PlaybackInfo):
}
)

def search_albums(self, access_token, search=None, offset=0) -> List[Album]:
def search_albums(
self, access_token, search=None, offset=0, limit=50
) -> List[Album]:
if search:
response = requests.get(
f"https://api.spotify.com/v1/albums/{id}",
data={"q": search, "type": "album", "limit": 50, "offset": offset},
data={"q": search, "type": "album", "limit": limit, "offset": offset},
headers={
"content-type": "application/json",
},
Expand Down Expand Up @@ -456,6 +461,41 @@ def is_album_in_playlist(self, access_token, playlist_id, album: Album) -> bool:
album_track_ids = [track.id for track in album.tracks.items]
return all(e in playlist_track_ids for e in album_track_ids)

def pause_playback(self, access_token) -> Response:
response = requests.put(
url="https://api.spotify.com/v1/me/player/pause",
headers={
"content-type": "application/json",
},
auth=BearerAuth(access_token),
)
foo = self.response_handler(
make_response("", response.status_code), jsonify=False
)
return foo

def start_playback(
self, access_token, start_playback_request_body: StartPlaybackRequest = None
) -> Response:
request_json = start_playback_request_body.model_dump_json(exclude_none=True)
if request_json == {}:
data = None
else:
data = request_json

response = requests.put(
url="https://api.spotify.com/v1/me/player/play",
data=data,
headers={
"content-type": "application/json",
},
auth=BearerAuth(access_token),
)
foo = self.response_handler(
make_response("", response.status_code), jsonify=False
)
return foo


def get_playlist_duration(playlist_info: List[PlaylistTrackObject]) -> int:
return sum(track.track.duration_ms for track in playlist_info)
Expand Down
4 changes: 2 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
build:
context: frontend
dockerfile: Dockerfile
target: production
target: development
ports:
- "8080:8080"
env_file:
Expand All @@ -21,7 +21,7 @@ services:
build:
context: backend
dockerfile: Dockerfile
target: production
target: development
ports:
- "5000:5000"
env_file:
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,21 @@ export const addAlbumToPlaylist = async (
albumId,
});
};

export const pausePlayback = async (): Promise<Response> => {
return jsonRequest(`spotify/pause_playback`, RequestMethod.PUT);
};

export const startPlayback = async (
context_uri?: string,
uris?: string[],
offset?: {position: number} | {uri: string},
position_ms?: number
): Promise<Response> => {
return jsonRequest(`spotify/start_playback`, RequestMethod.PUT, {
context_uri,
uris,
offset,
position_ms
});
};
2 changes: 2 additions & 0 deletions frontend/src/api/jsonRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const backendUrl = process.env.BACKEND_URL;
export enum RequestMethod {
GET = "get",
POST = "post",
PUT = "put",
}

export const jsonRequest = async <I, O>(
Expand All @@ -16,6 +17,7 @@ export const jsonRequest = async <I, O>(
let fetchOptions: RequestInit = { credentials: "include" };
switch (method) {
case RequestMethod.POST:
case RequestMethod.PUT:
fetchOptions = {
...fetchOptions,
method: method,
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/context/PlaybackContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import React, {
useState,
} from "react";
import { PlaybackInfo, PlaylistProgress } from "../interfaces/PlaybackInfo";
import { useQuery } from "@tanstack/react-query";
import { QueryObserverResult, RefetchOptions, useQuery } from "@tanstack/react-query";
import { getPlaybackInfo, getPlaylistProgress } from "../api";

interface PlaybackContext {
playbackInfo?: PlaybackInfo;
refetchPlaybackInfo?: (options?: RefetchOptions) => Promise<QueryObserverResult<PlaybackInfo, Error>>
playlistProgress?: PlaylistProgress;
}

Expand All @@ -24,7 +25,7 @@ export const PlaybackContextProvider: FC<PlaybackContextProviderProps> = ({
children,
}) => {
const [playbackRefetchInterval, setPlaybackRefetchInterval] = useState(5000);
const { data: playbackInfo } = useQuery<PlaybackInfo>({
const { data: playbackInfo, refetch: refetchPlaybackInfo } = useQuery<PlaybackInfo>({
queryKey: ["playbackInfo"],
queryFn: () => {
return getPlaybackInfo();
Expand All @@ -50,7 +51,7 @@ export const PlaybackContextProvider: FC<PlaybackContextProviderProps> = ({
});

return (
<PlaybackContext.Provider value={{ playbackInfo, playlistProgress }}>
<PlaybackContext.Provider value={{ playbackInfo, playlistProgress, refetchPlaybackInfo }}>
{children}
</PlaybackContext.Provider>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/interfaces/PlaybackInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface PlaybackInfo {
track_duration: number;
album_progress: number;
album_duration: number;
is_playing: boolean;
}

export interface PlaylistProgress {
Expand Down
18 changes: 15 additions & 3 deletions frontend/src/presentational/PlaybackFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import React, { FC, useEffect, useState } from "react";
import { getPlaybackInfo, getPlaylistProgress } from "../api";
import { getPlaybackInfo, getPlaylistProgress, pausePlayback, startPlayback } from "../api";
import { PlaybackInfo, PlaylistProgress } from "../interfaces/PlaybackInfo";
import { ProgressCircle } from "../components/ProgressCircle";
import useWindowSize from "../hooks/useWindowSize";
Expand All @@ -12,14 +12,26 @@ import { Link } from "react-router-dom";

const PlaybackFooter: FC = () => {
const { isMobileView } = useWindowSize();
const { playbackInfo, playlistProgress } = usePlaybackContext();
const { playbackInfo, playlistProgress, refetchPlaybackInfo } = usePlaybackContext();

if (!playbackInfo) return null;

const handlePausePlayClick = (): void => {
if (playbackInfo.is_playing) {
pausePlayback()
} else {
startPlayback()
}
refetchPlaybackInfo?.()
}

return (
<div className="w-full h-fit bg-primary-300 px-4 py-2 text-sm sm:text-base">
<div className="flex space-x-4 sm:space-x-6">
<div className="flex flex-col space-y-2 w-1/5 max-w-32">
<img src={playbackInfo.artwork_url}></img>
<button className="opacity-80 w-full h-full" onClick={handlePausePlayClick}>
<img src={playbackInfo.artwork_url}></img>
</button>
<div>Playing:</div>
<div className="text-balance">
{playbackInfo.album_artists.join(", ")}
Expand Down

0 comments on commit 8f82157

Please sign in to comment.