Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #352

Merged
merged 50 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a939419
🎉Playlists controller & service
Douglasj02 Sep 5, 2024
0e3040f
🚀Method to get current playback state for YouTube service
Douglasj02 Sep 5, 2024
30b53c0
🚧new background component
21434809 Sep 3, 2024
cecc013
🚧fixed bakcground positioning
21434809 Sep 3, 2024
7dbaf03
🎉new background Blob animation component w/ mood colors
21434809 Sep 3, 2024
59ade0d
📐fixed echoing module
21434809 Sep 3, 2024
484c5bf
🚀created skeletone song card
21434809 Sep 3, 2024
9dc4126
🎉added skeleton song card
21434809 Sep 3, 2024
531375e
📐Update sidebar component and toast component
21434809 Sep 4, 2024
b007b8f
📐updated toats card issue with skeleton card
21434809 Sep 4, 2024
a8e1d01
🚀Add EchoComponent for echoing tracks
21434809 Sep 4, 2024
49a74b1
🚧eching page
21434809 Sep 4, 2024
1d091f7
🚧 Update echo-button component with hover animation and emit button c…
21434809 Sep 5, 2024
9374021
🎉Echo Button animation
21434809 Sep 5, 2024
b5d6ba6
📐echo button small upgrades
21434809 Sep 5, 2024
314fd1d
📐Update routes and song-cards component
21434809 Sep 5, 2024
39bbc3c
🎉Completed Echo page with proper template
21434809 Sep 5, 2024
fe01f0b
📐Removed Scrollbar
21434809 Sep 5, 2024
6386cde
🚧Refactor big-rounded-square-card and svg-icon components
21434809 Sep 6, 2024
7f6419c
🎉mood play button
21434809 Sep 6, 2024
afeadb9
📐Refactor SvgIconComponent to add animation to path element
21434809 Sep 6, 2024
5b66c4d
📐Refactor SvgIconComponent to add circle animation on/off
21434809 Sep 7, 2024
22c5707
📐updated animation speed for the echo button
21434809 Sep 7, 2024
ce44330
🚀on hover effect for moods list cards play icon
21434809 Sep 7, 2024
3fd4ec2
📐 big-rounded-square-card component to adjust size and click behavior
21434809 Sep 7, 2024
bef837a
📐made echo button start animating sooner
21434809 Sep 7, 2024
85e2d16
📐 info-bar and side-bar components to adjust layout and styling
21434809 Sep 7, 2024
e6924b8
🚧Readded Youtube provider
21434809 Sep 7, 2024
76a5db0
🚀YouTube top tracks endpoint
Douglasj02 Sep 7, 2024
045d9bb
🚀Create playlist endpoint
Douglasj02 Sep 7, 2024
4bd0422
🚀Updated YouTube service (frontend)
Douglasj02 Sep 7, 2024
d1ef888
📐Added parameters to YouTube type definition
Douglasj02 Sep 7, 2024
ffe4f8a
🎉YouTube playback from sidebar
Douglasj02 Sep 7, 2024
3a73e4d
📐Updated Bottom Player's OnInit method
Douglasj02 Sep 7, 2024
63ece4b
📐Moved YouTube div to app component
Douglasj02 Sep 8, 2024
3309e69
🚀Updated bottom player to subscribe to current track for YouTube
Douglasj02 Sep 8, 2024
c3baec2
🚀Initialise YouTube service after login
Douglasj02 Sep 8, 2024
ac66223
🚀Updated YouTube service to dynamically initialise when necessary
Douglasj02 Sep 8, 2024
a0e3959
📐Added checks for bottom player rendering in app component
Douglasj02 Sep 8, 2024
7903f77
📐Update progress bar in bottom player for YouTube service
Douglasj02 Sep 8, 2024
63c0fee
📐Added loggedIn observable for rendering bottom player
Douglasj02 Sep 8, 2024
caf0b44
📐Changes in YouTube service to update currently playing track
Douglasj02 Sep 8, 2024
392d0a1
📐Adjusted volume calculation for YouTube volume
Douglasj02 Sep 8, 2024
add29bb
Merge pull request #345 from COS301-SE-2024/feat/mediumFrontendUpdates
21434809 Sep 9, 2024
b2652ba
Merge branch 'develop' into feat/youtubeIntegration
Douglasj02 Sep 9, 2024
3cf96ac
🚧Fixes for YouTube playback
Douglasj02 Sep 9, 2024
860ffe8
💪Fixed bottom player rendering bug
Douglasj02 Sep 10, 2024
10701f4
📐Updated register to show toasts for errors
Douglasj02 Sep 11, 2024
b61f23d
⚡Improved performance for spotify seekTo in bottom player
Douglasj02 Sep 11, 2024
eca8b92
Merge pull request #349 from COS301-SE-2024/feat/youtubeIntegration
Douglasj02 Sep 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Backend/src/playlist/controller/playlist.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Body, Controller, Get, HttpException, HttpStatus, Post, Put, UnauthorizedException } from "@nestjs/common";
import { PlaylistService } from "../services/playlist.service";

