diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index dd94be8..c39d6d2 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -4,10 +4,6 @@ on: paths-ignore: - "diagrams/*" - "**/README.md" - pull_request: - paths-ignore: - - "diagrams/*" - - "**/README.md" jobs: build: diff --git a/backend/src/app.py b/backend/src/app.py index 00c5da0..1fcced1 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -20,6 +20,7 @@ def create_app(): SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE="True", ) + cors = CORS( app, resources={ diff --git a/backend/src/controllers/spotify.py b/backend/src/controllers/spotify.py index 6cee3ac..4b2bcbb 100644 --- a/backend/src/controllers/spotify.py +++ b/backend/src/controllers/spotify.py @@ -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 @@ -125,4 +126,23 @@ 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) if request_body else None + ) + return spotify.start_playback(access_token, start_playback_request_body) + + @spotify_controller.route("pause_or_start_playback", methods=["PUT"]) + def pause_or_start_playback(): + access_token = request.cookies.get("spotify_access_token") + return spotify.pause_or_start_playback(access_token) + return spotify_controller diff --git a/backend/src/dataclasses/playback_info.py b/backend/src/dataclasses/playback_info.py index 92563ef..39cd0f4 100644 --- a/backend/src/dataclasses/playback_info.py +++ b/backend/src/dataclasses/playback_info.py @@ -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) diff --git a/backend/src/dataclasses/playback_request.py b/backend/src/dataclasses/playback_request.py new file mode 100644 index 0000000..467ad6a --- /dev/null +++ b/backend/src/dataclasses/playback_request.py @@ -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 diff --git a/backend/src/spotify.py b/backend/src/spotify.py index f81b8ad..e30e975 100644 --- a/backend/src/spotify.py +++ b/backend/src/spotify.py @@ -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 @@ -15,7 +16,7 @@ from src.dataclasses.user import User from src.exceptions.Unauthorized import UnauthorizedException from src.flask_config import Config -from src.utils.response_creator import ResponseCreator +from src.utils.response_creator import add_cookies_to_response scope = [ "user-library-read", @@ -28,6 +29,7 @@ "playlist-read-collaborative", "playlist-modify-private", "playlist-modify-public", + "user-modify-playback-state", ] @@ -93,12 +95,9 @@ def refresh_access_token(self, refresh_token): token_response = TokenResponse.model_validate(api_response) access_token = token_response.access_token user_info = self.get_current_user(access_token) - resp = ( - ResponseCreator() - .with_cookies( - {"spotify_access_token": access_token, "user_id": user_info.id} - ) - .create() + resp = add_cookies_to_response( + make_response(), + {"spotify_access_token": access_token, "user_id": user_info.id}, ) return resp @@ -119,16 +118,13 @@ def request_access_token(self, code): token_response = TokenResponse.model_validate(api_response) access_token = token_response.access_token user_info = self.get_current_user(access_token) - resp = ( - ResponseCreator(redirect(f"{Config().FRONTEND_URL}/")) - .with_cookies( - { - "spotify_access_token": access_token, - "spotify_refresh_token": token_response.refresh_token, - "user_id": user_info.id, - } - ) - .create() + resp = add_cookies_to_response( + make_response(redirect(f"{Config().FRONTEND_URL}/")), + { + "spotify_access_token": access_token, + "spotify_refresh_token": token_response.refresh_token, + "user_id": user_info.id, + }, ) return resp @@ -293,7 +289,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), @@ -345,6 +341,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, } ) @@ -366,11 +363,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", }, @@ -456,6 +455,45 @@ 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), + ) + return self.response_handler( + make_response("", response.status_code), jsonify=False + ) + + def start_playback( + self, access_token, start_playback_request_body: StartPlaybackRequest = None + ) -> Response: + if not start_playback_request_body: + data = None + else: + data = start_playback_request_body.model_dump_json(exclude_none=True) + + response = requests.put( + url="https://api.spotify.com/v1/me/player/play", + data=data, + headers={ + "content-type": "application/json", + }, + auth=BearerAuth(access_token), + ) + return self.response_handler( + make_response("", response.status_code), jsonify=False + ) + + def pause_or_start_playback(self, access_token) -> Response: + is_playing = self.get_current_playback(access_token).is_playing + if is_playing: + return self.pause_playback(access_token) + else: + return self.start_playback(access_token) + def get_playlist_duration(playlist_info: List[PlaylistTrackObject]) -> int: return sum(track.track.duration_ms for track in playlist_info) diff --git a/backend/src/tests/mock_builders/playback_info_builder.py b/backend/src/tests/mock_builders/playback_info_builder.py index 105eded..2a316e2 100644 --- a/backend/src/tests/mock_builders/playback_info_builder.py +++ b/backend/src/tests/mock_builders/playback_info_builder.py @@ -11,6 +11,7 @@ def playback_info_builder( track_duration=180000, album_progress=1000000, album_duration=18000000, + is_playing=True, ): return PlaybackInfo.model_validate( { @@ -26,5 +27,6 @@ def playback_info_builder( "track_duration": track_duration, "album_progress": album_progress, "album_duration": album_duration, + "is_playing": is_playing, } ) diff --git a/backend/src/utils/response_creator.py b/backend/src/utils/response_creator.py index 842dffc..fba32e3 100644 --- a/backend/src/utils/response_creator.py +++ b/backend/src/utils/response_creator.py @@ -1,14 +1,7 @@ -from flask import Response, make_response +from flask import Response -class ResponseCreator: - def __init__(self): - self.response = make_response() - - def with_cookies(self, cookie_dict: dict): - for key, value in cookie_dict.items(): - self.response.set_cookie(key, value, samesite="None", secure=True) - return self - - def create(self) -> Response: - return self.response +def add_cookies_to_response(response: Response, cookie_dict: dict): + for key, value in cookie_dict.items(): + response.set_cookie(key, value, samesite="None", secure=True) + return response diff --git a/compose.yaml b/compose.yaml index 012e8f8..1a7c246 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,7 +4,7 @@ services: build: context: frontend dockerfile: Dockerfile - target: production + target: development ports: - "8080:8080" env_file: @@ -21,7 +21,7 @@ services: build: context: backend dockerfile: Dockerfile - target: production + target: development ports: - "5000:5000" env_file: diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1b1cc90..ab53d0b 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -104,3 +104,25 @@ export const addAlbumToPlaylist = async ( albumId, }); }; + +export const pausePlayback = async (): Promise => { + 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 => { + return jsonRequest(`spotify/start_playback`, RequestMethod.PUT, { + context_uri, + uris, + offset, + position_ms + }); +}; + +export const pauseOrStartPlayback = async (): Promise => { + return jsonRequest(`spotify/pause_or_start_playback`, RequestMethod.PUT); +}; diff --git a/frontend/src/api/jsonRequest.ts b/frontend/src/api/jsonRequest.ts index 69e93c5..9bf56ca 100644 --- a/frontend/src/api/jsonRequest.ts +++ b/frontend/src/api/jsonRequest.ts @@ -5,6 +5,7 @@ export const backendUrl = process.env.BACKEND_URL; export enum RequestMethod { GET = "get", POST = "post", + PUT = "put", } export const jsonRequest = async ( @@ -16,6 +17,7 @@ export const jsonRequest = async ( let fetchOptions: RequestInit = { credentials: "include" }; switch (method) { case RequestMethod.POST: + case RequestMethod.PUT: fetchOptions = { ...fetchOptions, method: method, diff --git a/frontend/src/interfaces/PlaybackInfo.ts b/frontend/src/interfaces/PlaybackInfo.ts index 4a0d1c9..21cce30 100644 --- a/frontend/src/interfaces/PlaybackInfo.ts +++ b/frontend/src/interfaces/PlaybackInfo.ts @@ -11,6 +11,7 @@ export interface PlaybackInfo { track_duration: number; album_progress: number; album_duration: number; + is_playing: boolean; } export interface PlaylistProgress { diff --git a/frontend/src/presentational/PlaybackFooter.tsx b/frontend/src/presentational/PlaybackFooter.tsx index 32466f8..14b5366 100644 --- a/frontend/src/presentational/PlaybackFooter.tsx +++ b/frontend/src/presentational/PlaybackFooter.tsx @@ -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, pauseOrStartPlayback, pausePlayback, startPlayback } from "../api"; import { PlaybackInfo, PlaylistProgress } from "../interfaces/PlaybackInfo"; import { ProgressCircle } from "../components/ProgressCircle"; import useWindowSize from "../hooks/useWindowSize"; @@ -15,11 +15,18 @@ const PlaybackFooter: FC = () => { const { playbackInfo, playlistProgress } = usePlaybackContext(); if (!playbackInfo) return null; + + const handlePausePlayClick = (): void => { + pauseOrStartPlayback() + } + return (
- +
Playing:
{playbackInfo.album_artists.join(", ")} @@ -28,7 +35,7 @@ const PlaybackFooter: FC = () => {
- +
{playbackInfo.track_title}