@Controller("playlist")
export class SpotifyController
{
constructor(private readonly playlistService: PlaylistService)
{
}

@Post("create")
async createPlaylist(@Body() body: {playlistTracks: any; playlistName: string; accessToken: string; refreshToken: string;}): Promise<any>
{
return await this.playlistService.createPlaylist(body.playlistTracks, body.playlistName, body.accessToken, body.refreshToken);
}
}
38 changes: 38 additions & 0 deletions Backend/src/playlist/services/playlist.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Injectable, HttpException, HttpStatus } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { firstValueFrom, lastValueFrom } from "rxjs";
import { createSupabaseClient } from "../../supabase/services/supabaseClient";
import { SupabaseService } from "../../supabase/services/supabase.service";
import { accessKey } from "../../config";

@Injectable()
export class PlaylistService
{
constructor(private httpService: HttpService, private supabaseService: SupabaseService)
{
}


async createPlaylist(playlistTracks: any, playlistName: string, accessToken: string, refreshToken: string)
{

}
}

interface PlaylistTrack
{
id: string;
name: string;
albumName: string;
albumImageUrl: string;
artistName: string;
previewUrl: string | null;
spotifyUrl: string | null;
youtubeUrl: string | null;
}

interface Playlist
{
tracks: PlaylistTrack[];
}

1 change: 1 addition & 0 deletions Backend/src/spotify/controller/spotify.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class SpotifyController
return await this.spotifyService.getRecentlyPlayedTracks(accessToken, refreshToken);
}

// TODO DOUBLE CHECK issues when called multiple times
// This endpoint is used to get suggested tracks from the ECHO API (Clustering recommendations).
@Post("queue")
async getQueue(@Body() body: {
Expand Down
2 changes: 1 addition & 1 deletion Backend/src/spotify/services/spotify.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class SpotifyService
async getRecentlyPlayedTracks(accessToken: string, refreshToken: string): Promise<any>
{
const providerToken = await this.getAccessToken(accessToken, refreshToken);
const response = this.httpService.get("https://api.spotify.com/v1/me/player/recently-played?limit=15", {
const response = this.httpService.get("https://api.spotify.com/v1/me/player/recently-played?limit=20", {
headers: { "Authorization": `Bearer ${providerToken}` }
});
return lastValueFrom(response).then(res => res.data);
Expand Down
39 changes: 35 additions & 4 deletions Backend/src/youtube/controller/youtube.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, Controller, HttpException, HttpStatus, Post } from "@nestjs/common";
import { Body, Controller, Get, HttpException, HttpStatus, Post } from "@nestjs/common";
import { YouTubeService } from "../services/youtube.service";

@Controller("youtube")
Expand All @@ -8,9 +8,40 @@ export class YouTubeController
{
}

// Endpoint to search YouTube by track name and artist name
@Post("track-details-by-name")
async getTrackDetailsByName(@Body() body: {
accessToken: string;
refreshToken: string;
trackName: string;
artistName: string
})
{
const { accessToken, refreshToken, trackName, artistName } = body;

if (!accessToken || !refreshToken)
{
throw new HttpException("Access token or refresh token is missing.", HttpStatus.UNAUTHORIZED);
}
if (!trackName || !artistName)
{
throw new HttpException("Track name or artist name is missing.", HttpStatus.BAD_REQUEST);
}

const result = await this.youtubeService.getTrackDetailsByName(trackName, artistName);
return result;
}

// Fetch top YouTube tracks if the user has no recent listening
@Get("top-tracks")
async getTopYouTubeTracks() {
const topTracks = await this.youtubeService.getTopYouTubeTracks();
return topTracks;
}

// This endpoint will be used to search for videos/songs on YouTube.
@Post("search")
async search(@Body() body: { accessToken: string; refreshToken: string; query: string} )
async search(@Body() body: { accessToken: string; refreshToken: string; query: string })
{
if (!body.accessToken || !body.refreshToken)
{
Expand All @@ -26,7 +57,7 @@ export class YouTubeController

// This endpoint will be used to retrieve details of a video/song on YouTube.
@Post("video")
async getVideo(@Body() body: { accessToken: string; refreshToken: string; id: string})
async getVideo(@Body() body: { accessToken: string; refreshToken: string; id: string })
{
if (!body.accessToken || !body.refreshToken)
{
Expand All @@ -42,7 +73,7 @@ export class YouTubeController

// This endpoint will be used to retrieve a YouTube API key.
@Post("key")
async getKey(@Body() body: { accessToken: string; refreshToken: string})
async getKey(@Body() body: { accessToken: string; refreshToken: string })
{
if (!body.accessToken || !body.refreshToken)
{
Expand Down
40 changes: 39 additions & 1 deletion Backend/src/youtube/services/youtube.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface TrackInfo
albumName: string;
albumImageUrl: string;
artistName: string;
previewUrl: string;
previewUrl: string | null;
youtubeId: string;
}

Expand Down Expand Up @@ -61,4 +61,42 @@ export class YouTubeService
{
return this.API_KEY;
}

async getTrackDetailsByName(trackName: string, artistName: string)
{
const query = `${artistName} ${trackName}`;
const url = `${this.API_URL}/search?part=snippet&type=video&q=${encodeURIComponent(query)}&key=${this.API_KEY}`;
const response = await this.httpService.get(url).toPromise();
const items = response.data.items;

return items.map((item: any) => this.mapYouTubeResponseToTrackInfo(item));
}

// Method to fetch top YouTube music tracks
async getTopYouTubeTracks(): Promise<TrackInfo[]> {
const url = `${this.API_URL}/videos?part=snippet&chart=mostPopular&videoCategoryId=10&regionCode=US&type=video&key=${this.API_KEY}`;

try {
const response = await this.httpService.get(url).toPromise();
const items = response.data.items;

if (!items || items.length === 0) {
throw new Error('No tracks found in the YouTube API response');
}

return items.map((item: any) => ({
id: item.id,
name: item.snippet.title,
albumName: "Top Charts",
albumImageUrl: item.snippet.thumbnails.high.url,
artistName: item.snippet.channelTitle,
previewUrl: null,
spotifyUrl: null,
youtubeUrl: `https://www.youtube.com/watch?v=${item.id}`,
}));
} catch (error) {
console.error('Error fetching top YouTube tracks:', error.response?.data || error.message);
throw new Error('Failed to fetch top YouTube tracks');
}
}
}
24 changes: 13 additions & 11 deletions Frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
<app-background-animation class="absolute inset-0" style="z-index: -1;"></app-background-animation>
<div *ngIf="isCurrentRouteAuth()">
<router-outlet></router-outlet>
</div>
<div *ngIf="!isCurrentRouteAuth()" [ngClass]="backgroundMoodClasses[this.moodService.getCurrentMood()]">
<div *ngIf="screenSize === 'desktop'" class="grid grid-cols-[1fr,1fr,1fr,1fr,200px] grid-rows-[0.01fr,0.01fr,0.67fr] min-h-screen pl-2 pr-2">
<div *ngIf="!isCurrentRouteAuth()">
<div *ngIf="screenSize === 'desktop'" class="grid grid-cols-[1fr,1fr,1fr,1fr,200px] grid-rows-[0.01fr,0.01fr,0.67fr] min-h-screen pl-2 pr-2 relative">
<app-header class="col-start-2 col-span-1 row-start-2 row-span-1"></app-header>
<app-other-nav class="col-start-5 col-span-1 row-start-1 row-span-1 justify-self-end"></app-other-nav>
<app-left class="col-start-1 col-span-1 row-start-1 row-span-3"></app-left>
<div class="no-scrollbar col-span-4 row-start-3 row-span-2 overflow-y-scroll ">
<div id="center" class="no-scrollbar col-span-4 row-start-3 row-span-2 overflow-y-scroll overflow-hidden">
<router-outlet></router-outlet>
</div>
</div>
<div *ngIf="screenSize === 'mobile'">
<div class="grid grid-cols-3 grid-rows-[0.1fr,1fr,1fr] min-h-screen" [ngClass]="backgroundMoodClasses[moodService.getCurrentMood()]">
<app-header class="col-start-2 col-span-1 row-start-1 row-span-1"></app-header>
<div class="col-start-1 col-span-3 row-start-2 row-span-2">
<router-outlet></router-outlet>
</div>
<div class="grid grid-cols-3 grid-rows-[0.1fr,1fr,1fr] min-h-screen relative">
<app-header class="col-start-2 col-span-1 row-start-1 row-span-1"></app-header>
<div class="col-start-1 col-span-3 row-start-2 row-span-2">
<router-outlet></router-outlet>
</div>
<app-bottom-nav></app-bottom-nav>
</div>
<app-bottom-nav></app-bottom-nav>
</div>
<app-bottom-player *ngIf="isReady()"></app-bottom-player>
<app-bottom-player *ngIf="isReady()"></app-bottom-player> <!-- Use playerReady here -->
</div>


<!-- Hidden YouTube player, used to play videos in the background -->
<div id="youtube-player" style="display: none;"></div>
18 changes: 13 additions & 5 deletions Frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Inject, OnInit, PLATFORM_ID } from "@angular/core";
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from "@angular/core";
import { RouterOutlet, Router, NavigationEnd, Event as RouterEvent, ActivatedRoute } from "@angular/router";
import { BottomPlayerComponent } from "./components/organisms/bottom-player/bottom-player.component";
import { BottomNavComponent } from "./components/organisms/bottom-nav/bottom-nav.component";
Expand All @@ -10,13 +10,15 @@ import { SideBarComponent } from "./components/organisms/side-bar/side-bar.compo
import { ProviderService } from "./services/provider.service";
import { PageHeaderComponent } from "./components/molecules/page-header/page-header.component";
import { MoodService } from "./services/mood-service.service";
import { BackgroundAnimationComponent } from "./components/organisms/background-animation/background-animation.component";

//template imports
import { HeaderComponent } from "./components/organisms/header/header.component";
import { OtherNavComponent } from "./components/templates/desktop/other-nav/other-nav.component";
import { LeftComponent } from "./components/templates/desktop/left/left.component";
import { AuthService } from "./services/auth.service";
import { PlayerStateService } from "./services/player-state.service";
import { Observable } from "rxjs";

@Component({
selector: "app-root",
Expand All @@ -29,12 +31,13 @@ import { PlayerStateService } from "./services/player-state.service";
PageHeaderComponent,
HeaderComponent,
OtherNavComponent,
LeftComponent
LeftComponent,
BackgroundAnimationComponent
],
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
export class AppComponent implements OnInit, OnDestroy {
update: boolean = false;
screenSize!: string;
displayPageName: boolean = false;
Expand All @@ -45,6 +48,7 @@ export class AppComponent implements OnInit {
currentMood!: string;
moodComponentClasses!: { [key: string]: string };
backgroundMoodClasses!: { [key: string]: string };
isLoggedIn$!: Observable<boolean>;

constructor(
private router: Router,
Expand All @@ -57,9 +61,8 @@ export class AppComponent implements OnInit {
private playerStateService: PlayerStateService,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.currentMood = this.moodService.getCurrentMood();
this.moodComponentClasses = this.moodService.getComponentMoodClasses();
this.backgroundMoodClasses = this.moodService.getBackgroundMoodClasses();
this.isLoggedIn$ = this.authService.isLoggedIn$;
updates.versionUpdates.subscribe(event => {
if (event.type === "VERSION_READY") {
console.log("Version ready to install:");
Expand Down Expand Up @@ -98,4 +101,9 @@ export class AppComponent implements OnInit {
return this.playerStateService.isReady();
return false;
}

ngOnDestroy()
{
this.authService.signOut();
}
}
4 changes: 3 additions & 1 deletion Frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { NgModule } from "@angular/core";
import { InsightsComponent } from "./pages/insights/insights.component";
import { HelpMenuComponent } from "./pages/help-menu/help-menu.component";
import { LoginComponentview} from "./views/login/login.component";
import { EchoSongComponent } from "./components/templates/desktop/echo-song/echo-song.component";


export const routes: Routes = [
{ path: "landing", component: LandingPageComponent },
Expand All @@ -31,7 +33,7 @@ export const routes: Routes = [
{ path: "search", component: SearchComponent},
{ path: "newlogin", component: LoginComponentview},
{ path: "library", component: UserLibraryComponent},
{ path: "search", component: SearchComponent}
{ path: "echo Song", component: EchoSongComponent}
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<div class="rounded-lg transition-transform duration-300 hover:scale-110 cursor-pointer flex-none w-[27.5vh] mr-[1vw]" (click)="onMoodClick()">
<div class="card w-full h-full relative ml-3 my-3 aspect-w-16 aspect-h-9 shadow-xl rounded-lg">
<img [src]="mood.image" [alt]="mood.name" class="w-full h-full object-cover rounded-lg">
<div class="card-body absolute bottom-2 right-3 text-white text-lg font-bold">
{{ mood.name }}
</div>
<div class="rounded-lg transition-transform duration-300 hover:scale-110 cursor-pointer flex-none w-[12.5vw] mr-[1vw]" (click)="onMoodClick()">
<div class="card w-full h-full relative ml-3 my-3 aspect-w-16 aspect-h-9 shadow-xl rounded-lg">
<img [src]="mood.image" [alt]="mood.name" class="w-full h-full object-cover rounded-lg">
<div class="card-body absolute bottom-2 right-3 text-white text-lg font-bold">
{{ mood.name }}
</div>
<div class="ng-content-container absolute bottom-2 right-3">
<ng-content></ng-content>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@keyframes growHeightAndMoveUpSmall {
0%, 100% {
height: 20%; /* Smaller height */
transform: translateY(37.5%); /* Moved down */
}
50% {
height: 100%; /* Original height */
transform: translateY(0); /* Original position */
}
}
@keyframes growHeightAndMoveUpMedium {
0%, 100% {
height: 60%; /* Smaller height */
transform: translateY(20%); /* Moved down */
}
50% {
height: 100%; /* Original height */
transform: translateY(0); /* Original position */
}
}
@keyframes growHeightAndMoveUpLarge {
0%, 100% {
height: 85%; /* Smaller height */
transform: translateY(9.5%); /* Moved down */
}
50% {
height: 100%; /* Original height */
transform: translateY(0); /* Original position */
}
}

.group:hover .rect-3 {
animation: growHeightAndMoveUpLarge 1s infinite;
animation-fill-mode: forwards;
}
.group:hover .rect-2 {
animation: growHeightAndMoveUpMedium 1s infinite;
animation-delay: 0.2s; /* Adjusted delay to create wave effect */
animation-fill-mode: forwards;
}
.group:hover .rect-4 {
animation: growHeightAndMoveUpMedium 1s infinite;
animation-delay: 0.2s; /* Adjusted delay to create wave effect */
animation-fill-mode: forwards;
}
.group:hover .rect-1 {
animation: growHeightAndMoveUpSmall 1s infinite;
animation-delay: 0.4s; /* Adjusted delay to create wave effect */
animation-fill-mode: forwards;
}
.group:hover .rect-5 {
animation: growHeightAndMoveUpSmall 1s infinite;
animation-delay: 0.4s; /* Adjusted delay to create wave effect */
animation-fill-mode: forwards;
}
Loading
Loading